Compare commits

..
Author SHA1 Message Date
Garry TanandClaude Fable 5 48890e35be v0.46.4.0 feat(opencode): full-parity client support — bootstrap, harness, connect, claw-test, real-binary e2e door (#4162)
* docs(mcp): pin observed opencode CLI behavior (OPENCODE-CLI-PIN.md) + registration guide

Phase-0 hermetic observation of opencode-ai@1.18.18 (npm wrapper + platform
payload integrities pinned). Load-bearing observations: keyless anonymous
free tier answers headless runs AND drives MCP tool calls without --auto
(nonce SMOKE proven end-to-end against a real gbrain serve --surface verbs);
mcp list is the honest discriminator (spawns servers, exit 0 regardless —
parse the text); mcp add takes '-- command' (undocumented in --help) but
always writes user-global opencode.jsonc; project-defined local servers
spawn with NO trust gate (drives the user-global bootstrap default); JSONC
parses in .json-named files and both filenames merge; OPENCODE_CONFIG* env
vars observed inert (docs-contradiction, called out); OPENCODE=1 set in bash
children (detectHarness probe); AGENTS.md loads, CLAUDE.md not double-loaded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): opencode-json managed config writer + opencode-2026-08 host spec

- src/core/bootstrap/opencode-json.ts: comment-preserving JSONC writer
  (jsonc-parser surgical edits — opencode's own mcp add preserves comments,
  the writer matches that bar). Ownership is a 4-state structural
  fingerprint (ours-same-source | ours-other-source | foreign | absent)
  keyed on GBRAIN_SOURCE EQUALITY ([FIX7] parity), never a marker key.
  Distinct read-failure classes (ENOENT create / empty-as-{} / unreadable
  refuse); foreign refusal on write AND remove; post-render validation
  (our entry round-trips, every other key survives) keeps the original on
  failure; 0600 + .bak-0600 only for inline-bearer entries; bearer
  recovery helper for harness --status.
- host-specs.ts: TARGETS['opencode-2026-08'] (verified 2026-08-15 against
  a hermetic opencode-ai@1.18.18) + opencodeConfigDir/GlobalConfigPath/
  ProjectConfigPath (XDG-only — OPENCODE_CONFIG* observed INERT in
  1.18.18, honoring them would be a silent no-op install) +
  OPENCODE_HAS_HOOKS=false.
- atomic-write.ts: rule-of-three extraction of the symlink-resolving,
  mode-inheriting atomic writer; codex-toml.ts + hooks.ts ported onto it
  (behavior pinned by their existing suites).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): opencode workspace lane — hooks --harness opencode, scope-aware direct-writer registration, channels, templates

The Harness union widening is typecheck-SILENT at every existing
'claude-code ? A : B' ternary, so the hot sites now dispatch exhaustively
(HARNESSES satisfies anchor; exec-lane bin map returns null for opencode —
its registrations go through the JSONC writer whose fingerprint IS the
[FIX7] check, never through <host> mcp get).

Scope INVERSION for opencode: default user-global (opencode spawns
project-config-defined MCP servers with NO trust prompt — verified; a
committed project entry would auto-execute on every collaborator machine).
MCP_SCOPE=project is an explicit opt-in that writes the workspace
opencode.json with a PATH-resolved command (committed-candidate file: no
absolute machine paths, no fail-open analog exists) and prints the sharing
warning + enabled:false opt-out. detectHarness probes OPENCODE/OPENCODE_PID
(observed 1.18.18). Ownership: a remote-type mcp.gbrain in the global
config makes the stdio lane step aside (codexBlockOwnsName analog); foreign
entries refuse. Verification: writer post-render parse-back is
authoritative; best-effort 'opencode mcp list --pure' probe (skipped on
plugin-bearing configs — mcp list is a code-execution surface).

