Skip to content

v7.200.10

Choose a tag to compare

@github-actions github-actions released this 29 Oct 11:40
4a6053c

Preserve interrupted coder settlement work

Coder settlement recovery now retains surviving recorded source commits even
when a lane checkout and branch are missing. Merge recovery can resume on a
compatible advanced primary branch while retaining conflict, dirty-overlap and
source-identity checks. Pending settlements protect their artifacts from
cleanup through shared dispatch/deletion admission. That protection covers
dispatch and lane cleanup performed by the plugin; it does not cover an
explicit /swarm close, which archives the session and then removes the
.swarm/coder-settlements/ directory.

Mixed foreground/background completion now hands off settlement ownership
before removing the lane. Interrupted handoff and leftover cleanup can replay
from saved provenance without merging twice; changed lanes remain preserved
for manual recovery.

Recovery also detects the specific historical missing-worktree self-heal that
may already have marked a settlement aborted. It reports the exact recoverable
commit and retained backup ref without reviving an old task generation. That
RECOVERABLE_ORPHAN_COMMIT report is advisory: task status, re-dispatch and
close are not blocked by it. Committed recovery releases its backup; unstaged
work and historical work that has not landed by ancestry remain retained for
explicit operator review. See
coder settlement recovery for discovery
and safe cleanup commands.

No settlement schema or configuration migration is required.


Fix: macOS sandbox profile embeds kernel-resolved system paths

What changed

The macOS sandbox-exec profile builder now writes the kernel-resolved form of a path when it starts with one of the three macOS system links: /tmp, /var and /etc become /private/tmp, /private/var and /private/etc. The rewrite applies to scope paths and to the temp dir, in both the production and the probe profile, and only when this host's realpath confirms the link.

The rewrite is a prefix swap on the input string. No other symlink is ever followed, so a link created inside a project can neither change the result nor widen the profile to its target. A path that is already resolved is used as it is. A path under any other symlink (for example ~/projects -> /Volumes/data), a host without the link, or an error leaves the path exactly as written (the previous behaviour): that path stays unmatched by Seatbelt and writes to it are denied.

Seatbelt matches subpath against the kernel-resolved path. Before this change a scoped (allow file-write* (subpath "/var/…")) line never matched, so the allow was inert while the profile looked correct. Verified with raw sandbox-exec: the /tmp/… form exits 1 (Operation not permitted), the /private/tmp/… form exits 0.

The wrapped command now passes the profile inline (sandbox-exec -p '<profile>' bash -c …) instead of staging it in a file under the temp dir. The temp dir is inside the write grant (see below), so a staged profile there could be rewritten by a still-running sandboxed process before a later call's sandbox-exec read it; with no file there is nothing to race, and no per-call profile files accumulate in the temp dir any more.

Behaviour changes you may notice (macOS, guardrails.sandbox_macos_enabled: true)

  • The temp-dir write grant is now effective. Every wrapped command has always been given write access to os.tmpdir(). On macOS that is the per-user $TMPDIR (/var/folders/<xx>/<yyyy>/T), which sits under the /var link, so the grant never matched and temp writes were denied. It now matches: wrapped commands can write anywhere under $TMPDIR, including files other processes of the same user keep there. The grant is a whole-temp-tree allowance, not one scratch directory per command, and it is wider than the per-run temp of the Linux executor (--tmpfs) and of the Windows native runner (a per-run temp_root); the Windows PowerShell wrapper has no OS-enforced write containment.
  • The capability probe reports filesystem: real. The probe's own in-scope write used to be denied, so it reported weak on macOS. It now reports real.
  • guardrails.sandbox.mode: required can now enforce. With guardrails.sandbox.require_filesystem: true the probe's weak made required mode block every shell command on macOS. With real it can proceed, but only when guardrails.sandbox.network_mode: on is also set and guardrails.sandbox.network_allowlist is empty: macOS reports network_mode: off as unsupported, so the default off is still blocked. guardrails.sandbox.require_network and guardrails.sandbox.require_process stay blocked because macOS reports no real containment for them. Advisory mode and require_filesystem: false are unchanged.
  • Scope paths. Paths that exist are already resolved by the scope resolver, and a not-yet-existing path under a symlinked root is rejected as a traversal. In normal tool use the rewrite therefore mostly affects the temp dir and the probe; it matters directly for callers that build a profile from a /tmp, /var or /etc path.

