Skip to content

v7.201.2

Choose a tag to compare

@github-actions github-actions released this 04 Nov 21:31
d71e5ae

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 binds prFeedbackBaseRef/Sha in gate state (fail-open: a
    resolution failure records prFeedbackBaseBindingError and 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.
    The gh subprocess runs BEFORE the checkout/session mutation locks so the
    network call never stalls concurrent gate mutations. Stage A verifies its
    caller-asserted base_sha against the binding: matching runs carry the
    root-level prFeedbackStageABaseTrust: 'binding-verified' verdict; a
    differing base ignores the caller provenance entirely (trusted
    base-identical scripts unavailable — identical to no-provenance) and
    persists a bounded prFeedbackStageABaseMismatch marker (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 disclosed caller-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> and cp -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-existing cp treatment (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 terminal echo/printf progress tail with
    at least one real validation segment. ||/;/pipe/newline compounds lose
    base-identity trust (.pr-validation.json remains 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 nested prepretest forms) are now base-checked: PR-added or
    modified prebuild/postlint/pretest hooks 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: true without 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).
    readSddSourceSetting now 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 supply sdd.source, while every acceptance the loader
    performs (stripped-key recovery, legacy Epic migration, malformed-value sanitization,
    merge-dependent healing) still does. /swarm sdd status origin 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
    neutralizes XDG_CONFIG_HOME unconditionally (the original value is stashed in
    SWARM_TEST_NEUTRALIZED_XDG_CONFIG_HOME), so a developer machine's user-level sdd.source
    — or any other user-level config value — cannot change suite outcomes; suites that pin their
    own XDG in beforeEach keep working.
  • Diagnostics note: the sdd.source rejection 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).