Atomic with this commit (each-commit-green): questions.json MCP_SCOPE +
SURFACE_PRIMARY copy, AGENTS/GITHUB template pull-protocol generalization,
BOOTSTRAP_FOR_AGENTS.md scope guidance + opencode wiring bullet,
status.ts interview/wire resume hints, check-bootstrap-templates.sh §(e)
pins (now 'Claude Code and opencode' + the 'NO trust prompt' spawn-gate
rationale pin), guard-test fixtures, the status-test hint pin, the vendored
template-repo regen, the offline docker opencode leg, and uninstall's
receipt-keyed opencode removal. Channels: 'opencode' joins
VOLUNTEER_CHANNELS + HARNESS_CHANNELS (reserved attribution slot, codex
precedent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): opencode harness-mode target — managed remote entry with inline bearer, remove/status/rollback

HarnessSelector gains 'opencode' (forced-wire like codex: the JSONC writer
needs no opencode CLI). Wiring is one managed mcp.<name> remote entry with
the inline Authorization bearer in the user-global opencode config, 0600,
under the [X11] lock ordering (config-dir → opencode-dir). Ownership [C8]:
idempotent re-runs match on the serve url; rotation across a url change
recognizes the old entry via the PRIOR receipt's url; anything else under
the name refuses inside the writer. Failed-smoke rollback restores the .bak
or removes a fresh entry, and the fresh mint is revoked (impostor-guard
economics hold). --remove classifies against the receipt url and skips
not-ours entries with a note; --status recovers the bearer from the entry
(url-matched — a foreign entry's credential is never transmitted). Consent
copy: per-host numbered item, joined-list reach statement (a fourth harness
can no longer silently mislabel the ternary tree), opencode off-ramp.

Fixture hygiene: the harness serial fixture now injects opencodeConfig +
detectOpencode — the default path resolution reaches the operator's REAL
~/.config/opencode (the claudeUserSettingsPath lesson, caught live when the
registrar-mode test wrote a fixture token there; cleaned up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(connect): --agent opencode — env-interpolated bearer, direct-writer --install

buildOpencodeMcpAddArgv pins the validated one-liner: the --header value
carries opencode's {env:GBRAIN_REMOTE_TOKEN} interpolation LITERALLY, so the
token never enters argv, the config file, or --json output. The print block
mirrors codexBlock (export line + one-liner + restart note). --install goes
through a new ConnectDeps.writeOpencodeRemoteEntry member (the existing
injectable seam, connect.ts:ConnectDeps) wrapping the JSONC writer in env
token mode — no opencode binary required, idempotent re-runs, foreign
same-name entries refuse with the writer's message (token-redacted), and
the D4 probe smoke-tests the credential end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(claw-test): opencode runner — detection, pinned one-shot invoke, multi-provider env allowlist

OpencodeRunner (4th AgentRunner): detectBinary('OPENCODE_BIN','opencode');
argv pinned to 'run <brief> --format default' (explicit format so an
upstream default flip cannot silently change the transcript shape; NO
--auto — MCP tool calls fire in run mode without it, verified). Env
allowlist = BASE + an EXPLICIT multi-provider delta (XAI / Google / Gemini
/ OpenRouter keys — BASE carries only Anthropic+OpenAI, and a live-lane
operator on other providers would otherwise see a misleading auth failure)
+ XDG dirs + OPENCODE_CONFIG(_DIR) + OPENCODE_DISABLE_AUTOUPDATE;
OPENCODE_CONFIG_CONTENT (the inline config-shadow channel) deliberately
absent. Bare-semver version preamble (the SST-vs-claimant discriminator)
+ global-config mcp.gbrain contamination tripwire (JSONC-tolerant, checks
BOTH merged filenames). --list-agents pin moves to all four runners with
the openclaw<opencode ordering note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(test): door-family extraction in agent-harness — shared resolver/childEnv/spawn core; grok+hermes ported; opencode first consumer

The 'P3 — Door-adapter extraction + CI-tail composite action' TODO armed
this at 'the NEXT door agent (4th)' — opencode is the 4th. Test-side only
(the composite CI-tail action stays deferred until the first green
grok-door AND opencode-door dispatches; workflow yaml can't be proven
locally).

- makeBinaryResolver: one shape (fail-closed $*_BIN > which > landing
  spots + nvm/PATH sweeps); claude/codex/hermes/grok resolvers become
  factory products with identical candidate lists.
- makeAgentChildEnv + GITHUB_STEP_META_KEYS: hermeticChildEnv + per-agent
  overrides + key deletion + the step-metadata scrub + binDir prepend.
  hermesChildEnv GAINS the GITHUB_* deletion via the factory (the filed P2
  backport; truth-table extended).
- runOneShotSpawn: shared timeout/kill/kill-9-escalation/bounded-drain core;
  hermesOneShotTurn gains the escalation + bounded drain (strictly safer,
  nothing pinned the old unbounded wait); grokOneShotTurn is now a thin argv
  builder over it.
- 5a-opencode family (first consumer): resolveOpencodeBinary (fail-closed
  OPENCODE_BIN), hasOpencodeAuth (PAID-leg-only gate — the keyless free
  tier carries the core SMOKE), opencodeChildEnv (HOME + BOTH XDG dirs,
  anthropic re-admission, other-provider + OPENCODE_CONFIG* shadow-trio
  deletes), seedOpencodeConfig (config half of the double autoupdate kill),
  opencodeOneShotTurn, and parseOpencodeJsonl (event shapes pinned from the
  live v1.18.18 observation — {type,part} with part.text / part.tool).

EV1 gate (local keyless grok-door): 4 pass / paid-skip in 20.5s BEFORE and
AFTER the port, against a hermetically npm-pinned @xai-official/grok@1.0.4.
The hermes door self-skips without a binary — its port is pinned by the
unit truth-tables (stated honestly, per the plan).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): opencode door — split-gated real-binary e2e on the extracted family (keyless SMOKE included)

The door goes a step beyond grok's split gating: opencode's anonymous free
tier drives MCP tool calls with zero credentials (observed, load-bearing),
so even the nonce SMOKE runs keyless. Tiers: T1 bare-semver version pin
(the SST-vs-claimant discriminator), T2 INSTALL via the documented
'mcp add … -- gbrain serve --surface verbs' shape + the honest 'mcp list'
discriminator (spawns servers; ✓/✗ text asserted — exit code is 0 even on
failure), T2b spawn-gate CANARY (a project-config decoy is spawn-attempted
with no trust prompt — if this ever gates, the bootstrap user-global
default rationale changed: re-observe), T3 writer parity (gbrain's
opencode-json.ts output handshakes through the real binary; cross-tool
preservation both ways incl. the autoupdate seed), T4 keyless SMOKE
(per-run nonce + STRUCTURAL gbrain_* tool_use proof via parseOpencodeJsonl,
list preflight before any turn, 2 attempts), and the paid T5 anthropic leg
(hasOpencodeAuth-gated; self-validating models-gate pins the model id
BEFORE any spend). Hermeticity: HOME + both XDG dirs per child, tmp cwds,
config/credential tripwire over the operator's real opencode state,
checkout guard, --pure on every probe (mcp list autoloads plugins), --pure
placed BEFORE the '--' separator (a trailing append lands inside the server
command — caught live). run-e2e.sh scrubs the OPENCODE_ prefix.

Verified live: 6/6 pass in 35.8s (keyless tier + paid anthropic leg)
against the hermetically pinned opencode-ai@1.18.18.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(heavy): opencode-door job (day-one full posture, keyless SMOKE) + canary leg + pin guards + hermes installer re-pin

opencode-door takes hermes-door's triggers (nightly + labels + dispatch;
cadence policy: nightly for the NEWEST door agent) with grok-door's
internals — keyless-first ordering, secretless npm provisioning with
wrapper AND per-platform integrity pre-checks, pass-count + paid sentinels,
mid-job version-drift tripwire, evidence scrub RE-KEYED to
ANTHROPIC_API_KEY + auth.json (not XAI/mcp_credentials), unconditional
credential removal. No dedicated dispatch input (any workflow_dispatch
already passes the non-PR arm — an input would be dead yaml). The keyless
tier includes the nonce SMOKE (free tier), so the core door needs NO
secret; the paid anthropic leg rides the secret hermes-door already
consumes. opencode-door-canary lands IN-WAVE (schedule-only,
continue-on-error, unpinned latest): opencode ships near-continuously — a
red canary is a pin-refresh signal, never a gate. real-agent-e2e adds the
opencode door file + env pins.

