feat(zai): a headless GLM worker backend - #5
Draft
yury-procoders wants to merge 25 commits into
Draft
Conversation
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.
yury-procoders
marked this pull request as draft
August 4, 2026 15:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
zai, a sixth dispatch backend, so codex, claude and z.ai can run jobsconcurrently on separate quotas. Worker-only and fallback-only in this PR:
never a reviewer, never an arbiter seat.
Mechanism
A Bash-spawned
claude -pin its own git worktree, under the process-groupsupervisor, with
env -i, a scratchHOME/CLAUDE_CONFIG_DIR, and z.ai'sAnthropic-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.207with a real Coding Plan key. Wire-level facts came from alocal stub HTTP server, so they cost no quota.
400 [1211]).write_allowed: six concurrentjobs, all touching only the allowed file, with README and test decoys left
alone.
session_idis a real UUID, so the codex worker's validator applies unchanged.Two findings worth a reviewer's attention
--toolsand--allowedToolsare different things and both are required.--allowedToolsdecides which tools run unprompted;--toolsdecides whichexist. An earlier draft pinned only the former and would have shipped a worker
with
Bashit meant to withhold and noWriteat all.GrepandGlobdo notexist as tools in this CLI version.
--barecannot be used. In bare mode the built-in set is exactlyBash, Edit, ReadandWritecannot be restored, so a bare worker cannot createa file.
HOME/CLAUDE_CONFIG_DIRredirection buys the same isolation whilekeeping
Write— 4 tools, ~3.3k tokens per request, and atools/systemprefix that is byte-identical across worktrees, so the cache survives parallel
workers.
Fixes beyond the backend
FALLBACKhad nozaikey, and amissing key reads as
None, which the policy turns intohalt. (devinandopencodeare still absent from that table — pre-existing, untouched here.)zaibranch. Its finalelseis_CODEX_RULES, so an unrecognised GLM error came back asauth, advising theoperator to run
codex login.--output-format jsonis a JSON document, not JSONL. Line-wise parsing inthe usage extractor yielded nothing the moment it was pretty-printed.
CI
Two gates that did not exist are now enforced:
shellcheckoverscripts/*.sh(it covered only
hooks/*.sh) and a step that runsscripts/test-*.sh(nothingran them). Turning both on found four issues, all in the new files.
scripts/test-zai-wire-smoke.shruns the real binary against a local stub serverand 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 diffdetects an in-worktreescope 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/.