Skip to content

feat(zai): a headless GLM worker backend - #5

Draft
yury-procoders wants to merge 25 commits into
procoders:mainfrom
yury-procoders:feat/zai-backend
Draft

feat(zai): a headless GLM worker backend#5
yury-procoders wants to merge 25 commits into
procoders:mainfrom
yury-procoders:feat/zai-backend

Conversation

@yury-procoders

Copy link
Copy Markdown
Contributor

Adds zai, a sixth dispatch backend, so codex, claude and z.ai can run jobs
concurrently on separate quotas. Worker-only and fallback-only in this PR:
never a reviewer, never an arbiter seat.

Mechanism

A Bash-spawned claude -p in its own git worktree, under the process-group
supervisor, with env -i, a scratch HOME/CLAUDE_CONFIG_DIR, and z.ai's
Anthropic-compatible endpoint injected. Enforcement is the existing git-derived
scope gate — the backend adds no new enforcement mechanism.

z.ai ships no headless CLI of its own (ZCode is a desktop app). Claude Code is a
tier-1 officially supported tool for the GLM Coding Plan, so driving the genuine
binary is the compliant path — and the only one available. Codex cannot reach
z.ai at all: it requires the Responses API, which z.ai does not expose. That is
why this is its own backend rather than a codex provider entry.

Verified live, not reasoned

On claude 2.1.207 with a real Coding Plan key. Wire-level facts came from a
local stub HTTP server, so they cost no quota.

  • The redirect works; z.ai validates model names (an invented one returns
    400 [1211]).
  • The agentic loop completes and stays inside write_allowed: six concurrent
    jobs, all touching only the allowed file, with README and test decoys left
    alone.
  • session_id is a real UUID, so the codex worker's validator applies unchanged.
  • Six concurrent jobs finished in 13s with zero 429s.

Two findings worth a reviewer's attention

--tools and --allowedTools are different things and both are required.
--allowedTools decides which tools run unprompted; --tools decides which
exist. An earlier draft pinned only the former and would have shipped a worker
with Bash it meant to withhold and no Write at all. Grep and Glob do not
exist as tools in this CLI version.

--bare cannot be used. In bare mode the built-in set is exactly
Bash, Edit, Read and Write cannot be restored, so a bare worker cannot create
a file. HOME/CLAUDE_CONFIG_DIR redirection buys the same isolation while
keeping Write — 4 tools, ~3.3k tokens per request, and a tools/system
prefix that is byte-identical across worktrees, so the cache survives parallel
workers.

Fixes beyond the backend

  • A z.ai quota wall no longer halts a run. FALLBACK had no zai key, and a
    missing key reads as None, which the policy turns into halt. (devin and
    opencode are still absent from that table — pre-existing, untouched here.)
  • The classifier needed an explicit zai branch. Its final else is
    _CODEX_RULES, so an unrecognised GLM error came back as auth, advising the
    operator to run codex login.
  • --output-format json is a JSON document, not JSONL. Line-wise parsing in
    the usage extractor yielded nothing the moment it was pretty-printed.

CI