Guards: check-opencode-pin.sh (stamp↔workflow parity, job-block anchored so
the UNPINNED canary leg cannot satisfy it; fail-closed when the door exists
without the pin doc) and check-pin-doc-privacy.sh (placeholder discipline
for ALL docs/mcp/*-CLI-PIN.md — no operator home paths, no key-shaped
material outside sha512 pins, no non-example emails), both in bun run
verify + guards-manifest, both with fixture-tree bun tests.

Maintenance: hermes-door installer pin refreshed (upstream install.sh
drifted past the prior digest — last two nightlies red; reviewed: the
--commit payload-pin path is intact and the payload pins are unchanged).
docs/TESTING.md gains the opencode door entry + the door cadence policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: opencode across the install/testing/architecture surface

README client roster + remote-connect bullet (with the not-OpenClaw
disambiguation and the bootstrap-supported banner), INSTALL_FOR_AGENTS
'If you are opencode' block (routes bootstrap-capable readers to the
runbook; brain-only registration otherwise), docs/INSTALL per-client list,
MEMORY_VERBS register snippet, bootstrap guide (degradation-matrix row
naming the INVERTED scope default + rationale; harness-mode opencode
bullet; dx-explore scenario line), ambient-recall/push-context harness
mentions, and KEY_FILES current-state entries (opencode-json.ts,
atomic-write.ts, connect/harness/hooks/claw-test entry refreshes).
llms bundles regenerated (build:llms chaser).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* dx(explore): opencode-install TTY scenario — bootstrap paste block under the real interactive TUI

Unlike grok's brain-only prompt, opencode gets the FULL bootstrap paste
block (it is a bootstrap-supported harness) under a hermetic HOME + both
XDG dirs, the double autoupdate kill (config seed + env), BROWSER=false so
a first-run can never bounce the operator's browser, and auth.json
pre-registered for the secret scrub. Keyless posture INVERTS the grok
scenario: the anonymous free tier means a --keyless run should COMPLETE
the flow — a sign-in wall here is itself a pin-refresh signal, and the
generic early-stop in runInstallSession records it as friction if it ever
appears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(todos): file opencode-wave follow-ups + retire fired triggers by title

Door-adapter extraction (test-side) and cadence policy: DONE — the 4th-door
trigger fired. CI-tail composite action re-filed with the sharpened trigger
(first GREEN grok-door AND opencode-door dispatches). hermesChildEnv
GITHUB_* backport: DONE via the shared factory. PIN-doc privacy guard:
DONE (check-pin-doc-privacy.sh in verify). New follow-ups: first-dispatch
watch, plugin/event-system wiring (ambient-recall lane), BrainBench
adapter (with hermes+grok), connect --oauth authorization-code lane,
OPENCODE_CONFIG* re-observation on bumps, opencode-install PTY promotion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage pins for the opencode channel widenings

Ship-audit additions: hook.ts --harness opencode flag-parse attribution
end-to-end, and 'opencode' membership in VOLUNTEER_CHANNELS +
isHarnessChannel (a regression here silently rebadges opencode deliveries
as claude-code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bootstrap): close two plan-audit gaps — ACCESS_POLICY opencode scope paragraph + doctor host:opencode pin

Plan-completion audit (ship Step 8) flagged both as PARTIAL: the
ACCESS_POLICY template's MCP-scope section didn't state opencode's
inverted default (user-global; project spawns with NO trust prompt),
and bootstrap_harness_health had no named pin proving an opencode
receipt flows through the host-generic filter. Template-repo regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pre-landing review fixes — review-army + red-team wave

Security: opencode error-path snippets render <paste-token-here> instead
of the live bearer; the harness opencode catch redacts like the claude
lane; test-harness child envs unconditionally drop GITHUB_TOKEN/ACTIONS_*.
Correctness: bootstrap-lock coverage for every shared-config writer
(hooks/connect/uninstall); uninstall + step-aside gate sweep BOTH global
opencode filenames (merged namespace); uninstall passes a sourceId
expectation and skips other-workspace entries; cross-kind fingerprint
matches classify ours-other-source instead of silently replacing;
dangling-symlink writes preserve the link; failed-smoke rollback restores
atomically; registration probe pins OPENCODE_DISABLE_AUTOUPDATE, a 20s
cap, ANSI-stripped exact-name matching. Guard: check-opencode-pin now
cross-checks per-platform integrities + every OPENCODE_VERSION copy.
Plus deny-path/uninstall/rollback/truth-table/symlink test coverage,
help-prose cosmetics, downgrade doc note, 3 P3 TODOs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: adversarial-review fix wave — cross-model (Claude + Codex) findings

P0: the registration probe no longer executes from the invoking cwd
(mkdtemp cwd for user scope; project scope skips the live probe —
parse-back is authoritative), so a cloned repo's committed opencode.json
can't gain code execution during bootstrap. Probe timeouts now kill the
child (SIGTERM→SIGKILL, bounded drain) instead of abandoning it over the
PGLite lock. Global writes reconcile mcp.<name> across BOTH merged
global filenames. Stale-target cleanup takes the target config-dir lock.
Backups are unique per operation; rollback is content-guarded and
remove-path backups tighten to 0600 when token-bearing. connect --force
now works on the opencode lane with url-appropriate refusal copy.
atomic-write cleans tmp litter and survives the exists/realpath race.
Bun-lane fingerprint is fail-closed on gbrain-less args. Scope answers
trim. bounded() clears its drain-cap timer. CI installs opencode from
byte-verified tarballs. Consent-semantics + hermetic-live-runner
follow-ups filed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: regen cli-flag-registry after review-fix waves

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.46.2.0 feat(opencode): full-parity client support — bootstrap, harness, connect, claw-test, e2e door

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: fold the post-review opencode fix-wave behaviors into KEY_FILES

Three current-state completions the fix agents didn't carry into the
per-file index: connect --agent opencode --install's --force semantics
(maps to the writer's allowReplaceOtherSource — ours-at-old-url
replaceable, foreign still refuses), removeOpencodeMcpEntry's
skipOtherSource option, and bootstrap uninstall's expectation-keyed
opencode sweep (both merged global filenames under the config-dir lock
plus the project file; other-workspace entries skipped with a note).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: cross-model doc-review fixes — opencode roster + probe/remote accuracy

Findings from the standard post-ship Codex doc review, verified against
the shipped code: the bootstrap guide's intro, install table, and door
inventory still described a two-client product (opencode added to all
three); INSTALL_FOR_AGENTS' grok section said the personal-agent path is
Claude Code/Codex only; OPENCODE.md called its recipe the bootstrap
"manual equivalent" (bootstrap additionally pins GBRAIN_SOURCE + full
surface), lacked the mcp-list trust caution the pin doc carries, and
never documented the connect --install / --force remote lane; the pin
doc's provisioning bullet now names the pack-verify-install posture the
CI job actually runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.46.4.0 chore(release): re-bump 0.46.2.0 → 0.46.4.0 (version slots claimed by in-flight PRs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 21:43:31 -07:00

Diff Content Not Available