@sema-agent/core 7.34.0 → 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 +123 -3
- package/dist/brain/errors.d.ts +23 -1
- package/dist/brain/errors.js +19 -2
- package/dist/brain/status-sink.d.ts +7 -1
- package/dist/brain/stream-shared.d.ts +3 -1
- package/dist/brain/stream-shared.js +16 -4
- package/dist/core/ask-origin.d.ts +34 -6
- package/dist/core/ask-origin.js +24 -23
- package/dist/core/ask-question.js +2 -1
- package/dist/core/checkpoint-store.d.ts +4 -0
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +5 -4
- package/dist/core/governance-codes.d.ts +17 -1
- package/dist/core/governance-codes.js +11 -1
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/message-utils.d.ts +2 -1
- package/dist/core/message-utils.js +2 -1
- package/dist/core/mode-policy.d.ts +3 -0
- package/dist/core/mode-policy.js +5 -0
- package/dist/core/permission-rule-model.d.ts +4 -0
- package/dist/core/permission-rule-model.js +4 -0
- package/dist/core/permission-rules.js +21 -1
- package/dist/core/persisted-rule-arms.d.ts +4 -1
- package/dist/core/persisted-rule-arms.js +4 -0
- package/dist/core/retired-keys.js +1 -1
- package/dist/core/runner/active-skill-scope.js +5 -4
- package/dist/core/runner/assemble-result.js +2 -2
- package/dist/core/runner/contracts.d.ts +25 -3
- package/dist/core/runner/denial-limit-arms.d.ts +6 -1
- package/dist/core/runner/denial-limit-arms.js +4 -4
- 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/permission-rule-lanes.d.ts +2 -1
- package/dist/core/runner/permission-rule-lanes.js +14 -11
- package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
- package/dist/core/runner/prepare-ask-lane.js +2 -2
- 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 +4 -3
- package/dist/core/runner/prepare-gate-stations.js +15 -11
- package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
- package/dist/core/runner/prepare-inherited-gate.js +10 -4
- package/dist/core/runner/prepare-park-ask.d.ts +4 -2
- package/dist/core/runner/prepare-park-ask.js +5 -4
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -2
- package/dist/core/runner/prepare-policy-chain.js +171 -175
- package/dist/core/runner/prepare-task.js +7 -4
- package/dist/core/runner/run-brain-sinks.js +3 -1
- package/dist/core/runner/run-leg.js +5 -3
- package/dist/core/runner/run-notification-lane.d.ts +3 -0
- package/dist/core/runner/run-notification-lane.js +2 -1
- package/dist/core/runner/runtask.d.ts +1 -0
- package/dist/core/runner/runtask.js +5 -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/secret-env.d.ts +30 -58
- package/dist/core/secret-env.js +149 -586
- package/dist/core/sensitive-path-policy.js +2 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
- package/dist/core/tool-policy.d.ts +31 -6
- package/dist/core/tool-policy.js +35 -16
- package/dist/core/tool-registry.d.ts +36 -0
- package/dist/core/tool-registry.js +22 -1
- package/dist/core/tool-roster.d.ts +17 -2
- package/dist/core/tool-roster.js +11 -1
- package/dist/core/trace.d.ts +11 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -1
- package/dist/engine/execution-env/node-execution-env.js +5 -2
- package/dist/engine/harness/agent-harness.js +2 -1
- package/dist/engine/lsp/node-lsp-manager.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- 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/fs/file-read-check.d.ts +2 -1
- package/dist/tools/fs/file-read-check.js +15 -7
- package/dist/tools/fs/shell-read-check.d.ts +10 -2
- package/dist/tools/fs/shell-read-check.js +12 -8
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +15 -1
- package/wire-changes/core@7.35.0.json +584 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,123 @@
|
|
|
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
|
+
|
|
1
121
|
## 7.34.0 — 2026-09-29
|
|
2
122
|
|
|
3
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)
|
|
@@ -362,7 +482,7 @@
|
|
|
362
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).
|
|
363
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.
|
|
364
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.
|
|
365
|
-
- **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.
|
|
366
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.
|
|
367
487
|
|
|
368
488
|
## 7.33.6 — 2026-09-28
|
|
@@ -445,7 +565,7 @@
|
|
|
445
565
|
|
|
446
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 …`).
|
|
447
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.
|
|
448
|
-
- **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).
|
|
449
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).
|
|
450
570
|
|
|
451
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)
|
|
@@ -459,7 +579,7 @@
|
|
|
459
579
|
|
|
460
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.
|
|
461
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.
|
|
462
|
-
- **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.
|
|
463
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).
|
|
464
584
|
|
|
465
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)
|
package/dist/brain/errors.d.ts
CHANGED
|
@@ -70,11 +70,33 @@ export declare class BrainError extends Error {
|
|
|
70
70
|
apiError?: boolean;
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The shape a provider ECHO field must have to be carried: the request id a provider states on a response
|
|
75
|
+
* header (`AssistantMessage.requestId`, projected as `TaskResult.apiFailure.requestId`) and the model name it
|
|
76
|
+
* reports answering (`AssistantMessage.responseModel`). Both are UPSTREAM BYTES that end up persisted in a
|
|
77
|
+
* transcript and rendered by a UI, so one alphabet governs both: visible ASCII (0x21–0x7E — no space, no
|
|
78
|
+
* control byte, no non-ASCII), 1–128 characters. A value outside it is dropped (the field is absent), never
|
|
79
|
+
* truncated or rewritten; the empty string is outside it. No flags, so `test` is stateless. This object is the
|
|
80
|
+
* consumer's copy: the engine judges with its own instance built from the same source, so recompiling or
|
|
81
|
+
* otherwise mutating this one cannot change what the engine admits.
|
|
82
|
+
*/
|
|
83
|
+
export declare const PROVIDER_ECHO_SHAPE: RegExp;
|
|
84
|
+
/** Does `value` fit {@link PROVIDER_ECHO_SHAPE}? A non-string answers `false` (a JS caller's number is not an echo). */
|
|
85
|
+
export declare function isProviderEchoShape(value: string): boolean;
|
|
86
|
+
/** How the brains read one provider echo value: a non-string or the empty string states nothing (`absent` — no field,
|
|
87
|
+
* nothing counted); a string in {@link isProviderEchoShape} is `carried`; any other string is `refused` (dropped and
|
|
88
|
+
* counted as `echo_dropped`). */
|
|
89
|
+
export declare function providerEchoReading(value: unknown): "absent" | "carried" | "refused";
|
|
73
90
|
/**
|
|
74
91
|
* Lift the provider's own request identifier off a response, when it stated one in a shape worth
|
|
75
|
-
* carrying (
|
|
92
|
+
* carrying ({@link isProviderEchoShape}). This is the handle a provider's support channel asks for, and
|
|
76
93
|
* without it a failed turn's only evidence is prose the operator cannot correlate upstream.
|
|
77
94
|
*
|
|
95
|
+
* A response whose header values were all refused by the shape yields no id and one `echo_dropped`
|
|
96
|
+
* telemetry event (`field: "requestId"`, bridged to the `brain.echo_dropped` trace frame) — one per
|
|
97
|
+
* response, whichever header was refused; a response that states no id at all (no header, or an empty one:
|
|
98
|
+
* {@link providerEchoReading}) yields none.
|
|
99
|
+
*
|
|
78
100
|
* Duck-typed on `headers.get` rather than requiring a real `Response`: the engine's transport is a
|
|
79
101
|
* caller-injected `fetch`, so a deployment (or a test) may hand back any response-shaped object, and
|
|
80
102
|
* a missing/throwing header bag must degrade to "no id" instead of failing the call it is describing.
|
package/dist/brain/errors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isErrorLike, readMember, thrownText } from "../core/thrown-value.js";
|
|
2
|
+
import { emitBrainTelemetry } from "./status-sink.js";
|
|
2
3
|
export const ZERO_USAGE = {
|
|
3
4
|
input: 0,
|
|
4
5
|
output: 0,
|
|
@@ -30,20 +31,36 @@ export class BrainError extends Error {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
const REQUEST_ID_HEADERS = ["request-id", "x-request-id"];
|
|
33
|
-
const
|
|
34
|
+
const PROVIDER_ECHO_SOURCE = "^[\\x21-\\x7E]{1,128}$";
|
|
35
|
+
const providerEchoShape = new RegExp(PROVIDER_ECHO_SOURCE);
|
|
36
|
+
export const PROVIDER_ECHO_SHAPE = new RegExp(PROVIDER_ECHO_SOURCE);
|
|
37
|
+
export function isProviderEchoShape(value) {
|
|
38
|
+
return typeof value === "string" && providerEchoShape.test(value);
|
|
39
|
+
}
|
|
40
|
+
export function providerEchoReading(value) {
|
|
41
|
+
if (typeof value !== "string" || value.length === 0)
|
|
42
|
+
return "absent";
|
|
43
|
+
return isProviderEchoShape(value) ? "carried" : "refused";
|
|
44
|
+
}
|
|
34
45
|
export function readProviderRequestId(res) {
|
|
46
|
+
let refused = false;
|
|
35
47
|
try {
|
|
36
48
|
const get = res?.headers?.get;
|
|
37
49
|
if (typeof get !== "function")
|
|
38
50
|
return undefined;
|
|
39
51
|
for (const name of REQUEST_ID_HEADERS) {
|
|
40
52
|
const raw = get.call(res.headers, name);
|
|
41
|
-
|
|
53
|
+
const reading = providerEchoReading(raw);
|
|
54
|
+
if (reading === "carried")
|
|
42
55
|
return raw;
|
|
56
|
+
if (reading === "refused")
|
|
57
|
+
refused = true;
|
|
43
58
|
}
|
|
44
59
|
}
|
|
45
60
|
catch {
|
|
46
61
|
}
|
|
62
|
+
if (refused)
|
|
63
|
+
emitBrainTelemetry({ kind: "echo_dropped", field: "requestId" });
|
|
47
64
|
return undefined;
|
|
48
65
|
}
|
|
49
66
|
export function namesTheFailure(status) {
|
|
@@ -31,7 +31,7 @@ export declare function emitBrainStatus(status: BrainStatus): void;
|
|
|
31
31
|
* Silent-fallback telemetry (C1/C4/C5/C6) — the brain→runner TELEMETRY channel, a
|
|
32
32
|
* SECOND ALS sink parallel to the status one. Statuses are user-facing liveness frames (wire
|
|
33
33
|
* `status` events); telemetry is operator-facing fallback accounting the Runner bridges to trace
|
|
34
|
-
* events (`brain.failover` / `breaker.transition` / `brain.retry` / `vision.placeholder`). Kept
|
|
34
|
+
* events (`brain.failover` / `breaker.transition` / `brain.retry` / `vision.placeholder` / `brain.echo_dropped`). Kept
|
|
35
35
|
* separate so widening telemetry never bloats the shell-visible status vocabulary. Same contract:
|
|
36
36
|
* fire-and-forget, no-op outside a {@link runWithBrainTelemetry} scope, MUST NOT throw.
|
|
37
37
|
*/
|
|
@@ -66,6 +66,12 @@ export type BrainTelemetry = {
|
|
|
66
66
|
| {
|
|
67
67
|
kind: "document_placeholder";
|
|
68
68
|
count: number;
|
|
69
|
+
}
|
|
70
|
+
/** A provider echo field ({@link import("./errors.js").PROVIDER_ECHO_SHAPE}) refused by its shape: the value was dropped
|
|
71
|
+
* and the field is absent. One event per provider response per field. */
|
|
72
|
+
| {
|
|
73
|
+
kind: "echo_dropped";
|
|
74
|
+
field: "requestId" | "responseModel";
|
|
69
75
|
};
|
|
70
76
|
/** Run `fn` with a per-task brain-telemetry sink in scope (Runner-side; compose with runWithStatusSink). */
|
|
71
77
|
export declare function runWithBrainTelemetry<T>(emit: (t: BrainTelemetry) => void, fn: () => Promise<T>): Promise<T>;
|
|
@@ -47,7 +47,9 @@ export declare function createRepetitionPoll(): RepetitionPoll;
|
|
|
47
47
|
* message every lane spreads `...partial` into) — ONE rule for the three lanes' spellings (`chunk.model` /
|
|
48
48
|
* `message_start.message.model` / `response.model`), WRITE twin of `providerReportedModel`
|
|
49
49
|
* (`core/message-utils.ts`). The LAST frame that NAMES a model decides and the seat carries it only when it
|
|
50
|
-
* differs; naming nothing
|
|
50
|
+
* differs; naming nothing (no name, or the empty string — {@link providerEchoReading}) is not an update, naming the
|
|
51
|
+
* REQUESTED id RETRACTS an earlier substitution. A name outside {@link isProviderEchoShape} is dropped — not an
|
|
52
|
+
* update — and counted once per response (`echo_dropped`, `field: "responseModel"`). */
|
|
51
53
|
export declare function noteResponseModel(partial: {
|
|
52
54
|
responseModel?: string;
|
|
53
55
|
}, reported: unknown, requestedId: string): void;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isProviderEchoShape, providerEchoReading } from "./errors.js";
|
|
1
2
|
import { inspectDegenerate } from "./repetition.js";
|
|
3
|
+
import { emitBrainTelemetry } from "./status-sink.js";
|
|
2
4
|
export const STREAM_TORN_WARNING = "Connection closed mid-response. The response above may be incomplete.";
|
|
3
5
|
export function parseStreamedToolArgs(json) {
|
|
4
6
|
if (!json)
|
|
@@ -40,11 +42,21 @@ export function createRepetitionPoll() {
|
|
|
40
42
|
},
|
|
41
43
|
};
|
|
42
44
|
}
|
|
45
|
+
const refusedModelCounted = new WeakSet();
|
|
43
46
|
export function noteResponseModel(partial, reported, requestedId) {
|
|
44
|
-
|
|
47
|
+
const reading = providerEchoReading(reported);
|
|
48
|
+
if (reading === "absent")
|
|
45
49
|
return;
|
|
46
|
-
if (reported === requestedId)
|
|
50
|
+
if (reported === requestedId) {
|
|
47
51
|
delete partial.responseModel;
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (reading === "refused") {
|
|
55
|
+
if (!refusedModelCounted.has(partial)) {
|
|
56
|
+
refusedModelCounted.add(partial);
|
|
57
|
+
emitBrainTelemetry({ kind: "echo_dropped", field: "responseModel" });
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
partial.responseModel = reported;
|
|
50
62
|
}
|
|
@@ -443,6 +443,10 @@ export interface ModeArbitrationRow {
|
|
|
443
443
|
readonly sandboxAdmission: boolean;
|
|
444
444
|
/** A persisted allow rule may clear it. */
|
|
445
445
|
readonly ruleMayClear: boolean;
|
|
446
|
+
/** The reference implementation's rule / safety check answers `ask` for it, so a hook's ask beside it is not the hook's
|
|
447
|
+
* forced decision: the call runs the full permission pipeline (the reference's `hookAskFloor`) — under `dontAsk`, a
|
|
448
|
+
* refusal rather than a person's question. */
|
|
449
|
+
readonly hookAskFloor: boolean;
|
|
446
450
|
}
|
|
447
451
|
/**
|
|
448
452
|
* The mode × class table. `bypassImmune` is the one new column (nothing said it before); the classifier and
|
|
@@ -451,7 +455,9 @@ export interface ModeArbitrationRow {
|
|
|
451
455
|
* `ruleMayClear` restates {@link ruleClearanceOf}'s conclusion per class (a real-approval bit is never rule-clearable).
|
|
452
456
|
* `mode` is the permission mode's OWN question (`plan`'s two sentences, `decisionReason: "mode"`): the reference
|
|
453
457
|
* implementation puts it to a person past every allow rule and never to an automatic lane, and its bypass conversion
|
|
454
|
-
* releases it like any other non-immune ask.
|
|
458
|
+
* releases it like any other non-immune ask. `hookAskFloor` marks the classes the reference's rule / safety check answers
|
|
459
|
+
* `ask` for — an ask rule, a question the tool needs a person for, a dangerous removal, a protected or unresolvable write
|
|
460
|
+
* target, the session transcript, another's shared artifact.
|
|
455
461
|
*/
|
|
456
462
|
export declare const MODE_ARBITRATION: {
|
|
457
463
|
readonly content_question: {
|
|
@@ -459,120 +465,140 @@ export declare const MODE_ARBITRATION: {
|
|
|
459
465
|
readonly classifierEligible: false;
|
|
460
466
|
readonly sandboxAdmission: false;
|
|
461
467
|
readonly ruleMayClear: false;
|
|
468
|
+
readonly hookAskFloor: true;
|
|
462
469
|
};
|
|
463
470
|
readonly hook: {
|
|
464
471
|
readonly bypassImmune: true;
|
|
465
472
|
readonly classifierEligible: false;
|
|
466
473
|
readonly sandboxAdmission: false;
|
|
467
474
|
readonly ruleMayClear: false;
|
|
475
|
+
readonly hookAskFloor: false;
|
|
468
476
|
};
|
|
469
477
|
readonly ancestor: {
|
|
470
478
|
readonly bypassImmune: true;
|
|
471
479
|
readonly classifierEligible: false;
|
|
472
480
|
readonly sandboxAdmission: false;
|
|
473
481
|
readonly ruleMayClear: false;
|
|
482
|
+
readonly hookAskFloor: false;
|
|
474
483
|
};
|
|
475
484
|
readonly unverifiable_delete: {
|
|
476
485
|
readonly bypassImmune: true;
|
|
477
486
|
readonly classifierEligible: true;
|
|
478
487
|
readonly sandboxAdmission: false;
|
|
479
488
|
readonly ruleMayClear: false;
|
|
489
|
+
readonly hookAskFloor: true;
|
|
480
490
|
};
|
|
481
491
|
readonly transcript_write: {
|
|
482
492
|
readonly bypassImmune: false;
|
|
483
493
|
readonly classifierEligible: true;
|
|
484
494
|
readonly sandboxAdmission: false;
|
|
485
495
|
readonly ruleMayClear: false;
|
|
496
|
+
readonly hookAskFloor: true;
|
|
486
497
|
};
|
|
487
498
|
readonly org_rule: {
|
|
488
499
|
readonly bypassImmune: true;
|
|
489
500
|
readonly classifierEligible: false;
|
|
490
501
|
readonly sandboxAdmission: false;
|
|
491
502
|
readonly ruleMayClear: false;
|
|
503
|
+
readonly hookAskFloor: false;
|
|
492
504
|
};
|
|
493
505
|
readonly governance_unavailable: {
|
|
494
506
|
readonly bypassImmune: true;
|
|
495
507
|
readonly classifierEligible: false;
|
|
496
508
|
readonly sandboxAdmission: false;
|
|
497
509
|
readonly ruleMayClear: false;
|
|
510
|
+
readonly hookAskFloor: false;
|
|
498
511
|
};
|
|
499
512
|
readonly rule_unreadable: {
|
|
500
513
|
readonly bypassImmune: false;
|
|
501
514
|
readonly classifierEligible: false;
|
|
502
515
|
readonly sandboxAdmission: false;
|
|
503
516
|
readonly ruleMayClear: false;
|
|
517
|
+
readonly hookAskFloor: false;
|
|
504
518
|
};
|
|
505
519
|
readonly ask_rule: {
|
|
506
520
|
readonly bypassImmune: true;
|
|
507
521
|
readonly classifierEligible: false;
|
|
508
522
|
readonly sandboxAdmission: false;
|
|
509
523
|
readonly ruleMayClear: false;
|
|
524
|
+
readonly hookAskFloor: true;
|
|
510
525
|
};
|
|
511
526
|
readonly denial_limit_fallback: {
|
|
512
527
|
readonly bypassImmune: true;
|
|
513
528
|
readonly classifierEligible: false;
|
|
514
529
|
readonly sandboxAdmission: false;
|
|
515
530
|
readonly ruleMayClear: false;
|
|
531
|
+
readonly hookAskFloor: false;
|
|
516
532
|
};
|
|
517
533
|
readonly operator_forced: {
|
|
518
534
|
readonly bypassImmune: true;
|
|
519
535
|
readonly classifierEligible: true;
|
|
520
536
|
readonly sandboxAdmission: true;
|
|
521
537
|
readonly ruleMayClear: false;
|
|
538
|
+
readonly hookAskFloor: false;
|
|
522
539
|
};
|
|
523
540
|
readonly write_protection_unresolved: {
|
|
524
541
|
readonly bypassImmune: true;
|
|
525
542
|
readonly classifierEligible: false;
|
|
526
543
|
readonly sandboxAdmission: true;
|
|
527
544
|
readonly ruleMayClear: false;
|
|
545
|
+
readonly hookAskFloor: true;
|
|
528
546
|
};
|
|
529
547
|
readonly probe: {
|
|
530
548
|
readonly bypassImmune: false;
|
|
531
549
|
readonly classifierEligible: true;
|
|
532
550
|
readonly sandboxAdmission: false;
|
|
533
551
|
readonly ruleMayClear: false;
|
|
552
|
+
readonly hookAskFloor: false;
|
|
534
553
|
};
|
|
535
554
|
readonly write_protection: {
|
|
536
555
|
readonly bypassImmune: false;
|
|
537
556
|
readonly classifierEligible: true;
|
|
538
557
|
readonly sandboxAdmission: true;
|
|
539
558
|
readonly ruleMayClear: false;
|
|
559
|
+
readonly hookAskFloor: true;
|
|
540
560
|
};
|
|
541
561
|
readonly tool_marks_mcp: {
|
|
542
562
|
readonly bypassImmune: false;
|
|
543
563
|
readonly classifierEligible: true;
|
|
544
564
|
readonly sandboxAdmission: true;
|
|
545
565
|
readonly ruleMayClear: false;
|
|
566
|
+
readonly hookAskFloor: false;
|
|
546
567
|
};
|
|
547
568
|
readonly tool_marks_a2a: {
|
|
548
569
|
readonly bypassImmune: false;
|
|
549
570
|
readonly classifierEligible: true;
|
|
550
571
|
readonly sandboxAdmission: true;
|
|
551
572
|
readonly ruleMayClear: false;
|
|
573
|
+
readonly hookAskFloor: false;
|
|
552
574
|
};
|
|
553
575
|
readonly policy_rra: {
|
|
554
576
|
readonly bypassImmune: true;
|
|
555
577
|
readonly classifierEligible: true;
|
|
556
578
|
readonly sandboxAdmission: false;
|
|
557
579
|
readonly ruleMayClear: false;
|
|
580
|
+
readonly hookAskFloor: false;
|
|
558
581
|
};
|
|
559
582
|
readonly artifact_shared_read: {
|
|
560
583
|
readonly bypassImmune: false;
|
|
561
584
|
readonly classifierEligible: false;
|
|
562
585
|
readonly sandboxAdmission: false;
|
|
563
586
|
readonly ruleMayClear: false;
|
|
587
|
+
readonly hookAskFloor: true;
|
|
564
588
|
};
|
|
565
589
|
readonly mode: {
|
|
566
590
|
readonly bypassImmune: false;
|
|
567
591
|
readonly classifierEligible: false;
|
|
568
592
|
readonly sandboxAdmission: false;
|
|
569
593
|
readonly ruleMayClear: false;
|
|
594
|
+
readonly hookAskFloor: false;
|
|
570
595
|
};
|
|
571
596
|
readonly ordinary: {
|
|
572
597
|
readonly bypassImmune: false;
|
|
573
598
|
readonly classifierEligible: true;
|
|
574
599
|
readonly sandboxAdmission: true;
|
|
575
600
|
readonly ruleMayClear: true;
|
|
601
|
+
readonly hookAskFloor: false;
|
|
576
602
|
};
|
|
577
603
|
};
|
|
578
604
|
/** The fence over the arbitration table: `never` while every class has a row. */
|
|
@@ -613,8 +639,9 @@ export interface AskClassification {
|
|
|
613
639
|
export declare function decisionAskClassesOf(d: Extract<PermissionResult, {
|
|
614
640
|
action: "ask";
|
|
615
641
|
}>): Set<AskClass>;
|
|
616
|
-
/** Fold a class set onto one row: `bypassImmune` by OR (any immune member keeps the ask
|
|
617
|
-
* switches by AND (one refusing member withholds the lane). An empty
|
|
642
|
+
/** Fold a class set onto one row: `bypassImmune` and `hookAskFloor` by OR (any immune member keeps the ask; any floored
|
|
643
|
+
* member floors a hook's ask), the three permissive switches by AND (one refusing member withholds the lane). An empty
|
|
644
|
+
* set is `ordinary`. */
|
|
618
645
|
export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>): ModeArbitrationRow;
|
|
619
646
|
/**
|
|
620
647
|
* The ONE class derivation: the decision half ({@link decisionAskClassesOf}) joined with the gate's own facts —
|
|
@@ -680,9 +707,10 @@ export type Arbitration = {
|
|
|
680
707
|
classes: readonly AskClass[];
|
|
681
708
|
};
|
|
682
709
|
/**
|
|
683
|
-
* The ONE mode read point. `dontAsk` refuses every ask but
|
|
684
|
-
*
|
|
685
|
-
*
|
|
710
|
+
* The ONE mode read point. `dontAsk` refuses every ask but an operator authority's (an org rule, an operator's forced
|
|
711
|
+
* confirmation, unreadable governance — a mode stance may not settle an operator-mandated gate) and a hook's that no
|
|
712
|
+
* member of the set floors (`hookAskFloor`: a hook's ask beside a rule's or a safety check's ask is refused, as the
|
|
713
|
+
* reference's full pipeline refuses it); `bypassPermissions` refuses a write whose protection could not be judged (a person cannot be asked, and
|
|
686
714
|
* releasing would be looser than the allow the judge already refuses), keeps every immune class and releases the
|
|
687
715
|
* rest; `acceptEdits` answers an ORDINARY ask itself on a call the caller vouches is one of the mode's file commands
|
|
688
716
|
* (`call.fileCommand`, `accepted`); `plan`, `default` and `auto` hand every ask on (plan's own questions were raised by
|