The local stack
The local stack brings up a complete apomesh deployment on your machine: Postgres, Redis, one orchestrator, and one worker, with a sample project mounted as the agent’s workspace. It is a local-evaluation reference, not a production template — but it exercises the real substrate end to end.
This page is the operator reference for that stack. The recipes below use just (the human-facing command alias); each one wraps a plain docker compose invocation, shown alongside where it helps.
Prerequisites
Section titled “Prerequisites”- Docker with the
docker composeplugin. justfor the operator recipes (brew install juston macOS).- OpenSSL, only for the optional mTLS cert generator under
deploy/local/certs/. uv, only for the sample dispatch and the opt-in auth profile.
The services
Section titled “The services”The default docker compose up starts four services. All ports bind to 127.0.0.1 — nothing is exposed off your host.
| Service | Image | Port | Role |
|---|---|---|---|
postgres | pgvector/pgvector | 5432 | Durable state store and semantic-memory vector column. The same image the substrate’s Hybrid backend references. |
redis | redis | 6379 | Write-through hot-replay buffer for the event bus. |
orchestrator | apomesh-orchestrator:dev | 50051 (gRPC), 50052 (REST + OpenAPI) | The control plane — sessions, routing, cost rollups, the credential store. |
worker | apomesh-worker:dev | — | The execution plane — runs every tool inside its sandbox boundary and dispatches LLM calls. |
A fifth service, zitadel, starts only under the opt-in auth profile (see Profiles below); the zero-config stack leaves it out.
The worker runs privileged: true in this recipe because Docker Desktop’s Linux VM blocks the unprivileged user namespaces Bubblewrap needs. The worker container itself is the isolation boundary here — treat the whole recipe as one trusted dev sandbox. A production Linux host does not need privileged.
Two config homes
Section titled “Two config homes”Local config lives in exactly two places, split by one question — is this value the same for every developer, or specific to my machine?
deploy/local/(tracked) holds fixed, same-for-every-dev config: the Compose file,config.<profile>.toml, andproviders.yaml. The repo commits only neutral*.example.*skeletons;just bootstrap(run automatically byjust up) copies each one to its real, git-ignored counterpart without clobbering your edits. No secrets live here.~/.apomesh/env(out of repo) holds per-operator values: every secret (provider keys, sealing key, OAuth tokens) and machine-specific knobs. Compose loads this file straight into both daemons viaenv_file, so you never source it by hand — a credential that must reach the containers belongs in this file, not exported in a shell.
Each value is set in exactly one home. A value hardcoded in Compose is never also in ~/.apomesh/env, and a secret is never inlined into a tracked file. See .claude/rules/local-dev-config.md for the full contract.
Profiles: dev, oauth and mtls
Section titled “Profiles: dev, oauth and mtls”The active daemon config is config.${APOMESH_PROFILE}.toml, chosen by the single selector APOMESH_PROFILE in deploy/local/.env:
dev(the default, whenAPOMESH_PROFILEis absent) →config.dev.toml, with[auth] backend = "none". Every caller is thedevtenant with every scope. This is the zero-config path.oauth→config.oauth.toml, wired to a local Zitadel identity provider so the console, the SDK, andcurlall authenticate the way a real deployment does.mtls→config.mtls.toml, giving the orchestrator↔worker hop mutual TLS. This is the profile where the secure secret path actually runs: secret forwarding is fail-closed, so a plaintext hop forwards nothing, and thedevprofile’s relaxation means the secure path is never exercised there.
Switch on the auth profile — it provisions Zitadel, renders the oauth config, and records the selector:
just up-auth # start the stack + Zitadel, provision it, apply the configjust smoke-auth # prove the caller-identity + authorization plane end to endReturn to the zero-config stack — this drops the selector and leaves the provisioned IdP intact, so just up-auth can re-enable it in one step:
just deauthjust upFor the seed identities, the browser sign-in flow, and the JWKS-over-HTTP dev caveat, see the full walkthrough in deploy/local/README.md.
The mutual-TLS profile
Section titled “The mutual-TLS profile”mtls is the one profile that proves the credential plane’s fail-closed rule rather than relaxing it. Mint the org CA once, put the printed token in ~/.apomesh/env, then bring the stack up:
just mint-enroll # mints the org CA + the orchestrator leaf; prints the enrollment tokenjust up-mtls # renders config.mtls.toml with the CA, starts under the override fileThe worker enrolls for its certificate rather than mounting one. Confirm the hop actually came up mutually authenticated:
docker compose logs orchestrator | grep -i 'worker registered'docker compose logs worker | grep -i 'certificate'Two mechanics differ from the other profiles and will bite otherwise:
-
mtlsis an override file, not a Composeprofiles:gate.docker-compose.mtls.ymlis what bind-mounts the identity material and redeclares the listeners, so a baredocker compose up -d— which resolves only the base file — starts a daemon that readsconfig.mtls.toml, finds no CA, and exits.just updetects the selector and adds the override for you. -
Tear it down with the base file alone. The override declares
APOMESH_DEV_ENROLL_TOKENwith:?, so an unsourced shell fails loudly instead of silently blanking the token — but that guard fires ondownas well asup:Terminal window docker compose -f docker-compose.yml down
Leave the profile with just demtls, which drops the selector and returns to the plaintext dev hop. The worker’s enrolled identity persists on its volume, so re-entering needs no fresh token; docker compose down -v is what discards it.
Wire an LLM provider
Section titled “Wire an LLM provider”Without a credential the orchestrator falls back to a fake provider and every session fails at the first planner dispatch. Put a real credential in ~/.apomesh/env — Anthropic via a Claude Max OAuth token is the simplest path:
echo 'CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat...' >> ~/.apomesh/envjust upAn Anthropic Console static key works too (ANTHROPIC_API_KEY), as do Gemini and the embedding-provider keys for semantic memory. Confirm the orchestrator picked it up:
just logs orchestrator | grep "LLM provider"# "LLM provider configured from environment" → real provider active# "no LLM provider configured" → still falling back to fakeThe full env-var surface — provider selection, per-class model pins, embedding keys, and the durable providers.yaml pre-config — is documented in deploy/local/README.md.
Lifecycle
Section titled “Lifecycle”just up # bootstrap config, then start the stack (detached)just logs orchestrator worker # tail one or more servicesjust down # stop; keeps volumes (Postgres data, Redis snapshot)just nuke # stop and WIPE volumesThe raw equivalents run docker compose from deploy/local/:
cd deploy/localdocker compose up -ddocker compose logs -f orchestratordocker compose down # keep volumesdocker compose down -v # wipe volumesRebuild after a code change
Section titled “Rebuild after a code change”A source edit reaches a container only through an image rebuild — bringing the stack up does not rebuild a stale image. just up builds any missing image on first run, but never re-builds one that already exists.
After changing orchestrator or worker code, rebuild first, then recreate:
just build-images # rebuild both images from infra/Dockerfile.*just up # start against the fresh imagesjust restart chains this for you — it stops the stack, rebuilds both images, and starts fresh (preserving volumes). The raw image builds run from the workspace root:
docker build -t apomesh-orchestrator:dev -f infra/Dockerfile.orchestrator .docker build -t apomesh-worker:dev -f infra/Dockerfile.worker .Drive a session
Section titled “Drive a session”With the stack healthy, drive the shipped sample task — it fixes a deliberately-planted bug in deploy/local/sample-project/ and verifies the fix with pytest:
just dispatch-sampledispatch-sample runs the Python SDK against the orchestrator at 127.0.0.1:50051 and tails the session’s events to your terminal. To exercise the substrate over REST + SSE instead — no SDK, no console — run a Deep Research goal through the smoke driver:
just smoke "What is today's top news story?"Next steps
Section titled “Next steps”- The operator console — drive and observe sessions from the desktop app.
- Budgets & cost — cap a session’s spend and read the rollup.
- Monitoring — forward the event bus to Prometheus and Grafana.
- Sessions, events & durability — the event-sourced model underneath the stack.