Why

Closes #3462. Sibling of #2590 / PR #2630 (invalid setenv in the same profile).

Known caveats

  • The sandbox stays opt-in (sandbox_macos_enabled defaults to false).
  • Windows and Linux are unaffected.
  • Other symlinked roots are not rewritten by the profile builder; a path under one stays unresolved and Seatbelt does not match it (writes denied).

Fix: config-doctor no longer reports top-level agents as deprecated

What changed

config-doctor warned The "agents" field is deprecated. Use "swarms" instead and proposed removing the key. The key is not deprecated: it is documented as the per-agent override for the default swarm (docs/configuration.md), it is merged into every swarm at runtime (src/agents/index.ts), and the v7.11.0 notes name agents.<role>.model as where to set a model. Following the suggestion would delete working configuration.

An object-valued agents now produces no finding. A non-object value (for example a string, number or array) is still rejected: config loading discards it and config-doctor reports invalid-config-value (error). The invalid-agents-type check in config-doctor is a defense-in-depth guard that keeps the key's validation case, which the schema-introspection test requires for every top-level key.

Separately, when config-doctor validates a config that has not been through the loader (direct runConfigDoctor callers and the re-run after --fix, whose result is written to the doctor artifact), an array given to an object-typed key such as agents, swarms, pipeline or guardrails now reads got array instead of the contradictory got object. In a config file such a value is dropped by the loader and reported as invalid-config-value, as before.

Why

Closes #3463.


PR-review verdict-row pipeline integrity

