Creating a custom agent
This chapter walks through adding a new agent to a deployment, from the first
directory to the first chat. It is written as one worked example: an agent
called scribe, running the ganglion harness. Read
the admin guide first if you have not met tenants,
subscriptions and scopes yet.
What an agent is
An agent is a named personality that members chat with. alpha and beta
are the two that ship. An agent is not a container: every member who talks to
scribe gets their own container, their own workspace and their own history,
all cloned from the same starting point. See
agents and workspaces for that isolation model.
Three things have to exist before a member can reach a new agent.
- An entry in the proxy’s agent catalog, which names the agent, the runtime it uses, its lifecycle mode and its default model.
- A template directory on disk, which supplies the agent’s identity files.
- A route in the gateway, because Mycelium is the front door and it will not forward a request for a service it has never heard of.
The rest of this chapter is those three, in that order, plus the environment variables that tie them together.
Step 1: the template directory
The proxy resolves an agent’s template at <data-root>/templates/<template>/
(TemplatesDir in crab/crab-shell-proxy/internal/config/config.go). On the
host, <data-root> is CRAB_HOST_DATA_ROOT; inside the proxy container the
same tree is mounted at CRAB_CONTAINER_DATA_ROOT, which defaults to /data.
For a ganglion agent the part of the template that matters is workspace/,
because that directory is the bottom layer of the persona cascade:
data/templates/scribe/
└── workspace/
├── AGENT.md what the agent does and how it behaves
├── SOUL.md its voice
├── HEARTBEAT.md its recurring task list
└── USER.md what the agent starts out knowing about the member
Those four names are the complete set. PersonaFiles in
crab/crab-shell-proxy/internal/docker/persona.go lists exactly
AGENT.md, SOUL.md, HEARTBEAT.md and USER.md, and the first three are
delivered as read-only bind mounts while USER.md is seeded once and then left
alone — the agent writes to it as it learns about the member.
A ganglion agent has no
config.jsonand no.security.ymlin its template. Those are picoclaw’s files.ganglion_config.gostates it plainly: the harness’s configuration is not seeded fromtemplates/<agent>/config.jsonand never was. The proxy renders a configuration file for each workspace instead, and credentials arrive as environment variables. Do not copy a stock agent’sconfig.jsoninto a ganglion template; nothing will read it.
The template’s workspace/skills/ and workspace/memory/ are likewise
picoclaw-only. seedWorkspace in
crab/crab-shell-proxy/internal/docker/provision.go copies the
config.WorkspaceSeed allowlist — USER.md, memory/ and skills/ — and it
is on the picoclaw creation path; createGanglion never calls it. To give a
ganglion agent skills, publish them as shared skills
from the admin area.
A template: value is still required for every agent, whatever the harness:
validate in config.go refuses an agent that declares none. For a ganglion
agent it points at the directory holding those identity files.
Step 2: the catalog entry
The catalog is crab/crab-shell-proxy/config.yaml. The Dockerfile copies it to
/etc/crab-shell-proxy/config.yaml and sets CRAB_CONFIG to that path, so the
committed file is baked into the proxy image. A deployment that mounts its own
file over that path, or points CRAB_CONFIG elsewhere, can edit the catalog
without rebuilding.
Add the agent under agents::
agents:
scribe:
harness: "ganglion"
serviceName: "scribe"
token: { env: "MYC_PICOCLAW_SCRIBE_TOKEN" }
template: "scribe"
mode: "scale-to-zero"
idleTimeout: 30s
model:
provider: "deepseek"
name: "deepseek-chat"
apiKeyEnv: "SCRIBE_API_KEY"
serviceName must match the value Mycelium injects as
x-mycelium-service-name, which is the gateway’s service key from step 3.
token is the bearer the gateway presents; the proxy rejects any request whose
Authorization does not match it.
Declare harness: "ganglion" explicitly. The key is optional and
DefaultHarness is the ganglion today (config.go), so omitting it would work
— but the same file argues against relying on that: every agent in this
repository’s own config.yaml spells its harness out, because a config upgrade
should not change an agent’s runtime by omission. Picoclaw is still fully
served, and harness: "picoclaw" is still the right value for an agent that
needs it; see harnesses for the difference.
mode decides the container lifecycle. scale-to-zero stops the container
after idleTimeout of no activity; continuous keeps it running.
idleTimeout must be greater than zero when the mode is scale-to-zero, and
validate refuses the agent otherwise. A ganglion agent is a good candidate for
scale-to-zero: it writes every turn to the transcript before the model is
called and rebuilds a missing context window from that transcript, so a stopped
container loses nothing.
The model: block is the agent’s floor — the model a workspace runs on when the
model inventory resolves nothing for it. You may
also list alternatives under models:, which becomes the selectable allowlist;
SelectableModels in config.go returns the default followed by that list,
deduplicated by provider and name.
Step 3: the image, and what happens without it
A ganglion agent needs CRAB_GANGLION_IMAGE. It has no default, on purpose:
the comment on GanglionImage in config.go records that a moving tag once
left a deployment running a three-week-old binary for weeks, because the harness
image is not a compose service and a redeploy never pulls it. Set it to a digest
or a per-commit tag.
When it is unset the agent does not take the proxy down. ganglionUnprovisioned
removes the agent from the catalog at load, records it in DisabledAgents with
the name of the missing setting, and the agent’s routes then answer 404. The
same happens when the agent’s apiKeyEnv resolves to an empty value, or when
its token environment variable is unset. Read the proxy’s boot log if a new
agent seems not to exist: the reason is there, and it names the variable.
docker-compose.yamldefaultsCRAB_GANGLION_IMAGEtozombie-crab/crab-ganglion:dev, which is a locally built tag.docker-compose.prod.yamlsets no ganglion image at all and no workflow in.github/workflows/publishes one, so a production deployment has to build and push the harness image itself before a ganglion agent can start.
Step 4: the gateway route
Mycelium routes by the first path segment, and that segment is the literal
service key. Copy the [[alpha]] block in
deploy/standalone/config.standalone.toml — the service block, its
[[alpha.secret]] and every [[alpha.path]] — and rename it to scribe.
Callers then reach the agent at /scribe/.... Keep host,
healthCheckPath, the full path set and the protectedByRoles groups as they
are; only the name changes.
Do it in every mode you deploy, because they are separate files:
deploy/standalone/config.standalone.toml and deploy/prod/config.base.toml.
The protectedByRoles entries also declare the guest role. A guest role’s
name is the agent key — the gateway declares
protectedByRoles = [{ name = "alpha", permission = "write" }] and Mycelium
creates those roles at boot, which is the fact
crab/crab-exoskeleton-webapp/lib/invitations.ts is built on. So a new agent
brings its own role into existence, and inviting someone to it is then an
ordinary invitation from the Members section of the admin area.
Step 5: the environment
Two variables, in .env and never in a config file:
MYC_PICOCLAW_SCRIBE_TOKEN=<the same shared secret the gateway route uses>
SCRIBE_API_KEY=<the provider API key>
The token authenticates the gateway to the proxy. The API key never reaches a
file the agent can read: for a ganglion agent the proxy passes it as
GANGLION_API_KEY, or as one GANGLION_MODEL_KEY_<NAME> variable per model
when the inventory governs the workspace. Models and
providers covers that naming.
Step 6: restart, and the first chat
The gateway configuration is mounted from deploy/<mode>/, so a route change
needs a restart. The agent catalog is copied into the proxy image, so a catalog
change needs a rebuild — unless your deployment mounts its own catalog, in which
case a restart is enough. Locally, the safe catch-all is to rebuild both:
docker compose up -d --build crab-shell-proxy mycelium-gateway
The first time a member with the scribe role sends a message, the proxy
creates their workspace: it makes workspace/ with its memory/, public/,
sessions/ and windows/ subdirectories, mints a per-user bearer token for the
container, seeds USER.md from the persona cascade if anything provides one,
renders the harness’s configuration file and starts the container.
Files the agent delivers back to the member go in public/attachments/. See
files and delivery.
Checklist
-
data/templates/scribe/workspace/{AGENT.md,SOUL.md,HEARTBEAT.md,USER.md} - Catalog entry in
crab/crab-shell-proxy/config.yamlwithharness: "ganglion",serviceName,token,template,modeandmodel -
CRAB_GANGLION_IMAGEset to an immutable reference -
[[scribe]]service block in the gateway config of every mode you deploy -
MYC_PICOCLAW_SCRIBE_TOKENand the model’sapiKeyEnvin.env - Proxy rebuilt or its catalog remounted, gateway restarted
- A member invited to the
scriberole from the Members section
When it does not work
The agent’s routes answer 404. Either the gateway has no scribe service —
check that you edited the file the mode you are running actually mounts — or the
proxy disabled the agent at load. The boot log names which.
Requests are rejected as unauthorized. MYC_PICOCLAW_SCRIBE_TOKEN in the
proxy’s environment and the token = { env = ... } in the gateway’s secret
block must resolve to the same value.
The member chats but the agent has no personality. GANGLION_SYSTEM_FILE
points at workspace/AGENT.md inside the container, and that file arrives as a
read-only bind from the persona cascade. If no layer of the cascade provides
AGENT.md — not the subscription, not the tenant, not the template — no bind is
emitted and the agent runs with no identity. Check that your template’s
workspace/AGENT.md exists.
A template edit did not reach an existing member. AGENT.md, SOUL.md and
HEARTBEAT.md are re-resolved on every ensure, so an edit does reach them. But
USER.md is seeded only when the workspace has none, deliberately: it is the
file the agent writes back, and overwriting it would erase what the agent has
learned. A new value for it reaches new workspaces only.
Where to go next
Models and providers explains how the model in your catalog entry relates to the inventory an administrator manages, and which tools an agent gets. The admin guide covers inviting members and giving the new agent shared skills and shared files.