Repository navigation
v7.201.2
Guardrails: verifier-config protection for every role, unconditional under guardrails.enabled=false (#3120)
.secretscanignore and every built-in verifier-config glob (tsconfig*.json,
biome.json(c), .eslintrc*, eslint.config.*, .prettierrc*,
prettier.config.*, oxlintrc*, .golangci*) are now denied for every
agent role — including the OpenCode-native roles (general/build/plan/
explore) previously exempt from swarm write restrictions — and the denial
fires even with guardrails.enabled: false (and in observe contexts):
these files are owner policy for the verification system itself (the secretscan
Stage-A gate trusts .secretscanignore), so their integrity is not switchable
by the general enforcement knob. The shell path additionally routes no-scope
lenient roles through the verifier-config check (native-role echo x > .secretscanignore via bash is blocked). Disclosed boundary: script runners
(npm run <script>, bun run <script>, node <file>) remain NOT treated as
shell write vectors — their targets are not statically knowable and blocking
them would break every legitimate build; see the shell-write-detect module
documentation.
Shell write detection: patch, checkout-ref, rsync vectors (#3120)
git apply / patch (explicit -p<n>; both diff endpoints so a rename/delete
of a protected preimage cannot hide behind the postimage; closed option set —
--directory, -d, -o, -r, backup options, and index-writing modes
--cached/--index/--3way fail closed; patch without explicit -p fails
closed because GNU patch then picks basenames itself), git checkout <ref> <pathspec>… (no --), and rsync <src>… <dest> are now detected as write
vectors and authorized per-target through the cd-tracking resolver. Targets
named inside a patch are extracted at authorization time from the patch file
(bounded read): tab- and space-prefixed ---/+++ headers, C-style octal
escapes (\ooo — accumulated as bytes and decoded once as UTF-8; unknown
escapes and undecodable sequences fail closed), all four quoted/unquoted
diff --git header combinations (git quotes each name independently
under its default core.quotePath=true), pure
rename sections (rename from/rename to — no ---/+++ lines), and
rename/delete endpoints that only exist as section headers. Patch targets
are judged under BOTH landings (git apply is patch-root-relative, GNU patch
is cwd-relative), and rsync also flags --exclude/--include/--filter
arguments as write paths. Unreadable/undecodable patches and stdin patches
block.
PR_FEEDBACK: durably bound merge base + Stage A trust screens (#3396)
- PR_FEEDBACK activation resolves the merge base via ONE bounded
gh pr view
and durably bindsprFeedbackBaseRef/Shain gate state (fail-open: a
resolution failure recordsprFeedbackBaseBindingErrorand never wedges
activation; a failed binding is retried on the next matching re-activation
and a successful re-resolution clears the stale advisory). The bound value
is the real merge base (git merge-base base_ref pr_head_sha), not the
base-branch tip, so callers whose base advanced keep trusted provenance.
Theghsubprocess runs BEFORE the checkout/session mutation locks so the
network call never stalls concurrent gate mutations. Stage A verifies its
caller-assertedbase_shaagainst the binding: matching runs carry the
root-levelprFeedbackStageABaseTrust: 'binding-verified'verdict; a
differing base ignores the caller provenance entirely (trusted
base-identical scripts unavailable — identical to no-provenance) and
persists a boundedprFeedbackStageABaseMismatchmarker (durable-wins
evidence, appended without advancing the gate revision, cleared by a head
rebind). The receipt's strict child keeps the BASE two-value trust floor
(rollback-safe: the 4-value verdict rides the passthrough-safe root field
older binaries ignore). With no binding, the disclosedcaller-asserted
floor stays. The Stage A receipt now actually PERSISTS its base identity
(the pre-fix call passed flat option keys the record schema ignored, so
even the disclosure never landed). - Directory-copy boundary (disclosed):
rsync <src>… <dest>andcp -r
write authorization judges the DESTINATION operand only — directory
CONTENTS are not enumerated, so a source tree carrying a protected file
copies without per-file authority. This is deliberate parity with the
pre-existingcptreatment (content enrichment is a separate, larger
change); direct writes to protected paths remain denied on every route. - Base-identical compound scripts pass a closed plausibility screen before
base-identity trust: the supported grammar is exclusively the non-empty
all-&&chain, literal heads only, no substitution/redirection/control
grammar/wrapper builtins/blocked executables/eval or workspace-override
flags; the sole exemption is a terminalecho/printfprogress tail with
at least one real validation segment.||/;/pipe/newline compounds lose
base-identity trust (.pr-validation.jsonremains the sanctioned contract
path). The screen is a plausibility screen over base-identical text, not a
sandbox (accepted risks disclosed: literal unblocked tools can still write;
NODE_OPTIONS/variable-expanded arguments stay outside the literal screen). - npm lifecycle hooks (
pre*/post*around category stems, repeated
prefixes for nestedprepretestforms) are now base-checked: PR-added or
modifiedprebuild/postlint/pretesthooks fail Stage A closed with a
remedy; base-identical hooks are trusted (compound bodies screened) and
attach to their primary script's obligation rather than creating
unsatisfiable standalone obligations. Direct lifecycle-hook check commands
(npm run prebuild) are rejected — the check must run the primary script.
A PR-changed category-matched script whose simple parse is implausible now
fails closed instead of being silently ignored (base-identical scripts keep
the base's own choice).
OpenCode 2.x: worktree-isolated coders can be resumed again
What
On the OpenCode 2.x hosts tested (2.0.26, 2.1.4 and 2.2.5), every worktree-isolated
coder dispatch failed with "Session … is not a child of the current session". The plugin's v2 session
shim created each worktree-lane session without a parent (on OpenCode 2.0.x a
parentless create honours a foreign directory, while a parented one does not),
but the host's
native subagent tool only resumes sessions whose recorded parent is
the calling session.
The shim now creates the lane session as a real child of the dispatching
session and then moves it into the lane worktree with the host's
session.move, re-reading the session until it reports the lane directory.
Hosts that already place a parented session in the requested directory are not
moved. Every placement step after the create has a time limit and honours the
caller's cancellation: each placement read is limited to 1 second, the
confirmation re-reads to about 5 seconds in total, and the move to 10 seconds.
Because the host cannot cancel a move, a move that times out or is cancelled
is given up to 10 more seconds to finish before the session is removed. If the
move is rejected, unavailable, or never observed, or the host returns
something other than a promise, the created session is removed and the
dispatch degrades through the existing STANDARD_WORKTREE_SESSION_CREATE_FAILED
path, exactly as a placement mismatch did before.
If a move still has not finished when the session is removed, the error text
says the removal is unconfirmed and names the session (cleanupUnconfirmed).
This is informational only: it reaches the architect inside the
STANDARD_WORKTREE_SESSION_CREATE_FAILED message, the lane is torn down
regardless, and no code acts on the flag (the existing cleanupFailed flag
works the same way).
Who is affected
- Users running opencode-swarm on OpenCode 2.1+ with worktree isolation
enabled (including Epic Mode's isolated coders): isolated coder dispatch works
again. Verified live end to end on OpenCode 2.1.4 and 2.2.5, and (A/B) on 2.0.26. - OpenCode 2.0.26 hosts take the same path (parented create, then
session.move); there is no version gate. The 2.0.26 host contains the same
resume check, so isolated coders failed there too: an A/B on 2.0.26 shows the
unpatched build failing with "not a child of the current session" and the
patched build resuming the lane and running the coder in it. A placement
failure fails closed exactly as on 2.1+. - Factory/ephemeral helper sessions (for example the knowledge curator)
started from inside a lane for a directory outside it: these are now parented
children of the lane session, moved to the project root. Before, they were
created parentless in the project root. - OpenCode 1.x hosts are unaffected.
Migration
None. A worktree.policy: "disabled" workaround added for this bug can be
removed after upgrading.
Receipt task-arm outcomes now derive from the durable record (#3560)
What
Task-kind verification receipts (and the evidence-summary bridge that shares
the src/evidence/gate-outcome.ts derivation) no longer hardcode
outcome:'pass' for every gate present in the durable task record. A
durable gates.council entry now renders its recorded verdict —
REJECT→block, CONCERNS→pass with code:'CONCERNS' (the task-level
advisory form), APPROVE→pass with code:'APPROVE', and a present
verdict that is not a recognized string — including a non-string value —
→error, so a malformed or future-vocabulary verdict can never attest a
pass. Presence-only gate kinds keep pass-on-presence, honest because their
producers record only on positive settlement and delete the key on failure.
The empty-gates synthesis no longer fabricates a passed gate: it emits
not_applicable, or pass with code:'no_mutation_settlement' only when
the platform's canonical isDurableNoMutationSettlement predicate holds
(authoritative generation-0 idle/complete workflow with a bound proof) — a
marker merely retained through a blocked transition does not qualify.
responsible_actor comes from the recorded agent, swarm only as
fallback. The bridge reports the RECORDED verdict (rejected/approved/
warn/fail/pass with the token in the summary and durable_verdict
metadata), and every dynamic cell of the rendered receipt Markdown is
escaped so durable bytes cannot forge a table row absent from the
hash-bound receipt.json.
Receipt rows attest the RECORDED verdict only: council quorum adequacy,
identity-digest validity, and workflow freshness remain the domain of the
task-completion gate and the state-rehydration path — a record the platform
would refuse still renders as its recorded verdict (disclosed; tracked for
the first authenticity-gating consumer).
Found by the swarm-pr-review round on PR #3557 (executed probes by the
reviewer and an independent critic: a seeded council REJECT previously
rendered | council | pass | — | swarm |), refined by the review round on
PR #3561 itself (non-string verdict fail-open, stale settlement synthesis,
Markdown row-injection, bridge verdict collapse).
Compatibility
format_version is unchanged (1.0.0): receipts emitted before this fix
remain verify-green (verify checks internal consistency, not external
attestation). Such persisted task receipts carry flattened outcomes —
re-emit /swarm receipt task <id> to supersede (content-addressed;
newest-8 retention prunes the old files).
Config loader: full_auto.locked survives every recovery branch; SDD source reads are hardened, cached, and host-isolated
- Locked configs can no longer be silently unlocked by config recovery (#3105). A
readable-but-schema-invalid project config used to send the loader down a recovery branch that
rebuilt the config from the user file (or bare defaults) — dropping an administrative
full_auto.locked: truewithout classifying the load as errored, so/swarm full-auto on
(and resume) authorized a locked project. The lock is now re-applied at a single choke point
covering every recovery branch of the shared sync+async loader core; recovery metadata and all
non-lock keys are unchanged. - SDD source selection can no longer be steered by configs the loader discards (#3354).
readSddSourceSettingnow derives its answer from the same loader pipeline as
loadPluginConfig(in a silent diagnostics mode that neither buffers warnings nor arms
warning-dedup state), so a project config thrown away by recovery — or refused as a
prototype-pollution payload — cannot supplysdd.source, while every acceptance the loader
performs (stripped-key recovery, legacy Epic migration, malformed-value sanitization,
merge-dependent healing) still does./swarm sdd statusorigin reporting follows the
surviving file, not raw-key presence. - Non-regular files at config paths are never opened (#3354). A FIFO or device at
.opencode/opencode-swarm.json(or the user config path) used to block the read — reachable
from gate/preflight paths that previously performed no synchronous config read. Both the sync
and async raw readers now classify non-regular files fail-secure without attempting the open. - SDD source reads are cached per directory keyed by the config files' mtime+size (#3354):
unchanged files are no longer re-read on every gate call. The freshness contract is
intentional: mtime+size is the invalidation signal (the model build tools use); a content
change that preserves both is invisible until the next real mtime change — within that
window the resolver can briefly serve the previous source while the full loader already
accepts the new one; the divergence self-corrects at the next real mtime change. Bounded
(64 directories, FIFO). - Host user configs no longer leak into test suites (#3355). The bun test preload now
neutralizesXDG_CONFIG_HOMEunconditionally (the original value is stashed in
SWARM_TEST_NEUTRALIZED_XDG_CONFIG_HOME), so a developer machine's user-levelsdd.source
— or any other user-level config value — cannot change suite outcomes; suites that pin their
own XDG inbeforeEachkeep working. - Diagnostics note: the
sdd.sourcerejection advisory (Ignoring sdd.source=...) now
also reports an invalid value in the user config even when a valid project source wins —
previously a winning project source masked the user-side rejection entirely. The channel is
display-only and never influences selection. - The
--source>SWARM_SDD_SOURCE>sdd.source> auto-detect precedence is now
documented in the generated command reference and the specify/clarify-spec skill docs, and
docs/planning.md's description of the (now removed) loader/resolver divergence is corrected
(#3356).