Files
clawmates/images/agent-glm/Dockerfile
T
Omar SobhandClaude Opus 5 f7f3dfe495 feat(fleet): GLM as a real microVM backend, and per-role models for claws
Three threads, all of which end at the same place: a mission whose verifier does
not share a model with the coder it reviews.

**GLM has a credential contract now.** `microvm_credential_for` returned one env
var name, which quietly assumed every provider reads its secret from the same
place Anthropic does. It returns a `Credential { source, target }` instead —
z.ai's key lives in the server's `ZAI_API_KEY` and Claude Code reads it as
`ANTHROPIC_AUTH_TOKEN`, and collapsing those two names is what forces a guess at
the other end. A wrong guess here sends one provider's credential to another
provider's endpoint.

`images/agent-glm` is the same CLI at the same pinned version as `agent-claude`
with `ANTHROPIC_BASE_URL` baked in. The split is deliberate: the ENDPOINT is a
property of the image, the CREDENTIAL is a property of the turn. That makes the
dangerous mix-up unrepresentable — a GLM VM cannot be handed an Anthropic
subscription token, and a claude VM cannot be pointed at z.ai. Asserted both
ways, because "the GLM VM must not carry CLAUDE_CODE_OAUTH_TOKEN" is the
property that costs a credential if it ever stops holding.

Kimi stays refused. `KIMI_API_KEY` is set and Moonshot serves an
Anthropic-compatible API, but I have not verified its base URL against the
running service, and this function is precisely where guessing a URL is
expensive. It becomes an arm the day someone measures it.

`api.z.ai` joins the node's default egress allow-list. A default that cannot
run the images we ship is a trap rather than a policy — the alternative is an
operator discovering it as a hung agent with no model access.

**Per-role models for claws** (migration 0071). `template_roles` had no model
column, so `mint_team_from_template` bound every role of every mission team to
one literal — a template whose whole point is an independent reviewer minted a
reviewer sharing a model with the coder. A role may now name its own; roles that
say nothing still take the mint's default, so every template written before this
behaves exactly as it did. The literal is now that default rather than a
hardcode.

**A harness scenario for the roster flow.** `verify-mission-delivery.sh roster`
runs the whole Slice 5 loop — planner proposes, human approves, mission runs —
and asserts the roster LANDED on the mission row rather than trusting the API's
answer. That distinction is not theoretical: the first live approval returned an
error while leaving the proposal marked approved.

Built and proven on tank ahead of the deploy: `clawmates/agent-glm:dev` reports
`2.1.223` and `BASE=https://api.z.ai/api/anthropic`, and
`fc-build-rootfs.sh … glm 8G` boots a VM from it that has git, can write
/mission, and answers `claude --version`.

533 tests pass, clippy clean. Migration 0071.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 17:14:53 -07:00

46 lines
2.2 KiB
Docker

# Plan A6, second of three: Claude Code pointed at GLM.
#
# The same CLI as `agent-claude`, the same toolchain underneath, and a different
# endpoint. That is the whole difference, and it is deliberate: z.ai serves an
# Anthropic-compatible API, so a second provider costs an env contract rather
# than a second agent harness with its own failure modes.
#
# Why this image exists at all: a composed mission's `verifier` node reviewing
# work its own model wrote is a correlated failure — the same one the
# cross-provider judge exists to break, one layer down. A roster can only put a
# node on another provider if another provider's rootfs is on the fleet.
#
# Build (on the node that will run it):
#
# ssh osobh@tank "cd ~/clawmates && \
# docker build -f images/agent-glm/Dockerfile -t clawmates/agent-glm:dev images/agent-glm/"
# scripts/fc-build-rootfs.sh osobh@tank clawmates/agent-glm:dev glm 8G
FROM clawmates/agent-toolchain:dev
# Pinned to the SAME version as agent-claude on purpose. A solo run and a
# composed run's verifier node should differ by provider and by nothing else; two
# CLI versions in one graph would make "the verifier disagreed" ambiguous between
# the model and the harness.
ARG CLAUDE_CODE_VERSION=2.1.223
RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
&& npm cache clean --force \
&& rm -rf /root/.npm \
&& claude --version
# The endpoint is baked in; the CREDENTIAL never is. `mission_runtime::
# microvm_provider_env` injects `ANTHROPIC_AUTH_TOKEN` from the server's
# `ZAI_API_KEY` at turn time, so the key lives and dies with the VM.
#
# Baking the base URL rather than injecting it is the safer half of the split: an
# image whose URL is fixed cannot be handed a token for one provider and an
# endpoint for another. That mix-up — an Anthropic subscription token sent to
# z.ai — is precisely what `microvm_credential_for` refuses to allow.
ENV HOME=/root \
CLAWMATES_AGENT_CLI=claude \
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
RUN mkdir -p /root/.claude
# No CLAUDE_CODE_OAUTH_TOKEN and no ANTHROPIC_API_KEY: this backend authenticates
# with a z.ai key alone. The subscription token must never reach this image —
# it would be sent, verbatim, to another company's endpoint.