Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

crab-shell-proxy

This page describes the orchestrator as a component: what it owns, what it deliberately does not, and how its code is arranged. Read it before you open the repository for the first time.

What it is

crab-shell-proxy is a small Go service that sits between the Mycelium gateway and the agent containers. It reads which agent a request is for and which member made it, makes sure that member’s own container is running, and relays the conversation to it. The repository is crab/crab-shell-proxy, its Go module is github.com/LepistaBioinformatics/crab-shell-proxy, and the compose service has the same name as the directory.

It is the component that holds the Docker socket, and it runs as root. The Dockerfile’s runtime stage says why in as many words: it has to reach the socket (root:docker, mode 0660), read root-owned template files, and write per-user data directories. Everything else in the stack is arranged so that it does not need those privileges, because this one component already has them. The README’s security note puts it plainly: the proxy is the trusted control plane, and the agents it spawns are the non-root, sandboxed part.

A harness is the program inside an agent container that actually talks to the model and runs tools. The proxy orchestrates harnesses; it is not one. See Harnesses.

What it is responsible for

Resolving identity into a container. The gateway verifies the caller’s token and injects a profile header. The proxy takes the agent from the injected service name and the member from the profile’s accId, and ensures there is one container and one directory for the resulting (tenant, subscription, agent, user) tuple. The account id is used rather than the email because an email is mutable; the email is kept only as a human-readable marker in .crab-owner.json.

Lifecycle. An agent is declared either scale-to-zero, where the container cold-starts on the member’s first request and is stopped after an idle window, or continuous, where it is never stopped automatically. Both are configured per agent in config.yaml.

Choosing the harness. Each agent declares which runtime answers for it:

agents:
  alpha:
    serviceName: "alpha"
    harness: "ganglion"
    template: "alpha"
    mode: "scale-to-zero"
    idleTimeout: 30s

internal/pico runs a turn against picoclaw over its WebSocket protocol; internal/ganglion runs one against the ganglion over HTTP with SSE. Which of the two is used is decided by that one key. An agent that declares no harness: key gets the ganglion: the config loader in internal/config/config.go fills an empty harness: in with DefaultHarness before it validates anything, and requireHarnessFeature in internal/httpapi/harness_gate.go reads the same constant. Declare the key explicitly in every agent you write anyway — a ganglion agent with no image is disabled rather than started, so inheriting the default on an unprepared host takes that agent out of service.

Telling the truth about what a harness cannot do. harness_gate.go keeps a table of features that are not universal, and a feature the agent’s harness cannot serve answers 501 naming the harness rather than quietly succeeding. The file records the incident that produced the rule: a harness once accepted project creation it did not implement, so a project could be created, stored and listed while changing nothing about the agent that answered.

Everything done to a container. Volume provisioning and ownership, secrets materialization, the model registry, the memory-graph MCP server, scheduled tasks and the admin API all live here. The harness specification states this as a permanent boundary: a harness inside a container has no business starting, stopping or provisioning anything, including itself.

The HTTP surface. The member-facing part is OpenAI-shaped — POST /v1/chat/completions, GET /v1/models, GET /v1/sessions/history — with GET /healthz for liveness and GET /doc/openapi.json for the OpenAPI document embedded in the binary. Administration lives under /v1/admin/..., and GET /v1/instances is a read-only inventory of running instances, which exists so that nothing else in the stack has to ask Docker itself.

What it is not responsible for

It does not authenticate anyone. Identity arrives already verified from the gateway, and the proxy’s job is to trust that header rather than to reproduce the check.

It does not run the agent loop. Deciding which tool to call, when to stop, and what the answer is belongs to the harness.

It does not render anything. The member-facing UI is crab-exoskeleton-webapp, which reaches the proxy through the gateway and never talks to an agent container directly.

It does not collect metrics about the stack. That is harness-sphere, and the division is load-bearing: because the proxy already holds a Docker socket, nothing else in the stack is given one.

How it is built and tested

The build is the test gate. The Dockerfile’s build stage runs go vet ./... and go test ./... before it links the binary, so a failing test means no image is produced and therefore nothing is published. release-image.yml is the only workflow in the repository; it builds and pushes that image on a push to main or a version tag. There is no separate pull-request workflow, which means the checks a contributor runs locally are the same ones that gate the image:

go vet ./... && go test ./...

A second suite talks to a real Docker daemon and is kept behind a build tag, so it does not run in the command above and does not run in the image build either:

go test -tags integration ./internal/docker -run TestIntegration -v

config.yaml is baked into the image at /etc/crab-shell-proxy/config.yaml and holds environment-variable names rather than values, so one image stays usable across deployments. See Configuration.

How the code is laid out

cmd/crab-shell-proxy/main.go is the entry point; everything else is under internal/. The packages worth knowing before you start reading:

PackageWhat lives there
configthe agent catalog, defaults and validation, and the path helpers for a member’s directory
httpapievery route, including the harness feature gate
dockerthe hand-written Docker Engine API client and everything done to a container or its volume
picorunning a turn against picoclaw
ganglionrunning a turn against crab-ganglion-harness
registrymodels, their cascade, and who may use which
historyreading transcripts back out of a member’s directory
memgraph, mcpserver, mcptokenthe memory graph and the MCP endpoint agents reach it through
cron, projects, restart, authz, identity, turnscheduled tasks, projects, restart control, authorization, the profile header, and the shared turn types

internal/docker is by a wide margin the largest package, which is a fair signal of where the work is: most of what this service does is careful filesystem and container manipulation on behalf of someone whose request it has already trusted.

Where to go next

Harnesses explains the two runtimes and how one is chosen. Agents, workspaces and projects describes the directory layout this service reads and writes. If you are about to change the code, Working on the stack has the build and test commands for every repository in one place.