Architecture
blueclaw is split into three parts that compile against each other.
blueclaw is split into three parts that compile against each other.
| Part | Owns | Where |
|---|---|---|
| Host (blueclaw) | connectors, policy, identity, POSIX isolation, task store, approvals, tool catalog, capabilities, memory, delivery | this repository |
| Harness | the agent loop: run a turn and report what happened | bluecollar at .dependency/bluecollar, or an external agent through internal/acpharness and internal/cliharness |
| Contract | the types both sides compile against, and the harness port | agentcontract and toolcontract in the bluecollar module |
bluecollar is a separate repository pinned as a submodule and pulled in through a replace directive in go.mod. agentcontract travels with it because both sides need identical types. Building with -tags nobundledharness leaves the loop out of the binary; agent.harness.name must then name an external harness, and a test in cmd/blueclaw fails if the loop creeps back in.
chat platform (via chatd) / HTTP / ACP client
|
v
blueclaw daemon
connectors · intake · policy · task store · approvals · tool catalog · POSIX projection
|
+-- agentcontract.Harness --+-- bluecollar (Go, in process)
| +-- ACP or CLI agent, started as the requester
|
+-- tool execution --> blueclaw-posix-helper --> the requester's UID, GID and groupsThe harness port
type Harness interface {
RunTurn(context.Context, AgentTurnRequest) (AgentTurnResult, error)
}The host opens the task run and hands the harness an ExistingTaskRunID to settle, so a first turn is recorded the same way whichever loop ran it. Everything else the host needs (events, cancellation, run lookup) it takes from the task store.
Deciding whether an inbound message becomes a task at all is host policy. One call to the decision model answers every closed question about a message: who it is addressed to, whether it follows on from a running task, and how the turn should be routed. The answer is memoized on the inbound event (internal/connectors/intake_decision.go, implemented by bluecollar's intake package).
The path of one message
- Ingress. A connector persists the raw event, resolves the sender to a person in the policy, and refuses accounts the policy does not know.
- Decision. Addressing is one of four outcomes: ignore, react only, reply, or react and reply. A message that is only an image is described by a vision model first.
- Busy routing. If the person already has an active task, the decision's
busyRouteis one ofstatus,steer,replace,cancel,new_taskorunrelated. - Launch.
TaskLauncher.Launch(internal/agentruntime/task_launcher.go) runs nine steps, each recorded as an event so a failure names its step: resolve the requester's email, resolve the active circle, build the conversation's artifact manifest, provision the requester's workspace, build the tool set, audit the tool registry, load memory, carry out an approved call, run the turn. - Delivery. The result is enqueued in the connector outbox and delivered.
Choosing a harness
agent.harness.name selects the loop, and internal/harnessselection is the only place a harness is named.
| Name | What runs |
|---|---|
bluecollar (default) | the bundled in-process loop |
acp | any Agent Client Protocol agent at agentCommandPath |
claude-code | the claude CLI in headless mode |
codex | the codex CLI |
antigravity | the agy CLI |
An external harness changes where the agent runs, how it reaches tools, and how the outcome is known. It starts inside the requester's POSIX identity. It reaches the catalog over MCP at /harness/tool-catalog with a session token revoked when the turn ends; agents that advertise MCP over HTTP get the endpoint directly, and the rest get a stdio server (the daemon binary in mcp-tool-catalog mode) that proxies to it. Because an ACP turn ending or a CLI exiting says nothing about whether the work was done, internal/turnoutcome decides the status from the agent's final message and the catalog tools that actually succeeded.
internal/acpharness also refuses ACP's own filesystem and terminal methods, which pushes file and shell work onto the catalog. Tools an agent runs inside its own process are answered yes and recorded as harness.tool_permitted or harness.tool_refused; the POSIX identity is the limit on those.
Serving ACP
blueclaw can also be the agent. Started with --acp-socket <path>, it serves ACP on that unix socket (recreated at start, mode 0600). --inbound acp makes ACP the only path that admits a message, and POST /connectors/<platform>/events then answers 409. See ACP sessions.