@sema-agent/core 7.33.8 → 7.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +462 -3
- package/dist/agents/cross-session-judge.d.ts +16 -7
- package/dist/agents/cross-session-judge.js +18 -15
- package/dist/agents/peer-session-drain.d.ts +4 -0
- package/dist/agents/peer-session-drain.js +5 -2
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/subagent.d.ts +3 -0
- package/dist/agents/subagent.js +12 -3
- package/dist/brain/anthropic.js +1 -0
- package/dist/brain/errors.d.ts +23 -1
- package/dist/brain/errors.js +19 -2
- package/dist/brain/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/status-sink.d.ts +7 -1
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -1
- package/dist/brain/stream-shared.js +16 -4
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +379 -154
- package/dist/core/ask-origin.js +224 -45
- package/dist/core/ask-question.js +2 -1
- package/dist/core/ask-unresolvable-notice.d.ts +5 -2
- package/dist/core/ask-unresolvable-notice.js +4 -3
- package/dist/core/auto-mode-arming.d.ts +17 -4
- package/dist/core/auto-mode-arming.js +31 -6
- package/dist/core/auto-mode-defaults.d.ts +16 -7
- package/dist/core/auto-mode-defaults.js +2 -1
- package/dist/core/auto-mode-directive.d.ts +18 -30
- package/dist/core/auto-mode-directive.js +9 -18
- package/dist/core/auto-mode-prompt.d.ts +62 -1
- package/dist/core/auto-mode-prompt.js +42 -4
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +1 -0
- package/dist/core/auto-mode.d.ts +32 -3
- package/dist/core/auto-mode.js +80 -41
- package/dist/core/checkpoint-store.d.ts +54 -35
- package/dist/core/checkpoint-store.js +58 -3
- package/dist/core/child-clamp-seats.js +1 -0
- package/dist/core/classifier-unavailable-arm.d.ts +57 -0
- package/dist/core/classifier-unavailable-arm.js +17 -0
- package/dist/core/effective-path-target.d.ts +12 -0
- package/dist/core/effective-path-target.js +18 -7
- package/dist/core/engine-notice.d.ts +53 -2
- package/dist/core/fs-write-gate-policy.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.js +4 -1
- package/dist/core/gate-fold.js +19 -3
- package/dist/core/gate-lanes.js +116 -50
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +18 -2
- package/dist/core/governance-codes.js +23 -1
- package/dist/core/hooks.d.ts +53 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/message-utils.d.ts +2 -1
- package/dist/core/message-utils.js +2 -1
- package/dist/core/mode-policy.d.ts +185 -0
- package/dist/core/mode-policy.js +184 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +20 -3
- package/dist/core/permission-rule-model.js +40 -3
- package/dist/core/permission-rule-provider.d.ts +22 -11
- package/dist/core/permission-rule-provider.js +17 -7
- package/dist/core/permission-rules.d.ts +23 -1
- package/dist/core/permission-rules.js +56 -9
- package/dist/core/persisted-rule-arms.d.ts +14 -10
- package/dist/core/persisted-rule-arms.js +13 -6
- package/dist/core/present-plan-tool.d.ts +1 -1
- package/dist/core/present-plan-tool.js +3 -3
- package/dist/core/read-only-shell.d.ts +19 -19
- package/dist/core/read-only-shell.js +38 -12
- package/dist/core/retired-keys.d.ts +9 -3
- package/dist/core/retired-keys.js +5 -1
- package/dist/core/runner/active-skill-scope.js +5 -4
- package/dist/core/runner/assemble-result.js +2 -2
- package/dist/core/runner/classifier-ladder.d.ts +43 -1
- package/dist/core/runner/classifier-ladder.js +29 -0
- package/dist/core/runner/contracts.d.ts +62 -38
- package/dist/core/runner/denial-limit-arms.d.ts +35 -7
- package/dist/core/runner/denial-limit-arms.js +15 -9
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/file-history-request.d.ts +26 -0
- package/dist/core/runner/file-history-request.js +90 -0
- package/dist/core/runner/gate-exit.d.ts +30 -4
- package/dist/core/runner/gate-exit.js +28 -7
- package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
- package/dist/core/runner/permission-rule-lanes.js +55 -48
- package/dist/core/runner/persisted-path-rows.d.ts +64 -0
- package/dist/core/runner/persisted-path-rows.js +89 -0
- package/dist/core/runner/prepare-artifact.d.ts +5 -1
- package/dist/core/runner/prepare-artifact.js +9 -3
- package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
- package/dist/core/runner/prepare-ask-lane.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
- package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
- package/dist/core/runner/prepare-file-history.d.ts +9 -3
- package/dist/core/runner/prepare-file-history.js +4 -90
- package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
- package/dist/core/runner/prepare-gate-stations.js +46 -29
- package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
- package/dist/core/runner/prepare-hands-readface.js +64 -53
- package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
- package/dist/core/runner/prepare-inherited-gate.js +45 -25
- package/dist/core/runner/prepare-park-ask.d.ts +6 -4
- package/dist/core/runner/prepare-park-ask.js +7 -5
- package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
- package/dist/core/runner/prepare-policy-chain.js +203 -180
- package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
- package/dist/core/runner/prepare-protocol-tools.js +13 -3
- package/dist/core/runner/prepare-run-refs.d.ts +6 -6
- package/dist/core/runner/prepare-run-refs.js +9 -4
- package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
- package/dist/core/runner/prepare-safety-scan.js +19 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
- package/dist/core/runner/prepare-suspend-saga.js +6 -6
- package/dist/core/runner/prepare-task.js +56 -24
- package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
- package/dist/core/runner/prepare-wiring-manifest.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +9 -0
- package/dist/core/runner/read-root-grant.js +12 -0
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-brain-sinks.js +3 -1
- package/dist/core/runner/run-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +29 -18
- package/dist/core/runner/run-notification-lane.d.ts +3 -0
- package/dist/core/runner/run-notification-lane.js +3 -1
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +1 -1
- package/dist/core/runner/runtask.d.ts +1 -0
- package/dist/core/runner/runtask.js +8 -11
- package/dist/core/runner/session-mode-seats.d.ts +48 -0
- package/dist/core/runner/session-mode-seats.js +73 -0
- package/dist/core/runner/session-rule-policy.js +3 -2
- package/dist/core/runner/stream-mode-verb.d.ts +27 -0
- package/dist/core/runner/stream-mode-verb.js +19 -0
- package/dist/core/runner/turn-attachments.d.ts +3 -3
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -1
- package/dist/core/runner/write-protection-station.js +6 -3
- package/dist/core/runner-deps.d.ts +31 -34
- package/dist/core/runtime-caps.d.ts +12 -3
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +1 -1
- package/dist/core/secret-env.d.ts +30 -58
- package/dist/core/secret-env.js +149 -586
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +41 -31
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
- package/dist/core/task-event.d.ts +1 -1
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +41 -89
- package/dist/core/task-stream.d.ts +16 -0
- package/dist/core/terminal-cause.js +1 -0
- package/dist/core/tighten-task-spec.d.ts +2 -2
- package/dist/core/tighten-task-spec.js +12 -14
- package/dist/core/tool-catalog-entries.js +4 -3
- package/dist/core/tool-policy.d.ts +84 -30
- package/dist/core/tool-policy.js +76 -16
- package/dist/core/tool-registry.d.ts +38 -0
- package/dist/core/tool-registry.js +25 -1
- package/dist/core/tool-roster.d.ts +17 -2
- package/dist/core/tool-roster.js +11 -1
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +18 -6
- package/dist/core/types.d.ts +2 -1
- package/dist/core/wiring-manifest.d.ts +31 -5
- package/dist/core/wiring-manifest.js +28 -1
- package/dist/core/write-protect.d.ts +11 -1
- package/dist/core/write-protect.js +26 -15
- package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
- package/dist/engine/execution-env/node-execution-env.js +16 -3
- package/dist/engine/harness/agent-harness.js +2 -1
- package/dist/engine/harness/types.d.ts +6 -7
- package/dist/engine/llm/types.d.ts +2 -4
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/lsp/node-lsp-manager.js +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +15 -6
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-size-guideline.js +2 -2
- package/dist/stores/file/index.d.ts +5 -3
- package/dist/stores/file/session-store.d.ts +8 -2
- package/dist/stores/file/session-store.js +5 -5
- package/dist/tools/artifact/artifact-tool.d.ts +17 -0
- package/dist/tools/artifact/artifact-tool.js +16 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
- package/dist/tools/fs/bash-readonly-classifier.js +88 -0
- package/dist/tools/fs/containment-face.d.ts +9 -3
- package/dist/tools/fs/containment-face.js +2 -0
- package/dist/tools/fs/file-read-check.d.ts +50 -0
- package/dist/tools/fs/file-read-check.js +79 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-read.d.ts +1 -0
- package/dist/tools/fs/fs-search-tools.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.js +4 -4
- package/dist/tools/fs/fs-shared.js +20 -6
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +39 -3
- package/dist/tools/fs/index.js +38 -15
- package/dist/tools/fs/read-deny-home.d.ts +93 -0
- package/dist/tools/fs/read-deny-home.js +225 -0
- package/dist/tools/fs/read-deny.d.ts +176 -34
- package/dist/tools/fs/read-deny.js +268 -61
- package/dist/tools/fs/safety.d.ts +6 -1
- package/dist/tools/fs/safety.js +18 -5
- package/dist/tools/fs/search.d.ts +3 -0
- package/dist/tools/fs/search.js +47 -9
- package/dist/tools/fs/shell-read-check.d.ts +52 -13
- package/dist/tools/fs/shell-read-check.js +199 -20
- package/dist/tools/fs/shell-read-establish.d.ts +7 -3
- package/dist/tools/fs/shell-read-establish.js +134 -21
- package/dist/tools/fs/shell-read-identity.d.ts +7 -0
- package/dist/tools/fs/shell-read-identity.js +21 -0
- package/dist/tools/fs/shell-read-judge.d.ts +92 -32
- package/dist/tools/fs/shell-read-judge.js +43 -18
- package/dist/tools/fs/shell-read-plan.d.ts +4 -0
- package/dist/tools/fs/shell-read-plan.js +26 -4
- package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
- package/dist/tools/fs/shell-read-write-targets.js +173 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +117 -11
- package/wire-changes/core@7.34.0.json +2621 -0
- package/wire-changes/core@7.35.0.json +584 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,462 @@
|
|
|
1
|
+
## 7.35.0 — 2026-09-29
|
|
2
|
+
|
|
3
|
+
**Hard-breaking changes — the whole release in three sentences.**
|
|
4
|
+
|
|
5
|
+
① *Who is affected.* (#1201) A deployment that relied on 7.33.x withholding a credential written into git's env config (`GIT_CONFIG_*`) or into a proxy variable; a deployment that keeps connection strings, auth headers, webhook URLs or token-bearing URLs in the engine's own environment; a deployment that relied on v7.34.0 withholding `GIT_SSH_COMMAND` / `GIT_PROXY_COMMAND` whose value carried a credential (`sshpass -p …`, `ssh -o SetEnv=TOKEN=…` — read by 7.34.0's git-family lexer, handed on as written now, KL-1201c); a caller passing `env` or `platform` to `isSecretEnvKey` / `scrubSecretEnv`; a consumer that enumerates `SecretEnvFindingKind`. (#1199) A consumer exhaustive over `GovernanceCode` (a `Record<GovernanceCode, …>`: server's HTTP mapping, cli / client-core tables) or one that reads `config.tool_mount_denied` as a governance verdict. (#765) A consumer mapping the `spec.tools` declaration door's refusal codes by hand (server S-271). (#1194) A host policy, hook script or hook matcher keyed on a tool's alias spelling; a host driving `runToolGate` directly (and its own `shellReadCheck`); a stored grant keyed on an alias; a caller tool that takes an engine tool's name (`Bash`, `Read`, `Write`, …) as an alias. (#1204) A consumer that validates `requestId` against the old alphabet `^[A-Za-z0-9._:-]{1,128}$`, or that reads a `responseModel` over 128 characters or with a space or a non-ASCII byte. (#1205) Code that constructs a `ModeArbitrationRow`. (#1186) A host that builds `NodeExecutionEnv` from configuration holding a malformed `inheritEnv`.
|
|
6
|
+
|
|
7
|
+
② *How to change.* (#1201) Proxies need nothing (a child now receives the proxy, credential included); a credential in git's env config moves to a credential helper or a file (the family is handed on as written); a value a child needs goes through `shellEnv`, an `inheritEnv` allow list or `NodeLspManagerOptions.env`; kinds: `suffix-rule` → `name-rule`, add `prefix-rule` / `value-rule`, drop `family-rule`; calls drop their third / fourth argument. (#1199 / #765) Delete the `config.tool_mount_denied` row from a `Record<GovernanceCode, …>` and key the declaration codes on `ToolDeclarationCode` (`Record<ToolDeclarationCode, …>`, `TOOL_DECLARATION_CODES`). (#1194) Key a policy, hook or grant on the wire name, or match through `ruleNamesOf(req)`; resolve `event.toolName` with `rosterToolName` before `runToolGate`; a caller tool drops the engine name from its aliases or declares its own `pathTarget`. (#1204) Accept what `isProviderEchoShape` accepts (`PROVIDER_ECHO_SHAPE` is the consumer's copy); read an absent `responseModel` as "not reported". (#1205) Supply `hookAskFloor`. (#1186) Write `"scrub"`, `"all"` or a list of variable names.
|
|
8
|
+
|
|
9
|
+
③ *If you do not.* (#1201) A credential in git's env config or in a proxy variable reaches the model's shell and every other spawned child (leak direction); a variable whose value carries a credential is gone from the child and named in `config.secret_env_scrubbed`; a `switch` exhaustive over the old kinds and a three- or four-argument call fail to compile. (#1199) A `Record<GovernanceCode, …>` literal that still spells the retired row fails to compile, and a failed child carrying `config.tool_mount_denied` classifies `logic` (not retryable) whatever your table says. (#765) A hand-kept code table has no compile-time tie to the door and misses the next code the set gains. (#1194) The policy, PreToolUse hook or matcher keyed on the alias no longer sees it — every policy check and PreToolUse hook receives the wire name (a PostToolUse hook still receives the model's spelling, KL-1194-c); a stored alias grant no longer matches and the call is asked once more (fail-closed); a caller tool wearing an engine tool's name is held by that name's checks under every name it is called by — asked under `default`, refused under `dontAsk` (KL-1194-e). (#1204) A validator on the old alphabet refuses a gateway id holding `/`, `#`, `[` or similar that the engine now carries. (#1205) A hand-built row fails to compile. (#1186) The constructor throws `NodeExecutionEnv: <reason>` where it used to run scrubbed.
|
|
10
|
+
|
|
11
|
+
**Closed sets, one by one.**
|
|
12
|
+
- `SecretEnvFindingKind` (#1201): `"suffix-rule" | "namespace-rule" | "exact-name" | "family-rule"` → `"exact-name" | "name-rule" | "namespace-rule" | "prefix-rule" | "value-rule"` — `suffix-rule` renamed `name-rule`, +`prefix-rule`, +`value-rule`, −`family-rule`.
|
|
13
|
+
- `GOVERNANCE_CODES` / `GovernanceCode` (#1199): −`config.tool_mount_denied` (a caller-shape refusal, now in `TOOL_DECLARATION_CODES`); the table's admission rule is unchanged.
|
|
14
|
+
- `TOOL_DECLARATION_CODES` / `ToolDeclarationCode` (#765): a new exported closed set of 9 — `config.tool_mount_denied`, `config.tool_roster_bound`, `config.tool_name_too_long`, `config.tool_contract_too_long`, `config.tool_schema_bound`, `config.tool_face_invalid`, `config.tool_name_invalid`, `config.egress_requires_write_effect`, `config.reserved_tool_name`. `RosterBoundCode` keeps its 3 members, each one of these.
|
|
15
|
+
- `MODE_ARBITRATION` (#1205): +1 column `hookAskFloor` on every row — not a closed-set change (the rows and their classes are as before); `ModeArbitrationRow` gains it as a required member.
|
|
16
|
+
- Package exports (`npm run handoff:diff v7.34.0 <RC>`): +7 / −0 — `rosterToolName`, `ruleNamesOf` (#1194), `wrapToolPolicy` (#1200), `PROVIDER_ECHO_SHAPE`, `isProviderEchoShape` (#1204), `TOOL_DECLARATION_CODES`, `ToolDeclarationCode` (#765); `isSecretEnvKey` / `scrubSecretEnv` narrowed in place (#1201). Codes / wire keys +0 / +0; one new trace kind `brain.echo_dropped` (#1204).
|
|
17
|
+
|
|
18
|
+
**The wire list's BREAKING items** (`node scripts/wire-changes.mjs v7.34.0 <RC>`), one per line:
|
|
19
|
+
- closed set lost members: `SecretEnvFindingKind` −`family-rule`, −`suffix-rule` (#1201; +`name-rule` / `prefix-rule` / `value-rule`).
|
|
20
|
+
- closed set lost members: `ExactEnvNameVerdict` −`credential`, −`exempt`, −`unknown` (#1201; a module-level type retired with `classifyExactEnvName`, never a package export).
|
|
21
|
+
- type gained required keys: `ModeArbitrationRow` +`hookAskFloor` (#1205).
|
|
22
|
+
- type gained required keys: `Prepared` +`endModeCeiling` (#1192; the runner's prepared record, reached from the package root through `Runner.SuspendReap.env`).
|
|
23
|
+
- not on the wire list (a keyed table, not a spelled array): `GOVERNANCE_CODES` −`config.tool_mount_denied` (#1199) — stated here by hand.
|
|
24
|
+
- narrowings the list carries from the criteria: F3 (every non-exempt variable's value is read), F5 (every `OTEL_*` withheld) (#1201).
|
|
25
|
+
|
|
26
|
+
### The child-process env scrub takes the reference's form: one judgment per variable — exempt families, known credential names, the `OTEL_` prefix, the name shape, the value predicate (#1201, design/1159, C-R178; **BREAKING — API / closed set / behavior in both directions**; package export symbols ±0, 2 signatures narrowed, `SecretEnvFindingKind` renamed +2 −1; +0 wire keys, +0 codes) (@server @cli @client-core @test)
|
|
27
|
+
|
|
28
|
+
- **Before (v7.34.0).** Names: the credential words (`KEY` / `TOKEN` / `SECRET` / `PASSWORD` / … and plurals) only at the END of a name, 19 exact names (connection strings, `NPM_AUTH`, the seven OTLP header lists, …) and the `SEMA_` namespace. Values: read only in git's env config family and six proxy variables — git's grammar (`GIT_CONFIG_PARAMETERS` parsed, `GIT_CONFIG_COUNT` read as `strtoul`, index pairing, a withheld entry replaced in place by `sema.withheld`, platform-dependent name folding) and a shell lexer over helper / `sshCommand` / alias values. Every other value was unread: `REDIS_URL=redis://:<pw>@…`, `CURL_HEADER=Authorization: Bearer …`, `SENTRY_DSN`, a remote URL with a token userinfo reached the child.
|
|
29
|
+
- **Now (the reference 2.1.280 subprocess-env scrub: the per-variable loop `Zs` cli280.js @8152531, exemption `Brt` @7081059, names `ni` @7071746, name shape `$J` @7089877 / `rPt` @7091280 / `nPt` @7090190, value predicate `Rv` @7098946).** One judgment per variable, the first rule that answers wins, the action is always "delete the whole variable" (a value is never rewritten):
|
|
30
|
+
1. **exempt** — git's env config (`GIT_CONFIG_COUNT`, `GIT_CONFIG_PARAMETERS`, `GIT_CONFIG_KEY_<n>`, `GIT_CONFIG_VALUE_<n>`, in capitals) and the proxy family (`http_proxy` / `https_proxy` / `ftp_proxy` / `all_proxy` / `no_proxy`, `npm_config_(https_)proxy` / `npm_config_noproxy`, `yarn_proxy`, `yarn_` / `global_agent_` / `docker_` / `claude_code_` `http(s)_proxy` / `no_proxy`, `cloudsdk_proxy_*`, `electron_get_use_proxy`, any case): handed on as written — name and value unread;
|
|
31
|
+
2. **`exact-name`** (high) — the reference's 95 credential names and their `INPUT_` spellings, compared upper-cased with `-` read as `_`;
|
|
32
|
+
3. **`prefix-rule`** (high) — every `OTEL_*` variable (any case, also `INPUT_OTEL_*`);
|
|
33
|
+
4. **`namespace-rule`** (medium) — the `SEMA_` namespace, unchanged;
|
|
34
|
+
5. **`name-rule`** (medium) — the reference's name shape: a credential word where it starts the name or follows `_` and is followed by the end, `_` or a digit (anywhere in the name, not only at its end), the words `AUTH` / `COOKIE` / `PAT` / `DSN` / `WEBHOOK` / `CREDS` / `…KEY` compounds, `PWD` / `PASS` / `JWT` after `_`, `TOKEN` / `SECRET` / `PASSWORD` / `PASSWD` / `PASSPHRASE` anywhere (`AUTHTOKEN`), the same read over the camel-case split (`githubToken`), connection strings (`DB_CONN_STR`), Bundler source credentials (`BUNDLE_<HOST>__<TLD>`), package publishers' login names (`ORG_GRADLE_PROJECT_*User`, `POETRY_HTTP_BASIC_*`, `INPUT_*_USERNAME`); plus the plural `TOKENS` unless a count word touches it (`MAX_TOKENS` kept, `CONTEXT_GITHUB_TOKENS` withheld); the GitHub token names are withheld; a leading `AUTH0_` is not a word; `GIT_CONFIG_KEY_<digit>…` is not a credential name. The 18 exact exemptions (`SORT_KEY`, `MAX_NEW_TOKENS`, …) pass this rule only;
|
|
35
|
+
6. **`value-rule`** (high) — the reference's value predicate over every non-exempt value's first 8192 UTF-16 code units: a URL userinfo (after every `scheme://`, or a leading `//`) holding a password or a token-like user (a known token prefix — `ghp_`, `glpat-`, `xoxb-`, `sk-`, `AKIA`, `eyJ`, `npm_`, … — or ≥ 20 characters mixing letters and digits; `ssh://git@host` and `https://jane@host` are not credentials); a scheme-less `user:pass@host` (drive paths, image digests, npm aliases, Maven coordinates, `host:port/path@<ref>` and mail / chat addresses excluded); a credential assignment (`password=` / `api_key:` / `…_auth=` with a value that is not a flag word); a Slack / Discord / Teams webhook URL; `sonar.login`; a private-key header; `Authorization` / `token` / `Bearer` / `Basic` followed by credential-shaped content.
|
|
36
|
+
`isSecretEnvKey(key, value?)` and `scrubSecretEnv(env, findings?)` answer with that one judgment (with `value`, `isSecretEnvKey` is exactly the scrub's answer for the entry). `src/core/secret-env.ts` no longer imports `src/shell/lex.ts`. The `inheritEnv` three-state policy, the default `"scrub"`, `announceSecretEnvScrub` and the `config.secret_env_scrubbed` notice (code, message prefix, `detail.names`) are unchanged — only which names it lists.
|
|
37
|
+
- **BREAKING.** This entry's three sentences are the #1201 rows of the release's hard-breaking block at the head of this section.
|
|
38
|
+
- **API.** `isSecretEnvKey(key, value?, env?, platform?)` → `isSecretEnvKey(key, value?)`; `scrubSecretEnv(env, findings?, platform?)` → `scrubSecretEnv(env, findings?)`; `SecretEnvFindingKind` `"suffix-rule" | "namespace-rule" | "exact-name" | "family-rule"` → `"exact-name" | "name-rule" | "namespace-rule" | "prefix-rule" | "value-rule"` (confidence: `exact-name` / `prefix-rule` / `value-rule` high, `name-rule` / `namespace-rule` medium). This entry adds and removes no package export symbol (the release's count is at the head of this section); the module-level (non-package) exports `readGitConfigParameters`, `classifyExactEnvName` and the type `ExactEnvNameVerdict` are removed. The placeholder entry `sema.withheld` is retired: a child's `git config --list` no longer shows `sema.withheld=` lines.
|
|
39
|
+
- **Name delta** (the design's corpus, 104 recoverable rows, `test/fixtures/secret-env-name-table.ts`). *Newly withheld:* `SKIP_AUTH` · `DISABLE_AUTH` · `BASIC_AUTH` · `REQUIRE_AUTH` · `NO_AUTH` · `SESSION_COOKIE` · `MY_JWT` · `SENTRY_DSN` · `FILE_PAT` · `DB_CONN_STR` · `MyApiToken` · `githubToken` · `SSH_AUTH_SOCK` (`name-rule`), `DISCORD_WEBHOOK` (`exact-name`), `OTEL_SERVICE_NAME` (`prefix-rule`). *No longer withheld by name:* `DATABASE_URL` (its value decides: `postgres://u:p@h/db` is withheld, `postgres://h/db` is not), `GIT_CONFIG_KEY_0` (exempt). *Beyond the corpus:* a credential word as any segment now withholds (`API_KEY_FILE`, `MODEL_KEY_deepseek`, `SEMAPHORE_KEY_COUNT`, `SORT_KEY_2`), as do `WEBHOOK_URL` and every `OTEL_*`. *Kind changes:* `PGPASSWORD` / `MYSQL_PWD` / `SSHPASS` / `GPG_PASSPHRASE` / `GH_PAT` / `GITHUB_PAT` / `NPM_AUTH` / `NPM_CONFIG__AUTH(TOKEN)` / `CONNECTION_STRING` / `DSN` `exact-name` (high) → `name-rule` (medium); the OTLP header lists `exact-name` → `prefix-rule`; `ANTHROPIC_API_KEY` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` / … `suffix-rule` (medium) → `exact-name` (high); `SEMA_API_KEY` `suffix-rule` → `namespace-rule`.
|
|
40
|
+
- **Who is hurt / compensation.** *server*: the S-844 cross-check by name holds (21 of the catalog's 24 secret-typed names withheld by core, the three list-shaped knobs as before); the S-845 pin (a credential in `GIT_CONFIG_PARAMETERS` does not reach the child) and the S-776 × #1150 negative control (a proxy userinfo is withheld) invert — at core's layer both families now reach the child as written, and a proxy behind git's libcurl receives the proxy credential exactly as from the unscrubbed environment (the wire sheet states how to read each pin now); `DEPLOY-PREREQS.md` 110–117 / 155–195 need the new reading; the `secret_env_scrubbed_total{kind}` buckets rename and `test/helpers/family-rule-bucket.ts` has nothing to count. *cli*: L-812 / L-821 — the shell no longer needs a git-family reading of its own. *client-core*: the notice code is unchanged — no code change. *test*: the 7.33.x git-family cells are void (wire sheet). *A child that needs ssh-agent or OpenTelemetry*: `SSH_AUTH_SOCK` and every `OTEL_*` are withheld — hand them back through `shellEnv` / an `inheritEnv` allow list. *A child that reads a connection string or a token-bearing URL from the environment*: it is withheld and named in the notice — hand it back the same way. *A deployment that relied on the model not seeing a proxy password or a credential written into git's env config*: it now does (KL-1201c) — keep such credentials out of the engine's own environment (a credential helper, a file, a proxy that authenticates by source).
|
|
41
|
+
- Pins: `test/secret-env-name-table.test.ts` (the delta rows against a frozen 7.34.0 name oracle, the 95 names in four spellings, the 18 exemptions × value, the prefix, the catalog, kinds × confidence, the writer, the notice, a real oracle-gate spawn — both entry points on every row); re-pinned `backlog1099-sema-namespace-secret-env` (namespace consulted before the name shape; its reach against a segment oracle), `backlog1180-otlp-headers-secret-env` (the family by prefix, real shell and language server), `rb219-secret-env-exemptions`, `rb209-findings-channel`, `rb209-secret-redaction`, `hardening-permission-core` I8, `secret-env-export`. The value side and the exempt families: `secret-env-value-predicate` (the corpus in `test/fixtures/secret-env-value-corpus.ts`: 82 withheld / 90 handed on / 12 residue values, each labelled with the part of the reference predicate that answers — userinfo, token-like user, `host:port/path@<ref>` and its host tests, package references, scheme-less `user:pass@host`, assignments, webhooks, `sonar.login`, private-key headers, `authorization` / `token` / `Bearer` / `Basic`, the 8192-code-unit limit in ASCII and astral fillers; both entry points on every row; every verdict equal to a verbatim port of the reference predicate; each of the predicate's 105 pattern alternatives pinned by a row that flips without it, but three that an earlier test always answers first), `backlog1129-git-config-key-family` (the four name forms handed on whatever they hold, every other spelling judged as an ordinary variable, real-git controls), `backlog1152-git-env-config-sources` (rewritten: the #1150 loopback proxy now hears the same `Proxy-Authorization` before and after the scrub; the whole proxy table × three spellings; names outside the table; a malformed `GIT_CONFIG_PARAMETERS` / `GIT_CONFIG_COUNT` handed on and refused by git alike), `secret-env-git-differential` (rewritten: 8 samples — ordinary family, one credential entry, `GIT_CONFIG_PARAMETERS` — on the PATH git and the command-line tools' git: `git config --list` and `git credential fill` byte for byte as before the scrub, the family's credential helper answering its password on both sides, the planted credential name / value withheld from the shell git starts), `backlog1073-lsp-child-env` (the language server receives both families), `secret-env-spawn-site-controls` (one positive control per start site: oracle gate, language server, tree-kill `ps`, lock-probe `ps`), `backlog1183-point-free-callbacks` (the gate's real-site pin now on `memory-recall.ts`). Known limits: KL-1201a (what the value predicate does not read), KL-1201b (what it and the name shape over-withhold — among the values, as in the reference, a URL whose query carries another URL with an `@` in its path (`https://example.invalid/callback?next=https://docs.invalid/@scope/pkg`: everything before the last `@` is read as userinfo, the nested scheme's `:` as the password separator) and a bracketed IPv6 host followed by `/@scope` (`https://[::1]:4873/@scope/pkg`: the `host:port/path@<ref>` exemption's host test does not take brackets, so the port's `:` reads as the separator); both variables are withheld whole), KL-1201c (the exempt families reach the child); KL-1129a/b/c/d/e, KL-1150, KL-1159 and KL-1180 are closed or rewritten.
|
|
42
|
+
|
|
43
|
+
### BREAKING (closed set) — the `spec.tools` declaration door's refusal codes are one exported closed set, and `config.tool_mount_denied` leaves `GOVERNANCE_CODES` (#765, #1199; +2 exports `TOOL_DECLARATION_CODES` / `ToolDeclarationCode`; `GOVERNANCE_CODES` / `GovernanceCode` −1 member) (@server @cli @client-core)
|
|
44
|
+
|
|
45
|
+
- **Before (v7.34.0).** A caller tool refused at prepare reached `terminal.code` as an open string: three roster-bound codes (`RosterBoundCode`), `config.tool_schema_bound`, `config.tool_face_invalid`, `config.tool_name_invalid` and `config.egress_requires_write_effect` sat in no registry, while the same door's `config.tool_mount_denied` sat in `GOVERNANCE_CODES` against that table's own admission rule (governance = a per-principal policy verdict; a caller or deployment configuration error is `logic`) — so a failed child carrying it was classified `governance` / terminal.
|
|
46
|
+
- **Now.** `TOOL_DECLARATION_CODES` (with `ToolDeclarationCode`) is every code the `spec.tools` declaration door refuses a tool entry with: `config.tool_mount_denied`, `config.tool_roster_bound`, `config.tool_name_too_long`, `config.tool_contract_too_long`, `config.tool_schema_bound`, `config.tool_face_invalid`, `config.tool_name_invalid`, `config.egress_requires_write_effect` (also minted, same meaning, for a protocol tool's resolved axes), `config.reserved_tool_name` (a name the run reserves for a tool the engine injects — structured output, the skill tool, tool search, the offload reader, an injected MCP / A2A tool; also `runGoal`'s completion tool, refused before any run). Every mint site at the door is typed against the set (a new door code outside it does not compile); `RosterBoundCode` is a subset of it. `config.tool_mount_denied` is no longer a governance code (`governanceRetryClass` → `undefined`; a failed child carrying it classifies `logic`, not retryable); the table's admission rule is unchanged. `config.tool_model_gate_invalid` is not in the set (it also judges `RunnerDeps.toolModelGate`).
|
|
47
|
+
- **Who is hurt / compensation.** *An exhaustive consumer of `GovernanceCode`* (a `Record<GovernanceCode, …>` — server's HTTP mapping, cli / client-core tables): delete the `config.tool_mount_denied` row and map it from `ToolDeclarationCode` instead. *A consumer mapping the declaration codes by hand* (server S-271): key a `Record<ToolDeclarationCode, …>` on the set. *A parent reading a child's `errorKind`*: `logic` for an unconstructable tool entry.
|
|
48
|
+
- Pins: `test/backlog765-tool-declaration-codes.test.ts` — D1 (the set, `RosterBoundCode` ⊆), D2 (each code from a real run's terminal, the reserved name beside `outputSchema` included), D3 (disjoint from governance; `logic`). Mutations D922–D923, D943. Known limit: KL-765-a (`config.tool_model_gate_invalid` outside the set; a non-array `aliases` refused without a code).
|
|
49
|
+
|
|
50
|
+
### The tool shell refuses a malformed `inheritEnv` at construction — the oracle gate's judgment (#1186; behavior (a bad value is loud); +0 exports, +0 codes)
|
|
51
|
+
|
|
52
|
+
- **Before (v7.34.0).** `new NodeExecutionEnv({ inheritEnv })` with an unknown word (`"none"`, `"Scrub"`), a non-string, or a list entry that is not a variable name (empty, holding `=` or a NUL) constructed, and every exec inherited the scrubbed environment — the safe direction, but silent. The oracle gate's `inheritEnv` has refused the same values since 7.33.8 (an `error` gate).
|
|
53
|
+
- **Now.** The constructor asks `inheritEnvPolicyProblem` — the one judgment both spawn sites share — and throws `NodeExecutionEnv: <reason>`. Absent, `"scrub"`, `"all"` and a list of names construct as before; a judged list is the environment's own copy (a name pushed onto the caller's array afterwards is not inherited).
|
|
54
|
+
- **Who is hurt / compensation.** A host that builds the environment from untyped configuration holding a malformed value: construction throws where it used to run scrubbed — write one of the three forms.
|
|
55
|
+
- Pins: `test/backlog1186-shell-inherit-env-bad-value.test.ts` — ten malformed values (the reason text equal to the gate's), five controls, the copied allow list over a real exec. Mutation D939.
|
|
56
|
+
|
|
57
|
+
### A child's permission-mode ceiling outlives the leg that spawned it — after that leg ends it is the session's current leg, so a session-scoped background child follows the session's next leg; an ancestor's classifier answers only while the ancestor runs `auto` (#1192, #1193; behavior (tighten); +0 exports, +0 codes, +0 wire keys) (@server @cli @test)
|
|
58
|
+
|
|
59
|
+
- **Before (v7.34.0).** The chain a leg hands its children carried that leg's own mode controller as the ceiling. A `backgroundScope:"session"` child outlives its leg, so it kept that leg's mode: run 1 under `bypassPermissions` spawned it, run 2 of the same session started in `plan`, and the child still wrote outside the root (Write and shell) with nobody asked. Separately, an ancestor's classifier frozen onto a child's chain entry kept answering the ancestor's asks after the ancestor was steered out of `auto`: the parent's own `sensitive_op` asked the person, the child's same call was auto-allowed and ran.
|
|
60
|
+
- **Now (one ceiling for both).** The Runner keeps one mode seat per session, bound by each leg when its prepare commits (the most recently committed leg). The chain a leg hands its children carries the leg's CEILING: the leg's own live mode while the leg runs — exactly what a child read before — and, once the leg has STOPPED (its loop settled; a cancelled leg whose tool is still pending has not), its session's seat. A leg still running keeps answering its own children whatever another leg of the session commits meanwhile (a run on a new session is not serialized by the session lock). An ancestor's classifier on a chain entry rides with the same ceiling and answers the ancestor's asks only while it reads `auto`; otherwise they go to the ancestor's frozen approver — as the ancestor's own gate does under `default` / `acceptEdits`; an ancestor running `dontAsk` refuses such an ask at its own gate, while the child's inherited ask reaches the frozen approver (a person). When the session's next leg folds a child's own declared mode, the child's fold notice (`config.permission_mode_unavailable`, `by:"inherited_ceiling"`) is delivered as at prepare; a child with no word of its own follows silently, as it does under a steer (KL-1192-a). Probes: run 2 in `plan` ⇒ the child's out-of-root Write and shell write do not happen; steer `auto`→`default` after the spawn ⇒ the child's call asks the person.
|
|
61
|
+
- **Who is hurt.** A session-scoped background child whose session's next leg runs a narrower mode is refused or asked where it ran before — the point. The rule reads the session's mode both ways: a child spawned under `default` whose session's next leg is `bypassPermissions` has its shell gate released (its file tools keep the root fence its own prepare resolved). A seat is per Runner instance: a session continued on another Runner or process does not rebind it (KL-1192-b).
|
|
62
|
+
- Pins: `test/backlog1192-session-mode-seat.test.ts` — S1 (bypass → plan, real Runner, two runs), S2 / S3 controls (bypass → bypass; the same-run steer), S4 (default → bypass), F1–F3 (the seat and the fold notice), F4 (the fold notice on a real Runner: a surviving child's own word, the parent session's next leg), F5 / C1 / C2 / C3 (an overlapping leg of the same session does not move a running leg's children, nor a cancelled one's whose tool is still pending; real Runner on a new session), A1 / A2 (#1193, real Runner); `test/prepare-inherited-gate.test.ts` (the entry's classifier rides with the chain's ceiling). Mutations D926–D932, D934–D935, D937–D938.
|
|
63
|
+
|
|
64
|
+
### A tool's alias and its wire name are one name at the permission gate — resolved before `policy.check` (#1194; behavior (tighten: an alias no longer slips past a rule); +2 exports `rosterToolName` / `ruleNamesOf`; +1 optional member `ToolCallFace.names`, +1 optional member on the park row `PendingAction` (`tool_approval.names`), +1 optional key on the fence view `ToolCallLike.face` (`names`); `ToolGateInput.shellReadCheck`'s `judge` / `unanswered` take the call (`{ toolName, face }`) instead of the name; +0 wire keys, +0 codes) (@server @client-core @cli @test)
|
|
65
|
+
|
|
66
|
+
- **Before (v7.34.0, and every release since `ToolSpec.aliases`).** A call reached every permission layer under the name the model spelled, and every rule matcher compared names raw: a deny / ask / allow written against a tool's wire name did not reach a call made under one of its aliases, and a rule written against the alias did not reach a wire-name call — session `toolDeny` / `toolAllow`, `createPermissionRulePolicy`, `createAllowDenyPolicy`, `createApprovalPolicy`, the persisted rule lane, the shell-tool lists of the command-name policies, `createSensitivePathPolicy({ tools })`, a skill manifest's `allowTools`, the frozen projection replay. Probe on v7.34.0 (real Runner, `DeployKill` aliased `KillAlias`): `toolDeny:["DeployKill"]` × a call made as `KillAlias` ran; `toolDeny:["KillAlias"]` × a call made as `DeployKill` ran; the same-name pairs were refused.
|
|
67
|
+
- **Now (the reference judges a call by the tool object it resolved, aliases included).** **The name is resolved before `policy.check`**: the Runner's gate station resolves the call's name to the wire name of the tool it dispatches to on the leg's live roster (`rosterToolName` — the wire name first, then an alias, the dispatcher's order) before any station runs, so every policy's `req.toolName`, the PreToolUse hook's `toolName`, the `permissionDenied` payload, the approval request (`AskRequest.toolName`) and the park row carry the wire name. The face the gate stamps carries `names` — the wire name and every alias that resolves to it — and every name-keyed matcher reads a rule's tool name against that list (`ruleNamesOf`): a rule written against an alias and one written against the wire name are one rule, for deny, ask and allow alike. An alias another tool holds as its wire name, or that an earlier tool claims, names that tool and never this one. The person's stored rule rows are judged under every one of the call's names their grammar speaks for (the answers folded deny > unreadable > ask > allow), so a stored `Bash(…)` or `Edit(…)` row reaches a tool that answers to that name under another wire name, whichever name the call used. The engine's own tightens keyed on one of its tool names hold for a tool that wears the name under any of its names — the peer-referral classifier for `SendMessage`, the content-question class and `createDurableQuestionPolicy` for `AskUserQuestion` (the fail-safe direction the wire-name shadow rule already takes); a grant or a routing keyed on an engine tool name reads the wire name. A durable park row carries the names beside its face, so the pre-CAS recheck of an approver's edit reads rules on either name as the live gate did. `tool_start` / `tool_end`, the transcript and the PostToolUse hook keep the model's spelling (a hook pair keyed on one name sees the wire name before the call and the alias after it — KL-1194-c). Same probe ⇒ both cross-name pairs refused.
|
|
68
|
+
- **The path floor under an alias.** *Before (v7.34.0):* a deny-list that reads a write's path — a `Write(<path>)` / `Edit(<path>)` deny rule, the sensitive-path guard, write protection — took the catalog's path slot for the call's NAME when the tool declares no path target of its own; the name was the model's spelling, so a tool `CustomWrite` aliased `Write`, called as `Write`, was judged on `Write`'s `file_path`. *Now:* the call's name is the wire name (`CustomWrite`), which the catalog does not know, so the floor reads every name the call answers to — the face's `names`, the wire name first — and takes the first one the catalog declares a slot for (`protectivePathTargetOf`): the call made as `Write` keeps `Write`'s slot, and a call made as `CustomWrite` gains it (this entry's tighten direction). A tool that declares its own path target is judged on its own slot, never the floor's. A grant that admits a write by its path (session `allowDirs`, the fs-write gate's accept / exempt directories) still reads the tool's own declaration only and refuses a tool that declares none. `ToolCallLike.face`, the fences' view of the face, shows the face's existing `names` — an optional key, not breaking. *Who is hurt:* a write tool that takes `Write` / `Edit` / `NotebookEdit` as an alias and declares no `pathTarget` of its own, called under its WIRE name: the floor now judges it as a path write, so the write gate covers it — under `default` / `acceptEdits` it asks where it ran (0 → 1 card; `CustomWrite` aliased `Write` writing `<root>/x.txt`), under `dontAsk` it is refused where it ran (KL-1194-e). Compensation: declare `pathTarget`, or drop the write name from the aliases. *Review round 2 (the fix diff):* the face minted for a call lists the name the call was made under FIRST (`toolCallFaceOf(roster, spelling).names[0]`), then the wire name and the other aliases, so the floor reads the spelled catalog slot even when the tool also answers to a second catalog name with another slot (`Custom` aliased `["NotebookEdit","Write"]`, called as `Write` with a clear `notebook_path` decoy: refused, as on v7.34.0). A call made under the OTHER catalog name is judged on that name's slot — the pre-resolution reading, kept (KL-1194-d; judging every catalog slot of such a tool is #1208).
|
|
69
|
+
- **The org rule lane reads the names too.** The org layer speaks the persisted grammar and now receives the face the gate stamped, at the live gate and at the durable resume belt, as the person's lane does: a tool `MyShell` aliased `Bash` (the engine's shell not mounted) × an org `Bash(echo hi)` deny ⇒ the call made as `Bash` and the one made as `MyShell` are both refused (without the face the lane saw only `MyShell`, and the call made as `Bash` ran under bypass where v7.34.0 refused it); a call parked under `Bash` and approved while the org publishes a `Bash(…)` deny is not executed on resume. *Who is hurt:* nobody.
|
|
70
|
+
- **The engine's name-keyed tightens read every name (C-R99 final round, opus leg R-2).** *Before the fix (this release's own resolution):* the tables the engine keys on its own tool names — the shell seat's tier and probe (installed on `Bash` / `Monitor`), the shell and file read stations' memberships, the egress set, the compliance veto, the rule-offer door's marks and an ask's `riskAxes` — were read under the call's name, which after the resolution is the wire name alone: a caller tool `MyShell` aliased `Bash` (the engine's shell not mounted) called as `Bash` lost what v7.34.0 held — `rm -rf <root>/y` under `default` asked 1 → 0 times, under `dontAsk` ran 0 → 1; `cat <outside the root>` the same; `CustomRead` aliased `Read`, a read outside the root, the same. *Now:* the gate reads them under the call's names (`ruleNamesOf`, the spelling first): a membership holds the call under any of its names, a seat is read under the first name that holds one, and every key of that seat — the tier, the probe installed beside it, the shell seat's provenance of it — under that same name (a seat is never assembled from two names). The live gate, the approved-edit re-judge, the resumed leg's read belt, the rule-offer door, the park row's risk descriptor and an ask's `riskAxes` read one way. A grant or a routing keyed on an engine tool name keeps the wire name: the sandbox containment of the engine's hand tools, the rule-offer door's own lane, a tool's effect. *Who is hurt:* a caller tool that wears an engine tool's name as an alias is held by that name's tightens whichever name the model calls it by — the wire name too, which v7.34.0 did not hold (`MyShell` called as `MyShell`: `rm -rf` 0 → 1 card under `default`; a read outside the root refused under `dontAsk`; KL-1194-e); compensation = do not take an engine tool's name as an alias, or accept the tightening. *A host driving `runToolGate` with its own `shellReadCheck`*: `judge` / `unanswered` receive the call — read its names with `ruleNamesOf`.
|
|
71
|
+
- **Who is hurt / compensation.** *A host policy, hook script or hook matcher keyed on an alias spelling*: it now reads the wire name — list the wire name, or match through `ruleNamesOf(req)`. *A host driving `runToolGate` directly*: resolve `event.toolName` with `rosterToolName` and stamp `toolFaceOf` from the roster (the face carries the names). *A stored grant / exemption keyed on an alias*: it no longer matches — the call is asked once more (fail-closed). *A call parked on 7.34.0 under an alias*: its row keeps the parked name and carries no names — the post-CAS rechecks read the rules under both names (their face is read off the live roster), the pre-CAS edit recheck under the parked name alone. *A store implementer running the checkpoint contract kit*: the face group gains `names`, which must round-trip verbatim. *A caller tool that answers to an engine tool's name only by alias* (`Bash`, `AskUserQuestion`, with the engine's own tool not mounted): the engine's grants for that name — the shell's read-only admission, standing-rule offers on its card, the content-question lane — no longer reach it (they did when the model spelled the alias); its tightens reach it under both names.
|
|
72
|
+
- Pins: `test/backlog1194-tool-alias-canonical.test.ts` — three routes × {wire-name rule / alias call, alias rule / wire-name call, same-name control}: session rules S1–S4, settings compile P1–P5 and the name lists N1 / N2, the persisted lane R1 / R2 (a tool aliased `Bash`) / R3 (a tool `Write` aliased `Edit`); the engine tightens under an alias R4 (`SendMessage`, auto) / R5 (`createDurableQuestionPolicy`) / R6 (the content-question class under bypass); the durable park's edited resume D1; the name before `check` and the hooks G1; neighbours X1–X3 (an alias another tool owns, a contested alias, a shell tool listed by alias); the one function C1–C5 (the path guard's tool list, a skill manifest); `test/backlog1194b-alias-path-floor.test.ts` — the path floor under an alias (a `Write(<path>)` deny and the sensitive-path guard × a call made as `Write` to an undeclared `CustomWrite`, the wire-name call, the declared-slot and the `allowDirs` controls; real Runner); `test/backlog1194c-org-lane-alias.test.ts` — O1 (live gate: an org deny × the call made as `Bash` and as `MyShell`, a control), O2 (the resume belt); `test/backlog1194e-tighten-tables-any-name.test.ts` — ① the shell seat, ② the shell read station, ③ the file read station (each under an alias, `default` / `dontAsk`), ④a–④b controls (an unaliased caller shell, the engine's own `Bash`), ④c the wire-name call of an aliased tool (the tighten direction), ⑤ an egress-marked tool under both names. Mutations D890–D901 (+ D890b–D890f, D891b), D940, D942 / D942b, D946 / D946b.
|
|
73
|
+
|
|
74
|
+
### A request refused on its own body is refused before the session branches — `resumeAt` beside a file-history contradiction no longer moves the leaf (#1195; behavior (a refused request has no side effect); +0 exports, +0 codes) (@server @test)
|
|
75
|
+
|
|
76
|
+
- **Before (v7.34.0).** Prepare acquired the session and moved its leaf to `resumeAt`, and only then did the file-history phase judge the request: `fileHistory:"disabled"` + `resumeAt` + `restoreFiles:true` failed `rewind.capture_disabled` with the session already forked (server's reproduction: the leaf moved from the old tail to the anchor). Same shape for `rewind.store_unconfigured` (no history backend), `rewind.conflicting_targets` (`resumeAt` + `rewindFilesTo`), `rewind.rewind_files_retired`, a bad `fileHistory` word and a bad `RunnerDeps.onTrackFailure` / `fileHistoryStore` / `fileHistoryCapture` value.
|
|
77
|
+
- **Now.** The file-history request door (`runner/file-history-request.ts`) runs before the session is acquired and holds every refusal the request and the deployment's seats decide on their own — the three bad-value doors, `rewind.invalid_spec`, `rewind.rewind_files_retired`, `rewind.conflicting_targets`, `rewind.capture_disabled`, `rewind.store_unconfigured`. A request it refuses leaves the session log byte-for-byte as it was. The phase takes the door's resolution; a refusal that needs the env or the store's contents (`rewind_snapshot.unresolvable`, `rewind.restore_failed`, `rewind.child_scope_unsupported`) still comes after the branch, as documented (KL-1195-a). No rollback anywhere: the order is the fix.
|
|
78
|
+
- **Who is hurt.** Nobody loses a function. A request that carries both a contradiction and a `resumeAt` naming no entry now fails with the contradiction's code (it used to fail `resumeAt entry … not found`).
|
|
79
|
+
- Pins: `test/backlog1195-refusal-before-branch.test.ts` — R1 `capture_disabled`, R2 `store_unconfigured`, R3 `conflicting_targets`, R4 (`rewind_files_retired`, a bad `fileHistory` word, a bad `onTrackFailure`), each × `resumeAt`: code, leaf and session file bytes unchanged; the control (a legal `resumeAt` branches). Mutations D924–D925.
|
|
80
|
+
|
|
81
|
+
### A session repo's corrupt-read disclosure names the session (#1197; additive: `onCorruptRead` payload +`sessionId`; callback shape, no notice) (@server)
|
|
82
|
+
|
|
83
|
+
- **Before (v7.34.0).** `FileSessionRepoOptions.onCorruptRead` delivered `{ kind, path, reason, … }` for a session log's skipped records, its replay tally and a log skipped from a listing — with no `sessionId`, although `FileStorageCorruptReadInfo` promised one for a store keyed by session.
|
|
84
|
+
- **Now.** Every disclosure about ONE session carries its `sessionId` (a record / replay of its log — the id the repo was reading; its log skipped from `list()` or `listPlaced()` — the id its file name spells). A disclosure about the sessions directory itself carries none. Through `FileStorageBackend` the one sink receives the same. Same callback, no notice, no frame.
|
|
85
|
+
- **Who is hurt.** Nobody: an optional member on a payload.
|
|
86
|
+
- Pins: `test/backlog1197-corrupt-read-session-id.test.ts` — S1–S4 (open, export, listing, placed listing), S5 (the directory control), S6 (the package root on a real Runner resume). Mutations D919–D921.
|
|
87
|
+
|
|
88
|
+
### Wrapping a `ToolPolicy` keeps every member of the wrapped policy — `wrapToolPolicy` (#1200; behavior (a composed deployment policy's path deny rows and write guard reach the shell again); +1 export `wrapToolPolicy`; +1 optional member `ToolPolicy.nameSets` (declared; `NamedToolPolicy` keeps it required)) (@server @test)
|
|
89
|
+
|
|
90
|
+
- **Before (v7.34.0).** `ToolPolicy` carries `projection`, `pathDenyRules` and `writeTargetGuard` (a named policy also `nameSets`) beside `check`, and a `{ check }` wrapper dropped them all. In core, the Runner's `gateBaseline.toolPolicy` — what `runSpec` and any assembler compose over — carried `nameSets` only: on a composed run a deployment policy's `Read(<path>)` deny rows never joined the deny tables and its write-target guard never reached the shell's write station. Probe on v7.34.0: a deployment `Read(secrets/**)` deny composed over the baseline, bypass, `cat secrets/k.txt` ⇒ allowed, the bytes printed. server's two governance wrappers drop the same members.
|
|
91
|
+
- **Now.** `wrapToolPolicy(inner, check)` is the one wrap: the wrapper decides through `check`, and every member `ToolPolicy` declares beside it — `projection`, `nameSets`, `pathDenyRules`, `writeTargetGuard` — rides as `inner` holds it. The members are one table, read BY NAME: a class instance's prototype accessor or a non-enumerable own member rides too (an object spread would copy own enumerable members only — the shape v7.34.0's baseline forwarded by name for `nameSets`); a member `ToolPolicy` does not declare does not ride. `nameSets` is now a declared optional member of `ToolPolicy` (the name-keyed factories' `NamedToolPolicy` keeps it required; `toolPolicyNameSets` reads it without a cast) — the wire list reads "type gained optional key", not breaking. `combinePolicies` folds the same table (its fold record is keyed by it), and a compile-time fence makes a new `ToolPolicy` member an error until the table names it. The carried members describe what `inner` refuses, so the wrapper must refuse at least that (an observer, a tightener, a decorator of the decision); one that releases calls `inner` refuses is a policy of its own and states its own members. The ask-class brand does not ride (it attests the replaced `check`). The Runner's gate baseline and the policy chain's decorating layers wrap this way. Same probe ⇒ `deniedBy:"read_boundary"`, the rule named, no bytes.
|
|
92
|
+
- **The other form, weighed and not taken.** Moving the members off the policy object (registered at the composition root, `ToolPolicy` = `{ check }`) relocates the carry duty from wrappers to every composition seat (deps, task, resume-edit, inherited constraints, the host's own composition) and changes every factory's return shape — more seats to remember, not fewer, and hard-breaking for every host. The wrap is one rule and asks nothing of a consumer beyond swapping a hand-rolled wrapper.
|
|
93
|
+
- **Upgrade window (drain child parks first).** A durable child parked on v7.34.0 whose parent chain was composed over `gateBaseline.toolPolicy`, with a deployment policy that carries a `projection`, is refused on resume under 7.35.0 with `resume.parent_constraint_mismatch` at the chain-digest check (3c.ii, before the CAS — the row stays pending): v7.34.0's baseline carried no projection, so the recorded chain reads a live remainder (`requiresLiveRemainder:true`), while 7.35.0's baseline carries the policy's deny components and the re-supplied chain digests differently (KL-1200-b). Drain such parks (resume or cancel them) before upgrading.
|
|
94
|
+
- **Who is hurt / compensation.** server: the governance-ask marking wrapper is an observer — `wrapToolPolicy` replaces it; the write-target-form scoping wrapper releases calls its inner refuses, so it is not a wrap and states its own members (the wire sheet names both); S-877's own helper retires.
|
|
95
|
+
- Pins: `test/backlog1200-wrap-tool-policy.test.ts` — W1 (five member-bearing policies, the key sets read off the instances), W2 (the gate baseline), R1 (real lane: the composed deny row reaches `cat`); `test/backlog1200b-wrap-by-member.test.ts` — M1 (a class instance whose four members are prototype accessors), M2 (a non-enumerable `nameSets`), M3 (a wrap of a wrap; `combinePolicies` over it), M4 (real Runner: a class-form deployment policy naming a retired tool name, composed over the gate baseline ⇒ `config.legacy_tool_name`, as v7.34.0). Mutations D902–D903, D941.
|
|
96
|
+
|
|
97
|
+
### The raw-git scan reads tracked files (#1203; tests only)
|
|
98
|
+
|
|
99
|
+
- `test/gate-git-unavailable.test.ts`'s #1030 scan lists `git ls-files` under `scripts/` and `test/` instead of walking the directories — another test's scratch directory could be removed while it was being read (ENOENT). Tests only, so no criterion; its mutation is D918 in the mutation table's Part C.
|
|
100
|
+
|
|
101
|
+
### One shape for the two provider echo fields — `PROVIDER_ECHO_SHAPE` / `isProviderEchoShape` govern `requestId` and `responseModel` (#1204; behavior (a request id is admitted in a wider alphabet; a model name is gated); +2 exports; +1 trace frame `brain.echo_dropped`) (@server)
|
|
102
|
+
|
|
103
|
+
- **Before (v7.34.0).** A provider's request-id header was carried only in `^[A-Za-z0-9._:-]{1,128}$` (a gateway id with `/` or `#` was dropped) and the rule was not exported; the model name a provider reports (`responseModel`) was carried as it came — any length, any bytes.
|
|
104
|
+
- **Now.** `PROVIDER_ECHO_SHAPE` (visible ASCII 0x21–0x7E, 1–128 characters) and `isProviderEchoShape(value)` are the one rule: `isProviderEchoShape("")` is `false`; a non-string is `false`. The request id (`AssistantMessage.requestId`, `TaskResult.apiFailure.requestId`) and the reported model name (`AssistantMessage.responseModel`, `turn_end.responseModel`, `TaskResult.responseModel`) are carried only in that shape; a value outside it is absent — never truncated — and counted: one trace frame `brain.echo_dropped` `{ field: "requestId" | "responseModel" }` per provider response per field (minted in `brain/errors.ts` `readProviderRequestId` and `brain/stream-shared.ts` `noteResponseModel`, bridged in `core/runner/run-brain-sinks.ts`); no notice. An empty value states nothing: `model:""` on a chunk and an empty `request-id` header are absence — no field, no count, no frame (the brains read each echo through one reading, `providerEchoReading`: absent / carried / refused); `isProviderEchoShape("")` stays `false`. A host Brain's own message is read with the same predicate (no frame): the result faces of `responseModel`, the run-level `TaskResult.apiFailure.requestId` and the engine's persisted API-failure record in the transcript. The host Brain's own message object on the live stream is its own bytes, forwarded as it made them (KL-1204-a). The exported regex is the consumer's copy — the engine judges with its own instance.
|
|
105
|
+
- **Who is hurt.** A deployment whose gateway reports a model name over 128 characters or with a space or non-ASCII byte: `responseModel` is absent where it rode before (the trace counts it). A request id with `/`, `#`, `[` or similar now rides where it was dropped.
|
|
106
|
+
- Pins: `test/backlog1204-provider-echo-shape.test.ts` — E1 (the predicate, the exported copy), E2 (request id: widened, dropped + one count, no-id control), E3 (model name: gated, one count over three frames, the read twin), E4 (real Runner: `apiFailure`, `responseModel`, the trace), E5 (a host Brain's out-of-shape request id: the result and the persisted record), E6 (an empty echo is absence: the reading, empty headers, a real Runner stream opening with `model:""`). Mutations D910–D917, D933, D936, D944.
|
|
107
|
+
|
|
108
|
+
### `dontAsk` refuses a hook's ask that also meets a rule's or a safety check's ask; an operator authority's ask still reaches a person (#1205; behavior (tighten); +1 column `MODE_ARBITRATION[*].hookAskFloor`, +1 required member `ModeArbitrationRow.hookAskFloor`) (@cli @test)
|
|
109
|
+
|
|
110
|
+
- **Before (v7.34.0, KL-7340rg-a).** Under `dontAsk` a PreToolUse hook's ask reached a person whatever else the call carried: a hook's ask beside a `Bash(ls)` ask rule was one card.
|
|
111
|
+
- **Now (the reference 2.1.280: the hook arm consults its rule / safety check, cli280.js 14667998 → `ck` 15741441; an `ask` there runs the full permission pipeline with `hookAskFloor` and no forced decision, and the pipeline's dontAsk conversion refuses it, 15717035; a hook's ask alone is a forced decision the pipeline never sees, `bf` 15715970).** `MODE_ARBITRATION` gains the column `hookAskFloor` on the classes the reference's check answers `ask` for — `ask_rule`, `content_question`, `unverifiable_delete`, `transcript_write`, `write_protection`, `write_protection_unresolved`, `artifact_shared_read` — folded by OR; `dontAsk` keeps a hook's ask only while no member of the set floors it. A hook's ask beside an ask rule or a protected write ⇒ `deniedBy:"mode"`, zero cards; a hook's ask alone, or beside an ordinary ask or a probe, still reaches a person. **Kept, a registered divergence:** `dontAsk` keeps an operator authority's ask (`org_rule`, `operator_forced`, `governance_unavailable`) for a person, the hook floor included — the reference's conversion refuses every unforced ask, a managed-settings ask rule among them (`governance_unavailable` has no counterpart there); sema puts the operator's gate to a person instead of refusing it, and headless the result is the same refusal.
|
|
112
|
+
- **Who is hurt / compensation.** *A `dontAsk` session whose hook asks on a call that also meets an ask rule or a protected write*: refused (`deniedBy:"mode"`) instead of carded. *A consumer constructing a `ModeArbitrationRow`* (none known): add `hookAskFloor`.
|
|
113
|
+
- Pins: `test/backlog7340rg-dontask-hook-ask.test.ts` ② (the arbitration, controls included) and ④ (real lane), `test/backlog1111-permission-mode-arbitration.test.ts` (the fold rows, the column set). Mutations D904–D909.
|
|
114
|
+
|
|
115
|
+
### The retired `approverPosture` key's refusal no longer points at `bypassPermissions` (server [8668] ②; text only)
|
|
116
|
+
|
|
117
|
+
- **Before (7.34.0).** A spec still carrying `approverPosture` was refused by name (`config.spec_retired_key`) with the sentence *the permission mode is one key now — write `permissionMode: "bypassPermissions"`* — a posture never authorized anything, so the sentence nudged a caller that had only declared a posture toward releasing every ask.
|
|
118
|
+
- **Now.** The same refusal says: *TaskSpec.approverPosture is retired and is not read by this engine — a posture never authorized anything; drop the key (the engine renders the posture from the effective mode). Releasing asks is a separate decision: `permissionMode: "bypassPermissions"`.* The `autoModeRequested` sentence (`→ "auto"`, a synonym swap) is unchanged. Code, key table and wire keys unchanged.
|
|
119
|
+
- **Also in this release (no behaviour):** the call-side name canonicalization (`ruleNamesOf` / `callAnswersTo` / `toolNameSetHas`) moved one layer down to `tool-registry.ts`, beside the protective path floor that reads it; `tool-roster.ts` re-exports the three, so every import path and the package export are unchanged — one judgment, one definition.
|
|
120
|
+
|
|
121
|
+
## 7.34.0 — 2026-09-29
|
|
122
|
+
|
|
123
|
+
### acceptEdits answers a file command only where a Write would be answered: off the write-protection table and off the deployment's write guards; a shell write onto a protected target carries the table's class in every mode (C-R99 opus #1, car-7340-ri; behavior (tighten-only); +1 optional member `ToolPolicy.writeTargetGuard`, +2 optional members on `ShellReadBoundary` (`writeProtectionJudge`, `writeTargetGuard`), +1 optional member `ShellReadVerdict.writeProtected`, +1 optional member `WriteProtectionJudgeContext.canonical`, +1 optional member `EstablishedRead.written`; +0 wire keys, +0 codes) (@server @cli @test)
|
|
124
|
+
|
|
125
|
+
- **Before (this release's stage).** #1138 placed a file command's writes after the write roots, the two deny tables, removals and the line's order — never the write-protection table nor a deployment's sensitive-path deny. acceptEdits answered `sed -i s/deny/allow/ .sema/settings.json`, `cp payload.sh .git/hooks/pre-commit`, `mv x .mcp.json` and, at a HOME root, `rm -r .sema` with no card, while a Write onto the same targets asked (the table) or was refused (`createSensitivePathPolicy`). In every mode a shell write onto a protected target was the shell's ordinary card, which a standing `Bash(cp:*)` rule cleared.
|
|
126
|
+
- **Now (the reference 2.1.280: the write path check `sv` asks its settings / dangerous-file check `gre` before its acceptEdits working-directory allowance, cli280.js 10881943 / 9759255; Bash reaches it through `eM` → `zxn` → `sv`, 10886833).** Every target a shell command's programs WRITE, at the seat each writes it (`EstablishedRead.written`) — a redirection, `tee`, and the file commands' targets (an in-place `sed`'s file and backups, a copy's or a move's destination and — where the destination is a directory — its children, a move's source and a removal at their entry; a copy READS its source, which is not asked: `cp .git/config backup.txt` stays unasked — unless the copy carries an option word, which can write a source's name elsewhere (`cp --parents .git/config out` asks with the table's class)) — is asked of the write-protection table's own judge (for a target written where it lands: the spelling, the real target, the symlink chain's hops — the judge a Write meets, one compile per leg; the target the station established is judged as the key it is — `WriteProtectionJudgeContext.canonical` — so the engine's data root reached through a link keeps its exemption; a key walks no link chain, so a target written at its ENTRY is judged as that entry: `rm hk` with `hk` → `.git/hooks` removes the link, which is not the table's) and of the chain's `ToolPolicy.writeTargetGuard` (`createSensitivePathPolicy` carries one: its own real-target judgment, `true` on a target it cannot judge; `combinePolicies` carries any member's). A covered target is not placed and the ask carries `write_protection` (`mandated: true`, `mandate: "write_protection"`): acceptEdits does not answer it, a standing allow rule does not clear it (`persistedRuleShadowed`), `bypassPermissions` releases it (`released.askClass: "write_protection"`), `dontAsk` refuses it, `auto` hands it to the classifier. A guarded target is not placed and the ask stays ORDINARY — a person answers it; the shell is not refused where a Write is (KL-7340ri-a). A guard is a pure judgment: no side effect, no audit record, no hook, no approver; a guard that throws guards. A view the program cannot write on the tree the line starts from — one below an existing file, or a destination child whose destination is not a directory (a copy onto a new name writes that name) — is no write target and is not asked — `cp a.txt b.txt && cp a.txt c.txt` onto existing files stays placed under the sensitive policy — unless a segment run before it (or beside it) writes on its parent's line — compared with letter case and composition folded, so `mkdir BOX && cp .bashrc box` on a case-insensitive volume is judged too (`rm f && mkdir f && cp x f/.envrc`, `mkdir d && cp .bashrc d` are judged). A target the station established LEXICALLY (an isolated sandbox) is a spelling to the judge, which resolves it. A write or create whose operand is a PATTERN is never placed — the reference's rule, "Glob patterns are not allowed in write operations" (cli280.js ≈10887947): its matches are read before the line runs, a match can be an option word the program obeys (`mv * .out` beside a file named `--target-directory=.git` moves into `.git`), and a write earlier in the line can add a match (`cp p .mcp && sed -i.json s/a/b/ .mc?`) — KL-7340ri-e.
|
|
127
|
+
- **Who is hurt / compensation.** *An acceptEdits session whose file commands wrote `.git` / `.husky` / `.vscode` / shell rc / agent configuration unasked*: one card per such command, the look a Write gets. *A person whose `Bash(<cmd>:*)` rule cleared a shell write onto a protected target*: it asks now (disclosed as shadowed). *A deployment wiring `createSensitivePathPolicy` (server's default recommended set)*: acceptEdits `cp x .env` / `*.pem` / `id_rsa*` / `.aws/…` ask. *A redirection onto a protected target* (`echo x > .bashrc`): its card now carries the mandate (#1179's pin re-read). Compensation: approve the card; `writeProtectedPaths` / the policy's `patterns` without the row.
|
|
128
|
+
- Pins: `test/rev7340ri-file-command-write-guards.test.ts` (A · the table on the real lane — ten commands, the HOME-root removals, four link shapes (the entry reached through a linked parent included); pattern write operands asked, a copy onto an existing file under the sensitive policy, the data root reached through a link; a file turned into a directory earlier in the line, a pattern after a write, a literal backup in the linked data root, a lexically established target; the class under bypass / dontAsk / a stored rule; the sensitive guard with and without the policy, the Write tool's refusal beside it, a throwing guard; the seats read directly); `test/rev7340rm-shell-write-guards.test.ts` (car-7340-rm: copies out of protected files under acceptEdits and a standing `Bash(cp:*)`; `rm` / `mv` of a link into `.git/hooks`; a child into an existing or a just-created directory; two-writer lines onto existing files; pattern writes including an option-word match); re-pinned `backlog1179-credentials-env-deny` (the redirection's card is mandated). Mutations D700–D710, D714–D720, D780–D787, D796, D798.
|
|
129
|
+
|
|
130
|
+
### A classifier allow that lands after the leg left the classifier's modes is not taken — the leg's own classifier and an ancestor's on a child's chain (C-R99 opus #3, car-7340-ri; behavior) (@server @cli @test)
|
|
131
|
+
|
|
132
|
+
- **Before (this release's stage).** An `auto` leg steered to `plan` / `default` / `dontAsk` while its classifier was deciding a call took the classifier's late allow: the write ran with zero cards (`disposition {kind:"allowed", classifier:{…}}`). The same on a delegated child: an ancestor's frozen classifier answering the child's call after the leg was steered to `dontAsk` / `plan` ran the call.
|
|
133
|
+
- **Now (the reference 2.1.280 re-reads the mode after the classifier and falls back to the ask when it changed — `mode_changed_while_queued`, cli280.js 15727164).** The allow — the classifier's grant — is dropped once the leg is no longer in a mode that hands the ask to that classifier (the leg's own: `auto`; an ancestor's on the chain: any mode but `dontAsk` / `bypassPermissions` / `plan` — the station's own dispatch predicate, read again after the answer), nothing recorded, no `classifier` member; the standing ask reaches the arbitration point under the mode now in force (`plan` / `default` ⇒ the person; `dontAsk` ⇒ refused, `deniedBy: "mode"`; `bypassPermissions` ⇒ released, disclosed). A block keeps its refusal and an unavailable classification keeps #1125's exit (KL-7340ri-b: the reference drops a late block too).
|
|
134
|
+
- **Who is hurt / compensation.** Nobody: the steer is honored for the call in flight.
|
|
135
|
+
- Pins: `test/rev7340ri-file-command-write-guards.test.ts` (B · plan / default / dontAsk, the late block, the no-steer control; B′ · a delegated child's call under an ancestor's classifier, steered to dontAsk / plan, the no-steer control). Mutations D711–D713.
|
|
136
|
+
### A path deny rule is judged at its spelling and where its head leads — a rule written through a link holds at the real location (car-7340-rj B; behavior (tighten); `PathRuleDenyRow` (unreleased) trades `bases` / `withBases` for `head`; +0 wire keys, +0 codes) (@test)
|
|
137
|
+
|
|
138
|
+
- **Before (this release's stage).** The deny tables re-resolved a rule row only at the canonical spelling of its BASE (root / cwd / home): an absolute pattern, or a link inside the pattern's own literal prefix, was judged as spelled only — while the person's `Read(//dir/**)` allow row is canonicalized into the read directories. With `Read(//L/**)` allowed and `Read(//L/sub/**)` denied (`L` a link — macOS `/var` / `/tmp`, or any user symlink — to `R`), `cat R/sub/s.txt`, the Read tool and Grep read the denied file with no card, `echo x > R/sub/n.txt` wrote into it, and under `bypassPermissions` a deny row alone (no allow row) was released at `R` (main-session reading: `/var/…` rows × `/private/var/…` reads, `cat` and Read served the file).
|
|
139
|
+
- **Now (the reference 2.1.280: the physical twin of every deny / ask rule's literal prefix, cli280.js `ei` 9766583, compiled in `mt` 9767083).** Every rule row also holds at the location its HEAD leads to, and where every part above the head leads (car-7340-rm F2) — the head is the pattern's literal prefix under its base (`pathRuleHeadOf`: `//a/b/**` ⇒ `/a/b`, `//a/*.txt` ⇒ `/a`, `~/.ssh/**` ⇒ `<home>/.ssh`, a bare name ⇒ the working directory, `/**` at a drive or share root ⇒ that root), resolved the way the faces mint their keys (the longest existing prefix, so a head that does not exist yet is placed too — the one resolution the home-anchored segment rows already used, `whereItLeads`; a head the environment does not answer for is placed at its longest answered prefix, never narrower than its base's canonical spelling — `whereItsAnsweredPrefixLeads`; on a win tree the head is spelled natively, `c:\…` / `\\host\share\…`); a path at or below the resolved location is judged as its spelling at or below the head. Read and write faces alike (the shell station, the file tools, the search roots, the write tools). The base twin (the row re-resolved at the canonical root / cwd / home) is one of those parts, so a link inside the prefix re-pointed after prepare still meets the row through the part above it (`Read(//<spelled>/alias/**)` refuses `<real>/alias/x` after `alias` moves); a part that only extends its parent's resolution adds no row; the rule count is unchanged.
|
|
140
|
+
- **Who is hurt / compensation.** *A person whose deny row was written through a link*: it now holds at the real location (in-root and out-of-root, every mode). Resolution is per prepare: a link created or re-pointed mid-run is followed from the next prepare (KL-7340rj-b).
|
|
141
|
+
- Pins: `test/backlog7340rj-deny-row-head-twin.test.ts` (rows through the link × reads at the real location: `cat` / Read / Grep; the reverse spelling; the write face; bypass with a deny row alone; a head created mid-run; the twin and the head of every row form, the win drive / UNC / root-level mappings and the answered-prefix floor, unit — the floor on the real lane too); `test/rev7340rm-deny-row-parts.test.ts` (a head link re-pointed after prepare, real lane; the parts' rows, unit). Mutations D750–D758 in the pin's head, D793–D794.
|
|
142
|
+
|
|
143
|
+
### A person's rule store that cannot be read is SKIPPED and announced — its rows take no part until a read succeeds (C-R172; car-7340-rk (supersedes car-7340-rj C); behavior (the store card is gone — a loosening against v7.33.8); **BREAKING — wire / closed set**: the mechanism member `ruleStoreUnreadable` retired (wire key −1), exports −3 `RULE_STORE_UNREADABLE_KINDS` / `RuleStoreUnreadable` / `isRuleStoreUnreadable`; notice `permission.rule_store_unavailable` — in the closed-set sentence of the design/1111 entry; +1 optional member `EffectivePermissionRules.unreadableSources`) (@server @cli @client-core @sdk @test)
|
|
144
|
+
|
|
145
|
+
- **Before (v7.33.8).** A per-call read of the person's rule store that failed or did not answer made every call the rule lane speaks for (the shell, the path-target tools) a real-approval card — origin `rule_store_unavailable`, `ruleStoreUnreadable: "store"`, class `governance_unavailable`: `bypassPermissions` / `dontAsk` kept it for a person, the classifier never answered it, headless it was refused — and a durable resume whose lane read failed vetoed the approved call. This release's stage (the person's path rows read once at prepare, #1144) had added a whole-run form: a failed prepare read put every call of the engine's hands to that card until the next run.
|
|
146
|
+
- **Now (the reference 2.1.280 skips a settings source it cannot read or parse — "Files with errors are skipped entirely, not just the invalid settings" / "Continue without these settings", cli280.js 4919286; `POe` / `Sl` answer `settings: null` for such a file, 6783440; the merged rule set takes nothing from it, `KWe` 9298119 / 9294779; a changed file reloads the rules, 20256710 / 15746454).** A store that cannot be read is SKIPPED: its allow, ask and deny rows take no part in the decisions that read serves — the call is judged as if the person had no persisted rows, in every mode (no card from the store, no `rule_store_unavailable` origin, no class). The unit skipped is the SOURCE, as the reference skips one settings file (car-7340-rm): a personal partition that cannot be read — the durable one (the user / project rows) or the session one — takes out its own rows, and the partition that was read keeps judging (a readable deny row keeps refusing; where its rows say nothing, the lane answers as for a store it could not read, so the ask's evidence still says `personalRuleDotsAbsent: "unavailable"`); `EffectivePermissionRules.unreadable` names the failure and `unreadableSources` lists the sources skipped. Per call: the lane's read (bounded at the gate's own 15 s) that fails or does not answer is skipped for that call, and the next call reads again — the rows apply as soon as a read succeeds. At prepare: a failed read of the path rows leaves that run's read / write deny tables and read directories without them (the shell's file commands and the search walks judge without the person's rows for the run; the file tools' own per-call read still applies the rows whenever it succeeds); the next prepare reads again, as for a row the store gains mid-run. The compaction attachment reader follows the tables. A durable resume whose lane read fails runs the approved call (a task cancelled during that wait still does not). The ask's evidence keeps saying the lane could not look (`personalRuleDotsAbsent: "unavailable"`). The person is told ONCE per run: `permission.rule_store_unavailable` (audience `user`, `detail: { at: "prepare" | "call", sessionId, runId }`), minted by whichever read failed first, naming the sources where a partition failed alone — `at: "prepare"`: the run's path rows were skipped (a deny row among them protects nothing on this run; a call whose own read fails too is judged without them), and the next run reads them again; `at: "call"`: that call's own rule check skipped them (path rows the run took at start still apply to its file and shell checks), and every later call reads again; the trace keeps one `permission.rule_store_unreadable` line per skipped read. A wait the run's own cancel ends is no fact about the store and announces nothing. A call the lexer cannot read against the rows (`{ unreadable: true, reason }`) still fails closed (origin `rule_store_unavailable`, class `rule_unreadable`) — and so does a call the lane cannot judge against rows it DID read (a row that makes the judgment throw: the store was read, so it is not skipped; the reason says the judgment threw, and the trace keeps a line) (car-7340-rm).
|
|
147
|
+
- **BREAKING — wire / closed set.** With the store form minting no origin, `origin: "rule_store_unavailable"` has ONE mechanism left — the call could not be read against the person's rows — and the member that told the two apart is retired: `ruleStoreUnreadable` is gone from `PermissionResult`'s ask arm, `AskRequest` (so from the live `tool_approval` frame a serving layer projects from it), `AskCarry`, the park row's `PendingAction.tool_approval` and `CheckpointSummary` (so from an inbox row read off it) — wire key −1; the closed set `RULE_STORE_UNREADABLE_KINDS` / `RuleStoreUnreadable` / `isRuleStoreUnreadable` goes with it (exports −3). The gate's origin fact is `AskClassFacts.ruleCallUnreadable?: boolean` (it was `ruleStore`, a type new in this release). A row written by 7.33 with the key (`store` or `call`) reads back with its origin and without it — `summarizeCheckpoint` copies only the members the row type declares.
|
|
148
|
+
- **Who is hurt / compensation.** *A person whose rule store cannot be read*: while it stays unreadable, their persisted deny and ask rows do not protect them (and their allow rows clear nothing) — the price C-R172 accepts, the reference's shape; the notice says so once per run, and the rows apply again at the next successful read (per call) or the next run (the path tables). *A deployment that relied on the store card to stop a run during a store outage*: the run proceeds under its mode; an org-governed deployment's org partition keeps its own fail-closed answer (`org_unavailable`) — and, residual KL-7340rk-a, a personal store whose read HANGS (or throws synchronously) still takes the org read down with it there, so such a deployment still sees the `org_unavailable` card. *sdk / client-core / cli / server*: drop `ruleStoreUnreadable` wherever it is declared, read, projected or copied — the sdk's two `enum` sites and two type sites, client-core's copy of the set with its sentence and narrow reader, the cli's reader and wording, the server's frame projection and three test fixtures (the wire sheet lists each site); the origin word alone now says "this command could not be read against your rules" (a row 7.33 wrote with `ruleStoreUnreadable: "store"` reads back with the same origin, where it meant the STORE could not be read — an in-flight row's origin keeps that older meaning). `permission.rule_store_unavailable` rides with no card beside it and its `detail` is `{ at, sessionId, runId }`.
|
|
149
|
+
- Pins: `test/backlog7340rj-unread-path-rows.test.ts` (rewritten: failures 0 / 1 / 2 × default / bypass × `cat` / Read — no card, the skipped rows, one notice; a skipped call then a refused one in one run; the next run; a call's own read failing with the tables intact; the attachment reader; the lane's own bound; the answer rule and the notice latch, unit); re-pinned `backlog625-tightening-floors` ④ ⑪, `backlog625-content-rules` #632 (the cancelled wait; +1 cell: a store that throws at resume runs the approved call, one notice), `backlog688-contract-batch` C3 / R3 / N4 (the retired member on no face, a policy's own key reaching neither the request nor the row, a 7.33 row carrying the key), `backlog803-read-root-candidate`, `backlog1111-permission-mode-arbitration`, `backlog1144-persisted-path-deny-rows` ⑦, `design252-ask-rule-evidence`; `export-surface.snapshot.json` (−3, and +1 `DerivedDenyRule`); `test/rev7340rm-rule-store-partitions.test.ts` (car-7340-rm: a malformed row beside a deny row fails the call closed; one partition failing while the other's deny row refuses, per call and at prepare; the notice naming the sources). Mutations D760–D770 in the pin's head, D788–D792, D797.
|
|
150
|
+
|
|
151
|
+
### A persisted `Read(//dir/**)` allow row is a read directory of the run, and Read / Grep / Glob outside the read directories is a question (#1173, C-R116 Q4 / C-R106 ②; behavior; +1 card-bearing ask on the file tools; +1 trace `field` value `persistedReadRule` — a closed-union member, listed with the release's BREAKING closed sets (design/1111 entry)) (@server @cli @client-core @test)
|
|
152
|
+
|
|
153
|
+
- **Before (this release's stage).** A person's `Read(//dir/**)` allow row was accepted by the rule model and consumed by nothing on the read faces: `cat <dir>/f` stayed a mandated card the row could not clear, and Read / Grep / Glob of `<dir>/f` refused `path_not_in_root` with no card — with or without the row, under every answer and every mode.
|
|
154
|
+
- **Now (the reference 2.1.280: the path check asks the Read rules first, cli280.js 10883433; the file-tool read outside the working directories is an ask whose suggestion is that very row, 9783824 / 9746197).** The row's directory joins the run's read directories at prepare (canonicalized beside `additionalReadDirectories`; a directory that does not resolve is skipped out loud, trace `config.additional_directory_skipped` `field: "persistedReadRule"`; so is one that holds the local artifact store, `reason: "admits_artifact_store: …"` — the in-flight grant's store rule, and the Artifact tool stays mounted): the shell's read station and the three file tools read it with no card. Without a row, the file tools' out-of-root read is ONE ordinary card — `tool "<Read|Grep|Glob>" reads outside the directories allowed for this session — explicit approval required`, `readRootCandidate` = the directory (the file's directory for Read, the directory itself for a search root): yes reads that one target (on the canonical key the gate judged — a path re-pointed meanwhile is refused; a judgment the gate's wait gave up on admits nothing), yes + `readRootGrant` widens the run (`approval.read_root_granted`), no / nobody ⇒ `path_not_in_root` on the refusal; `bypassPermissions` releases it, `dontAsk` refuses it, `auto` hands it to the classifier, a park lane parks it. The deny tables refuse first; the artifact store is never admitted; the engine's background output file (the launch receipt's path) is no question, as the Read tool's own fence already admits it.
|
|
155
|
+
- **Who is hurt / compensation.** *A deployment that relied on the file tools' silent refusal of out-of-root reads*: a card (or, under bypass / a blanket allow approver, a read — KL-7340rg-b; put the directories that must stay unreadable in `readDenyPatterns`). *server / cli / client-core*: the file tools' card is the read-root card already rendered for the shell. *A governed deployment*: one more org-source fetch at each hands prepare (KL-7340rg-c). *The governed-agent example*: Act IV's out-of-roots probe now parks on the question instead of being refused.
|
|
156
|
+
- Pins: `test/backlog1173-read-directory-rows-and-file-read-station.test.ts` (the row on five faces attended and headless, the skipped row, the card on three tools, the answers, the modes, the deny tables, the re-point, the approved edit; real lane — the selection, unit). Re-pinned: `backlog1093-read-root-grant`, `backlog1093-grant-store-exclusion` ①c, `backlog691-advertised-writable-dir`, `read-face-open`, `resume-verify-freeze-escape`, `design389-unified-store`, `examples-governed-agent`. Mutations D500–D505, D521–D522, D524 in the pin's head. Known limit: a link re-pointed while the question is parked is judged afresh on the resumed leg (KL-7340rg-f).
|
|
157
|
+
|
|
158
|
+
### The store's `Read(...)` / `Edit(...)` deny rows reach every face (#1144; behavior) (@test)
|
|
159
|
+
|
|
160
|
+
- **Before.** A person's persisted path deny row was judged by tool NAME only: `Read(<path>)` refused the Read tool and nothing else — `cat` / `head`, the search walks and the write tools read or wrote the path the person denied.
|
|
161
|
+
- **Now.** Read once at prepare with the allow rows (one read, two directions) and joined to the caller policy's rows (`effectivePathDenyRules`): every read face, the shell's file commands and traversals, and the write tools refuse it — including under a directory the person's `Read(//dir/**)` row admits. A `~/` row whose home the execution environment does not declare is left to the per-call lane (which fails it closed for the Read tool) and disclosed.
|
|
162
|
+
- **Who is hurt / compensation.** *A user whose store row was quietly bypassed by the shell*: it now holds everywhere.
|
|
163
|
+
- Pins: `test/backlog1144-persisted-path-deny-rows.test.ts` (five real-lane cells, the prepare read's floors and cut). Mutations D506–D509.
|
|
164
|
+
|
|
165
|
+
### In a compound command a read-only segment needs no rule (#1174; behavior) (@test @cli)
|
|
166
|
+
|
|
167
|
+
- **Before.** The persisted-rule conjunction required a rule on EVERY segment: `cat list.txt | xargs cat` under `Bash(xargs cat)` still asked until `Bash(cat:*)` was added; a card's batch offer named `git status` in `git status && npm test`.
|
|
168
|
+
- **Now (the reference 2.1.280: the Bash check splits the line into its commands, cli280.js 15550904, and judges each through one chain that ends in `isReadOnly` ⇒ allow, 15530771).** A segment the read-only reader admits (every word literal, no substitution, no launcher, no program named from its input) and that moves no cursor covers itself — on a line the reader has no line-level objection to (a `cd` beside a `git` leaves every segment to its rule: `cd <dir> && git status` under `Read(//<dir>/**)` alone still asks); at least one rule must still speak; the coverage table (and the batch offer) calls it covered. What it reads is still the read station's to judge.
|
|
169
|
+
- **Who is hurt / compensation.** *A card renderer*: batch offers are shorter (read-only members are gone). *A person with a bare-interpreter rule* (`Bash(node)`, `Bash(python3)`): a read-only producer piped into it now runs with no card, as in the reference (KL-7340rg-g) — do not keep such rules.
|
|
170
|
+
- Pins: `test/backlog1174-read-only-segments.test.ts`; re-pinned `design375-batch-consent`, `permission-rule-wiring`, `design382-p1-scope-three-axis` (their filler command moved off a read-only one), `backlog490-redirection-offer-lane`, `backlog502-out-of-root-read-mandate`, `design382-p2-directory-read`. Mutations D510–D513, D523.
|
|
171
|
+
|
|
172
|
+
### A read the station could not establish carries its own card, `read_unestablished` (#1175; wire: +1 `tool_end.structured.type` / `errorCode`) (@server @cli @client-core @test)
|
|
173
|
+
|
|
174
|
+
- **Before.** Every non-deny finding carried the containment card: an in-root `python3 -c "print(open('r.txt').read())"` reached `errorCode: "readonly_out_of_root"` with `paths: []`.
|
|
175
|
+
- **Now.** The card follows the finding: an established out-of-root target ⇒ `readonly_out_of_root` with its paths; nothing established (an unreadable or re-entered program text, a program named from its input, a station that could not answer) ⇒ `{ type: "read_unestablished", code: "read_unestablished" }` — on the full shell's refused / unanswered card and on the read-only face's refusal alike.
|
|
176
|
+
- **Who is hurt / compensation.** *A consumer table keyed on `errorCode`*: add the row (an in-root question, not an out-of-root read).
|
|
177
|
+
- Pins: `test/backlog1175-read-unestablished-card.test.ts`; re-pinned `backlog222-recursive-read-demote`, `backlog239-probe-reason`, `rb413-bash-readonly-boundary-refusal`, `produce-without-delivery-cards`, `backlog945-matrix` (flip class `E-1175`), `tool-catalog` A-3. Mutations D514–D516.
|
|
178
|
+
|
|
179
|
+
### The headless refusal speaks the reference's sentence (behavior; tool-result text) (@cli @test)
|
|
180
|
+
|
|
181
|
+
- **Before.** `approval required for "<tool>" but no approver is wired (headless auto-deny): <ask> — <remedy>`.
|
|
182
|
+
- **Now (the reference 2.1.280 `z_t`, cli280.js 16756440, raised at 15715561 `MF`).** `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (no approver is wired for "<tool>" — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: <ask> — <remedy>` (`headlessDenialMessage`, `src/core/ask-origin.ts`); the remedy stays sema's superset; the `delegation.ask_unresolvable` notice is unchanged.
|
|
183
|
+
- **Who is hurt / compensation.** *A pin on the old head* (cli L-780, test's text cells): move it to the new head.
|
|
184
|
+
- Pins: `test/backlog7340rg-headless-denial-sentence.test.ts`, `test/core-toolgate-coverage.test.ts`. Mutations D517–D518.
|
|
185
|
+
|
|
186
|
+
### Six engine fact tables at the package root (#1178; +6 exports) (@client-core)
|
|
187
|
+
|
|
188
|
+
- **Now.** `ASK_UNRESOLVABLE_CAUSES`, `DURABLE_MANDATE_SOURCES`, `TERMINAL_CAUSE_IS_REPLAYABLE`, `RESERVED_AGENT_NAMES`, `WORKFLOW_SIZE_GUIDELINE_AGENT_CAPS`, `ENGINE_AUTHORITY_ENVELOPE_TAGS` — each the defining module's own value (identity, never a copy); the five tables are frozen where they are defined. `RESERVED_AGENT_NAMES` is typed `ReadonlySet` — read-only at the type level only: the runtime value is a plain `Set` (its `add` succeeds), frozen with #1057 (7.35.0), which also gives it its new members.
|
|
189
|
+
- **Who is hurt / compensation.** *client-core*: replace the six hand mirrors (CC-230).
|
|
190
|
+
- Pins: `test/backlog1178-fact-table-exports.test.ts` (the source leg); the consumer smoke probe's `SMOKE_IDENTITY_EXPORTS` (the installed-tarball leg, `gate:blackbox`).
|
|
191
|
+
|
|
192
|
+
### No change, decided by the reference: `EnterPlanMode` asks nothing of its own, and `dontAsk` keeps a hook's question (#1176, 10-2) (@test)
|
|
193
|
+
|
|
194
|
+
- **EnterPlanMode.** The reference's check is its tool builder's default allow (cli280.js 9464192; the tool, 19680400, defines only `call`); the "Enter plan mode?" dialog (33270610) is the card a question about the call gets (an `EnterPlanMode` ask rule, a hook's ask). sema: the same (KL-7340re-c closed as no divergence). Pins: `test/backlog1176-enter-plan-mode-approval.test.ts` (G12.k).
|
|
195
|
+
- **dontAsk × a PreToolUse hook's ask.** The reference hands a hook's `ask` to `canUseTool` as a forced decision, past its dontAsk conversion (14669235 `ZJ`; `bf` 15715970) — the question reaches a person, which is sema's form. The one arm the reference refuses (a hook ask that also meets a rule / safety-check ask, 14667998 `hookAskFloor`) still asks here (KL-7340rg-a). Pins: `test/backlog7340rg-dontask-hook-ask.test.ts` (G12.j).
|
|
196
|
+
|
|
197
|
+
### An approved plan returns to the mode from before it — `prePlanMode` (KL-7340re-b; behavior; +1 member on the checkpoint row's `inheritedGate`: `prePlanMode`) (@server @test)
|
|
198
|
+
|
|
199
|
+
- **Before.** An approved or edited `plan_review` resumed under the re-passed spec's word, or `default` when the resume named none: an `acceptEdits` leg steered into plan (or entered by the model's `EnterPlanMode`) came back as `default` and asked about its next in-root write; a resume re-passing `permissionMode: "plan"` stayed in plan after the approval.
|
|
200
|
+
- **Now (the reference 2.1.280: entering plan records `prePlanMode`, `C9e` cli280.js 19371781; ExitPlanMode sets `mode: prePlanMode ?? "default"`, 19458661).** A steer into plan and the model's `EnterPlanMode` record the declared word they left (`PermissionModeController.prePlanMode`; only the newest steer writes it — an older steer whose classifier arming settles later does not); the spec's `plan` start records none. The checkpoint row carries it (`inheritedGate.prePlanMode`, written beside a recorded `plan`; a value that is not a mode a plan can return to refuses the resume, `config.permission_mode_invalid`). On an approve / edit the resumed leg requests that word (none ⇒ the leg's own default); a resume naming another mode is the approver's choice and stands; a re-passed `plan` no longer keeps an approved plan open; a reject keeps the plan and its recorded word. Beside #1170 ①, a bypass leg that planned comes back writing outside its root.
|
|
201
|
+
- **Who is hurt / compensation.** *server*: a checkpoint reader that enumerates `inheritedGate` keys sees `prePlanMode` (a `PermissionMode`, never `plan`) — pass it through; a resume that wants the pre-plan mode sends no word. *A resumer that re-passed the original spec word*: that word still wins (the approver's choice) — send no word to get the reference's restore.
|
|
202
|
+
- Pins: `test/backlog7340re-b-pre-plan-mode.test.ts` (acceptEdits → steer plan → approve, `EnterPlanMode` from acceptEdits, the spec's plan start, a re-passed `plan` / `default`, bypass → plan → approve with an out-of-root write — real lane; `modeRequestAfterResume`, unit), `test/backlog1139-plan-fold-and-review-exit.test.ts`, `test/inherited-gate-registry-hygiene.test.ts` (the durable projection's carriage note). Mutations D418–D421 in the pin's head.
|
|
203
|
+
|
|
204
|
+
### A bypass release past a read-deny table says so, naming the table: `permission.read_deny_unapplied` (C-R124; behavior; +1 notice code in `ENGINE_NOTICE_CODES` / `EngineNoticeCode`, audience `user`; +1 optional member `ShellReadVerdict.unestablished`) (@cli @server @client-core @test)
|
|
205
|
+
|
|
206
|
+
- **Before (this release's stage).** Under `bypassPermissions` a call whose reads the shell read station cannot establish — a scan decline (`find . -name k.txt | xargs cat`) or an unestablishable read (`python3 -c "…open(…)…"`, `node -e …`, `for f in ../*; do cat "$f"; done`) — is released (C-R116 Q3 / C-R124 (a): the read-deny table guards only the spellings the station can establish), and the only trace was the generic release account: `tool_end.gate.disposition.released {askClass}` and ONE `permission.ask_released_by_mode` per (mode, class) per run — identical with or without a deny table; under `Read(secrets/**)` the secret's bytes reached the result and nothing named the rule (main-session reading H5).
|
|
207
|
+
- **Now.** The station's verdict carries the fact on its own (`ShellReadVerdict.unestablished`: some read of the command could not be established, whichever finding won — an out-of-root read beside it does not hide it). Each such release on a leg whose read-deny table is in force — the active built-in tiers, the deployment's and the task's patterns, the read-face rule rows — carries `permission.read_deny_unapplied`, every call: `the bypassPermissions permission mode released "Bash" although a read-deny table is in force (built-in tiers credentials, agent-config; Read(secrets/**)): the read boundary could not establish what this command reads, so no row of the table was applied to it`, `detail: { mode, toolCallId, tiers, rows (first eight), rowCount, sessionId, runId }`. The release itself is unchanged (C-R124 (a)); a spelled deny (`cat secrets/k.txt`) is refused as before; a leg with no table hears only the release account. test [8568]'s three forms (no rule, headless, bypass) are released on this tree, not refused — pinned.
|
|
208
|
+
- **Who is hurt / compensation.** *cli / server / client-core*: one more user-audience notice code to render or pass through (a consumer diffing its table against `ENGINE_NOTICE_CODES` adds the row). With the default credential tiers on, the notice rides every bypass release of an unestablished read — that is the table in force.
|
|
209
|
+
- Pins: `test/backlog-cr124-bypass-read-deny-disclosure.test.ts` (the rule-row and two-call cells, test [8568]'s three forms, the no-table and spelled-deny controls; real lane), `test/governance-codes.test.ts` (audience). Mutations D415–D417 in the pin's head.
|
|
210
|
+
|
|
211
|
+
### A classifier request that fails with a provider error is `error`, not `timeout`, and reaches the person on a short ladder (#1172; behavior; `StreamOptions.maxRetries` is now honored as a per-call CAP by the built-in brains; +1 constant `AUTO_MODE_CLASSIFIER_MAX_RETRIES` on `src/core/auto-mode-defaults.ts`) (@server @cli @test)
|
|
212
|
+
|
|
213
|
+
- **Before.** The classifier call rode the brain's full transient ladder (10 retries, backoff doubling to 32 s): an HTTP 500 from the classifier's endpoint was retried until the rung's budget (180 s) ran out, then recorded `unavailable` / `timeout` — the person was asked only after the whole budget, and the card and `classifier.unavailable` said "(timed out)" (test [8555], 7.29.0: 7 re-sends to the then 60 s budget). Measured on this release's stage with the backoff base shortened to 20 ms: a 20 s budget ⇒ 10 attempts, the card at 20.35 s, `cause: "timeout"`.
|
|
214
|
+
- **Now (the reference 2.1.280: the classifier request carries `maxRetries: 4`, cli280.js 13682562 `J9e=4` / 15687934; a failure is recorded as an HTTP status, 15706184).** The classifier call passes `maxRetries: 4` — a per-call cap the three built-in brains now honor (`runStreamingBrain({ maxRetries })`: at most that many retries and never more than the brain's own budget) — so a provider error ends the rung after five attempts with `cause: "error"`, and the call goes on to the person at once (C-R136). Same stage measurement: 5 attempts, the card at 0.63 s, `cause: "error"`, no "(timed out)". With the default 500 ms base the capped ladder is ≈ 7.5 s of backoff (+ ≤ 25 % jitter) where the uncapped one was ≈ 160 s + jitter. A classifier that does not answer is still a timeout at the rung's budget; a main-agent call keeps the brain's own ladder.
|
|
215
|
+
- **Who is hurt / compensation.** *A caller that passed `StreamOptions.maxRetries` per call* (it was silently ignored by the built-in brains): it now caps that call's retries. *A deployment whose classifier endpoint recovers only after tens of seconds of 5xx*: the rung now gives up after ≈ 7.5 s and the question goes to a person (or to the next ladder rung) — the same outcome it had after 180 s. *Residual*: the card and the notice say "temporarily unavailable" with no status word (the reference says " (server error)" / " (rate-limited)" / " (overloaded)" by status); a rebuilt decider's completion (`rebuildAutoModeDecider`) is the deployment's own leg and keeps its own retries.
|
|
216
|
+
- Pins: `test/backlog1172-classifier-provider-error.test.ts` (500 ⇒ five attempts, `error`, asked at once; the hang control still `timeout`; a main-agent call keeps the brain's budget — real Runner, real brain over an injected fetch). Mutations D412–D414 in the pin's head.
|
|
217
|
+
|
|
218
|
+
### `cd <dir> && git status` in a default session: a `cd` that lands where the session stands is a no-op (no card), a `cd` elsewhere asks with the reference implementation's sentence — never "performs an irreversible action" (#1116 / #1170 ③; behavior; +2 exports on `src/core/read-only-shell.ts`: `CD_GIT_COMPOUND_SENTENCE`, `changesDirectoryBeforeGit`, not on the package) (@cli @test)
|
|
219
|
+
|
|
220
|
+
- **Before (this release's stage).** The shape tier now rides every full shell, and the allow layer's read-only reader refused every line with a `cd` beside a `git`: `cd <cwd> && git status && git diff` reached a person in every default session with `tool "Bash" performs an irreversible action — explicit approval required` (v7.33.5 through a serving layer's `shellGate: "classify"`: the same card; a bare library spec: no card).
|
|
221
|
+
- **Now (the reference 2.1.280: `cd-git-compound` ask, cli280.js 15554464; its no-op reading `Ovr` / `HOt`, 15538880 / 15542907).** One plain `cd <path>` — `.` / `..` / `./…` / `../…` / an absolute path, no expansion, no redirection, the only `cd` in the line — that resolves (lexically) to the session's working directory moves nothing: `cd <cwd> && git status`, `cd . && git status` run with no card (the git read is read-only). Any other `cd` beside a `git` keeps its question, now in the reference's words: "This command changes directory before running a version-control command, which can pick up untrusted hooks or repository configuration from the target directory. Approve only if you trust it." (a read the station itself asks about — `cd ..`, `cd "$PWD"` — keeps the station's own question).
|
|
222
|
+
- **Who is hurt / compensation.** *cli / a card renderer that matched the generic sentence*: the cd + git card carries the reference's sentence (same ask class, `ordinary`; a stored allow rule still clears it). *Residual*: the landing is compared lexically — a `cd` spelled through a symbolic link to the working directory still asks (the reference compares real paths); a no-op `cd` before a git WRITE (`git commit`) keeps the shape tier's generic question (the reference asks its generic one there too).
|
|
223
|
+
- Pins: `test/backlog1116-cd-git-compound.test.ts` (five no-card forms, three sentence forms, two station forms and the git-write control on the real lane; the no-op reading, unit). Mutations D409–D411 in the pin's head.
|
|
224
|
+
|
|
225
|
+
### The shell's provider-key file `credentials.env` is a built-in row on both faces, on by default (#1179; behavior (tighten-only); +1 row in `READ_FACE_BUILTIN_DENY_TABLE` (`credentials` tier), +1 row in `WRITE_PROTECTED_DEFAULT_TABLE` (`basename`); +0 wire keys, +0 codes) (@server @cli @test)
|
|
226
|
+
|
|
227
|
+
- **Before (this release's stage).** The host moves provider keys out of the settings file into `<config home>/credentials.env` (0600, `KEY=value` lines merged into the engine's environment), and no built-in row named that file: Read / `cat` / Grep of it served the keys — in the project with no card, in the home's config directory or a relocated config home after one out-of-root card — in every mode; an `acceptEdits` Write of a project's `credentials.env` ran with no card, the default-mode card was the mode's ordinary one, and bypass wrote it with no release on the disposition.
|
|
228
|
+
- **Now.** `credentials.env` is a `credentials` tier row of the read table (by name, in ANY directory, ASCII case-folded — the home's `.sema/credentials.env`, a relocated config home whatever its name, a project's copy): the Read tool refuses `read_path_denied` and the shell's file commands `deniedBy: "read_boundary"` with zero cards, in every mode (`bypassPermissions` / `dontAsk` / `acceptEdits` / `plan` / `auto` included); the search walks withhold it (a search outside the roots still asks its out-of-root question first). And it is a `basename` row of the default write-protection table: a Write that CREATES one is a MANDATED question — under `acceptEdits` the card names `table entry "credentials.env"`, in default mode the mode's card carries the `mandated` bit; headless: refused; `bypassPermissions` releases it with `released.askClass: "write_protection"`, as for every row. An EXISTING one is not writable by the file tools at all while the read row stands: Write and Edit refuse at their read-first check (`tool.validation_refused`, zero cards, even with an approver saying yes), and the shell's file commands that name it as an operand (`rm`, `sed -i`, `cp` / `mv` onto it, `tee`) are refused by the read row (`deniedBy: "read_boundary"`), bypass included. Only the exact name: `credentials.env.example`, `my-credentials.env`, `credentials.envrc` read and write as before.
|
|
229
|
+
- **Who is hurt / compensation.** *A project that keeps an application's own `credentials.env`*: the model can no longer read it, and a covered write to it asks; `grep -r` over a directory that holds one is refused as a whole (the #1143 posture of every credentials row). Compensation: `readDenyBuiltinExclude: ["credentials.env"]` (server `READ_DENY_BUILTIN_EXCLUDE=credentials.env`) removes the read row; `writeProtectedPaths: WRITE_PROTECTED_DEFAULT_TABLE.filter((r) => r.name !== "credentials.env")` removes the write row. *A person who wants the model to rewrite an existing key file*: it cannot (read-first refusal) — the host's own settings flow writes the file. *Unchanged, stated*: a shell redirection (`echo x > credentials.env`, `>>`) is not a covered write — it is the mode's ordinary question (refused on NO in default / acceptEdits, released under bypass, where it overwrites or appends to the file), exactly as for `.bashrc`.
|
|
230
|
+
- Pins: `test/backlog1179-credentials-env-deny.test.ts` (real lane, HOME a temporary directory: three places × Read / `cat` / Grep, five modes, the creating-write cards per mode, the existing-file refusals, the shell operand refusals under bypass, the redirection reading, the near-miss controls, the `grep -r` cost, both compensation seats; the rows, unit). Re-pinned: `test/backlog286-write-protect-default.test.ts` (51 ⇒ 52 rows: the sema basename tail gains `credentials.env`). Mutations D600–D602 in the pin's head.
|
|
231
|
+
|
|
232
|
+
### A command that NAMES a credential path without opening it is no read: `export KUBECONFIG=~/.kube/config`, `test -f ~/.kube/config`, `git commit -m '…~/.ssh/…'` run again (#1114 / #1170 ②; behavior; +2 exports `DECLARATION_BUILTINS` / `messageSlotWords` on the fs toolkit module, not on the package) (@test @cli)
|
|
233
|
+
|
|
234
|
+
- **Before (this release's stage).** The built-in credential tiers are on by default, and the read station handed the deny judge every word of a program it does not model and every assignment value: `export KUBECONFIG=~/.kube/config`, `test -f ~/.kube/config`, `[ -f ~/.ssh/id_rsa ]`, `F=~/.ssh/id_rsa echo hi` and `git commit -m 'update ~/.ssh/id_rsa docs'` were refused (`read_path_denied`) in EVERY mode, bypass included. v7.33.5 (no default tiers): a card or a run.
|
|
235
|
+
- **Now.** Read candidates come only from positions a program may open for reading. ① An assignment's value (a leading `NAME=value`, a declaration builtin's argument) is a candidate only in a text that runs a program able to open a file — anything but the inert builtins (`echo` / `true` / `false` / `pwd` and a bare declaration builtin — they exec and evaluate nothing: an external program's loader reads `LD_PRELOAD` even for `sleep`, `printf` reads `TZ` for `%(…)T` and assigns through `-v`, `test -v` and `declare -i` evaluate arithmetic), in segments whose words carry no `[` `]` `$` or backquote, or any input redirection, substitution, re-entry or launcher, anywhere in the text (order is not read). ② `export` / `declare` / `typeset` / `local` / `readonly` hand their `NAME=value` arguments' VALUES (a refusal names the real file, not `<root>/A=~/…`), never the word. ③ `test` / `[` join the no-path-operand programs (the reference implementation's path table, 2.1.280 `hx`, lists neither) for their SETTLED words; a word the shell or `test -v`'s arithmetic rewrites is handed over as before. ④ A `git commit` / `tag` / `merge` / `stash` message (`-m <text>`, a plain-letter cluster ending in `m`, `--message[=]<text>`) is prose; a message the shell rewrites (`-m "$(cat …)"`, the heredoc commit) keeps today's reading, read with each subcommand's own option grammar (git parse-options): a long option takes a value only as `--name=value` unless it is one of the listed value takers (`--message` is the message; `--file` / `--template` / `--pathspec-from-file` name a file; `--author` / `--date` / `--cleanup` / `--fixup` / `--squash` / `--trailer` … a plain value), so `--amend -m '…'` / `--allow-empty -m '…'` / `--no-verify -m '…'` read the message; `-S` takes its key only attached; a word another option consumes is never a message (`git tag -F -m/…` is `-F`'s file), nor is anything after an unknown short letter, an unsettled word, or a git global option the row does not know (`--attr-source` takes the next word); an unknown long option takes no value, so a path after it stays a positional word, judged. Unchanged: `ls ~/.ssh` / `stat ~/.ssh/…` stay refused (the reference reads `ls` / `stat`, and its Read deny covering `~/.ssh` refuses them — `hx` `ls:"read"`); `F=~/.ssh/id_rsa; cat "$F"`, `KUBECONFIG=~/.kube/config kubectl …`, `export F=…; cat "$F"`, `GIT_CONFIG_GLOBAL=~/… git …`, `git commit -F ~/…` stay refused.
|
|
236
|
+
- **Who is hurt / compensation.** *A user who relied on the refusal of a text that opens nothing*: none of these forms could read the file. *A text whose assignment value is spelled with `$` (`F="$HOME/.ssh/id_rsa" echo hi`)* keeps its refusal (a `$` word never makes a text inert). *Still refused (stated residuals, KL-1114a)*: `ls ~/.ssh` and `ls -la .npmrc` (the reference's form under a Read deny), `mkdir -p ~/.ssh && chmod 700 ~/.ssh` (a create / metadata command whose operands the station still judges as reads), `git config core.sshCommand 'ssh -i ~/.ssh/id_rsa'` (a config value); compensation `readDenyBuiltinExclude` for the row, or `readDenyBuiltinTiers: []`. Test fixtures that pinned "an assignment in front of no opener is refused" (#1100 / #1004) now pin the vouch and keep the refusal wherever a later program may read the value.
|
|
237
|
+
- Pins: `test/backlog1114-named-not-opened.test.ts` (seven forms run under bypass, thirteen controls stay refused with nothing leaked, the default-mode pair; the plan's rule and the message row, unit), `test/backlog1100-tilde-assignment-deny.test.ts`, `test/backlog1004-assignment-word-not-a-path.test.ts`, `test/backlog993-append-assignment-word.test.ts`, `test/backlog7300-shell-reentry.test.ts` (re-pinned), `test/backlog994-s1-dual-run.test.ts` (attribution `no_opener_values`). Mutations D404–D408 in the pin's head.
|
|
238
|
+
|
|
239
|
+
### `bypassPermissions` states the write face: an out-of-root `Write` / `Edit` / `NotebookEdit` lands under bypass without a serving layer's `writeFace: "open"` (#1170 ①, C-R28 path A in the engine; behavior; +1 member `ModePolicy.writeFace`, +1 optional input `ContainmentFaceInputs.modeFace`) (@server @test)
|
|
240
|
+
|
|
241
|
+
- **Before (this release's stage).** The mode table had no write-face column: a bypass session wrote outside the task root only where the serving layer translated `bypassPermissions` into `TaskSpec.writeFace: "open"` (server `MODE_ENGINE_FACES`) — the same release asks the server to retire that table, after which the out-of-root `Write` of a bypass session refused `path_not_in_root` (a Bash redirect outside the root kept running). v7.33.5 + the server's translation: the write lands.
|
|
242
|
+
- **Now.** `modePolicyOf("bypassPermissions").writeFace = "open"`, every other mode states nothing. The one containment resolver reads it as row 3′ — after the explicit seats (`TaskSpec.writeFace`, `RunnerDeps.writeFace`), before the defaults; the read-only mount and org governance never read it — and the write tools read the resolution per call, so a steer into bypass opens the next write and a steer out of it closes the next one. A resume leg folds its SEATS against the row (an `"open"` word the row did not record does not stand) and leaves the mode's face to the resumed mode.
|
|
243
|
+
- **Resume.** A checkpoint row records the write face the leg ran under: `open` when open, `roots` when a seat or a clamp declared the fence (the mode could not have opened it), nothing when the fence was only the default — so a resume keeps a declared fence under any mode it returns to (an approved plan back to bypass included), and a seat's `"open"` the row did not record does not stand.
|
|
244
|
+
- **Who is hurt / compensation.** *A deployment that declares `RunnerDeps.writeFace: "roots"`*: a bypass session stays fenced (7.33.5's task-seat `"open"` outranked it) — the seat is the deployment's ceiling; drop it to let bypass write out of root. *A bypass session's children* (they inherit the mode) write outside the root too, where 7.33.5's translated `"open"` never reached a child; a parent or deployment that wants them fenced declares `writeFace: "roots"`. *server*: retiring `MODE_ENGINE_FACES` needs no replacement for the write face (wire sheet). The system prompt's write bullet is rendered from the face at prepare: a mid-run steer does not rewrite it (the tool reads the live face).
|
|
245
|
+
- Pins: `test/backlog1170-write-face-mode-column.test.ts` (bypass start / steer in / steer out / default and acceptEdits still ask / deployment `roots` outranks the mode, on the real lane; the resolver's row 3′, unit), `test/backlog1111-permission-mode-arbitration.test.ts` (`modePolicyOf` column). Mutations D400–D403 in the file head.
|
|
246
|
+
|
|
247
|
+
### `plan` keeps every tool mounted and ASKS at the call; its three entrances (the spec's word, a steer, the model's `EnterPlanMode`) are one mode (C-R126; #1139 / #1148; **BREAKING** ×2: closed sets / wire) (@server @cli @client-core @sdk @test)
|
|
248
|
+
|
|
249
|
+
**BREAKING — closed sets / types.** `ModeRefusal.mode` (`tool_end.gate.disposition.mode` beside `deniedBy: "mode"`) is `"bypassPermissions" | "dontAsk"` — the word `plan` is gone (an exhaustive `switch` over it loses an arm). `DENIED_BY_VALUES` −1: `plan_mode` (the model-entered plan's direct deny) is no longer minted or listed (`DENIED_BY_MAY_VETO` / `DENIED_BY_MAY_CARRY_CAUSE` lose the row; a `Record<DeniedBy, …>` has one key too many). `ASK_CLASSES` +1: `mode` — the permission mode's own question (`decisionReason: "mode"`); `MODE_ARBITRATION` gains its row `{ bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false }` and every row loses the `planRefuses` column; `RELEASABLE_ASK_CLASSES` gains `mode`. `ModePolicy` loses `handsReadOnly`; `ModePolicy.writeGate` is `"none" | "ask" | "accept_root" | "plan"` (`"deny"` gone). The `ToolSpec` ctx primitive `enterPlanMode` returns `Promise<void>`. `FsWriteGatePolicyOptions.defaultAsk?` (+1 optional).
|
|
250
|
+
|
|
251
|
+
**Behaviour (the reference implementation 2.1.280, cited in the car receipt).** 1. `permissionMode: "plan"` mounts the write tools (the manifest lists `Write` / `Edit`; the model no longer meets `tool.not_found`) and the mode gate ASKS at the call with the reference's sentence — a covered path write `Cannot write to <path> while in plan mode.`, a call to a tool the engine does not define that is not read-only `Cannot call <tool> while in plan mode.` — as the `mode` ask class: a person's yes runs it, a no refuses it (`ask_resolution`), no stored allow rule clears it (the card offers none: `mandated`, `ruleOffersAbsence: "mandated"`), the auto classifier and the sandbox leg never answer it; headless it is refused with the plan remedy, or parks where a checkpoint store is wired. The scratchpad and the advertised writable roots (the memory home) admit a write under plan as under `default` (the reference exempts its scratchpad and agent-memory files the same way). 2. The arbitration point refuses nothing under `plan`: a marked tool's ask, a held / protected write and a probe reach a person (the 7.34.0 stage refused them `deniedBy: "mode"`, `mode: "plan"`). 3. `EnterPlanMode` steers the leg's one mode seat into `plan` (the reference sets the same `mode: "plan"`); the model-entered plan and the spec's word read alike — the direct `plan_mode` deny is gone; a REJECTED `plan_review` steers the resumed leg back into `plan`; the plan reminder attachment fires for every plan leg. 4. A steer into `plan` mid-run asks the same question as a leg that started in plan (#1148 closes; KL-1111a closed; KL-1111k half-closed — see KL-7340re-d). 5. A plan question the policy fold carried only as a CLASS (another layer's ordinary ask won the slot) stays a person's: `ruleClearanceOf` reads the folded classes against the arbitration table, so a stored allow rule cannot retire the composed ask (`RuleClearanceFacts.foldedAskClasses`, +1 optional). 6. An APPROVED or EDITED `plan_review` exits a model-entered plan: the resumed leg reads the spec's word (absent ⇒ `default`), not the row's recorded `plan` (`modeSeedAfterResume`; `PrepareResume.outcome`, +1 optional internal member); a REJECTED review keeps the plan (RB-444).
|
|
252
|
+
|
|
253
|
+
**Who is hurt / compensation.** *cli / server*: a renderer of `disposition.mode` never sees `plan`; a `DeniedBy` table drops `plan_mode`; an exhaustive `AskClass` table gains `mode` (render it as the mode's own question). *Deployments that relied on plan refusing writes headless*: the question is refused where nobody can answer (`ask_resolution` / `no_approver`; the file untouched) — the outcome word changes, the file system does not; with a checkpoint store the question parks (`gate.parked`, an `irreversible_ask` row) and a resume answers it. *Readers of `ModePolicy.handsReadOnly`*: the seat is gone; read `writeGate === "plan"`.
|
|
254
|
+
|
|
255
|
+
- Pins: `test/backlog1139-plan-asks.test.ts` (eight lane cells: yes / no / headless / read-only / steer in / steer out / MCP / `EnterPlanMode`), `test/backlog1111-plan-refuses-external-writes.test.ts` (the second face; MCP `readOnlyHint`), `test/backlog1111-permission-mode-arbitration.test.ts` (the `mode` row, the plan arm, the manifest), `test/enter-plan-mode.test.ts`, `test/permission-denied-hook.test.ts` PD2, `test/rb441-443-444-runtask-review-fixes.test.ts` RB-444.
|
|
256
|
+
|
|
257
|
+
### A user's `Read(<pattern>)` deny rule judges what a command SPELLS; a traversal's members are the built-in tiers' to judge (C-R127; #1143; behavior; +1 optional member on `EstablishedRead`, +1 optional parameter on `ShellReadBoundary.denyMatch`) (@test @cli)
|
|
258
|
+
|
|
259
|
+
- **Before.** The rule row joined the read matcher unqualified: `grep -r TODO .` / `find .` / `ls .` in a project holding one file a `Read(secrets/**)` deny names was refused whole (`read_path_denied` naming the rule) the moment the walk reached that file (test [8490]).
|
|
260
|
+
- **Now.** A traversal's enumerated MEMBERS (`EstablishedRead.walked`) are not the rule rows' to judge — the reference implementation reads a rule against a command's ARGUMENTS (best effort); the operands the command spells, the traversal root included, are: `cat secrets/k.txt` and `ls secrets` stay refused (the reference compiles the deny `Read(secrets/**)` to the ignore pattern `secrets`, which covers the directory itself). The built-in credential tiers and the deployment's `readDenyPatterns` keep judging every member a walk reaches (KL-r2-a stands); the Grep / Glob tools keep pruning the rule-denied file from their own traversals (they read nothing past the table). **User-visible:** a traversal command no longer fails whole because it met a file a user rule denies; a command that names a denied path — its directory included — is refused as before.
|
|
261
|
+
- **Who is hurt.** A user whose `Read(<pattern>)` deny relied on refusing whole traversals: the walk now reads the file (`grep -r` prints its lines). Compensation: a `readDenyPatterns` entry or a built-in tier for the path (both judge every walked member), or a narrower traversal root.
|
|
262
|
+
- Pins: `test/backlog1143-read-deny-explicit-operands.test.ts` (the lane: `grep -r TODO .` / `ls .` run, `cat secrets/k.txt` / `ls secrets` refused, a built-in `.ssh` row still refuses the walk, Grep prunes; the matcher's `walked` qualifier and the judge, unit).
|
|
263
|
+
|
|
264
|
+
### The acceptEdits card for a `cp` / `mv` with option words says why (KL-7340rc2-c; behavior; +1 optional member on `ShellReadVerdict`, +1 optional parameter on `headlessApprovalRemedy`, +1 export `fileCommandOptionsSentence`) (@cli @test)
|
|
265
|
+
|
|
266
|
+
- **Before.** A `cp -r a b` under `acceptEdits` is not placed (the reference's rule: any option word) and reaches a person with the shape tier's generic write question; the headless refusal's remedy did not name the reason.
|
|
267
|
+
- **Now.** The read station records the fact (`ShellReadVerdict.unplacedOptions: { program: "cp" | "mv" }`), the fold carries it onto the pass, and at the arbitration point an ORDINARY ask the mode would otherwise have answered carries the reference implementation's sentence (`cp with flags requires manual approval to ensure path safety: the mode cannot automatically validate cp commands that use flags, as some flags like --target-directory=PATH can bypass path validation.`); headless, the remedy opens with it and says to spell the command without option words. Under `default` the question is unchanged (the sentence belongs to the mode that would have answered).
|
|
268
|
+
- Pins: `test/backlog7340rc2c-file-command-options-card.test.ts` (acceptEdits × `cp -p` / `mv -f` cards, headless text, the placed `cp a b` control, the `default` control; the station's fact and the remedy, unit).
|
|
269
|
+
|
|
270
|
+
### The permission mode is ONE key, ONE table and ONE arbitration point — `TaskSpec.permissionMode` (design/1111; **BREAKING** ×3: API / persisted data / wire) (@server @cli @client-core @sdk @settings-schema @test)
|
|
271
|
+
|
|
272
|
+
**BREAKING — API, types, closed sets.** `TaskSpec.autoModeRequested` and `TaskSpec.approverPosture` are retired; a spec still carrying either is refused at the door by name (`config.spec_retired_key`, table `RETIRED_TASK_SPEC_KEYS`). The one key is `permissionMode?: "default" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | "dontAsk"` (`PERMISSION_MODES`; a word outside the set refuses `config.permission_mode_invalid`, never folds). `TaskSpec.shellGate` keeps ONE value, the operator floor `"always"` (`ShellGateDoctrine` narrowed); `"off"` / `"classify"` refuse `config.shell_gate_value_retired` — the shell's shape tier rides every full shell under every mode, so 7.33's absent seat (the retired off lane) has no successor value. `InheritedGate` loses `autoModeRequested` / `approverPosture` and gains `permissionMode?` (the declared word) and `permissionModeCeiling?` (`{ current, sessionId }`, the parent's live seat — in-process only). `RuntimeCaps` gains `bypassPermissions?: boolean` (the deployment's deny bit: `false` folds a requested bypass back). `RunnerDeps` gains `a2a?: { isolatePeers?: true }` (an A2A peer's egress default becomes an operator mandate). `PermissionResult`'s ask arm gains the public `authoredBy?: "deployment"` (an ask a deployment's own governance layer minted: a person's, under every mode) and the fold-internal `guard?` / `ruleUnreadable?` / `foldedAskClasses?`; an unreadable rule's ask carries `ruleUnreadable` and no `matchedAskRule` (it is not an explicit rule's ask: the class is `rule_unreadable`, releasable under bypass; the card's origin word is `policy` and the message names the rule). `ToolGateInput` gains the seats `permissionMode`, `toolMarkSources`, `a2aIsolatePeers`, `onModeReleased`, `ancestorArbitrationOf` (a host driving the gate without a mode seat arbitrates nothing). Exports — removed: `classifierMayAnswer`, `ApproverPosture` (type); added: `PERMISSION_MODES`, `PermissionMode`, `isPermissionMode`, `PERMISSION_MODE_RANK`, `clampPermissionMode`, `ASK_CLASSES`, `AskClass`, `isAskClass`, `MODE_ARBITRATION`, `ModeArbitrationRow`, `RELEASABLE_ASK_CLASSES`, `RRA_EXPLAINERS`, `classifyAsk`, `foldArbitrationRow`, `AskClassification`, `AskClassFacts`, `ToolMarkSource`, `arbitrateByMode`, `Arbitration`, `modePolicyOf`, `ModePolicy`, `PERMISSION_MODE_FOLD_REASONS`, `PermissionModeFoldReason`, `PermissionModeReceipt`, `ModeRelease`, `ModeRefusal`, `readDenyClassifierRules`, `renderClassifierDenyRules`, `DerivedDenyRule` (type). The `AskOrigin` closed set is unchanged (it is the DISCLOSED word; nothing arbitrates on it any more). Closed sets other entries of this release move, collected here for an exhaustive consumer (each is BREAKING for a `switch` / `Record` over the set): `RULE_STORE_UNREADABLE_KINDS` / `RuleStoreUnreadable` / `isRuleStoreUnreadable` retired with the `ruleStoreUnreadable` member (the C-R172 entry); `SHELL_READ_SEATS` / `ShellReadSeat` −`bash_off` −`bash_classify` +`full_shell`, and `SHELL_READ_DECISION`'s seat keys with them (the read station's seats entry); `TraceEvent`'s `config.additional_directory_skipped` `field` union +`persistedReadRule` (#1173).
|
|
273
|
+
|
|
274
|
+
**BREAKING — persisted data.** A checkpoint row's `inheritedGate` carries `permissionMode` (the declared word at suspend) in place of `autoModeRequested` / `approverPosture`. Rows written by 7.33.x are read ONCE by the resume leg (`migrateLegacyModeSeed`): `autoModeRequested: true` ⇒ `auto`; a posture is dropped (it never authorized anything); a doctrine `shellGate` (`"off"` / `"classify"`) is dropped and `"always"` kept — announced as `resume.legacy_mode_seed_migrated` `{ migrated: { permissionMode?, droppedKeys }, sessionId, runId }`; an encoding the migration cannot read (a non-boolean bit, an unknown floor word) refuses the resume `resume.legacy_mode_seed` rather than run under a guessed mode. The recorded word is the resumed leg's REQUEST (a re-passed spec's word or a steer may move it); the ceiling is the live chain's alone (a redeemed child's rebuilt chain). The next suspend writes the new member; a row with no mode key stays byte-identical.
|
|
275
|
+
|
|
276
|
+
**BREAKING — wire / request face.** `tool_end.gate.disposition` gains, on the allowed arm, `released?: { mode: "bypassPermissions", askClass, classes, origin?, mandate?, reason?: "rule_unreadable" }` (a mode released the ask nobody answered — no settlement beside it, screen I7) and, on the denied arm, the word `deniedBy: "mode"` with `mode: "bypassPermissions" | "dontAsk"`, `askClass`, `classes` (`DENIED_BY_VALUES` +1; screen I8; `plan` never refuses — see the entry above). `AskRequest.authoredBy?: "deployment"` (+1 optional wire key on the request a function approver receives; the park row's `PendingAction.tool_approval.authoredBy` and `CheckpointSummary.authoredBy` twins): the deployment's own governance question, carried through every request mint (the gate's, the three inherited stations', the edit round's, the park) — a host approval seat that settles asks from a session grant reads it to keep its hands off (@server @client-core). `wiring_manifest.manifest.permissionMode: { requested?, effective, foldedBy? }` — the leg's mode receipt (effective half, always present, OUTSIDE `configFingerprint`: a clamped child legitimately differs). `ruleStoreUnreadable` is retired from `AskRequest`, the `tool_approval` frame projected from it, the park row and `CheckpointSummary` (wire key −1; the C-R172 entry). `TaskStream.setPermissionMode(mode)` — the live steer verb: a receipt on every call (also when the word did not move), a word outside the set is a typed rejection (`steering.invalid_content`), a finished task `steering.not_running`. Notice codes — a closed-set expansion of this release, `ENGINE_NOTICE_CODES` +6 (every exhaustive consumer adds the rows): `permission.read_deny_unapplied` (C-R124), `classifier.unavailable` (#1125) and `permission.rule_store_unavailable` (a person's rule store could not be read, so its rows were skipped — C-R172, `{ at, sessionId, runId }`, once per run, audience `user`), each in its own entry, and this entry's three: `config.permission_mode_unavailable` (`{ requested, effective, by, sessionId, runId }`; a child's clamp also on the parent's stream with `childSessionId`), `permission.ask_released_by_mode` (once per (mode, class) per run, `{ mode, askClass, origin?, mandate?, count, firstToolCallId }`), `resume.legacy_mode_seed_migrated`. Four refusal codes, thrown rather than announced (not `EngineNotice`s, absent from `ENGINE_NOTICE_CODES`; read on the error's / terminal's `code`): `config.permission_mode_invalid`, `config.spec_retired_key`, `config.shell_gate_value_retired`, `resume.legacy_mode_seed`. The `AskRequest` a function approver receives is unchanged in shape; the request-side `approverPosture` is gone and `permissionMode`'s vocabulary gains `dontAsk` for every exhaustive consumer (the mode directive the engine states to the model renders from the EFFECTIVE mode — car-1111b).
|
|
277
|
+
|
|
278
|
+
**Behaviour (design/1111 §6.4).**
|
|
279
|
+
1. **Absent = `default`** (the reference implementation's rule), and `default` installs the mode's own write gate (`modePolicyOf`): every covered write (Write / Edit / NotebookEdit, by name) asks — in 7.33 a bare library spec shared bypass's write policy and wrote ungated. A library embedder that writes files with no approver now declares `permissionMode: "acceptEdits"` (writes inside the leg's write roots allowed, outside them ask) or `"bypassPermissions"` (no mode write gate), or wires an approver; a deployment that always went through the serving layer sees no change (it already asked). The default mode's ask stands FIRST when nothing else asked, so a protected write's card carries the judge's `mandated` bit and its structured mandate word rather than the row's name in the message text (the judge's own message is the card where a caller policy or the mode gate did not ask — `acceptEdits` in root, `bypassPermissions`).
|
|
280
|
+
2. **The shape tier rides every full shell** (Bash / Monitor) under every mode: a command the shape classifier cannot prove read-only asks its ordinary question under `default` / `acceptEdits` / `plan` (7.33's absent `shellGate` ran it), the classifier answers it under `auto`, `bypassPermissions` releases it; the read station's deny hit is a refusal under every mode and its out-of-root / unestablishable read a mandated ask (`probe`), which `auto` hands to the classifier and `bypassPermissions` releases. The off lane's `shell-gate-off` operator note is retired with the lane. `shellGate: "always"` still asks on every command (an operator mandate no mode releases).
|
|
281
|
+
3. **`bypassPermissions`** releases every non-immune ask — `ordinary`, `probe`, `write_protection`, `rule_unreadable` (with `released.reason`), `tool_marks_mcp` / `tool_marks_a2a`, `transcript_write`, `artifact_shared_read` — as an allow with `decisionReason: "mode"` and the `released` disposition member, announced once per (mode, class) per run; it KEEPS a hook's ask, an explicit ask rule's, an org rule's, governance-unavailable, an operator-forced ask (the `always` floor, a deployment-authored ask, a deployment-marked tool, the A2A default under `isolatePeers`), the AskUserQuestion and an ancestor's mark (K-ASK); it REFUSES a write whose protection could not be judged (D4b, `deniedBy: "mode"`). The Artifact tool's shared-read consent is arbitrated the same way and a release is never remembered as a person's approval.
|
|
282
|
+
4. **`dontAsk`** refuses every ask but a hook's or an operator authority's (`deniedBy: "mode"`, `decisionReason: "mode"`) — the engine executes the word the shell used to translate.
|
|
283
|
+
5. **`auto`** arms the classifier only with the deployment face (`RunnerDeps.autoMode`) and without the org's deny bit (`RuntimeCaps.autoMode: false`); otherwise the leg folds to `default` and says so (`config.permission_mode_unavailable`, `by: "face_absent" | "operator_deny"`). The classifier station reads the mode: on a `default` leg whose deployment merely wired the face, the decider is never consulted. The classifier's prompt lists the leg's effective read-deny table as `Read(**/…)` lines ahead of the deployment's own list (car-1111b), and the `probe` row is classifier-eligible.
|
|
284
|
+
6. **`plan`** mounts every tool, the plan pair included, and its write gate ASKS at the call with the plan sentence (the `mode` ask class — see the entry above; the arbitration point refuses nothing under plan); **`acceptEdits`** allows writes inside the task root and every additional working directory (`additionalDirectories`, advertised writable dirs) and asks outside them — the seats a serving layer used to derive now come with the word.
|
|
285
|
+
6b. **An ancestor's frozen classifier follows the modes**: a leg hands its children a chain carrying its classifier only while it runs `auto` NOW (a parent that steered out of `auto` before spawning hands none), and under `dontAsk` / `bypassPermissions` / `plan` the inherited stations do not dispatch a decider on the chain — the mode's arbitration decides the ancestor's ask first (7.33 dispatched a decider on the chain regardless). Under `default` / `acceptEdits` / `auto` the ancestor's classifier keeps its authority over the ancestor's ask (the F-012 form). A child that becomes `auto` through its parent's live seat arms its own classifier on its next gate read (the call that observed the transition may still meet the person).
|
|
286
|
+
7. **A child leg is clamped to its parent's live word** — a bypass child needs a bypass parent; `acceptEdits` and `auto` never adopt each other; otherwise the rank table — with a notice; a child stating nothing inherits; a parent's steer reaches a running child on the child's next gate read (no forwarding frame). A child stating `auto` under a `default` parent does not arm. The observer sidecar inherits the parent's chain the way the delegation lanes' children do.
|
|
287
|
+
8. `RuntimeCaps.bypassPermissions: false` folds a requested bypass back to `default` with a notice; without the bit bypass stands. The deployment's folds apply to a CLAMPED word too (a bypass child under an `auto` parent with no classifier face runs `default`, `by: "face_absent"`). A resolver fault, and a marshalled non-boolean on ANY deny bit, deny every bit (`autoMode` and `bypassPermissions` both `false`, each fault announced).
|
|
288
|
+
9. The mode directive the engine states to the model renders from the effective mode (car-1111b): a requested `auto` that folded says nothing about auto; a `bypassPermissions` leg is told the bypass paragraph.
|
|
289
|
+
10. The peer lane's mode class folds the effective permission mode beside the ask derivation (car-1111b); the read station's seats are FACES (`full_shell` / `readonly` / `rejudge`), never the mode — `declined` on the full shell is the ordinary ask a stored rule retires.
|
|
290
|
+
|
|
291
|
+
**Who is hurt / compensation.** *Library embedders*: the retired keys refuse by name (the door's sentence names the replacement); a bare spec that wrote files now asks — and headless, with no approver wired, the write is refused fail-closed (`approval for "Write" could not be resolved`); declare a mode (`permissionMode: "acceptEdits"` for writes inside the task root) or wire an approver (see 1). *server*: `deriveSettingsPolicy`'s mode switch (`task-settings.ts:1131-1153`) and `MODE_ENGINE_FACES` (`:267-300`) retire — pass `permissionMode` through; `disableBypassPermissionsMode` lands on `RuntimeCaps.bypassPermissions`; `S-85`. *cli*: the dontAsk arm (`liveToolApprovalWire.ts:575-612`) reads `deniedBy: "mode"` and `released`; capability bit `permissionModeArbitration`. *client-core*: the three mode mirrors (`permissionWireCaps.ts:28/38/54`, `seatContract.ts:592-605`) gain `dontAsk`; `normalizeShellPermissionModeForWire` keeps `manual → default`. *sdk*: `TaskRequest['permissionMode']` gains `dontAsk`; `approverPosture` leaves the request body. Coordinates are design/1111 §6.3's; the wire sheet is `docs/wire-consumption/unreleased-1111.md`.
|
|
292
|
+
|
|
293
|
+
- Pins: `test/backlog1111-permission-mode-arbitration.test.ts` (the closed sets and the tables; `classifyAsk` fact by fact with the F1–F3 shadowing pairs; `arbitrateByMode` per mode incl. D4b; the clamp table; the controller's folds, the live ceiling and the steer; the migration matrix; the gate's point through `runToolGate` — release / K-ASK kept / `authoredBy` negative control / U1 reason / dontAsk I8 / the classifier reads the mode; through the Runner — the three doors, absent ≡ default, `plan` keeps the write tools mounted and asks at the call, the bypass and auto folds with notices, bypass release on every call with one notice, an ask rule kept, dontAsk `deniedBy: "mode"`, the J2 classifier prompt, the child clamp, `setPermissionMode`, the checkpoint member), `test/backlog525-auto-intent-persistence.test.ts` ③b (the legacy row: `true` migrates and arms with the notice; `"true"` refuses), `test/backlog1002-auto-mode-directive.test.ts` (the directive from the effective mode), `test/backlog521-auto-arming-polarity.test.ts` (the chain carries the effective word), `test/prepare-inherited-gate.test.ts`, `test/tighten-task-spec.test.ts` (one floor word), `test/gate-stations-closed-set.test.ts` rows 32/33, `test/backlog939-deny-hit-card-names-pattern.test.ts` (a deny hit under bypass), `test/backlog930-decline-not-read-only-clearable.test.ts` (the `always` floor's ask is the operator's: a stored allow does not retire it), car-1111b's `test/backlog1111b-*.test.ts` (read seats, deny paragraph, directive, peer class). Re-pinned to the new truth (the off lane folded into the shape tier; the default mode's write gate): `backlog696` / `758` / `763` / `766` / `767` / `768` / `782` / `791` / `993` / `994` / `7300` / `681` (the probe row is the classifier's under auto) / `286` (default asks about every write; the row's mandate on the card) / `850` G8 / `897` / `approval-settled-by` / `design252` / `prepare-gate-stations-transcript`, and 27 files whose writes now declare `acceptEdits` (in-root) or `bypassPermissions` (a custom `Write` targeting `/…`, a containment pin, a bare-ctx child).
|
|
294
|
+
|
|
295
|
+
### The shell read station's seats are faces — `full_shell` / `readonly` / `rejudge`; a scan decline is one ordinary ask on the full shell in every mode (design/1111 §5.1; behavior; **BREAKING — closed set**: `SHELL_READ_SEATS` / `ShellReadSeat` / `SHELL_READ_DECISION` keys re-keyed (−`bash_off` −`bash_classify` +`full_shell`) — listed with the release's BREAKING closed sets (design/1111 entry); +0 codes) (@server @cli @test)
|
|
296
|
+
|
|
297
|
+
- **Before.** Two of the table's four seats were keyed on the shell-gate doctrine and disagreed on one row: a scan DECLINE (`echo <path> | xargs cat`, an unreadable program position) was a mandated ask where the station stood alone — the absent / `off` doctrine, which is also what a bypass session and a library embedder with no doctrine mounted — and an ordinary ask beside the shape classifier (`classify`). The stricter card went to the session with nobody watching.
|
|
298
|
+
- **Now.** The seats name the FACE (`full_shell` the writable shell, `readonly` the read-only face, `rejudge` after an approval); the station never reads the permission mode — that happens once, at the gate's arbitration point. `declined × full_shell = ordinary` (C-R67 (B), unchanged for the classify doctrine), every other row keeps the value both retired seats gave. The `deny_hit` contract now says what it always meant: refused for a spelling the station can ESTABLISH; an unestablishable spelling is the `unestablishable` row, never a silent vouch. `judgeShellRead({ seat })` callers pass `"full_shell"` where they passed either retired seat; a seat word outside the three (a retired word from an untyped caller) is refused loudly rather than answered with no disposition.
|
|
299
|
+
- **Who needs it.** One answer per finding on the full shell, whatever the configuration; the mode decides what happens to the ask in one place.
|
|
300
|
+
- **Who is hurt.** Where the station stood alone, a decline is now an ordinary ask: a person's stored allow rule for that exact command retires it, the auto-mode classifier may answer it, and under `bypassPermissions` it is released with disclosure. **Compensation.** The card is never vouched away (a person is still asked where one is asked); a deny row still refuses every spelling the station can establish; KL-945e updated. Test fixtures that asserted "the off seat mandates a decline" now assert the one-seat answer.
|
|
301
|
+
- Pins: `test/backlog1111b-read-seats.test.ts` (the seat set; the decline lane with the floor absent — one ordinary card, a stored answer retires it; J3 — clay's heredoc commit and variable-built path stay the station's mandated ask; J7 — unestablishable spellings of denied files are asked, never refused or vouched, the literal and a placeable variable are refused). Mutation-checked: `declined → mandated`, `unestablishable → ordinary`, `deny_hit → mandated` each red. Docs: `docs/CLOSED-SETS.md` (`ShellReadSeat`), `docs/INTEGRATION-CORE.md` (`judgeShellRead` seat), `docs/PERMISSION-MODES.md`.
|
|
302
|
+
|
|
303
|
+
### The auto-mode classifier is shown the engine's own deny rows — the effective read-deny table joins `<settings_deny_rules>` (design/1111 §5.3; behavior (auto only); +1 optional `BuildAutoModePromptOptions.derivedDenyRules`, +1 optional recipe body member `AutoModeArmingRecipe.derivedDenyRules`, `autoModeArmingRecipeOf` `bind.derivedDenyRules`; exports `readDenyClassifierRules` / `renderClassifierDenyRules` / type `DerivedDenyRule`) (@server)
|
|
304
|
+
|
|
305
|
+
- **Before.** The paragraph rendered only what a deployment spelled into `RunnerDeps.autoMode.settingsDenyRules`; the rows the read faces refuse never reached the classifier — while a spelling the read station cannot establish becomes the classifier's to answer in this release.
|
|
306
|
+
- **Now.** One line per effective read-deny row (built-in tiers, deployment and task additions), in the engine's own `Read(...)` path-rule grammar: `Read(//**/<row>)` with every run of stars written `*` (a row is a segment run matched anywhere that covers what is under it; the grammar reaches the path it names and everything under it), followed by `(any ASCII letter case)` when the row ignores letter case (the deny set's default); an all-star row (`*` — deny every path) is stated, only a row with no segment at all is skipped; duplicates fold after this encoding. The engine lines come ahead of the deployment's list, in the reference paragraph's own words. A row's text can come from a task (a spawned task's author included), so an engine line states the row as a rule and nothing else: whitespace, separators and format characters of every script (and their compatibility forms), controls, backticks, `<`, `>`, `(`, `)` and the glob classes the grammar has no form for (`?`, `[`, `]`, `{`, `}`, `\`) become `*`, an over-long pattern is cut and closed with `*` — never narrower than the row, still a rule, and no line break, tag, second rule or sentence can come through; the engine enforces the row as compiled. `AutoModeArmingRecipe.derivedDenyRules` / `BuildAutoModePromptOptions.derivedDenyRules` hold `{ rule, anyCase? }` lines. With no engine lines the prompt is byte-identical to 7.33 (every recorded digest stays valid). The persisted arming records the engine half as prompt BODY: it enters the digest, a rebuild reproduces it, and the fold compares it exactly as it compares `settingsDenyRules`.
|
|
307
|
+
- **Who needs it.** Auto-mode sessions whose deny rows must hold against a spelling the station cannot place.
|
|
308
|
+
- **Who is hurt.** A cross-process redemption of an armed ancestor: the redeeming host must recompute the engine half from its own read-deny configuration and the row's task additions (`readDenyClassifierRules`) and pass it through `autoModeArmingRecipeOf(face, { derivedDenyRules })`; a difference refuses the rebuild as `settings_moved` (the redemption keeps its honest refusal and a person decides). **Compensation.** The refusal is the existing, fail-closed path; the recompute is one exported call.
|
|
309
|
+
- Pins: `test/backlog1111b-deny-rules-prompt.test.ts` (the ORACLE — for every row in a matrix of plain, all-star, case-sensitive and case-insensitive rows, the line reaches, by the engine's own `parseRuleText` / `pathRuleReachOf`, exactly the sample paths the deny set refuses, mixed-case paths included; every line parses as a rule; the case note; the skip rule; the glob-class and format-character rendering; the paragraph from the engine's facts; the splice and the byte-identical no-lines case; the recipe member's canonical form, the fold's comparison — the case note included — and the rebuild's digest). Mutation-checked: engine lines dropped, raw characters kept, a trailing all-star written `**`, format / separator classes dropped, all-star rows skipped, glob classes kept, the case note dropped, fold ignores the member, rebuild drops it — each red.
|
|
310
|
+
|
|
311
|
+
### The mode directive renders from the engine's facts — effective mode and ask derivation (design/1111 §6.4 ②; behavior; `ApprovalPostureFacts` reshaped to `{ mode, askEffective }`; +0 wire keys) (@cli)
|
|
312
|
+
|
|
313
|
+
- **Before.** The first-turn directive read the request-side seats: the bypass paragraph only when a host declared the posture, the auto paragraph on a requested auto mode whether or not the classifier armed.
|
|
314
|
+
- **Now.** Bypass paragraph ⇔ the effective mode is `bypassPermissions` ∨ `wiring_manifest.ask.effective === "auto_allow"` (no person answers any ask); else the auto paragraph ⇔ the effective mode is `auto` (armed); exclusive, bypass first; the wording and the once-per-context restatement are unchanged.
|
|
315
|
+
- **Who needs it.** A session operating under bypass — including one whose shell answers every ask with a blanket seat — is told so, as the reference implementation tells its bypass sessions.
|
|
316
|
+
- **Who is hurt.** A deployment with a blanket `onAsk: "allow"` seat now gets the bypass paragraph (with its shell-first guidance) on the first turn, whether or not a shell is mounted; a requested auto mode that folded back gets no auto paragraph. **Compensation.** The paragraph is the reference wording, SHA-locked; a live approver seat gets none.
|
|
317
|
+
- Pins: `test/backlog1111b-directive.test.ts` (composer table; lane: a blanket allow seat ⇒ one marked bypass frame; a live approver ⇒ none). Mutation-checked.
|
|
318
|
+
|
|
319
|
+
### The peer mode class reads the effective mode (design/1111 §5.7 / §6.4 ⑦; behavior; `foldPermissionModeClass(effective, askEffective)`; `PeerLaneRefs.permissionMode`; `from-mode` word set unchanged) (@server @cli @client-core)
|
|
320
|
+
|
|
321
|
+
- **Before.** A session's `from-mode` was folded from the ask derivation alone, so a bypass session with a live approver seat attested `prompting`.
|
|
322
|
+
- **Now.** `bypass` ⇔ the effective mode is `bypassPermissions` ∨ the ask derivation is `auto_allow`; otherwise `unresolved` ⇒ nothing attested, else `prompting`; the class is read live, so a mid-run mode change moves the next send's attestation. `from-mode` is a presence label (is anyone watching this session's approvals), never a permission.
|
|
323
|
+
- **Who is hurt.** A bypass session's messages to a `prompting` peer are held for review (`mode-mismatch`) where they were delivered. **Compensation.** The hold is the peer's review queue, as for any mismatched pair; two bypass sessions deliver to each other.
|
|
324
|
+
- Pins: `test/backlog1111b-peer-mode-class.test.ts` (fold table; J12c; the lane's live read). Mutation-checked.
|
|
325
|
+
### A user's `Read(<pattern>)` / `Edit(<pattern>)` deny rule joins the engine's deny table — one table, a read face and a write face (#1132 read face / #1135 write face; behavior (tighten-only); `ToolPolicy.pathDenyRules` +1 optional member; exports +6 (`toolPolicyPathDenyRules`, `compileWriteDeny`, `DENY_FACES`, `DenyFace`, `PathRuleDenyRow`, `WriteDenyMatcher`); `compileReadDeny` +1 optional `rules` argument, `ReadDenyMatcher.ruleRows` +1 optional member; `HandsToolkitOptions.pathDenyRules` +1; `readDenyClassifierRules` +1 optional `rules` argument, `renderClassifierDenyRules` +1 optional `readDenyRules`; +0 wire keys, +0 codes) (@server @cli @test)
|
|
326
|
+
|
|
327
|
+
- **Before.** The rule DSL judged a path deny rule only on a call to the tool it names: a `Read(secrets/**)` deny stopped the Read tool while `cat` / `head` / `tail` / `sed -n` / a `<` redirection / Grep / Glob read the file into the model's context; an `Edit(secrets/**)` deny stopped the Edit tool while a Write, a new file, `echo > f`, `sed -i`, `tee`, `cp` / `mv` / `touch` / `mkdir` / `rm` changed the directory (bypass / acceptEdits: with no card). The reference reads these rules by what a call does to a path (a Read deny covers every file-reading tool and the shell's file commands, and refuses writing the same path; an Edit deny covers every tool that edits a file).
|
|
328
|
+
- **Now — one rule.** The rule compiler reads its Read / Edit path DENY rules as rows of the engine's deny table (`ToolPolicy.pathDenyRules`; `Read` ⇒ the read and write faces, `Edit` ⇒ the write face; carried through `combinePolicies`) and every face judges them with the rule's own reach (the path it names and everything under a directory it names, against the policy's bases):
|
|
329
|
+
- the **tool lane** (the policy itself): a path deny rule reaches any call whose declared path target is on one of its faces — a Read deny refuses a Write / a NotebookEdit / a Grep or Glob rooted at the path, an Edit deny a Write (a new file included) and a NotebookEdit; `Write(<path>)` keeps its tool's name (stricter than the reference, which does not check it); an ask rule and a command rule are unchanged;
|
|
330
|
+
- the **read face**: the rows join the read matcher every read face judges with (`compileReadDeny`) — Read / Grep / Glob / RepoMap and their traversals (a denied file's name and content stay out of a search over its parent; the rg leg's tripwire now judges each path it returns in its absolute spelling), the shell read station (`cat`, `head`, `sed`, redirections, substitutions — refused, `deniedBy: "read_boundary"`), the attachment reader;
|
|
331
|
+
- the **write face** (`compileWriteDeny`, the same rows and the same judgment): the shell station judges a command's WRITE targets — `>`-family redirections, a sed `w` file, the operands of `mkdir` / `touch` / `rm` / `rmdir` / `mv` / `cp` / `tee` / an in-place `sed` (`SHELL_WRITE_COMMANDS`; GNU option abbreviations such as `--in-p` read as the option), a copy / move destination's children (`cp a dir` writes `dir/a`; `-t` / `--target-directory` included) and an in-place sed's backup files (`-i.bak` writes `<file>.bak`) — and refuses a hit under every mode (`deniedBy: "read_boundary"`; the sentence names "the write deny list" and writes, with no `read_path_denied` card);
|
|
332
|
+
- the **write tools' own path resolution** (Write / Edit / NotebookEdit) judges the canonical target with the write face, as the read tools judge theirs with the read face — so a write spelled through a symlink into a denied directory is refused (a `write_path_denied` refusal, no card) even though its spelling passed the gate's rule lane;
|
|
333
|
+
- the **classifier paragraph** states the rows beside the table's segment rows (`Read(secrets/**)`, `Edit(...)`, the pattern stated inertly);
|
|
334
|
+
- the rows are resolved against the policy's bases as spelled AND, where the leg's execution environment spells a base differently (a project root reached through a symlink, `/tmp` → `/private/tmp`), against the canonical bases too (`withCanonicalRuleBases`) — the faces judge canonical keys, the spelled row keeps the lexical views.
|
|
335
|
+
- **Who needs it.** Every user who wrote a Read / Edit deny rule in their settings expecting it to hold.
|
|
336
|
+
- **Who is hurt.** A session that read or wrote a denied path through the shell or a search tool now gets a refusal. A `cp` / `mv` SOURCE under an Edit-denied directory is refused too (every operand of a write command is a write target, as the reference judges it). A cross-process redemption of an auto-armed ancestor recomputes the classifier's engine lines with the rule rows (`readDenyClassifierRules(entries, toolPolicyPathDenyRules([...]))`) — a difference refuses the rebuild as `settings_moved`, and a person decides. **Compensation.** The deny is the user's own rule, stated verbatim in every refusal; nothing else about the shell or the search tools changed (a command that names no denied path is judged as before).
|
|
337
|
+
- Pins: `test/backlog1132-deny-tables.test.ts` (the rows and their faces; composition; the tool lane by face; one table two faces; the seat; the write sentence; the station's write targets per arm; the classifier lines; the absolute tripwire; real lanes — the shell's reads refused and the sentinel absent, Grep / Glob keep the denied file out, the counter-controls; a new file under an Edit-denied directory refused under acceptEdits and bypass; the shell's nine write forms refused and the bytes unchanged; a Read deny refuses writing; an Edit deny does not refuse reading). Mutation-checked: eleven mutations, each red.
|
|
338
|
+
|
|
339
|
+
### A headless auto-deny names the change that would have let the call run (#1137; behavior: text; +1 optional `AskUnresolvableFacts.remedy`, +1 optional notice detail key `delegation.ask_unresolvable.detail.remedy`; +0 codes) (@server @cli @client-core)
|
|
340
|
+
|
|
341
|
+
- **Before.** With no approver wired, every Write / Edit / Bash question 7.34.0's `default` mode asks was refused as "approval required for "Write" but no approver is wired (headless auto-deny): …" — nothing told a library embedder or a `-p` user which declaration would have let it run.
|
|
342
|
+
- **Now.** One sentence (`headlessApprovalRemedy`, ask-origin.ts) follows the refusal in the tool text and rides the `delegation.ask_unresolvable` notice (message and `detail.remedy`), read off the effective mode, the ask's folded class row and whether the call is a covered write: a bypass-immune class ⇒ only an approver (`onAsk`) — no mode is offered as a way round a rule a person wrote; a covered write's ordinary question under `default` / `auto` ⇒ the mode's write gate asks every write there, `permissionMode: "acceptEdits"` lets writes inside the working directories through (a question another policy raised still stands); under `acceptEdits` ⇒ the mode asks only for a write outside the working directories — if the target is outside, add its directory to `additionalDirectories`; any other releasable ask ⇒ `permissionMode: "bypassPermissions"`. Every arm names the approver seat.
|
|
343
|
+
- **Who needs it.** Library embedders and `-p` users on the new `default` write gate.
|
|
344
|
+
- **Who is hurt.** A consumer that matched the auto-deny text byte-for-byte now sees a clause after it. **Compensation.** The original sentence is unchanged and still leads; the clause is also on the notice's `detail.remedy`, so a consumer reads it as a field instead of parsing text.
|
|
345
|
+
- Pins: `test/backlog1137-headless-remedy.test.ts` (the four arms; the notice carries the sentence verbatim and stays byte-identical without one; real lanes: default × Write in the root, acceptEdits × Write outside, default × `echo > f`, an ask rule under bypass — the tool text and the notice name the same sentence, the file is not written). Mutation-checked per arm.
|
|
346
|
+
|
|
347
|
+
### A mode's refusal carries no read-station card (KL-1111j; behavior: `tool_end.errorCode`; +0 keys) (@server @client-core)
|
|
348
|
+
|
|
349
|
+
- **Before.** A `dontAsk` refusal of a shell call the read station had mandated (`rm -rf "$UNSET/x"`) rode the station's containment card: `tool_end.errorCode` / `structured.code` said `readonly_out_of_root` (paths empty) while the cause was the mode.
|
|
350
|
+
- **Now.** A refusal whose disposition is `deniedBy: "mode"` carries no station card; its cause is the disposition (`mode`, `askClass`, `classes`). A read the station refuses on its own (a deny row) keeps its `read_path_denied` card, as does a refusal by any other layer.
|
|
351
|
+
- **Who is hurt.** A consumer that counted `readonly_out_of_root` on mode refusals. **Compensation.** Read `gate.disposition.deniedBy` — the field every other mode refusal already needs.
|
|
352
|
+
- Pins: `test/backlog1137-headless-remedy.test.ts` (KL-1111j block: dontAsk × unverifiable delete ⇒ no containment code; a deny row keeps `read_path_denied`). Mutation-checked.
|
|
353
|
+
|
|
354
|
+
### `acceptEdits` answers the reference implementation's seven file commands (#1138; behavior (acceptEdits only); `ModePolicy.fileCommands` +1 member, exports `ACCEPT_EDITS_FILE_COMMANDS` / `modeAcceptsFileCommand`; `Arbitration` +1 kind `accepted`; `ShellReadVerdict.writesPlaced` / `ShellReadBoundary.writeRoots` +1 optional member each; +0 wire keys) (@server @cli @test)
|
|
355
|
+
|
|
356
|
+
- **Before.** Under `acceptEdits` only the Write / Edit tools wrote inside the working directories without asking; `mkdir`, `touch`, `rm`, `rmdir`, `mv`, `cp` and `sed` asked (headless: refused) — the reference (2.1.280) accepts them ("auto-approve file edits and common file commands").
|
|
357
|
+
- **Now.** `modePolicyOf("acceptEdits").fileCommands` is that closed set (`ACCEPT_EDITS_FILE_COMMANDS`, verbatim from the reference); `modeAcceptsFileCommand` reads a call against it — every segment a simple command whose literal, unquoted command word is in the set (no redirection, pipe, background, substitution, leading assignment or wrapper; `/bin/rm` and `"rm"` are not `rm`, as in the reference); the arbitration point's acceptEdits arm answers such a call's ask when it is ORDINARY and nothing else (`accepted`: an allow with `decisionReason: "mode"`, like the mode's in-root write gate — no `released` record, no release notice, screen I7 unchanged). The order is the reference's: every rule lane runs first (a `Bash(rm:*)` deny still refuses; an ask rule, the unverifiable-delete guard, a read-boundary finding — e.g. a target outside the working directories — each add a class and keep the ask), and — the reference's path checks, dangerous removal included — the mode answers only a command the read station PLACED: `ShellReadVerdict.writesPlaced` = every write target established inside the WRITE roots (the task root and the additional working directories; a read-only grant or an open read face does not count), no `rm` / `rmdir` target that is a write root or contains one, no `rmdir -p` / `sed -f` (GNU abbreviations included), and — while the write face has rows — no command whose writes DERIVE from its operands (every `cp` / `mv`, a recursive `rm`, an in-place `sed` keeping a backup: members, children and twins the station does not enumerate), no re-entered shell or unreadable text. `rm -rf .`, `touch <read-only dir>/x`, `sed 'w /elsewhere'`, `sed -f cmds.sed` and a `run_in_background` call still ask.
|
|
358
|
+
- **Who needs it.** acceptEdits sessions and embedders whose file housekeeping went through the shell.
|
|
359
|
+
- **Who is hurt.** An acceptEdits user who relied on a card before `rm` / `mv` inside the working directories. **Compensation.** Rules still come first (`Bash(rm:*)` as ask or deny keeps the card or the refusal); `default` is unchanged; an unverifiable recursive delete and anything outside the working directories still ask.
|
|
360
|
+
- Pins: `test/backlog1138-accept-edits-file-commands.test.ts` (the closed set per mode; the call reading, per arm; the arm answers only an ordinary ask; real lanes with a real Bash: the seven land under acceptEdits as a plain allow; counter-controls — `default` still asks `mkdir`, a `Bash(rm:*)` deny refuses first, `echo > f` and `touch f > log` still ask, an out-of-root `touch` still asks, `rm -rf .` / `rm -rf <root>` still ask while a removal inside lands; a read-only grant, an open read face, `sed w` elsewhere, `sed -f`, `rmdir -p` and a background run still ask). Mutation-checked per arm.
|
|
361
|
+
|
|
362
|
+
### `plan` asks about a tool the engine does not define unless it is read-only (design/1111 plan column, in C-R126's asking form; behavior (`plan` only); +0 exports, +0 wire keys) (@server @cli @test)
|
|
363
|
+
|
|
364
|
+
- **Before.** Under `permissionMode: "plan"` the mode write gate refused only a covered PATH write (`isProtectedWrite`): a caller-supplied tool declaring `effect: "write"` (or no effect — the declared default) with no path target ran with no question and no approver (`disposition: {kind: "allowed"}`), and so did an MCP / A2A tool no annotation marked.
|
|
365
|
+
- **Now.** The plan arm asks about `planRefusesWrite(call, roster, readOnlyDeclared)`: a covered path write (the catalog floor), or a call to a tool the ENGINE DOES NOT DEFINE (roster source other than `builtin` / `synthetic`: a caller's, an MCP server's, an A2A peer's) that is not read-only — the reference implementation's plan arm for a tool it did not write (2.1.280: `mcpInfo && !isReadOnly` ⇒ an ask `{type: "mode", mode: "plan"}`), asked here as the reference asks it. Read-only = the tool's effect on the leg's roster is `read` (the deployment's word: `effect: "read"`, an MCP `toolAxes` override) OR — only where nobody declared the tool's effect (roster `effectProvenance: "default"`) — the mounted tool declares itself read-only (an MCP server's `readOnlyHint`, which the reference reads for this question). A declared effect always outranks the server's hint: a deployment's `toolAxes: { <tool>: { effect: "write" } }` (or `"idempotent"`) beside `readOnlyHint: true` asks under plan. Only a declared EFFECT counts: a `toolAxes` entry that states other members only (`{ egress: false }`, `{ irreversibility: "never" }`) declares no effect — the protocol fold registers an effect only where the axis carries one, and an A2A skill's axis no longer pre-fills `effect: "write"` (`MaterializedA2a.toolAxes[]` carries `effect` only when the caller declared it) — so such a tool's roster row reads `effect: "write"` (the default every reader applies, unchanged) with `effectProvenance: "default"` (it read `caller` for every MCP tool a server annotated destructive / open-world and every A2A skill, although nobody had declared an effect), and plan reads its server's `readOnlyHint`. A tool the server annotates both `readOnlyHint` and `destructiveHint` reaches a person under plan through its mark's own ask (`tool_marks_mcp`) — the arbitration point refuses nothing under plan (C-R126). The two are different questions: the roster effect stays the repeat-safety axis a server's self-claim never lowers (design F, unchanged); whether plan leaves a tool to run trusts the server's `readOnlyHint` as the reference does — for this question only (KL-1111l). The engine's own tools are not judged by their effect (plan keeps every tool mounted, a delegated child inherits `plan` as its ceiling, and the shell station and the tool marks raise the questions the arbitration point decides): TodoWrite, the task-list tools and Agent run as before. The question is the mode write gate's (class `mode`, the sentence `Cannot call <tool> while in plan mode.`; a no — or nobody, headless — refuses it as the ask's resolution, `deniedBy: "ask_resolution"`), as the covered path write's is (`Cannot write to <path> while in plan mode.`). The plan the model enters mid-run (`EnterPlanMode`) is the same mode seat and asks the same questions (C-R126).
|
|
366
|
+
- **Who needs it.** Plan sessions — CLI `--permission-mode plan`, a library embedder's `plan` — that mount the deployment's own, MCP or A2A tools.
|
|
367
|
+
- **Who is hurt.** A deployment whose plan leg ran its own write tool (a caller tool with no `effect` counts as a write), and an MCP tool that neither the server annotates `readOnlyHint` nor the deployment vouches read. **Compensation.** Declare `effect: "read"` on a caller tool that only reads, or `toolAxes: { <tool>: { effect: "read" } }` on an MCP server; leave plan to act. A reader of the roster that took `effectProvenance: "caller"` on an MCP / A2A row as "the deployment declared this effect" was reading a default for every annotated MCP tool and every A2A skill with no `toolAxes` effect; those rows now say `default` (the effect value is unchanged). Residuals: KL-1111l (KL-1111k: see the C-R126 entry).
|
|
368
|
+
- Pins: `test/backlog1111-plan-refuses-external-writes.test.ts` (real lanes: a caller `effect: "write"` tool and an effect-less one asked, refused headless and not run; a real stdio MCP server — `readOnlyHint: true` runs, `readOnlyHint: false` and no annotation asked, a `toolAxes` `effect: "read"` runs, `readOnlyHint: true` beside a `toolAxes` `effect: "write"` / `"idempotent"` asked; `readOnlyHint: true` beside a `toolAxes` that states only `egress: false` / `irreversibility: "never"` runs, roster `default`; `readOnlyHint` + `destructiveHint` asked through the mark (headless: `ask_resolution`); controls — a caller `effect: "read"` tool, TodoWrite, the read-only Bash, Read, and `default` × the write tool run; the predicate over MCP / A2A / caller / builtin / synthetic rows and the read-only input). Mutation-checked per conjunct.
|
|
369
|
+
|
|
370
|
+
### The auto-mode classifier's budget is 180 s, and a classification that cannot answer goes to a person where there is one (#1125, C-R118 (a)+(b); behavior (auto only); a departure from the reference implementation; exports +4 types, `ToolGateInput.autoMode.unavailable?`, notice +1 `classifier.unavailable`, thrown codes +2 `config.auto_mode_timeout_invalid` / `config.auto_mode_denial_limit_invalid`, +1 optional `AutoModeDecider.timeoutMs`) (@server @cli @client-core @test)
|
|
371
|
+
|
|
372
|
+
- **Before.** Each classifier rung had 60 s (`AUTO_MODE_DEFAULT_TIMEOUT_MS`), sized for a cloud API. On a self-hosted gateway a classification that took longer (queue + prefill; measured live up to 165 s) was cut and the call REFUSED — "The auto-mode classifier is temporarily unavailable (timed out) …" — whatever approver was wired: the TUI showed an error per call and no card, a `-p` run exited 0 with nothing about it on stderr (test UX-7). `RunnerDeps.autoMode.timeoutMs` moved the budget but was never screened: `0`, a negative or `NaN` timed every classification out at once.
|
|
373
|
+
- **Now.** (a) The default is **180 000 ms per rung** (a two-rung walk is 360 s at worst). A present `timeoutMs` that is not a number of milliseconds in [1, 2147483647] fails the prepare `config.auto_mode_timeout_invalid` — never clamped, never read as the default; there is no OFF. The face's three value knobs are screened at EVERY leg's prepare while `RunnerDeps.autoMode` is present, whatever mode the leg starts in (`screenAutoModeKnobs`: `timeoutMs`, `classifierFallback`, `denialLimit`) — a leg that starts in `default` with a bad knob used to prepare fine and meet the refusal only when a steer (or a parent's live ceiling) armed auto mode, where a failed steer left the leg in `auto` with no classifier and a child's arming was an unhandled rejection. A bad `denialLimit` (a bad bound, a value that is not a record, or a member that is not one of its three — `{consecutive: 0}` used to read as the defaults) now carries a code, `config.auto_mode_denial_limit_invalid` (thrown, like its two siblings; the message is unchanged). A steer into `auto` whose arming fails rejects with the arming's code (`setPermissionMode`) and stops counting: the requested word is the newest steer that still counts (a steer still arming, or one that took effect — which drops the older ones), else the spec's word, so in no completion order of overlapping steers does a failure undo a newer steer, leave the leg in `auto` on its own word, or leave it on a word none of them left after one succeeded; the leg arms again on its next move into `auto`; an arming a read started (a child whose parent's live ceiling moved into `auto`) reports a failure through `RunnerDeps.onError` (phase `config`; an observer that throws or rejects is contained — no unhandled rejection) — the leg then has no classifier and its asks go on to its approval seat. A cross-process rebuild (`rebuildAutoModeDecider` / `foldAutoModeArming`) screens each side's budget against the knob's range BEFORE it takes the minimum: the redeeming deployment's out-of-range `timeoutMs` is `malformed_current_settings`, a recorded one `malformed_recipe` — never hidden behind the other side's smaller value. (b) A classification that could not answer (an error, or the budget) forks on ONE fact, the leg's approval seat — `askReachesPerson(wiring_manifest.ask)`, i.e. `human_reachable` (a live approver) or `park_only` (a durable park): **seated ⇒ the ask goes on** to the park and the approver, carrying `requiresRealApproval` and re-classified at the station (its class set carries the bypass-immune `policy_rra`, so a switch to `bypassPermissions` while the classifier was deciding still puts it to the person) — no automatic lane answers it in the classifier's stead: the sandbox admission does not take it, a blanket `onAsk: "allow"` refuses it (`blanket_allow_refused`: a park that fails in front of a blanket seat ends in a refusal, never a yes), and its park is the non-budgetable `irreversible_ask` kind with the real-approval bit (a budget resolver never approves it); a live approver's card carries the bit, so it offers no standing rule — its text ending "The auto-mode classifier is temporarily unavailable (timed out), so auto mode could not judge this call; it needs an approval instead of the classifier's verdict."; **seatless** (no `onAsk`, or a blanket `onAsk: "allow"` with no park lane — not a person) ⇒ the refusal above, byte for byte (`deniedBy: "classifier"`, `cause: "unavailable"`). An ancestor's frozen classifier in a delegated child's fold forks the same way on the ancestor's live frozen approver. One exit for both stations (`classifierUnavailableArm`). Either way the user hears it: `classifier.unavailable` (audience `user`, `detail: { toolName, toolCallId, cause, disposition: "asked" | "denied", sessionId?, runId? }`; the sentence names the seat the station forked on — this run's, or, at the inherited station, the ancestor layer's frozen approver — and on a timeout the budget the classifier actually held, read off the decider's new optional `AutoModeDecider.timeoutMs` (a hand-built decider states none and the sentence names the knob alone)), once per classification that could not answer (it states the fork, not the outcome), not while the call is being aborted; the gate's classifier waits on the per-call signal, as the approver and the park do — a halt (Esc) ends the classification at once — whatever the classifier's request or the question build (the session's context read) does with its own signal: the bounded call settles on the abort, and an already-cancelled call is not dispatched (it used to wait out the whole per-level budget, up to 180 s per rung, and then announce `timeout` / `asked` for a call the halt had ended), and the user hears no `classifier.unavailable` for it; a handed-on ask's record carries no classifier attribution (it names the person's / the park's resolution). Neither way is it a block: the denial limit does not count it, and a person's refusal of the handed-on ask is the person's (`deniedBy: "ask_resolution"`). Types exported for a host driving `runToolGate`: `ClassifierUnavailableSeat` / `ClassifierUnavailableInfo` / `ClassifierUnavailableDisposition` / `ClassifierUnavailableObserver` (`ToolGateInput.autoMode.unavailable?: { approvalSeat?, onUnavailable? }` — stating nothing keeps the refusal).
|
|
374
|
+
- **Departure from the reference implementation.** CC refuses a call its classifier could not judge whether or not a person is there. Ruled C-R118 (on C-R44's latitude for the classifier to depart from CC for robustness): an attended auto session on a slow gateway must not become a string of refusals.
|
|
375
|
+
- **Who needs it.** Self-hosted deployments running auto (the cli's factory default) on gateways that queue and prefill for minutes; TUI users who saw "temporarily unavailable (timed out)" on every Bash; `-p` users who saw nothing at all.
|
|
376
|
+
- **Who is hurt.** A deployment that relied on the 60 s cut to fail fast: on a gateway that hangs without answering, a two-rung walk now waits up to 360 s before the refusal (or the card). **Compensation:** `RunnerDeps.autoMode.timeoutMs` (the server's `AUTO_MODE_CLASSIFIER_TIMEOUT_MS`), or `classifierFallback: "none"` for one rung. A test that pinned "an unavailable classification never reaches the approver" with a live approver wired now sees the card — re-pin headless for the refusal. A deployment that set a bad `timeoutMs` now fails at start instead of refusing every gated call. A user who switches to `bypassPermissions` while the classifier is deciding a call that then cannot be judged sees that call's card (it used to be released with nobody asked); a switch made before the classification releases it as before.
|
|
377
|
+
- Pins: `test/backlog1125-classifier-budget.test.ts` (fake-timer budget: the 70 s / 150 s / 165 s arms answered, 175 s / 185 s boundary, the knob, ten bad values by code, a bad knob through the Runner; the knob screen — each knob's code, a run that starts in `default` or states no mode fails at prepare, a failed steer rolled back, a read-started arming's failure reported, never unhandled) and `test/backlog1125-classifier-unavailable-exit.test.ts` (the gate fork seated / seatless / aborted / throwing observer / error cause / not counted; the shared exit; the seat table; the notice; Runner lanes — live approver, headless, blanket allow with and without a failing park, durable park, four timed-out calls with no fallback card, a person's refusal, the inherited station on all three arms). Re-pinned to a headless leg where they pinned the refusal: backlog661 / backlog616 / backlog742; backlog805 / design503 / f012 follow the new default and the seated arm. Mutation-checked (each pin file's header names its mutations and what they red). The steer rollback's ownership, the fault seat's containment, the cancellation race (a classifier and a session read that ignore their signal, a pre-cancelled call, a late rejection), the re-classified handed-on ask (a mid-classification switch to bypass, at the gate and through the Runner) and the rebuild's per-side screen: `test/backlog1125-classifier-budget.test.ts` / `test/backlog1125-classifier-unavailable-exit.test.ts` (mutations D140–D151, per-fixture table `.gate-logs/criteria-7.34.0-mutations-rd.md`); the A2A roster provenance: `test/a2a-materialize.test.ts`.
|
|
378
|
+
|
|
379
|
+
### An in-place `sed`'s backup name is judged where GNU and BSD sed put it (#1135 write face / #1138; behavior; +0 keys) (@server @cli @test)
|
|
380
|
+
|
|
381
|
+
- **Before.** A backup suffix carrying a separator was dropped, and a `*` was read as the operand's basename beside it: `sed '--in-place=/elsewhere/*' s/a/b/ f` (GNU sed renames the original to `/elsewhere/f`) was answered by `acceptEdits` without a question and released under `bypassPermissions` past an `Edit(//elsewhere/**)` deny.
|
|
382
|
+
- **Now.** Both readings are write targets: GNU (measured, sed 4.9) replaces every `*` in the suffix with the file operand AS SPELLED and resolves the name against the working directory (`bak/*` on `d/f` is `bak/d/f`; a suffix without `*` is appended); the BSD / BusyBox seds append the suffix literally. Under GNU `--follow-symlinks` every suffix form applies to the operand's FOLLOWED name (the entry "acceptEdits places a file command only where it writes" below). The BSD `sed -i ''` form is unchanged.
|
|
383
|
+
- **Who is hurt.** None expected — a backup the user's deny rule or the working directories exclude now meets the same refusal or question as the file it backs up.
|
|
384
|
+
- Pins: `test/backlog1135-sed-backup-names.test.ts` (the outside backup under acceptEdits and under bypass + deny; the GNU name under `Edit(x_d/**)`; the literal append under `Edit(d/f_*)`; `--follow-symlinks`; controls `-i.bak`, `--in-place=bak/*` inside, BSD `-i ''`). Mutation-checked per reading.
|
|
385
|
+
|
|
386
|
+
### `acceptEdits` never answers a copy whose destination members it did not enumerate; a dangling link is judged where it lands (#1138 / #1135 write face; behavior; +0 keys) (@server @cli @test)
|
|
387
|
+
|
|
388
|
+
- **Before.** With no write rows, `cp -R src dest` was placed after judging only the operands and `dest/src`; GNU cp writes THROUGH an existing link below its destination (coreutils 9.1, measured: `dest/src/file → /outside/file` is overwritten), so acceptEdits wrote outside the working directories without asking.
|
|
389
|
+
- **Now.** A command's writes at paths the station does not enumerate are split by whether it writes through what already exists there. A recursive copy (`-r` / `-R` / `-a`, GNU abbreviations included), a `--parents` copy, a copy of a globbed source, and a backup that a suffix carrying `/` or `*` sends away from a globbed operand are never placed. Members beneath a target the station established — a recursive removal's (`rm -r` unlinks a link it meets), a move's (`mv` renames over one) — are placed while no deny row of either table lies at or below the target (a write whose operand is a pattern is never placed at all — car-7340-rm, the first entry above) (the entry "acceptEdits does not answer a move, copy or removal a deny row lies at or below" below). A non-recursive copy or move of literal operands is placed, its destination child judged where it lands (an existing link there canonically). A write target whose component the resolver cannot find but the filesystem still holds — a link whose target does not exist — is not an absent target: a write through it creates the link's target, so the target is established where the link lands (one hop at a time, at most 32), for the working-directory containment and for the write face's deny rows alike (`touch dl` into a directory outside is not placed; `echo hi > dl` into an `Edit(secrets/**)` directory is refused under bypass; into the working directory it is placed); an environment that cannot read the link leaves it unestablished.
|
|
390
|
+
- **Who needs it.** acceptEdits sessions whose tree carries links out of the working directories.
|
|
391
|
+
- **Who is hurt.** An acceptEdits user whose `cp -R` or `cp *.txt dir/` went through without a card. **Compensation.** The card (a person answers it); `default` is unchanged; `bypassPermissions` releases it; plain copies, moves and removals inside the working directories still go through.
|
|
392
|
+
- Pins: `test/backlog1138-derived-writes.test.ts` (`cp -R` over a destination link and without one, `cp *.txt dest/`, the globbed backup sent away, `touch` / `cp` over a dangling link to a path outside, `echo >` over a dangling link into a denied directory; controls — `cp a.txt b.txt`, `cp a.txt d2` over a link outside (not placed), `rm -rf` over a link outside (runs, the outside file survives), `mv` over a link (the outside file keeps its bytes), `mv m.txt n.txt`, a plain backup beside a globbed file, `touch` over a dangling link landing inside). Mutation-checked per arm.
|
|
393
|
+
### The built-in read-deny table ships two tiers on — `credentials` + a home-anchored `agent-config` (R-2: C-R106 ① / C-R107 / C-R119 (a); behavior (tighten, in-root included); **BREAKING — operator knob**: the agent-config rows are renamed `~/…`, which is their `readDenyBuiltinExclude` key; `READ_DENY_DEFAULT_TIERS` value; `compileReadDeny` +1 optional `homes`, `HandsToolkitOptions.readDenyHomes?`; exports +3 `READ_DENY_HOME_WORD` / `readDenyPatternRun` / `readDenyHomesOf`) (@server @cli @client-core @test)
|
|
394
|
+
|
|
395
|
+
- **Before.** `READ_DENY_DEFAULT_TIERS = []` (C-R60, 7.22.0): an unconfigured deployment refused nothing from the built-in table, so `cat ~/.aws/credentials`, `cat ~/.ssh/id_rsa` and `cat ~/.sema/settings.json` ran at zero cards with the home as the root (measured on the v7.33.3 tree) — the bytes landed in the session record; the rows, when a deployment did switch them on, matched anywhere, so a project's own `.sema/settings.json` / `.mcp.json` / `.claude/settings.json` were refused too.
|
|
396
|
+
- **Now — one expression.** `READ_DENY_DEFAULT_TIERS = ["credentials", "agent-config"]`; `browser` / `wallet` / `shell-history` stay a deployment's opt-in; an explicit `[]` (server `READ_DENY_BUILTIN_TIERS=none`) is still the empty posture. `.npmrc` / `.pypirc` stay credentials rows (test matrix [8310] c: named in 1 of 2,021 real commands, 0).
|
|
397
|
+
- **Now — one matcher capability.** The pattern grammar gains a home word: a pattern whose first segment is exactly `~` matches its run only right below the execution environment's home (`ExecutionEnv.homeDir`, and its canonical spelling when the home is reached through a symlink); every other pattern still matches anywhere. The fifteen `agent-config` rows are written `~/.sema/settings.json`, `~/.claude/settings.json`, `~/.mcp.json`, `~/.sema/engine-data/.env`, … — the user's own configuration is refused, a project's same-named files (`./.sema/settings.json`, `./.mcp.json`, `./.claude/settings.json`, including a project that sits under the home) read. The word applies to deployment and task `readDenyPatterns` too. With no declared home a home-anchored row matches its run anywhere (fail-closed, KL-r2-b). The classifier's deny line states such a row in the rule grammar's home word (`Read(~/.sema/settings.json)`); the rg traversal carries no glob for it, so a Grep whose scope can hold such a row (the home or an ancestor) is searched by the walker, which prunes by path before it reads (`ReadDenyMatcher.unexcludableUnder?`; rg would read the file and the answer would differ by whether its content matched); the opt-in WRITE deny set mirrors each row's run and judges it anywhere, unchanged.
|
|
398
|
+
- **Who needs it.** Every unconfigured deployment (the server's `READ_DENY_BUILTIN_TIERS` unset, the shell's local engine): a floor under the user's credential files and agent keys that was ruled on (C-R106 ①) but not shipped.
|
|
399
|
+
- **Who is hurt.** (a) A session on an unconfigured deployment that reads a credentials-tier file anywhere (`.npmrc`, `.netrc`, `.git-credentials`, `.docker/config.json`, `.credentials.json`, `.ssh/`, `id_rsa*`, `.aws/`, `.kube/`, …) or the home's agent configuration: refused under every mode, no card — for every spelling the read station can establish; a spelling it cannot establish (brace expansion, `find -exec`, a `for` list) stays the station's card, which `bypassPermissions` releases with disclosure (C-R116 Q3, KL-r2-e). A spelling the station cannot place against the home (`$PWD/..`, an unsettled `cd`) is judged as spelled, the anchored run anywhere in it — so it is refused, also for a project's own `.sema/settings.json` (KL-r2-b). (b) **`grep -r` / `rg` / `tar c` over a project tree that carries such a file is refused as a whole** (the station enumerates the tree; the Grep tool only drops that file) — `sema-cli` / `sema-test` fixtures carry an `.npmrc`; this is #1143's question (KL-r2-a). A project's `.sema/settings.json` / `.mcp.json` / `.claude/settings.json` are **not** affected. (c) **BREAKING (operator knob):** a configuration excluding an agent-config row by its old spelling (`READ_DENY_BUILTIN_EXCLUDE=.mcp.json`) is refused at prepare / server boot; the sentence names the new row (`"~/.mcp.json"`). (d) `wiring_manifest.readDeny` appears on every unconfigured leg, so the configuration fingerprint moves once. (d) *A deployment that redeems a recorded auto-mode arming (`rebuildAutoModeDecider`)*: with the two tiers on by default, every unconfigured deployment's arming recipe carries a non-empty `derivedDenyRules` (the tables' deny lines), and `sameArmingBody` (`src/core/auto-mode-arming.ts:435-451`) compares that member — so the rebuild of an arming recorded by 7.33 (no such member), or one whose redeeming side does not pass it (`autoModeArmingRecipeOf(face)` without `bind.derivedDenyRules`), refuses `settings_moved` and the inherited ask flows to a person. Latent for sema-server, which makes no `rebuildAutoModeDecider` call today.
|
|
400
|
+
- **Compensation.** `READ_DENY_BUILTIN_TIERS=none` (`readDenyBuiltinTiers: []`) restores the empty table; `READ_DENY_BUILTIN_EXCLUDE` removes single rows (`.npmrc`, `~/.mcp.json`); the Grep tool searches a tree with such a file and leaves only that file out.
|
|
401
|
+
- Pins: `test/backlog-r2-default-tiers.test.ts` (the expression; the three home files refused on the real load path under an approver who says yes — project under the home and the home as the root, every home spelling; the project's files served; the explicit empty posture; exclusion by old / new name; the canonical-home twin through a symlinked HOME; no declared home; Grep over the home; the declared `grep -r` cost; the grammar, the classifier line, the write mirror). Re-pinned to the new default: `backlog889` (OFF = explicit `[]`), `backlog245`, `backlog685` / `backlog696` (the fixture root is the HOME), `backlog714` (the containment subject switches the table off), `backlog767` G-2 (now a refusal), `backlog804`, `design382` (the mint floor's stand-in judge), `read-deny`, `workflow-governance-tightening-keys` (the recovery sentence names the default). Red baseline: the same probe on v7.33.3 serves the three home files (receipt).
|
|
402
|
+
|
|
403
|
+
### The home-anchored read-deny rows are placed by the home's file identity — `FileSystem.fileIdentity` (behavior (tighten); +1 optional seam method `FileSystem.fileIdentity`; +0 wire keys, +0 codes) (@server @cli @test)
|
|
404
|
+
|
|
405
|
+
- **Before.** A home-anchored row (`~/.sema/settings.json` and the rest of the default `agent-config` tier) was placed at the declared home and its canonical spelling only. A spelling of the home that the canonicalizer keeps — macOS `/System/Volumes/Data<HOME>` (Node's `realpath` does not collapse a firmlink), a bind mount — was judged as an unrelated path: Read, Grep and `cat` under an open read face served `~/.sema/settings.json` through it in `default` mode with no card; with the home as the root, `bypassPermissions` released `cat` as an ordinary outside-the-roots card; Grep over such a scope was handed to ripgrep, which read the file.
|
|
406
|
+
- **Now — one reader.** Every face that judges a deny row by location also judges each path at its HOME VIEW (`homeViewsOf`, `src/tools/fs/read-deny-home.ts`): the path respelled through the home's directory chain (the canonical home and each of its ancestors) wherever one of its ancestors IS a directory on that chain, matched by `fileIdentity` (`<dev>:<ino>`, read as bigint, links followed). The faces: the read and write tools' target judgment (`resolveKey`) and the missing-file sibling suggestion; the shell station's establishment (every landed target, traversal members included — read and write targets alike); the search walker (Grep / Glob / RepoMap); the ripgrep leg's output check and its scope choice (a scope spelled through an alias of the home or of one of its ancestors is searched by the walker). The chain is read once per environment instance; a level that does not exist takes its parent's view (a write's missing tail); any other failure stops the view there, and the path is judged as spelled — never wider.
|
|
407
|
+
- **New seam member.** `FileSystem.fileIdentity?(path, signal)` — optional; `NodeExecutionEnv` implements it, the environment capability census announces a present non-function value. A host whose own `FileSystem` / `ExecutionEnv` does not implement it keeps the previous placement (spelling and canonical spelling; KL-7340ra-a). @server: decide whether the remote / sandbox environments implement it.
|
|
408
|
+
- **Who needs it.** macOS users (the data-volume spelling of every home always exists) and hosts whose home is reachable through a bind mount.
|
|
409
|
+
- **Who is hurt.** None expected: a view only adds a spelling to judge, and a read below a directory identical to the home is refused exactly where the home's own spelling is. Cost: one identity lookup per directory level of a judged path (a walk asks once per directory it enters). A Grep whose scope contains the home only across a firmlink or mount boundary BELOW the scope (`/System/Volumes/Data`) still starts on ripgrep; the output check abandons the pass and the walker withholds the row, and the abandonment note says a deny-listed entry's content matched (KL-7340ra-b, the #1149 class).
|
|
410
|
+
- Pins: `test/rev7340-home-identity.test.ts` — real lanes with a canonicalizer that keeps a link as macOS keeps a firmlink (Read, `cat`, `head` through a `..` detour, `cd` onto the alias then a relative `cat`, a near-name miss, Grep over the alias and its `.sema` and Grep aimed at the file; the home as the root under bypass), an identity seat that reports a project directory as the home (a bind mount: Read, `cat`, Grep over the project on whichever leg runs), a home declared through a symlink, a re-cased spelling on a case-insensitive host, and the real `/System/Volumes/Data` spelling on a macOS host; controls — an ordinary file through the alias, the project's own `.sema/settings.json`, the unanchored `.docker/config.json`; `homeViewsOf` unit rows (missing tail, no seat, no home, a failing level, the chain read once). Red on 4f71dfd36 (six lane cells); mutation-checked per arm (D24–D30).
|
|
411
|
+
|
|
412
|
+
### acceptEdits does not answer a move, copy or removal a deny row lies at or below (#1138 × the read table; behavior (acceptEdits only); `ReadDenyMatcher.rowAtOrBelow` +1 optional member, `ReadDenySeat.denyAtOrBelow` / `ShellReadBoundary.denyAtOrBelow` +1 optional member; +0 wire keys) (@server @cli @test)
|
|
413
|
+
|
|
414
|
+
- **Before.** The `beneath` class of derived writes (`mv`, a plain `cp`, `rm -r`, a backup beside a globbed file) was placed while the WRITE table had no rows; the read table was never consulted. Under acceptEdits `mv .docker d` then `Read d/config.json`, `mv .config c` then `cat c/gh/hosts.yml`, and — the home as the root — `mv .sema semadir` then `Read semadir/settings.json` ran with no card and read the file the row guarded; so did `mv <home> h2` with the home's parent as the root.
|
|
415
|
+
- **Now — one predicate over both tables.** A derived write beneath a target is placed only while no deny row lies AT or BELOW the target (`denyRowAtOrBelow`): the read table answers per row (`rowAtOrBelow` — a placed row when the path is at, inside or above its anchored location; any other row when the path is inside a match or a match would run on below it from the path's own segments, so `.docker` meets `.docker/config.json` while a directory that merely CONTAINS a whole run does not: the row travels with it); the write table's rows are rule rows whose reach below a path cannot be enumerated, so any of them answers (the previous rule, now one arm of this predicate). The same per-row predicate decides the Grep leg's `unexcludableUnder` (one definition, two consumers).
|
|
416
|
+
- **Who needs it.** acceptEdits sessions: a rename can no longer carry a credential file out from under its row without a person seeing it.
|
|
417
|
+
- **Since the rev-7340 back-review** a removal answers to the write table only (the entry above): `rm -r .docker` no longer asks for a read row.
|
|
418
|
+
- **Who is hurt.** An acceptEdits user whose `mv` / plain `cp` names a directory a row runs through (`.config`, `.docker`, the home, the home's `.sema`) or a row's own path now gets a card (headless: refused). **Compensation.** The card; `mv src lib` and every move no row reaches is still answered; `default` is unchanged; `bypassPermissions` releases the card.
|
|
419
|
+
- Pins: `test/rev7340-move-beneath-deny.test.ts` (real lanes, headless acceptEdits: `mv .docker d`, `mv .docker/ d`, `mv .config c`, `rm -r .docker`; the home as the root `mv .sema semadir`; the home's parent as the root `mv hh h2`; controls — `mv src lib`, `cp notes.txt n2.txt`, `mv a b` with `a/.docker/config.json` answered and the moved file's read still refused, a row's own path as a source or destination refused by the read face, `default` asks; `rowAtOrBelow` rows per row kind, the rule-row arm and the seat). Red on 4f71dfd36 (the six straddle cells run); mutation-checked (D31–D34).
|
|
420
|
+
|
|
421
|
+
### acceptEdits places a file command only where it writes — the line's order, GNU's argument grammar, the followed backup, the entry a rename or removal changes; a removal answers to the write table (rev-7340 back-review; behavior (acceptEdits placement; the write face in every mode sees the corrected targets); +0 exports, +0 wire keys, +0 codes) (@server @cli @test)
|
|
422
|
+
|
|
423
|
+
- **Before.** Five readings of `ShellReadVerdict.writesPlaced` (the station's fact the acceptEdits arm reads) did not match what the programs do. ① Every segment was judged on the tree as it stood before the line ran: `mv tree renamed && cp src renamed/link` (`tree/link` a link to a file outside) ran with no card and the copy overwrote the outside file; `mv tree renamed && sed -n p renamed/link` printed it; `cp -P a/link newlink && cp src newlink` and `rm d/x && cp src d/x` wrote outside the same way (measured, macOS). ② `cp -tdest src` read the bundle `-tdest` as a bare `-t` and took `src` as the directory, so `dest/src` (GNU writes through a link there) was never judged; `-Stxt` was read as `-t xt`; bypass released both past an `Edit(dest/src)` deny. ③ Under GNU `sed --follow-symlinks -i.bak … link` (`link` → `sub/f`) the backup was judged at `link.bak` — GNU writes `sub/f.bak`; a `*` suffix was never placed and never named. ④ `rm -r .docker` asked (a read row below the target) while `rm -r a` with `a/.docker/config.json` one level down ran. ⑤ Every write target was judged where its path LANDS: with `d` a link to a directory outside and `d/x` a link back in, `mv src d/x` replaced the outside entry and `rm d/x` unlinked it, with no card.
|
|
424
|
+
- **Now.** ⑥ A `cp` or `mv` with ANY word starting with `-` is not placed — the reference implementation's rule ("with flags requires manual approval to ensure path safety"; 2.1.280 `Xbr`): `cp -r`, `mv -f`, `cp -t d`, `cp --target-directory=d` ask under acceptEdits, `cp a b` / `mv a b` are answered (`UNPLACED_OPTIONS`, beside `rmdir -p` and `sed -f`). ① A segment that reshapes the tree — `mv` (a copy of a link or a directory carries an option and is not placed at all) — must be the last thing the line runs: nothing after it, nothing beside it (`RESHAPES_TREE`, shell-read-check.ts); `mkdir -p d && mv f d/` and single segments are unchanged. Any other write only creates, removes or replaces one entry, which changes a later word only when that word steps back up through it: a word with a parent step (`..`) may not run after or beside a write (`mkdir d && touch d/../../x`, `rm l && mkdir l && touch l/../../x`) — `lineOrderPlaced`. A removal is not a reshape (its entry is placed, ⑤), so `rm -rf build && mkdir build` is answered. ② `cp` / `mv` / `sed` argv is read by ONE getopt reader under BOTH grammars, the union of what they name (`readArgv` with `COPY_GRAMMARS` / `SED_GRAMMARS`): BSD's (`cp -S` and `sed -l` are flags — `cp -S src dest` overwrote a file outside through `dest/src`, `sed -l -i ''` edited a read-only grant; `sed -i` / `-I` require their extension, so `sed -i .bak …` writes `f.bak`) and GNU's (`--` ends the options; `-e` / `-f` / `-l` take a value): `-t` / `-S` take the rest of their word or the next word; `--target-directory` / `--suffix` / `--no-preserve` / `--sparse` any unambiguous prefix, `=` or the next word; a child the station cannot place is an unresolvable write target (not placed). ③ Under `--follow-symlinks` every suffix form applies to the operand's followed name — the chain walked as GNU sed walks it (a relative link joined to the directory part of the name that held it; `lstat` + `readLink` per hop, bounded); a chain the environment cannot read leaves the backup unplaced. ④ A removal carries no content anywhere: `rm` / `rmdir` answer to the WRITE table only, a move or a copy to both tables (`denyRowAtOrBelow(…, carriesContent)`) — the reference implementation reads the same way (2.1.280: `rm` / `rmdir` are "write" operations and its read-rule pass covers only a copy's or a move's sources and sed's operands). ⑤ Each write target is judged at the seat the program writes it (`WriteSeat`, shell-read-write-targets.ts — the write-target concern now has its own module): `rm` / `rmdir` / an in-place `sed` (and its backups) at the directory ENTRY — the canonical parent joined with the last name, not followed; `mv` and `cp` (and their destination children) at both the entry and where it lands (`mv` renames over the entry or into the directory a destination lands on; `cp` writes through a link, and replaces the entry under `--remove-destination` or a backup — GNU coreutils 9.11, measured); `touch -h` at both (it sets the entry's own times); every other write where it lands; a word ending in a separator, `.` or `..` resolves itself, so its landing joins the entry (`rm -r dlink/` empties the linked directory on BSD and GNU alike; `mv --strip-trailing-slashes` strips the separator again). Containment, the write face and the rows-below check read the seated target; the read face is unchanged.
|
|
425
|
+
- **Who needs it.** acceptEdits sessions in a tree that carries links out of the working directories; bypass sessions whose `Edit` deny rows name where these programs really write.
|
|
426
|
+
- **Who is hurt.** An acceptEdits user whose `cp` / `mv` carries any option (`cp -r`, `cp -p`, `mv -f`, `mv -n`) gets a card (headless: refused) — the reference's behaviour; one whose line runs anything after a `mv` (`mv a b && mv c d`, `mv a b && touch c`) gets one too; `cp src d/x` through a directory link that points outside asks although this `cp` writes inside. **Compensation.** The card; the same steps as separate calls are each answered; a line whose move runs last is answered. Loosened, by the same reading: `rm -r .docker` (no write rows) is answered; `sed --follow-symlinks '--in-place=bak/*' …` with the backup inside is placed.
|
|
427
|
+
- Pins: `test/rev7340-accept-edits-placement.test.ts` (① – ⑤ on the real lane; GNU-only grammar gate-only on a BSD host; controls per ticket); re-pinned `test/backlog1135-sed-backup-names.test.ts` (S4 on a followed name) and `test/rev7340-move-beneath-deny.test.ts` (`rm -r .docker` leaves the straddle list). Red on d3d4618b4; mutation-checked (D60–D73).
|
|
428
|
+
|
|
429
|
+
### The sensitive-path write list refuses the home word at wiring — `createSensitivePathPolicy({ patterns: ["~/…"] })` throws (#123 loud bad value; behavior: a value that silently guarded nothing now refuses; +0 exports, +0 wire keys) (@server)
|
|
430
|
+
|
|
431
|
+
- **Before.** The write list has no home placement (every pattern is a segment run judged anywhere in the real target), but a pattern spelled `~/.foo/x.json` compiled `~` as a literal directory name: it guarded nothing, and `createSensitivePathPolicy({ patterns: ["~/.foo/x.json"] })` allowed the Write of `<home>/.foo/x.json` that `[".foo/x.json"]` refuses — silently.
|
|
432
|
+
- **Now.** A pattern whose first segment is the home word (read through the one reader of that word, `readDenyPatternRun` — the same reading the read table's `~/…` rows get) throws at wiring, naming the pattern and the run to spell instead (`.foo/x.json`, which covers the home's file and every same-named one). `~` further along (`a/~/b`) or glued to a name (`~user/x`) stays a literal segment, as in the read grammar. Chosen over placing `~/` rows on the write face: one validation instead of a second placement rule on a face whose rows are deliberately judged anywhere.
|
|
433
|
+
- **Who needs it.** A deployment that wrote `~/…` into its write-deny set believing it guarded the home's file.
|
|
434
|
+
- **Who is hurt.** That deployment's wiring now throws (server: `SENSITIVE_WRITE_PATTERNS` with a `~/` entry fails the boot-time compile, as a zero-segment entry already does). **Compensation.** The message names the run to write instead; the resulting guard is wider (the file anywhere), which is the write list's documented reach.
|
|
435
|
+
- Pins: `test/rev7340-write-deny-home-word.test.ts` (the throw per spelling — `~/x`, `~/x/**`, `~` alone, a leading separator, beside a valid entry; controls — `a/~/b` / `~user/x` literal, the recommended list wires; a real lane: the run spelled without the word refuses the Write, nothing written). Red on 4f71dfd36 (constructs, allows); mutation-checked (D35–D36).
|
|
436
|
+
|
|
437
|
+
### A home-anchored read-deny row is also placed where its literal head leads — a home entry linked elsewhere is guarded at its target (behavior (tighten); `compileReadDeny` +1 optional parameter `linked`; +0 exports, +0 wire keys, +0 codes) (@server @cli @test)
|
|
438
|
+
|
|
439
|
+
- **Before.** A home-anchored row (`~/.sema/settings.json` and the rest of the default `agent-config` tier) sat at the home's spellings and its identity views. When a home entry is a link — `~/.sema` → `<dotfiles>/sema`, the common dotfiles layout — canonicalization led every other spelling away from the home: the data-volume alias `/System/Volumes/Data<HOME>/.sema/settings.json`, the target `<dotfiles>/sema/settings.json` written directly (`ls -la ~` shows it), a directory link naming `~/.sema`, a leaf link naming the file, and Grep over `~/.sema` itself (the walk runs on the canonical scope) all served the file with no card; only the plain `~/.sema/settings.json` was refused.
|
|
440
|
+
- **Now — one placement.** A mount resolves each home-anchored row's LITERAL HEAD (`readDenyRunHead`: the run before its first `*` segment) under the home's spellings the way the read faces mint their keys (`readDenyHomeLinksOf`, `canonicalizeTarget`); where a segment of it is a link, the same row is placed at that canonical location too (`compileReadDeny(…, linked)`; judged in rooted views only, the rest of the run matched below it; the same row name in every refusal). Every face judges canonical keys, so each spelling above meets the row, Read / `cat` / Grep / Glob alike. The Runner resolves once per run in prepare (`placeReadDenyAtHome`) and hands the band the same homes and locations. A home entry that is not a link adds nothing; a head that cannot be resolved (a probe that fails or throws) adds nothing and the heads are resolved together, so a mount waits at most one probe bound.
|
|
441
|
+
- **The hands band places its own when it is not handed one.** A library-direct `createHandsToolkit` without `readDenyHomes` / `readDenyHomeLinks` resolves the same placement (declared home, canonical spelling, linked locations) and its read tools (Read / Grep / Glob / RepoMap) wait for it before they judge; previously it placed the rows at the declared spelling only, so with HOME reached through a link the real spelling was served (KL-r2-c, closed). A placement that fails leaves the rows held anywhere (KL-7340rc1-e); a placement with a head that did not answer is redone before the next read; the wait ends with the call's own cancellation; the options are snapshotted at the mount (entry objects and the rule list copied); a table with no home-anchored row places nothing and waits for nothing.
|
|
442
|
+
- **A home chain that could not be read whole is asked again.** `homeViewsOf` kept a chain whose lookup failed (or was incomplete) for the environment's lifetime, so one transient identity failure left the identity views off for good; now only a chain read whole is kept, and the read runs under the tree's bounded-await deadline (KL-7340rc1-a states the per-judgment fallback). The home lookup of a placement runs under the same deadline.
|
|
443
|
+
- **A home the environment does not answer for places nothing.** When the canonical lookup of the declared home fails (other than "no such directory") or times out, the rows are held anywhere for that placement — a run, or the band until it places again — rather than placed at a spelling that may miss the home's canonical one (KL-7340rc1-e). The home-chain read never rejects: a lookup that throws or times out is an unanswered chain (no view).
|
|
444
|
+
- **Who needs it.** Users whose agent configuration lives in a dotfiles repository linked into the home; embedders mounting the tools directly.
|
|
445
|
+
- **Who is hurt.** A read of the linked target — or of a project file that IS the home entry's target — is now refused. **Compensation.** The refusal names the row; `readDenyBuiltinExclude` / `readDenyBuiltinTiers` remove it as before. Stated limits: a row whose head is empty (`~/.sema.*`, `~/.claude.*`) is placed by spelling only (KL-7340rc1-b); the placement is per run — a link made mid-run is followed from the next run (KL-7340rc1-c); an unanchored row (`.docker/config.json`) is not placed at a linked target (KL-7340rc1-d).
|
|
446
|
+
- Pins: `test/rev7340-rc1-read-face.test.ts` ① (real lanes over a linked `.sema`: Read and `cat` through the plain spelling, a symlink alias of the home, the data-volume firmlink on a mac, the target, a directory link and a leaf link; Grep over the linked directory four ways; a `~/.foo/*.json` row over a linked `.foo`; controls and the plain-directory counter-control; the rooted-only matcher; the empty-head limit), ② (the band: real spelling refused, homes carried without links, the at-mount read waits, a failing placement holds the rows anywhere), ⑤ (the same environment across a failing and a recovered identity seat). Red on d3d4618b4; mutation-checked (D74–D88).
|
|
447
|
+
|
|
448
|
+
### Deployment seats that shape the read and write tables are judged on their own values (#123 loud bad value; behavior: values that silently did something else now refuse; +0 exports, +0 wire keys) (@server)
|
|
449
|
+
|
|
450
|
+
- **Before.** `RunnerDeps.readDenyBuiltinTiers` / `readDenyBuiltinExclude` were spread before they were judged, so `""` became the explicit empty selection — `readDenyBuiltinTiers: ""` switched both default tiers off and `~/.sema/settings.json` read — and a non-empty string was judged character by character. `readDenyHomes` (the toolkit seat and `compileReadDeny`'s `homes`) placed whatever it was given: `["relative/home"]`, a bare string (read as its characters) and `[""]` mounted and the row guarded nothing. `RunnerDeps.writeProtectedPaths: ["~/.foo"]` kept `~` as a literal segment, so the row guarded a directory named `~` and an acceptEdits Write of `<home>/.foo` landed unasked.
|
|
451
|
+
- **Now.** The builtin seats are judged, then snapshotted (`readDenyBuiltinConfigOf`): anything but an array of names — `""` included — fails the prepare with `<seat>: expected an array …`; `[]` stays the explicit no-tier posture. `readDenyHomes` must be an array of absolute path strings (`readDenyHomes: expected an array of absolute home paths …`). A `writeProtectedPaths` name opening with the home word fails the prepare and gives the name without it (`.foo`), read through `readDenyPatternRun` as the write list's check reads it; `~` further along and `~user` stay literal names.
|
|
452
|
+
- **Who is hurt.** A deployment carrying one of these values now fails to start. **Compensation.** Each message names the seat and the value to write instead.
|
|
453
|
+
- Pins: `test/rev7340-rc1-read-face.test.ts` ③ ④ ⑥ (real lanes: the run ends `failed` with the message and no tool runs; the `[]` and absent controls; the snapshot's copy; the toolkit throws per bad shape; the `.foo` spelling protects the home's file). Red on d3d4618b4; mutation-checked (D70–D73).
|
|
454
|
+
### `RunnerDeps.graderEnvFactory` is removed (#1196; **BREAKING — API**: −1 optional member on `RunnerDeps`) (@server)
|
|
455
|
+
|
|
456
|
+
- **Before.** The seat was declared for an isolated grader env the engine never minted: no code path read it, so a deployment that set it got nothing.
|
|
457
|
+
- **Now.** The member is gone. The isolation contract it pointed at is unchanged: the trusted control plane mints the grader env and hands it to the repair loop as `RepairLoopConfig.graderEnv`, and `assertOracleIsolation` / `GraderEnv` stay exported.
|
|
458
|
+
- **Who is hurt.** A deployment whose `RunnerDeps` literal names `graderEnvFactory` fails to type-check (an excess-property error); one that still passes it untyped (JavaScript, a spread) is refused at `new Runner` by name (`config.deps_retired_key`, the retired-seat door every retired `RunnerDeps` seat meets) instead of the seat vanishing silently. Nothing else changes (the seat had no reader). **Compensation.** Delete the key; wire a real grader env through `RepairLoopConfig.graderEnv`.
|
|
459
|
+
|
|
1
460
|
## 7.33.8 — 2026-09-28
|
|
2
461
|
|
|
3
462
|
### Fix — every child process the engine starts gets its environment through the credential scrub; a `runSpec` oracle gate's command no longer inherits the engine's environment (#1181; security; behavior narrowing; +0 runtime exports; new optional type members `OracleGateSpec.inheritEnv`, `RunSpecOptions.onNotice`, `runOracle` options `onNotice`) (@server)
|
|
@@ -23,7 +482,7 @@
|
|
|
23
482
|
- **Before.** `OTEL_EXPORTER_OTLP_HEADERS` and the per-signal `OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_HEADERS` carry no credential word, so the name rules passed them and the scrub never reads their value: with `Authorization=Bearer <token>`, `api-key=<token>` or `Authorization=Basic <base64>` in the engine's environment, every tool shell and language server inherited the variable and a model-chosen `env` printed the collector credential (`isSecretEnvKey` answered `false` for all four names × all three values on 7.33.6).
|
|
24
483
|
- **Now.** The exporter's header-list names join the exact credential-name list: `OTEL_EXPORTER_OTLP_HEADERS`, `…_TRACES_HEADERS`, `…_METRICS_HEADERS`, `…_LOGS_HEADERS` (OpenTelemetry specification, `protocol/exporter.md`, v1.0.0 onward: the value is the request headers the exporter sends, `key1=value1,…`), `…_PROFILES_HEADERS` (opentelemetry.io OTLP exporter configuration, profiles signal) and the v0.7.0 per-signal spellings `…_SPAN_HEADERS` / `…_METRIC_HEADERS`. The name decides and the value is not read — an empty or credential-free header list is withheld too; findings are `exact-name`, confidence `high`. Exact names, not a `_HEADERS` shape: `CORS_ALLOW_HEADERS` and `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_*` (header names, not values) stay. The exporter's other settings (`OTEL_SERVICE_NAME`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `…_PROTOCOL`, `OTEL_RESOURCE_ATTRIBUTES`, …) are handed on as before.
|
|
25
484
|
- **Who needs it.** Deployments that configure OTLP export with header authentication in the engine's environment — including a server that merges its credentials file into the engine process environment, where `OTEL_EXPORTER_OTLP_HEADERS` is one of the credential slots.
|
|
26
|
-
- **Who is hurt / compensation.** A deployment that authenticates OTLP with headers and lets the model's shell inherit them: the child no longer has the variable, so an OpenTelemetry client run inside the model's shell (or a language server) exports without its authentication header and the collector may reject it. Compensation: hand the name back explicitly — `inheritEnv: [..., "OTEL_EXPORTER_OTLP_HEADERS"]` or `shellEnv: { OTEL_EXPORTER_OTLP_HEADERS: … }` on the exec env, `NodeLspManagerOptions.env` for a language server. Over-drop and residual (KL-1180): a credential-free header list is withheld too; a numbered or decorated spelling, a dotted spelling (OpenTelemetry's Java SDK also reads `otel.exporter.otlp.headers` / `OTEL_EXPORTER_OTLP.TRACES_HEADERS` as these settings), a vendor SDK's own header variable and an OTLP endpoint written with a userinfo are still handed on (values of other names are not read). The scope is the scrub's: the tool shell (`NodeExecutionEnv`, default `inheritEnv`) and the default language-server spawn; a `runSpec` oracle gate's command inherits the engine's environment as before.
|
|
485
|
+
- **Who is hurt / compensation.** A deployment that authenticates OTLP with headers and lets the model's shell inherit them: the child no longer has the variable, so an OpenTelemetry client run inside the model's shell (or a language server) exports without its authentication header and the collector may reject it. Compensation (an embedder that builds the exec env): hand the name back explicitly — `inheritEnv: [..., "OTEL_EXPORTER_OTLP_HEADERS"]` or `shellEnv: { OTEL_EXPORTER_OTLP_HEADERS: … }` on the exec env, `NodeLspManagerOptions.env` for a language server; on sema-server, see its DEPLOY-PREREQS (the model-driven host shell leg takes no credential-shaped names back by design). Over-drop and residual (KL-1180): a credential-free header list is withheld too; a numbered or decorated spelling, a dotted spelling (OpenTelemetry's Java SDK also reads `otel.exporter.otlp.headers` / `OTEL_EXPORTER_OTLP.TRACES_HEADERS` as these settings), a vendor SDK's own header variable and an OTLP endpoint written with a userinfo are still handed on (values of other names are not read). The scope is the scrub's: the tool shell (`NodeExecutionEnv`, default `inheritEnv`) and the default language-server spawn; a `runSpec` oracle gate's command inherits the engine's environment as before.
|
|
27
486
|
- Pins: `test/backlog1180-otlp-headers-secret-env.test.ts` — seven names × three header values (`exact-name`, `high`, value absent from findings), value-blindness (no value, empty, credential-free lists), eleven controls; the server's thirteen secret-env names each withheld by name with a harmless value; a real shell spawned by `NodeExecutionEnv` whose `env` holds none of the planted names or values and keeps the controls, with the notice naming them; the `inheritEnv` allow list and `shellEnv` put-back through a real shell; the default language-server spawn.
|
|
28
487
|
|
|
29
488
|
## 7.33.6 — 2026-09-28
|
|
@@ -106,7 +565,7 @@
|
|
|
106
565
|
|
|
107
566
|
- **Before.** The proxy screen read only a `GIT_CONFIG_VALUE_<i>` whose paired key names a proxy. `https_proxy="http://u:<password>@proxy:3128"` (and `http_proxy`, `all_proxy`, upper-case spellings) reached the child whole: a model could read the password, and git's libcurl sent it to the proxy (git 2.54 curl trace, and a loopback listener receiving `Proxy-Authorization: Basic …`).
|
|
108
567
|
- **Now.** `http_proxy` / `https_proxy` / `all_proxy` and their upper-case spellings (compared case-folded) are judged as the `http.proxy` entry they are to libcurl, by the same entry predicate as git's config record: a userinfo or `%40` anywhere in the value, a userinfo URL, or a credential header drops the variable (`family-rule`). A proxy address with no userinfo stays; `no_proxy` is not a proxy address and is not read.
|
|
109
|
-
- **Who is hurt / compensation.** A host whose tool children reach the network through a proxy that takes a password loses that proxy in the scrubbed child (git then connects directly and fails where egress needs the proxy).
|
|
568
|
+
- **Who is hurt / compensation.** A host whose tool children reach the network through a proxy that takes a password loses that proxy in the scrubbed child (git then connects directly and fails where egress needs the proxy). An embedder that builds the exec env hands it on explicitly: `NodeExecutionEnv` `inheritEnv: [..names]` (a strict allow list copied verbatim, no scrub) or `shellEnv` (passed verbatim). On sema-server, only the sites with a named allow list (`LSP_ENV_ALLOW`, `BAKE_ENV_ALLOW`, …) take names back; the model-driven host shell leg hands credential-shaped variables to no one by design — see sema-server's DEPLOY-PREREQS. Or use a proxy that authenticates the host without a password in the environment. Other `<scheme>_proxy` names (`ftp_proxy`, `npm_config_https_proxy`, …) are outside this list (KL-1150).
|
|
110
569
|
- Pins: `test/backlog1152-git-env-config-sources.test.ts` (#1150 block: five libcurl-read spellings with an unscrubbed control that sends the password to a loopback listener and a scrubbed run the listener never hears; `HTTP_PROXY` and mixed spellings; scheme-less / user-only / `socks5h` / single-slash / `%40` forms; `no_proxy` and address-only values stay).
|
|
111
570
|
|
|
112
571
|
### Security fix — git config entries whose value is a password, and credential helpers that are shell text rather than a helper's name, are withheld (#1151; existing, not a 7.33.4 regression; behavior narrowing; +0 exports, +0 wire keys) (@server)
|
|
@@ -120,7 +579,7 @@
|
|
|
120
579
|
|
|
121
580
|
- **Before.** The value screen recognised only `Authorization:` / `Proxy-Authorization:` / `Cookie:` headers. `http.extraHeader = X-Api-Key: <token>` (likewise `Private-Token:`, `X-Auth-Token:`, `X-Amz-Security-Token:`) reached the child whole and `git config --get-all http.extraheader` answered the token.
|
|
122
581
|
- **Now.** The key decides, in the same place as the password keys: an entry whose key is `http.extraHeader` or `http.<url>.extraHeader` (case-folded, an empty `<url>` included) is withheld whole whatever the header text — git sends it on every request and a credential header can carry any name. The same predicate serves the indexed record, `GIT_CONFIG_PARAMETERS` and non-index suffixes. A header-looking value under any other key is still judged by the value screen.
|
|
123
|
-
- **Who is hurt / compensation.** A deployment that sends a non-credential header through `http.extraHeader` in the environment (a trace or routing header, or the empty `Authorization:` that suppresses git's own header) loses that entry in the child; the rest of the record is handed on (#1152). Compensation as for #1151: configure it in a git config file the host owns, or pass the group explicitly (`inheritEnv` / `shellEnv`).
|
|
582
|
+
- **Who is hurt / compensation.** A deployment that sends a non-credential header through `http.extraHeader` in the environment (a trace or routing header, or the empty `Authorization:` that suppresses git's own header) loses that entry in the child; the rest of the record is handed on (#1152). Compensation as for #1151: configure it in a git config file the host owns, or, for an embedder that builds the exec env, pass the group explicitly (`inheritEnv` / `shellEnv`); on sema-server, see its DEPLOY-PREREQS.
|
|
124
583
|
- Pins: `test/backlog1152-git-env-config-sources.test.ts` (#1155 block: four header names × four key spellings × the indexed record and `GIT_CONFIG_PARAMETERS`, real `git config --get-regexp` before and after, a non-index suffix, `http.extraHeaders` control); `test/backlog1129-git-config-key-family.test.ts` empty-`Authorization:` and key-only extra-header pins re-pinned (intended flip).
|
|
125
584
|
|
|
126
585
|
### Security fix — `GIT_CONFIG_PARAMETERS` is read as git reads it and withheld whole when any entry carries a credential (#1141; existing since before 7.32.1, KL-1129e closed; behavior narrowing; +0 exports, +0 wire keys) (@server)
|