Two gates that did not exist are now enforced: shellcheck over scripts/*.sh
(it covered only hooks/*.sh) and a step that runs scripts/test-*.sh (nothing
ran them). Turning both on found four issues, all in the new files.

scripts/test-zai-wire-smoke.sh runs the real binary against a local stub server
and asserts the tool array that actually reaches the wire — the check that would
have caught the draft defect above, and that no argv assertion can.

Trust posture, stated plainly

No kernel write-confinement. The worktree plus git diff detects an in-worktree
scope leak but cannot prevent an out-of-worktree side effect — the same tier as
antigravity and cursor. Prefer codex for untrusted work. Since --bare's
"OAuth and keychain are never read" guarantee is not available here, the worker
asserts the response came from a GLM model and fails the job otherwise.

The spec, three pre-flight audits and the implementation plan are included under
docs/superpowers/.

Adds `zai` as a sixth dispatch backend so codex, claude and z.ai can run
concurrently on separate quotas. v1 role is worker + rate-limit fallback;
never a reviewer, never an arbiter seat.

Mechanism: Bash-spawned `claude -p --bare` in a git worktree, under the
process-group supervisor, with the dispatcher's provider credentials
scrubbed via `env -i` and z.ai's Anthropic-compatible endpoint injected.
Enforcement stays the caller's git-derived scope gate.

Grounded by live probes (claude 2.1.207, codex-cli 0.144.4, real Coding
Plan key): the redirect works, z.ai validates model names, the agentic
tool loop completes, `session_id` is a real UUID, and codex cannot reach
z.ai at all (it requires the Responses API, which z.ai does not expose).
The first draft's `--allowedTools` list did not do what it claimed. Measured
against the real binary: `--tools` decides which tools exist, `--allowedTools`
decides which run unprompted. With only the latter the worker had `Bash` (which
the draft said was withheld) and no `Write` at all — and under `--bare` `Write`
cannot be restored, so a bare worker cannot create a file.

Replaces `--bare` with HOME/CLAUDE_CONFIG_DIR redirection to a scratch dir plus
`--tools` + `--allowedTools` + `--exclude-dynamic-system-prompt-sections`. That
is the only measured configuration that both gives the worker `Write` and keeps
the cacheable prefix byte-identical across worktrees: 4 tools, ~3.3k tokens per
request, 2.30 credits per job.

Also corrected from the audits: real z.ai 429 codes replace "shapes unobserved";
an explicit classifier branch is mandatory or GLM errors silently get OpenAI's
needle set; `failure-policy.py` needs a FALLBACK entry or a credit wall halts the
run; `parallel-dispatcher.md`'s adapter table was missing from the file list; two
cited CI gates do not exist and must be added; `light` stays glm-5-turbo on a
head-to-head measurement (16% faster, and glm-4.7 only 7% cheaper, not 32%).

Adds the three pre-flight audits and their knowledge-base files.
light is glm-5-turbo on a head-to-head measurement, not on the multiplier
table: turbo 8.5s/2.56 credits vs glm-4.7 10.1s/2.38 — 16% faster for 7% more,
because glm-4.7 emits ~60% more output and eats its own lower multiplier.

Also locks the property that registering a backend does not widen advisor
eligibility: zai has no consult adapter and must never be selected.
…o codex rules

classify() ends with `else: rules = _CODEX_RULES`, so registering zai without
its own branch would classify an unrecognised GLM error with OpenAI's needles —
verified: 'please run `codex login`' came back as auth. It is now 'other'.

The CLI's --backend choices still omit devin and opencode, which cannot call
this script at all. Pre-existing; deliberately untouched here.
…of halting

A missing FALLBACK key reads as None, which decide() turns into halt — the
first credit wall would have stopped the whole run. Verified before the fix:
action=halt.

devin and opencode are still missing from this table and still halt.
Pre-existing; left untouched here.
… cost

The counts come from z.ai's own response, so zai stays OUT of
UNMEASURED_BACKENDS. total_cost_usd and modelUsage[*].costUSD are computed from
Anthropic's price table for a model that never ran and are never carried —
job_result.usage has no cost field to hold one.
…e assertion

The stub test caught two real defects before either could ship:

  * the worker never entered the worktree. claude has no --cd equivalent
    (contrast codex exec --cd), so it would have edited files in the launcher's
    cwd while the gate diffed an untouched worktree — a job that changed nothing
    where it was supposed to, waved through as clean.

  * a BLOCKED job crashed the worker. The scope gate signals its verdict through
    the exit code (0 clean, 1 blocked, 2 gate fault) and the worker treated any
    non-zero as fatal, so every out-of-scope write died instead of returning a
    blocked job_result.

It also forced a fix in the usage extractor: --output-format json emits one JSON
DOCUMENT, and line-wise parsing silently yields nothing the moment that document
is pretty-printed. It now parses the document first and falls back to JSONL.
The stub test validates argv; the defect that broke the first draft passed every
argv assertion because the argv was exactly as intended. This runs the REAL
claude binary against a local stub HTTP server — no network, no key, no quota —
and asserts the tool array actually sent, the Bearer header, the absence of
x-api-key, that git status is out of the cached system block, and that the tools
and system blocks are byte-identical across two different worktrees (the
property that makes the prefix cacheable across parallel workers).

SKIPs cleanly when claude is not on PATH.
The dispatcher's adapter table is load-bearing — without a row nothing maps
backend: zai to its worker script. devin and opencode are still absent from
that table; pre-existing, not fixed here.
Neither gate existed. shellcheck covered only hooks/*.sh, leaving the worker
scripts — the largest shell surface in the repo — with no static analysis at
all; and no step ran any scripts/test-*.sh, so every bash-level proof was
manual-only.

Turning both on found four issues, all in the new zai files, and none in the
pre-existing scripts. --read-only now does something real (it empties the
allow-file, so a read-only job that writes anything is BLOCKED) instead of being
accepted and forgotten.
The new shellcheck gate over scripts/*.sh failed on the runner: it has 0.9.0,
which reports SC2015 where 0.11.0 (used locally) stays quiet. Two of the three
sites are pre-existing files the gate had never looked at before — which is the
point of adding it.

Semantics are unchanged in all three: each meant 'if NOT (A and B) then C', which
is exactly what SC2015 warns may be misread.
The allow-list used to be written to a predictable path next to the worktree
($WT.art/write_allowed.globs) before the child started and read back after it
exited. The child has Bash + Write and no kernel confinement, so it could
overwrite that file before the gate read it — reproduced end to end: an
out-of-scope write plus an appended `**` to the allow-file came back as a
clean success.

The list is now built as repeated --allow arguments in the parent shell and
passed directly to compound-v-scope-check.py, which already supported --allow
alongside --allow-file. No file on disk sits between the worker and the gate
for zai to tamper with. Every other worker still uses the file-based allow-
list; the pattern is inherited across all six, and fixing it repo-wide is a
separate, larger change.
The token reached `claude` via `env -i ... ANTHROPIC_AUTH_TOKEN=<key> ... claude`
nested inside the timeout supervisor's own `--` command argument. The supervisor
is long-lived (it stays running for the whole job to enforce the wall-clock
timeout) and never execs away, so that whole string — including the plaintext
key — sat in its argv for the entire job: reproduced live, `ps -eo command`
showed the key, readable by sibling workers of other backends in the same run.

Fix: `env -i` now wraps the supervisor invocation instead of `claude` directly,
so the credential lives only in environment inheritance, never as one of a
long-lived process's own arguments. Since a live `ps` snapshot during a running
job no longer shows it anywhere, this closes the leak.

That alone isn't sufficient: this machine's python3 (a macOS Python.framework
build) injects SDKROOT/CPATH/LIBRARY_PATH/__CF_USER_TEXT_ENCODING and more into
its own process at startup regardless of how it's launched, and the supervisor's
default behavior (Popen with no explicit env=) would forward all of that noise
to claude too. Gave compound-v-run-with-timeout.py a purely additive --env-only
NAME[,NAME...] flag: when given, the child's environment is built from scratch
out of exactly those names read from the supervisor's own environment, discarding
everything else. Omitted (every other backend's worker), behavior is unchanged
byte-for-byte. Only the zai worker passes it.
…CLI renders

claude -p renders API errors as `API Error: <status> <message>` — error.code is
never a separate token — so the old needle set (quoted/bracketed codes like
"1310"/[1310]) never matched, and 1305/1308/1310/1311 fell through to `other`,
which retries once then halts the run, defeating this PR's own
FALLBACK["zai"] = "claude" reroute. Verified: replaying the old rule set
against realistic fixtures shaped like the CLI's actual rendering
misclassifies 5 of 8 documented codes.

Needles now match the documented message text instead. Two of the eight
messages share a substring ("Usage limit reached for the past N
{hours,days}. Insufficient balance for extra usage...") spanning a
rate_limited code (1316) and an out_of_credits code (1317) — the old bare
"insufficient balance" needle swallowed both before either reached its own
needle; every needle is now specific enough to avoid that collision. Added
the missing "overloaded" class (1305 had none).

Selftest fixtures rebuilt from the documented message templates
(docs/superpowers/expert/2026-07-31-zai-backend.md) instead of invented text
that happened to dodge the same collisions — that's what let 53 ok / 0 fail
coexist with four wrong classifications before.
Follow-up to 302613f: the doc still said the needle set was "documented
codes"; it's documented message TEXT now, since claude -p never surfaces
error.code as its own token.
A failed job's captured document has well-formed ZERO usage.input_tokens /
usage.output_tokens while modelUsage is {} (reproduced against a real
captured z.ai error response: is_error:true, modelUsage:{},
usage:{input_tokens:0,output_tokens:0}) -- _extract_zai accepted any dict
with two integer fields, so every hard failure recorded measured:true with
zero tokens, indistinguishable from a real free response. This is the exact
gap docs/superpowers/library-audit/_knowledge-base/claude-code-cli-flags.md
warned about and the spec never implemented.

_extract_zai now requires a non-empty modelUsage before trusting usage, and
switches to the same _valid_int() validation the other extractors already
use (rejects bool/negative/non-int) instead of a bare isinstance(int) check.

Caught a stale selftest fixture in the process: the "pretty-printed document
parses" case never carried modelUsage, so the new gate correctly zeroed it
out (input_tokens: None instead of 7) -- not a realistic capture (every real
zai success document has modelUsage), fixed by adding it to the fixture
rather than loosening the gate.
Five .pyc files were committed incidentally by the two CI commits.
__pycache__/ was never in .gitignore and no .pyc was tracked on main. In a
fresh worktree, running any Python selftest (the repo's standard acceptance
step) rewrites the tracked bytecode, and the scope gate unions git diff
--name-only HEAD with git ls-files --others --exclude-standard -- so those
rewrites surface as changed paths outside any job's write_allowed and
spuriously BLOCK the job. Reproduced before this fix; a fresh worktree run
now leaves no bytecode diff.
make_stub() declared a crash mode (echo to stderr, exit 1) that no
run_worker call site ever invoked. All six existing calls used
success/blocked/hang/nonglm, so the worker's entire exit_code != 0 branch
— the classify-failure.py call, failure_class, retry_after_seconds, ERR_TEXT
— had zero test coverage; 31 assertions passed and none touched it.

Added a seventh run_worker crash call asserting status, blocked, exit_code,
and a non-null classified failure_class. Verified the new assertion can
actually fail (planted a wrong expected value, confirmed FAIL, reverted)
before trusting the green run.
The worker's comment and adapter-zai.md both claimed HOME/CLAUDE_CONFIG_DIR
redirection kept CLAUDE.md out of the request, matching --bare's isolation.
Measured (marker file on the wire): it only buys the user-level half —
$WT is a checkout of this repo, so the project's own CLAUDE.md and
.claude/settings.json are still live and reach z.ai on every job. The
behaviour is arguably fine; the claim was not. Corrected both the worker
comment and adapter-zai.md (the --bare note plus a new Compliance
disclosure), since this is data egress to a third-party endpoint, not
merely an accuracy nit.
phase-3-parallel-opus-dispatch.md carries the per-backend dispatch
instructions the orchestrator actually follows and mentioned antigravity /
cursor four times each but zai zero, even though the manifest validator
already accepts backend: zai — an operator following this doc had no entry
telling them how to run it. Added the same two-part entry (adapter-launcher
row + tier/effort resolution bullet) every other live backend has.
…env allowlist

- agents/parallel-dispatcher.md: remove stray blank line that split the
  zai row out of the backend table.
- compound-v-classify-failure.py: drop the citation to a review doc that
  was never committed to the repo; keep the substantive claim.
- compound-v-run-zai-worker.sh: _SAFE_ENV_VARS carried LC_ALL/TERM beyond
  the spec's AC10 allowlist (PATH HOME TMPDIR LANG); narrow it back.
- test-zai-worker-stub.sh: EXPECTED_ENV was widened to match the code
  instead of the AC10 contract; narrow it back to match the spec.
…irst

Two real bugs found by an independent Codex spec-compliance review:

- ZAI_BASE_URL read from the dispatcher's own environment before env -i,
  letting an ambient value silently redirect the worker off the pinned
  z.ai endpoint the spec requires. Hardcode it — the wire-smoke test's
  local stub server points `claude` at ANTHROPIC_BASE_URL directly and
  never depended on this variable being overridable.
- The GLM assertion checked only the alphabetically-first modelUsage key
  (jq `keys | .[0]`), so a response mixing a glm-* key with a later-
  sorting non-GLM key would wrongly pass. Check every key.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant