Skip to content

feat(providers): add zcode as a Strict harness backend - #238

Merged
tzhouam merged 2 commits into
mainfrom
feat/zcode-provider
Sep 30, 2026
Merged

tzhouam merged 2 commits into
mainfrom
feat/zcode-provider

Conversation

@tzhouam

@tzhouam tzhouam commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds zcode (ZCode CLI, Z.AI / GLM subscription) as a Strict harness backend: STRICT_BACKEND=zcode. This is PR 1 of the reviewbot routing-arms plan: the later reviewbot arm table will be able to route Strict reviews to zcode / glm-5.3-flash.

How it runs: headless zcode --prompt=... --output-format stream-json --mode plan --cwd <scratch> --disallowed-tools=<every non-read tool>. The transport parses result.response, result.usage, session.updated.modelId and tool.updated (kind: scheduled).

Governance (measured on zcode 0.16.9, 2026-09-30)

  • Tool containment. --mode plan alone still exposes a node REPL MCP, subagents (Agent, which can reach a shell), workflows, and web fetch/search.
    • --disallowed-tools strips everything except reads: 28 tools → 11, all read-only.
    • Every session is post-audited against the read-only allowlist.
    • Native Read/Grep/Glob calls are containment-audited against checkout + run dir + session dir. This reuses audit.contained_in, and relative glob traversal (../../etc/*) is flagged.
    • complete() removes the native reads entirely. It keeps only Read when an oversized prompt rides an attachment, and fails the call on any read outside its scratch dir.
  • The session cwd is a scratch dir we own, never the PR worktree.
    • zcode loads .env from its cwd.
    • It discovers zcode.json / .zcode/config.json from the cwd up to the git root, and connects the stdio MCP servers declared there without a trust prompt.
    • So the PR under review must not be the cwd. The worktree is read by absolute path, and the prompt names it.
  • MCP tool bridge works. It is wired via the scratch dir's .zcode/config.json. In a live run mcp__infermatrix-tools__read_file was called, and bridge_trace.jsonl recorded it in scope.
  • Failed runs raise. A nonzero exit, or a stream with no result event, raises RuntimeError with zcode's stderr instead of reading as an empty review.

Model selection: an assertion, not a selector

zcode has no per-run model flag: there is no --model, and setting ZCODE_PERSONAL_PROVIDER_CONFIG_FILE was measured not to change the served model. It serves the host's configured default, which the host sets with /model in the zcode TUI.

STRICT_BACKEND_MODEL is therefore checked case-insensitively against the served modelId under MODEL_MISMATCH_POLICY. The default policy is fail, which raises ModelMismatchError.

Operational consequence for the planned 50/50 arm: each reviewbot host must set zcode's default model to GLM-5.3-Flash. The arm's model: GLM-5.3-Flash then acts as a guard that fails loudly if a host drifts.

Registry spec: capabilities = {mcp_tools, builtin_tools_off, usage_reporting}. No default_model is set, because zcode picks its own.

Test plan

  • test/test_provider_zcode.py, 16 tests, offline fake CLI; event shapes were recorded from a live 0.16.9 run. Covers:
    • argv / plan mode / denylist
    • scratch cwd and bridge config
    • sanitized env and ZCODE_DATA_BASE_DIR passthrough
    • allowlist audit and out-of-root reads, including relative glob traversal
    • oversized prompt → --attach
    • model assertion ok / mismatch-fail / mismatch-warn
    • nonzero exit and missing result
    • timeout salvage
    • complete() containment
  • Full suite: PYTHONPATH=$PWD/src pytest → 1926 passed, 17 skipped.
  • Live smoke through transport_for(...).run_session(...) with the real zcode CLI:
    • Short prompt: the bridge read_file returned the probe marker; audit clean; served GLM-5.3; model check ok.
    • Oversized (~120 KB) prompt: the attachment was read via native Read inside the session dir, the bridge read the probe, and the audit was clean.

🤖 Generated with Claude Code

STRICT_BACKEND=zcode drives the ZCode CLI (Z.AI / GLM subscription)
headless: --prompt= --output-format stream-json --mode plan, with
--disallowed-tools stripping every non-read tool (measured 28 -> 11 on
zcode 0.16.9) and a post-session audit against the read-only allowlist.

The session cwd is a scratch dir we own, never the PR worktree: zcode
loads .env from its cwd and connects stdio MCP servers declared in a
project .zcode/config.json without a trust prompt, so a PR checkout as
cwd would let the PR configure its reviewer. The MCP tool bridge rides
that scratch dir's config (verified live: bridge read_file called and
traced).

zcode has no per-run model flag, so STRICT_BACKEND_MODEL is asserted
against the served modelId under MODEL_MISMATCH_POLICY instead of
selected; the host picks the model with /model in zcode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T09:01:38.154167Z 25433ec PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Both pages already describe the zcode entry; bump verified-against so
check_spec_freshness --strict passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25433ec7f1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# -- transport contract --------------------------------------------------
def run_session(self, req: AgentSessionRequest) -> AgentOutcome:
session = Path(tempfile.mkdtemp(prefix="imc-zcode-session-"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep scratch directories outside the reviewed checkout

tempfile.mkdtemp() honors the process's TMPDIR, so when a runner or developer points TMPDIR inside the checkout, this session is created beneath the untrusted repository. ZCode then walks upward and discovers the repository's .env and zcode.json/.zcode/config.json, allowing a reviewed PR to launch its configured MCP commands—the exact attack the scratch directory is intended to prevent. The one-shot path has the same issue; create these directories under a trusted fixed root or reject locations within the checkout.

Useful? React with 👍 / 👎.

Comment on lines +82 to +86
_READ_TOOLS = frozenset({
"Read", "Grep", "Glob", "TodoRead", "TodoWrite", "TaskOutput",
"ReadSessionContext", "ListWorkflowRuns", "GetWorkflowRun",
"ListSavedWorkflows", "ListModels",
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deny host-state readers in untrusted sessions

When review text or another untrusted prompt induces a native call, this allowlist accepts ReadSessionContext, TaskOutput, and the saved/workflow-run tools in both run_session() and complete() without any root-containment check. These tools can inspect ZCode state outside the checkout, and tool_less=True only additionally disables Read, Grep, and Glob, so the one-shot path advertised as tool-less remains exposed as well. Deny these host-state tools and retain only filesystem reads whose paths can be containment-audited (with Read enabled solely for the oversized attachment case).

Useful? React with 👍 / 👎.

@tzhouam
tzhouam merged commit 1782074 into main Sep 30, 2026
2 checks passed
tzhouam added a commit that referenced this pull request Sep 30, 2026
The zcode provider (#238) landed after the spend-threshold change and
kept the old complete() signature. ModelGateway already refuses a
threshold for transports that cannot stop at one before dispatch, so
nothing broke, but every transport now takes max_budget_usd; zcode
accepts and ignores it (stops_at_spend stays False). A test now checks
every transport class, so the next provider cannot miss it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tzhouam added a commit that referenced this pull request Sep 30, 2026
The zcode provider (#238) landed after the spend-threshold change and
kept the old complete() signature. ModelGateway already refuses a
threshold for transports that cannot stop at one before dispatch, so
nothing broke, but every transport now takes max_budget_usd; zcode
accepts and ignores it (stops_at_spend stays False). A test now checks
every transport class, so the next provider cannot miss it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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