What changed

  • Quarantine escape closed (#3357, completing #3275 RC2): a verdict row that ARRIVED over its marker's legal arity was tail-merged down to exactly the legal count, so the #3310 arity-keyed quarantine never saw it — the Stage-B/echoed-format row entered the exact-set count with an unassigned id and voided every valid sibling row. Rows are now quarantined on their RAW arrival arity when they required tail-merge AND name an unassigned id (rawFieldCount is surfaced by the parser; indexVerdictRows takes the assigned-id set). The #3310 under-arity arm also feeds the same quarantine surface. The spoofing floor is unchanged and exact: a row that arrived at EXACTLY the legal arity naming an unassigned id still voids the lane — that is the forged-row class; over-arrival is by construction non-format-conformant and cannot authenticate.
  • Quarantine is now visible on the durable receipt (#3357 AC2): a verdict lane whose artifact contained quarantined strays settles with legacy-verdict-row-quarantine recovery entries (bounded id + raw arrival arity in the reason; itemization capped at 16 with an +N more overflow note on the last entry) on the same durable surface as legacy-verdict-row-recovery, in BOTH the TypeScript union and the persisted WorkflowLaneRecoverySchema. Previously a quarantine-influenced settle produced a receipt byte-identical to a clean settle.
  • Host-aborted verdict lanes are never typed (#3397 RC-03): a verdict lane the host aborted after producing partial text previously flowed to validation failure and was stamped workflowLaneFailureClass: 'verdict_contract', making a host cancellation admissible typed-dead evidence once the retry budget expired — contradicting the shipped "aborted terminals never typed" contract. Both stamp sites (the ordinary !ok leg and the transport-validation throw catch) now skip the stamp when terminalError.kind === 'aborted'. Such lanes settle as classless error terminals and BLOCK typed-dead admission by design (classless owners are rejected before the budget arm in verdict-settlement.ts), so the budget arm cannot admit them. Two operator escapes exist: a re-dispatched lane resolves the item on success, and the disclosed-INCOMPLETE completion path names the blocked item (also stated in the settlement BLOCKED remedy text).
  • Embedded credentials are redacted (#3382): the fixed-length credential patterns (bare AWS access key id, Google API key, the labeled AWS-secret value arm, the context arm's AKIA anchor) and the token prefixes (GitHub ghp_/gho_/ghu_/ghs_/ghr_/github_pat_, GitLab glpat-/glptt-, Slack xox[abprs]-) match credentials embedded inside longer charset-clean tokens. The github short-prefix bodies are tightened to underscore-free alnum runs (real classic tokens are base62), which keeps benign snake_case identifiers such as load_ghs_metadata_from_cache_store unredacted while embedded real tokens still match; github_pat_ keeps its underscore-tolerant body (fine-grained tokens contain one). Anchors are deliberately KEPT where the short alnum-only prefix collides with benign text (OpenAI sk- matches inside "task-"/"disk-", Stripe sk_/rk_ inside "risk_live_…", JWT eyJ inside base64 blobs). The whole-token bearer arm now runs FIRST, so a credential embedded inside a bearer token is redacted as one span instead of being split. Detected embedded shapes are redacted instead of passing byte-identical through redactSecrets/containsSecret into verdict-row diagnostics, telemetry, receipts, and analysis output (the verdict-row id sentinel path yields (redacted)).
  • Sequential-replacement masking fixed (pre-existing, found by this work): redactSecrets applies patterns sequentially, and the bare AWS access-key arm ran BEFORE the AWS-secret context arm, replacing the AKIA… anchor and leaving the 40-char context secret un-redacted even in the exact-40 shape (the existing test asserted detection only, not redaction). The AWS-secret arms now run first; the family count is unchanged.

Why

A single Stage-B grammar row with pipes in its summary could void an entire reviewer lane (the #3275 deadlock shape), a host abort could launder into typed-dead settlement evidence, and an embedded credential could ride a verdict-row id past redaction into operator-visible surfaces. All four are the same defect class: a decision keyed on a weaker discriminator than the grammar requires.

Migration / operator actions

  • REDACTION_POLICY_SALT 2→3 and FINGERPRINT_ALGORITHM_VERSION 2→3: detection semantics changed for unchanged config, so memory cohort members must re-run /swarm memory link after upgrading (legacy cohort files surface as a re-link instruction, not a config-mismatch error).
  • Memory durable-secret rejection is tighter for embedded credential shapes: content embedding fixed-length credential shapes or real token shapes is now rejected/redacted where it previously passed.
  • Downgrade: do not roll back to a pre-upgrade build after lanes have settled with legacy-verdict-row-quarantine recovery entries — the older strict schema does not know that kind and a pre-upgrade reader nulls the whole coordination read. Drain running PR-review workflows (or complete them) before downgrading.

Residual disclosures

  • Over-redaction trade-offs introduced by the wider matching, accepted deliberately: the now-live aws_secret_access_key_context arm redacts the ENTIRE span (AKIA id, up to 200 chars of interposed text, and the 40-char value), so non-secret prose between an access-key id and a 40-char token on one line is removed from redacted output (at base that shape leaked the actual secret value instead); an =-labeled AWS secret whose value is LONGER than 40 chars has its first 40 chars redacted and the tail survives (real AWS secret keys are exactly 40 chars); quarantined rows no longer emit verdict_row_pipe_recovery telemetry (the row is diverted before the emit; the receipt entry carries the same fact).
  • #3397 item 2 residuals (the fix-or-disclose set): ghost-owner BLOCKED wording can overstate owner intent for never-launched lanes (RC-01); output_length/transcript_incomplete provider terminals are typed resource as an evidence grade, not an exact cause (RC-04/14/40); an internal fault inside validation is typed verdict_contract because the lane's contract check did fail — with this change an ABORTED VERDICT lane is never typed, which is the authenticity boundary (RC-41); pre-widening records parse as classless by intentional legacy tolerance (RC-19/36); coherence entries can outlive an interrupted settlement pass, cosmetic only (RC-09); the BLOCKED attempts sentence and the verdictAttempts projection render uncapped per-item lists (RC-11/12/22); the council-dispatch behavior change from #3388 is hereby disclosed (RC-13); stale retry-advice wording was removed by #3388 and any reappearance is a new defect (RC-16); latent schema-versioning gaps remain a tracked design debt for future persisted kinds (RC-15/17); test-quality gaps around exhaustion boundaries and projection pins remain (RC-25/26/28), and the five dispatch-lanes suites' temp-cleanup debt is untouched (RC-38). RC-20 (verdictAttempts documented in docs/architecture.md) is fixed in this PR. operator_cancelled exclusion remains a recorded product decision awaiting maintainer confirmation.

Known limitation

Quarantine evidence in analysis.actual still renders arities only (quarantinedArities) on the FAILURE path; the success path now carries the capped, itemized entries on the receipt surface.


Shell write-detector: launcher prefixes, quoted payloads, canonical spellings, and cd/pipe/Windows evasions closed

What

The shell write-scope gate classifies a dozen previously-admitted evasion
shapes identically to their canonical forms (#3474, #3475). Every change is
additive detection (fail-closed); the only newly-admitted shapes are the
correctly-tracked ones below.

  • Launcher prefixes no longer hide builtin writes: command rmdir ../x,
    builtin rmdir ../x, exec rmdir ../x, and timeout 5 rmdir ../x are
    detected and scope-checked like their unprefixed spellings, and
    command rm -rf ../x now reaches the destructive-command layer's
    recursive-delete check like its unprefixed spelling. command -v /
    -V are query forms and stay admitted.
  • Single-quoted sh -c 'rmdir ../x' / bash -c '...' payloads unwrap like
    double-quoted ones, including payloads that contain the other quote kind.
  • Versioned and path-qualified spellings of the write-vector sets classify
    canonically: /usr/bin/curl -o, curl8.5 -o, /usr/bin/sed -i,
    /usr/bin/tar -xzf, bzip2.5 -d, /usr/bin/cp (three-tier version
    lookup — canonical bzip2 stays itself).
  • cd a && cd b && … chains track every cd (;-chains already did): the
    final tracked directory resolves redirect targets AND eval working
    directories, so cd src && cd .. && echo x > escape.txt and
    cd src && cd .. && python -c … now block for a src/-scoped coder.
    || chains keep their previous (conservative) tracking.
  • Windows-grammar interpreter evals: C:\Python311\python.exe -c "…" (and
    the forward-slash spelling) classify as interpreter_eval under both cmd
    and PowerShell grammars, with the raw-word operator so a spelling both
    grammars see keeps its tracked context. A backslash-only spelling
    authorizes against the command's starting directory (the workspace root —
    outside every proper declared scope).
  • Inline-literal pipe sources feeding a bare (or --flag) interpreter
    classify as eval: echo 'open("../x","w")' | python and
    printf '…' | python - block for scoped coders. Dynamic producers
    (grep … | python) and every script-path form (cat f.py | python,
    python f.py, python -m …, python - < f.py) keep the #1901/#1902
    carve-out (maintainer ruling on #3474).
  • Mid-word # operands no longer truncate: cp src/x.txt src/x.txt#../../../zz resolves the FULL operand (bash-parser's
    quote-removal stage used to stop the word at the #), so the in-scope
    prefix can no longer mask an out-of-scope target. Word-start # comments
    are unaffected.
  • A leading-= rmdir operand loses the bare-rmdir exemption on every
    host (zsh =-expansion resolves the word to a PATH location the static
    containment check never saw; decision on #3475).

Why

Every one of these shapes was an execution-verified fail-open of the write
guard: a coder scoped to src/ could write outside the scope (and sometimes
outside the workspace) by spelling the write differently. The detector's
own residuals — read-only classification, the #1901 script-path carve-out,
and the bare-rmdir exemption's plain-line contract are unchanged
(command -v queries, timeout 5 make test, cat script.py | python,
in-workspace plain rmdir, and the destructive-command layer's original
analysis are all pinned by new suites). The exemption itself narrowed in
one disclosed way: a leading-= operand loses it (above). Repeated
Windows-grammar interpreter evals are now each retained with an occurrence
suffix instead of collapsing into one row (matching the POSIX behavior).

Migration

None. touch and mkdir remain undetected write commands (parity with the
pre-fix behavior for both their plain and prefixed spellings); making them
write-vector commands would change the bare-rmdir exemption's documented
mkdir x && rmdir x example and is out of scope.


Coders can remove an empty directory outside their declared scope

What

The shell write-scope gate no longer blocks a bare rmdir <dir> just because
the directory is outside the coder's declared scope, as long as the directory
is inside the workspace.

  • Admitted: rmdir <dir> with no options and exactly one literal, unquoted
    operand whose location, with every symlink or junction in its parent chain
    resolved, is strictly inside the session workspace, when the command runs
    under a POSIX shell (the bash tool, or the shell tool on a non-Windows
    host). The target must not exist yet or must be a real directory.
  • The exemption applies only to a plain command line, judged on the
    command text after the gate's existing eval / sh -c unwrapping.
    Launcher prefixes (command, builtin, exec, timeout DURATION, stdbuf) are unwrapped before that judgement, and a single-quoted sh -c / bash -c payload unwraps like a double-quoted one, so command rmdir ../outside and sh -c 'rmdir ../outside' get the ordinary declared-scope check (#3474/#3475). An operand beginning with = also loses the exemption (zsh =-expansion makes the static literal unsound there). That
    text may hold only letters, digits, spaces, newlines and the characters
    _ . / - = & | ; < >, and every command in it must be one of rmdir,
    mkdir, echo, true, false or test. None of these commands can
    change the shell's working directory, and without $, quotes,
    backslashes, backticks, parentheses, braces, brackets or # nothing in
    that text can expand, be evaluated, be quoted, define a function or start
    a comment, so rmdir runs in the directory the gate checks and removes
    exactly the operand the gate checks. && / || / ; lists, pipelines,
    redirections such as 2>/dev/null, FOO=1 rmdir x prefixes and if and
    loop bodies are fine:
    rmdir .scratch-a/tmp1, rmdir dir/, rmdir ./dir, rmdir a && rmdir b,
    mkdir x && rmdir x, rmdir a 2>/dev/null || true,
    rmdir a && echo done and FOO=1 rmdir a keep the exemption, and each
    rmdir of a distinct operand is judged separately.
  • Every other line gives every rmdir in it the ordinary declared-scope
    check. That includes any text with a character outside the set above:
    rmdir "dir", rmdir a && echo "done", echo $HOME; rmdir a,
    [ -d src ] && rmdir a, subshells and brace groups ((rmdir a),
    { rmdir a; }), case, $(...), backticks, any # (rmdir d#/x
    removes the one directory d#/x in bash, and rmdir a # note loses the
    exemption too), and paths containing ~ + , : @ %. An interior tab is
    refused outright as unsafe command text, and an interior carriage return
    is never exempt either. It also includes any other
    command in the line: cd, pushd, popd, function
    definitions, source and ., trap, alias,
    an upper-case RMDIR, and every other command
    (ls d && rmdir d, cd build && rmdir tmp). A line with a
    here-document normally loses the exemption too, because the gate reads
    the body and delimiter lines as commands that must also be on the list
    (a body on a plain line cannot expand anything). This closes code that
    bash evaluates at run time, such as the bash 5.3 current-shell
    substitutions ${ cd ..; } and ${| cd ..; }, including a quoted one
    that arithmetic ($((x))) or a test -v / [ -v subscript evaluates.
  • Unchanged (still blocked as before): rmdir -p, any other option
    (--ignore-fail-on-non-empty, --), several operands, globs, brace
    expansion, variables or command substitution in the operand, targets
    outside the workspace, symlink escapes, the workspace root itself, every
    recursive form (rm -rf, rmdir /s, rd /s), a target that exists as a
    file or symlink, and any rmdir run by a Windows executor (a
    powershell -Command / pwsh -c / cmd /c wrapper, a PowerShell-shaped
    command, or the shell tool on Windows). In PowerShell rmdir is an
    alias of Remove-Item, which also deletes files.
  • Never exempt (the ordinary declared-scope check applies):
    • a sequence that names the same operand more than once, for example
      rmdir a; cd ../other && rmdir a or rmdir a; rmdir -p a, because the
      gate cannot tell those writes apart;
    • an operand with a .. path segment (link/../x), even on a plain
      line;
    • repository control state: a target under the workspace's .git or
      .swarm directory; the exemption never applies there for any role.
  • File-authority checks still apply to the target and are unchanged; only
    the declared-scope membership check is skipped. Default-protected prefixes such as .github/workflows,
    docs/releases and node_modules are not separately protected for a bare
    rmdir (issue #3450 scopes the exemption to the whole workspace), so a
    coder whose declared scope excludes them can now remove an empty
    directory there. The target's parent directory must already exist when
    the gate runs: mkdir x && rmdir x keeps the exemption (the parent is the
    workspace root), but mkdir -p d/e && rmdir d/e in one command still gets
    the ordinary scope check because d does not exist yet.
  • /swarm guardrail explain has no tool or platform flag and evaluates the
    command as the shell tool, so on a Windows host it conservatively reports
    block for a bare rmdir that the live bash tool admits. The
    scope_validate MCP advisory does not apply the exemption at all and also
    stays conservative.

Why

rmdir(2) refuses to remove a non-empty directory (ENOTEMPTY), so a bare
rmdir cannot destroy file data. Blocking it protected nothing, but it left
agent-created empty scratch directories behind and forced a human to clean
them up. The gate does not check whether the directory is empty first: that
check would race with the command (TOCTOU). The kernel's refusal is the
authoritative check.

Migration

None.


Merge-queue failure diagnostics: an orphaned-entry detector and a self-hosted
stale-runner classifier.

scripts/check-merge-queue-progress.mjs --pr <n> names a stuck merge-queue
entry in one line (<VERDICT> pr=<n> age=<m> state=<state>): ORPHANED
(accepted past the 150-minute default threshold with zero active queue
builds and an OPEN PR — recovery line printed, exit 2; any other PR state
suppresses the recovery), BUILDING (any non-terminal queue build counts —
queued runs are active under runner contention), IDLE, or UNKNOWN (a
removed entry cross-checked against PR state, or a run window that cannot
reach the entry event). The detection authority is the last
queue|auto_merge timeline event plus a 100-run merge-group window on the
gh-readonly-queue/main/pr-<n>- prefix — never elapsed time alone, which
would force-cycle a healthy in-flight build.

scripts/ci/merge-group-stale-runner-classifier.ts --run <id> classifies a
failed merge-group batch from per-job step evidence (exit 0 for every
verdict, 1 for usage/gh errors): routed jobs that died at actions/checkout
with hosted legs green (modulo the unit-passed cascade) read
runner-stale-clone — the 2026-10-27 stale-runner signature that cost six
re-enqueue cycles — while any work-phase failure (Biome, a test, a coverage
threshold) reads test-failure and outranks a coexisting checkout death,
never excused by missing logs; unattributable failures read unknown.
Malformed job payloads degrade to unknown instead of crashing, and echoed
job names are control-character-sanitized.

The ci-fix-monitor protocol gains the orphaned-entry and batch-classification
steps under "Push and monitor".