@sema-agent/core 7.51.0 → 7.51.1
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 +51 -1
- package/dist/core/engine-capabilities.d.ts +15 -0
- package/dist/core/engine-capabilities.js +1 -0
- package/dist/core/engine-notice.d.ts +8 -5
- package/dist/core/runner/tool-output-projection.d.ts +5 -0
- package/dist/core/runner/tool-output-projection.js +3 -3
- package/dist/core/secret-env.d.ts +41 -15
- package/dist/core/secret-env.js +84 -18
- package/dist/core/session-policy-store.js +1 -1
- package/dist/core/spec-contract.d.ts +5 -3
- package/dist/engine/execution-env/kill-tree.js +4 -4
- package/dist/engine/execution-env/node-execution-env.d.ts +11 -4
- package/dist/engine/execution-env/node-execution-env.js +9 -4
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -1
- package/dist/engine/lsp/node-lsp-manager.js +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/stores/file/fs-atomic.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -1
- package/wire-changes/core@7.51.1.json +259 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## 7.51.1 — 2026-10-14
|
|
2
|
+
|
|
3
|
+
### `inheritEnv: "all"` is defined once, in core: the whole environment less the deployment's own credentials (#1753; security; behavior narrowing where a deployment declares its list, open to veto; closed sets: +1 set `EngineCapability`, +1 member `SecretEnvFindingKind` `own-credential`; exports: +6, none removed; members: +1 optional notice detail key)
|
|
4
|
+
|
|
5
|
+
**Behavior narrowing (open to veto).** (1) **What changed:** a deployment declares its own credential names once per process, with `declareWithheldEnv(names)`. After that, no child spawned on this host inherits one of them, whatever its `inheritEnv` says. That covers the tool shell, background shells, oracle gates, the language server, and the engine's own helper programs looked up on PATH (shell discovery `which` / `where`, the tree kill's `ps` / `taskkill` / Git Bash kill, the lock probe's `ps`). The MCP stdio child is the SDK's own spawn: it receives the SDK's default inherited set plus the server entry's configured `env` (the host's explicit grant), never this process's whole environment — so the declaration has nothing to subtract there. `"all"` is the whole environment less the declared names, and `"scrub"` is the scrub's result less them. An allow list naming one is refused. One function says all of this, `inheritedHostEnv(policy, findings?)`. Every core spawn site goes through it (`scrubSecretEnv(` now has exactly one caller in src), and it is now a root export, so a host's own spawn site calls it instead of reading the policy a second time. (2) **Who breaks:** a deployment that declares names and relied on `"all"` handing them to the model's shell. That reliance is the defect being closed: an engine's approval bearer reached a model-driven shell under `"all"`, and one `curl` could approve its own card (the inheritance arm; the engine process's own initial environment stays readable to same-user processes on the host through `ps -E` / `/proc/<pid>/environ` — KL-1753-a — so a credential that never enters the environment remains the root fix). A deployment that declares nothing sees no change. (3) **How to migrate:** at start-up, before anything spawns, call `declareWithheldEnv` with the deployment's own credential names (every `type: secret` key of its configuration directory), and call `inheritedHostEnv(policy, findings)` at every host spawn site. A child that genuinely needs one of those values gets it through an explicit grant (`shellEnv`, a per-exec `env`, `NodeLspManagerOptions.env`), never by inheritance.
|
|
6
|
+
|
|
7
|
+
- **Before (7.51.0).** `inheritedHostEnv("all")` was `{ ...process.env }` and an allow list was handed out as written. The function was not exported, so every host re-read the three policies itself (server `src/plugins/remote-env-host.ts` `childEnv`: `all ⇒ { ...process.env }`). A malformed policy that reached the function was read as `"scrub"`.
|
|
8
|
+
- **Now.** `inheritedHostEnv(policy, findings?)` subtracts the declared names from every policy's result. Each name it subtracts is a finding `{ kind: "own-credential", confidence: "high", source: "secret-env" }`. A name the scrub already withheld stays one finding, the scrub's. Names are compared the way the platform compares them: exactly, except case-blind on Windows. `declareWithheldEnv` copies the list, and a later call replaces it whole (a configuration reload), taking effect from the next spawn. A malformed list throws `declareWithheldEnv: …` and leaves the previous declaration standing. Malformed means not an array, or an entry that is not a variable name (empty, or holding `=`, NUL, white space or an unpaired surrogate — a name written "almost right" is refused, never silently unmet), or a name some layer of the host hands every child whatever env the spawn passes, which therefore cannot be withheld. Those names are `NODE_V8_COVERAGE` (Node), the MCP stdio transport's default inherited set (`HOME LOGNAME PATH SHELL TERM USER`; its Windows list there), and on Windows libuv's required variables. A name holding an unpaired UTF-16 surrogate is not a variable name: the platform stores it as U+FFFD. This is refused in a declaration and, newly, in an `inheritEnv` allow list.
|
|
9
|
+
- **When an allow list naming a declared name is refused (fixed timing).** ① **At every spawn, always.** `inheritedHostEnv` reads the declaration as it stands and throws `inheritEnv[i] names X, which is this deployment's own credential and cannot be inherited by a child (a configuration error: no retry succeeds until the policy or the declaration changes)`. That is the invariant, whenever the declaration was made: a tool-shell exec answers `spawn_error`, a background shell `spawn_failed`, no child starts, and the `homeDir` / `cdPath` reads say "none" rather than throw. ② **Also early, when the declaration came first.** `NodeExecutionEnv` refuses at construction (`NodeExecutionEnv: …`), and the oracle gate refuses per gate (an `error` gate that starts nothing), both through `inheritEnvPolicyProblem(value)`, which reads the declaration. A host that declares after construction gets only ①. No new refusal code. `inheritedHostEnv` also throws for a value that is not a policy at all. It used to read such a value as `"scrub"`.
|
|
10
|
+
- **Disclosure.** `config.secret_env_scrubbed` still fires once per process. `detail.names` now includes the withheld own credentials, and the new `detail.ownCredentials` (present only when one was withheld) names exactly them. The raw `onSecretEnvScrub` seat now also fires on an `"all"` leg that withheld one.
|
|
11
|
+
- **One way in.** The language server's default environment and the five helper programs looked up on PATH now go through `inheritedHostEnv("scrub")`; they used to call `scrubSecretEnv(process.env)` directly. With nothing declared, every result is the same as before. `gate:spawn-sites` now lets one named data export of a process-starting module through as a non-site: the MCP SDK's `DEFAULT_INHERITED_ENV_VARS` (`INERT_SPAWN_MODULE_EXPORTS`), which the declaration reads instead of keeping a copy.
|
|
12
|
+
- **Capability word.** `ENGINE_CAPABILITIES` (`src/core/engine-capabilities.ts`, frozen; type `EngineCapability`, registered in `docs/CLOSED-SETS.md`) is the engine build's capability words. Its first word, `inherit_env.own_credentials_withheld`, means the behaviour above. A shell gates on the word, not on a version number, and the server projects it onto `/v1/capabilities` once it passes its list.
|
|
13
|
+
- **Consumers named.** *server*: declare once at boot (`declareWithheldEnv`, the directory's `type: secret` keys). `src/plugins/remote-env-host.ts` `childEnv` (`all` / allow-list / scrub arms, `:1936-1944`) and `src/server-secret-env.ts` `hostSubprocessEnv` move to `inheritedHostEnv(policy, findings)`. `src/request-value-closure.ts:164` `inheritEnvFieldIssue` asks `inheritEnvPolicyProblem(value)` for the list-naming refusal (early, at the request door). `src/boot/execution-env.ts:694` (the git base env under `"all"`) is covered by the same declaration. The bit on `/v1/capabilities` comes from `ENGINE_CAPABILITIES`. *cli*: `src/sema/inheritEnvWire.ts:40-46` gates sending `"all"` on that capability bit instead of the version (L-1408). *client-core*: `src/inheritEnvWire.ts:61` `inheritEnvDeclaration` takes the bit. *desktop*: `docs/W-WIRE-D8-DECISION.md` (B, does not send `inheritEnv`) can revisit once the bit is true.
|
|
14
|
+
- **Who is hurt / compensation.** A TypeScript consumer that switches exhaustively over `SecretEnvFindingKind` gets a compile error on `own-credential` (one new arm). A caller of the previously internal `inheritedHostEnv` with a malformed policy gets a throw where it used to get the scrub; no such caller existed outside core. A host that declares a name after building an environment whose allow list names it sees that environment's spawns refused, loudly, from then on.
|
|
15
|
+
- **Pins.** `test/backlog1753-own-credentials-withheld.test.ts`, W1–W11:
|
|
16
|
+
- `"all"` less the declaration, through a unit call and through a real shell's `printenv`; the user's undeclared token is still handed on.
|
|
17
|
+
- `"scrub"` unioned with the declaration.
|
|
18
|
+
- An allow list refused at the validator and in `inheritedHostEnv`. Declared before construction ⇒ `NodeExecutionEnv` refuses. Declared after ⇒ exec `spawn_error` and background `spawn_failed`, no child starts, the reads say "none". The oracle gate refuses it too.
|
|
19
|
+
- A malformed declaration throws and the previous one stands; the declaration is a copy and a later one replaces it.
|
|
20
|
+
- The disclosure and the raw seat.
|
|
21
|
+
- The oracle gate, through `runOracle` and the public `runSpec` path.
|
|
22
|
+
- The `shellEnv` grant.
|
|
23
|
+
- The Windows case fold, with the platform stubbed.
|
|
24
|
+
- The capability word against its CLOSED-SETS row.
|
|
25
|
+
- A planted PATH `ps` that sees no declared name, both on a tree kill and on a lock probe, with a control that sees it when nothing is declared.
|
|
26
|
+
- The AST pin: `scrubSecretEnv(` has one caller in src.
|
|
27
|
+
|
|
28
|
+
`test/backlog1753-lsp-withheld.test.ts` covers the language server's inherited environment, a declaration made after the manager was built, and the host's own `env` grant. Re-pinned: `test/backlog1181-spawn-site-env.test.ts` (a value that is not a policy now throws instead of reading as the scrub) and `test/public-knob-liveness.test.ts` (the three helper files no longer read the environment whole).
|
|
29
|
+
- **Known limits.** KL-1753-a: another local process reading the engine's initial environment (`ps -E`, `/proc/<pid>/environ`) is an OS concern, out of this reach. KL-1753-b: the Windows case fold is pinned with the platform stubbed; Windows `process.env` behaviour itself is inferred. KL-1753-c: what the host hands a child itself (`shellEnv`, a per-exec `env`, `NodeLspManagerOptions.env`, an MCP server entry's `env`) is not read against the declaration, and no notice names such a grant. A credential value the engine holds that the deployment did not name is the separate value arm (#1754), not covered here.
|
|
30
|
+
|
|
31
|
+
### `toolOutputBlockChars` is exported from the package root; two catalog sentences corrected (#1758, #1762; additive; hard-breaking: none; closed sets: none; exports: +1 function) (@server @client-core)
|
|
32
|
+
|
|
33
|
+
- **What changed.** `toolOutputBlockChars(block)` — the one per-block character counter that `tool_end.totalChars` sums over a tool result's blocks (`src/core/runner/tool-output-projection.ts`) — is exported from the root, so a consumer that re-renders a truncated result counts with the same function instead of a copy. Two sentences are corrected: the `permission.entry_mode_clamped` catalog detail is `{ requested, effective, ceiling, sessionId }` — the minter sits at intake, before any run exists, so the `runId` the 7.51.0 entry listed cannot be minted (erratum noted on that entry); the session-rule bucket refusal's example now reads "Bash(rm:*) matches a command whose first word is rm" — a rule matches the command's first word, `/bin/rm` is not matched, and the earlier "refuses every run of rm" overstated it.
|
|
34
|
+
- **Who breaks.** No one: an additive export and wording only.
|
|
35
|
+
- **How to migrate.** `import { toolOutputBlockChars } from "@sema-agent/core"` and sum it over the result's `content` blocks to reproduce `totalChars`.
|
|
36
|
+
|
|
37
|
+
### `headerOwnershipCollision` and `ProtocolHttpAuth` are exported from the package root (#1757; additive; hard-breaking: none; closed sets: none; exports: +1 function, +1 type) (@web-admin @server)
|
|
38
|
+
|
|
39
|
+
- **What changed.** The engine's one header-ownership judge — "a header name has exactly one owner in a protocol declaration" (7.51.0 #1491, `src/core/protocol-http-auth.ts`), until now reachable only at dial time as `config.protocol_header_collision` — is exported, with its input type, so a host console's write face and a server's deployment intake refuse a colliding declaration at the door by calling the same function the engine calls, instead of keeping their own copies of the rule.
|
|
40
|
+
- **Who breaks.** No one: additive. web-admin's two hand-written copies and server's intake (S-1571) retire onto this export.
|
|
41
|
+
- **How to migrate.** `import { headerOwnershipCollision } from "@sema-agent/core"`; `undefined` = no collision, else `{ first, second }` are the two spellings that collide.
|
|
42
|
+
|
|
1
43
|
## 7.51.0 — 2026-10-14
|
|
2
44
|
|
|
3
45
|
### A tool list naming a tool nobody answers to no longer fails the run — the entry is kept and told once; only a spelling no tool name can have is refused (#1736; **BREAKING**: behavior change, the reversal of 7.49.0 hard-breaking ①; hard-breaking: one, open to veto (C-R449); closed sets: none; exports: none; members: none)
|
|
@@ -47,7 +89,7 @@
|
|
|
47
89
|
**Now.**
|
|
48
90
|
- The sentence names the route taken at the fork (never whether a park then commits): "handed on to this run's live approver", "handed on to this run's durable approval park, for a person to answer later", or "this run has no approver and no park facility, so the call was refused" (`that layer's …` at the inherited station). A run that declared `approvalReach: "none"` takes the `none` route only when no park lane is armed: an armed durable park is the deployment's own statement that an answer can wait for a person, so such a run parks (`durable_park`) — the declaration silences the live callback, not the park. A print / SDK-stdio lane whose approver callback cannot reach a person declares `approvalReach: "none"` (client-core `approvalReachDeclaration`); the route is then `none` and the engine — not the lane — says the call was refused.
|
|
49
91
|
- `InheritedGate.autoModeFace` (new optional type key; opaque half, never serialized; present only when the ancestor's deps — or its own chain — carry an auto-mode face): `autoModeFaceOf(deps, chain)` is the one reading of "which face does this leg arm from" — the leg's own `RunnerDeps.autoMode`, else its ancestor's. A child whose runner wires none arms its own classifier (its own transcript, model and denial tracker) from the ancestor's deployment face; its own face wins where it has one; `RuntimeCaps.autoMode: false` still folds it.
|
|
50
|
-
- `permission.entry_mode_clamped` joins the notice catalog (closed-set addition; audience `user`, `NOTICE_MINTER` `"server"`, `detail: { requested, effective, ceiling, sessionId
|
|
92
|
+
- `permission.entry_mode_clamped` joins the notice catalog (closed-set addition; audience `user`, `NOTICE_MINTER` `"server"`, `detail: { requested, effective, ceiling, sessionId }`, once per run; erratum 7.51.1: this line first listed a `runId` the minter cannot have — the clamp happens at intake, before a run exists): the serving layer mints it where its entry floor clamps (server S-1306); this engine never mints it — its own folds stay `config.permission_mode_unavailable`. Readers: client-core / cli generated notice tables.
|
|
51
93
|
- C-R138 (bypass: a card nobody answers parks on a durable seat, else is refused) holds on every K-ASK cell, including an approver that answers `unavailable` (`approver_unavailable`): pinned, no behaviour change.
|
|
52
94
|
|
|
53
95
|
**Behavior change (narrowing of the person's reach).** A delegated child whose own runner wires no auto-mode face is classified under its ancestor's face (#1493 c): calls that went to a person in v7.50.0 because the child folded to `default` are now answered by the classifier like the parent's own calls; the deployment's deny bit (`RuntimeCaps.autoMode: false`) still wins. **Who is hurt.** A host that hand-wires the gate's classifier seat or reads the observer's `disposition` (hard-breaking 1). A deployment that relied on a face-less child runner refusing `auto` now gets a classifying child — the parent's face is the deployment's own trust gate; deny it per principal with `RuntimeCaps.autoMode: false`. **Compensation.** Fail-closed screen on the route word; the wire `disposition` keeps its two words; the parent stream now carries every child fold.
|
|
@@ -177,6 +219,14 @@
|
|
|
177
219
|
- **Pins.** `test/backlog1573-egress-tiers.test.ts` (A loop station × thrown Error / string, and the returned-content control; B child value × cause / details / name / AggregateError / identity / cycle / accessor / depth / JSON reading / proxy / a getter or `toJSON` answering twice / a self-returning `toJSON`, a bound getter, a `Date` delegating to its own `toISOString`, a credential-named member serializing to `Basic …`, a `DOMException` re-minted, a subclass whose prototype `toJSON` / `toString` hands back raw text (top level, nested, through the loop's station); C activity summary × four fields / signed URL / field choice / authentication names; D quote spellings × faces; E nested JSON × depth 1–3; H header schemes × faces, Bearer control, shape kept, white space on either side of `=`, a quoted parameter at 1 024 / 1 025 / 1 500 characters inside one and two JSON layers with the fields after it kept, a bound cutting through the value; F new rows + namespace words; G guard + type pins); `test/fixtures/secret-detect-fixtures.json` (+26 fixtures: npm, ya29, ABIA / ACCA / A3T, encoded quotes, header schemes); `test/backlog1099-sema-namespace-secret-env.test.ts` (oracle = the derived words); `test/backlog1508-subagent-failure-redaction.test.ts` ③ / ⑨ re-pinned as "same class, same own members, sentence byte-identical" (a snapshot, not the live value); the `scrubSecrets` face retired from the #532 / #954 / #958 / #1583 / #1675 / #1691 / RB-209 pins.
|
|
178
220
|
- **Known limits.** A credential carried by a PROTOTYPE accessor of a thrown class (`get message()` reading a closure) is not a member and is read only by the text face; KL-1717-b: white space after a parameter's `=` continues a header value, so a token68 that ends in `=` padding reads the next word as its value (`Authorization: Basic <…=> was sent` → `Authorization: [redacted] sent`) — the cost of not leaving `response= <value>` in clear; a field whose lower-case name glues a credential word to another word (`licensekey`) is no credential name to the record-key view (the old activity-summary filter matched substrings); a string member named with a credential word that holds no credential (`tokenCount: "5"`) reads the marker; the resume leg's thrown-error station (`run-leg.ts` — the `Error:` text and the marks of a thrown tool, both through the same-principal tier) and the compaction failure reason (`run-turn-boundary.ts`) are wired at the wave merge, so the live loop and the resumed leg mint the same redacted result; KL-1717-a: a credential passed as a program's flag argument (`curl -u alice:<password>`, `--password <value>`) is no text shape the vocabulary reads (it reads header grammar, not per-program flag grammar) — which flag takes a credential is a per-program grammar, the shell-argument model's, not the text vocabulary's.
|
|
179
221
|
|
|
222
|
+
### The gate's sentences built from a thrown value are redacted before they are contained (#1728; P1; fix; security; behaviour change — more is redacted; hard-breaking: none; closed sets: none; exports: +0 / −0) (@server @test) — *disclosed 2026-10-14 13:1x, after the v7.51.0 tag: both fixes (`7172aff05`, `56dceceb5`) are ancestors of the tag; this section, criterion G18 / flip F32 and the ship-post erratum were added together*
|
|
223
|
+
|
|
224
|
+
- **Before (v7.50.2; pre-existing — the base reads the same; found by the 7.51.0 stage review leg, probe R2).** `describeThrown` (`src/core/tool-policy.ts`), the one containment point for a thrown value's text at the gate, contained the text but did not redact it: a PreToolUse hook that crashed with a credential in its message, a live approver that threw ("approval errored for …", two sites), an approver outcome whose members threw on read ("could not be read", two sites) and the session-policy store's re-read failure notice put the credential on `tool_end.output`, into the model's next context, or onto the operator line.
|
|
225
|
+
- **Now.** `describeThrown` redacts at the `same-principal` tier BEFORE it contains, so every reader of the one containment point is redacted at once; the empty-message fallback (the structural description — a thrown value's own key names are its text too) is redacted on the same terms; the session-rule store's notice reads `egressThrownText` directly. The 7.50.1 policy-throw mint already redacted its own line; this closes the siblings.
|
|
226
|
+
- **Who is hurt.** A host or test that pinned one of those sentences with a credential-shaped string in it now reads the marker. No member, code, closed-set word or export changes.
|
|
227
|
+
- **Compensation.** None needed (over-redaction on egress is the declared direction, as for #1573).
|
|
228
|
+
- **Pins.** `test/backlog1728-gate-throw-texts.test.ts` (R2a the hook crash on the block reason; R2b the throwing approver on the model's next context; R2c the unreadable approver outcome in the refusal sentence; R2d `describeThrown` itself — redacted then contained, never empty, a non-Error value; R2e the empty-message fallback). Base (stage before the change): R2a–R2d red. Mutation: `describeThrown` contains `thrownText(err)` again ⇒ R2a–R2c red; the store notice reads `thrownText` ⇒ R2d red.
|
|
229
|
+
|
|
180
230
|
### A read station's answer is read one way by every reader: the resumed leg's belt refuses a returned silence (#1708; fix; security; behaviour narrowing; hard-breaking: none; exports: +0 / −0) (@server @test)
|
|
181
231
|
|
|
182
232
|
- **Before (v7.50.1; the same in v7.49.5).** The durable-resume leg re-judges an approved shell call on its own read station before it runs. It refused when the station's judgment THREW or timed out (the `unanswered()` fallback), but a judgment that RETURNED `{ mandated, unanswered, environmentSilent }` — a resolver answering an error result — passed on an already-seen call and the call ran: a link re-pointed onto a deny-listed file while the call was parked, with the resolver giving no answer on the resumed leg, put that file's content in the transcript under a person's yes to a different question. The gate's fold read the same silence before its arms (7.50.0); the belt read it twice.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine build's capability words — the one home for "this engine does X". A shell gates on a word, never on a
|
|
3
|
+
* version number; the server projects the words it also wires onto its `/v1/capabilities`.
|
|
4
|
+
*
|
|
5
|
+
* A word is added in the release whose code makes it true and is never re-spelt; a word whose behaviour is retired
|
|
6
|
+
* leaves the set in a breaking release. Each word:
|
|
7
|
+
* - `inherit_env.own_credentials_withheld` — a child spawned on this host never inherits a name the deployment
|
|
8
|
+
* declared as its own credential (`declareWithheldEnv`): `inheritedHostEnv` subtracts the list from every policy's
|
|
9
|
+
* result (`"all"` and `"scrub"` alike), and an allow list naming one is refused (`inheritEnvPolicyProblem`; every
|
|
10
|
+
* spawn throws it). The word speaks of INHERITANCE only: the engine process's own environment stays readable to
|
|
11
|
+
* same-user processes on the host (`ps -E`), so a credential that never enters it remains the root fix.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ENGINE_CAPABILITIES: readonly ["inherit_env.own_credentials_withheld"];
|
|
14
|
+
/** One word of {@link ENGINE_CAPABILITIES}. */
|
|
15
|
+
export type EngineCapability = (typeof ENGINE_CAPABILITIES)[number];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ENGINE_CAPABILITIES = Object.freeze(["inherit_env.own_credentials_withheld"]);
|
|
@@ -379,10 +379,13 @@ export interface EngineNotice {
|
|
|
379
379
|
* - `"config.execution_env_capability_invalid"` — a remote env DECLARES a boolean capability key
|
|
380
380
|
* (`isolation`, `suspendable`, `canonicalPathAuthoritative`) with a non-boolean value; ABSENCE is a
|
|
381
381
|
* supported reading of each and stays silent. Once per key per run; `detail: { key, valueType }`.
|
|
382
|
-
* - `"config.secret_env_scrubbed"` (#1081) — the D1 fail-closed `scrub` policy
|
|
383
|
-
*
|
|
382
|
+
* - `"config.secret_env_scrubbed"` (#1081) — the D1 fail-closed `scrub` policy, or the deployment's
|
|
383
|
+
* declared own credentials (`declareWithheldEnv`), withheld one or more keys from a spawned child's
|
|
384
|
+
* environment (a tool's shell, an oracle gate, or a language server).
|
|
384
385
|
* Once per PROCESS, shared by every scrub site ({@link import("./secret-env.js").announceSecretEnvScrub});
|
|
385
|
-
* `detail: { names }` — every withheld key's name, never a value
|
|
386
|
+
* `detail: { names, ownCredentials? }` — every withheld key's name, never a value; `ownCredentials`, present
|
|
387
|
+
* only when one was withheld, the subset withheld as declared own credentials (under any policy, `"all"`
|
|
388
|
+
* included).
|
|
386
389
|
* - `"config.permission_mode_unavailable"` — the permission mode a task requested is not the mode the leg runs
|
|
387
390
|
* under: the deployment denies it (`by: "operator_deny"` — `RuntimeCaps.bypassPermissions` / `autoMode` is
|
|
388
391
|
* `false`), a requested `auto` has no classifier face (`by: "face_absent"` — neither the leg's deps nor its chain carry
|
|
@@ -879,8 +882,8 @@ export interface EngineNotice {
|
|
|
879
882
|
* `clampPermissionMode`), so the run starts under another mode. The disclosure is minted where the clamp happens:
|
|
880
883
|
* a fold this engine makes itself (a parent's ceiling, the deployment's deny bits, a missing classifier face) is
|
|
881
884
|
* `config.permission_mode_unavailable`, never this row. Audience `"user"`; `detail: { requested, effective,
|
|
882
|
-
* ceiling, sessionId
|
|
883
|
-
*
|
|
885
|
+
* ceiling, sessionId }` — `requested` the caller's word, `effective` the word the run starts under, `ceiling`
|
|
886
|
+
* the floor that clamped it; no run id — the clamp happens at intake, before any run exists. Once per run.
|
|
884
887
|
* - `"instructions.source_changed"` — the project instruction file this run injects is not the one the same
|
|
885
888
|
* session's previous run injected (a file added, removed or renamed). Audience `"user"`; `detail: { previous,
|
|
886
889
|
* current, sessionId }` — `previous` / `current` are chain-member relative paths, `null` = that run had none.
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* state, zero runtask coupling — the module-level inputs are the text cap below and the shared media budget default
|
|
5
5
|
* (`mediaBudgetBytes`); the leg hands in its own `mediaByteCapBytes`.
|
|
6
6
|
*/
|
|
7
|
+
/** A block's own size in characters — the ONE counter `tool_end.totalChars` adds up over a tool result's blocks (a
|
|
8
|
+
* consumer that re-renders a result sums the same function): a text block its text plus whatever else it carries at
|
|
9
|
+
* its serialized length (the contract's text block carries nothing else), a media block its payload, any other block
|
|
10
|
+
* its serialization. */
|
|
11
|
+
export declare const toolOutputBlockChars: (b: unknown) => number;
|
|
7
12
|
type ToolOutput = {
|
|
8
13
|
output: unknown;
|
|
9
14
|
truncated: boolean;
|
|
@@ -4,9 +4,9 @@ import { engineCardTypes } from "../tool-registry.js";
|
|
|
4
4
|
import { readMember } from "../thrown-value.js";
|
|
5
5
|
const MAX_TOOL_END_OUTPUT_CHARS = 16384;
|
|
6
6
|
const marker = (n) => `…[+${n} chars truncated]`;
|
|
7
|
-
const
|
|
7
|
+
export const toolOutputBlockChars = (b) => isTextBlock(b) ? b.text.length + (JSON.stringify({ ...b, type: undefined, text: undefined })?.length ?? 2) - 2 : isMediaBlock(b) ? b.data.length : (JSON.stringify(b)?.length ?? 0);
|
|
8
8
|
const boundTextLane = (blocks, lane) => {
|
|
9
|
-
const sized = lane.map((i) => ({ i, size:
|
|
9
|
+
const sized = lane.map((i) => ({ i, size: toolOutputBlockChars(blocks[i]) }));
|
|
10
10
|
const stream = sized.reduce((n, { size }, k) => n + size + (k > 0 ? 1 : 0), 0);
|
|
11
11
|
const forms = new Map();
|
|
12
12
|
let used = 0;
|
|
@@ -61,7 +61,7 @@ const project = (content, mediaByteCapBytes) => {
|
|
|
61
61
|
return { output: content, truncated: false };
|
|
62
62
|
let next = 0;
|
|
63
63
|
const output = content.flatMap((_, i) => (media[i] ? (fit.blocks[next++] ?? []) : text.forms.has(i) ? [text.forms.get(i)] : []));
|
|
64
|
-
return { output, truncated: true, totalChars: content.reduce((n, b) => n +
|
|
64
|
+
return { output, truncated: true, totalChars: content.reduce((n, b) => n + toolOutputBlockChars(b), 0) };
|
|
65
65
|
};
|
|
66
66
|
export const toolOutputFrom = (result, mediaByteCapBytes) => {
|
|
67
67
|
try {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* A child the engine spawns may run what the MODEL chose (a tool shell, a gate over model-written code, a language
|
|
5
5
|
* server). Under the `"scrub"` policy (the default) it inherits every variable but those the judgment below names —
|
|
6
6
|
* deleted whole, never rewritten; `"all"` and a `string[]` allow list are the caller's explicit alternatives
|
|
7
|
-
* ({@link inheritedHostEnv}).
|
|
7
|
+
* ({@link inheritedHostEnv}). Whatever the policy, a name the deployment declares as its own credential
|
|
8
|
+
* ({@link declareWithheldEnv}) is never inherited. One judgment per variable ({@link classifySecretEnvEntry}), the first rule that answers:
|
|
8
9
|
* 1. exempt — git's environment config and the proxy variables, handed on as written ({@link isExemptEnvName});
|
|
9
10
|
* 2. `exact-name` — a known credential name (the vocabulary's exact env names);
|
|
10
11
|
* 3. `prefix-rule` — the OpenTelemetry settings ({@link OTEL_ENV_RE});
|
|
@@ -25,10 +26,11 @@ import { type EngineNotice } from "./engine-notice.js";
|
|
|
25
26
|
* Reads the first {@link VALUE_SCAN_LIMIT} code units. The value half of the judgment, exported apart from the env
|
|
26
27
|
* entry's exemptions ({@link isExemptEnvName}): a caller holding a value that is no environment variable asks this. */
|
|
27
28
|
export declare function looksLikeCredentialValue(value: string): boolean;
|
|
28
|
-
/** Which rule withheld an entry
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/** Which rule withheld an entry: one of the scrub's rules, or `own-credential` — the deployment declared the name as its
|
|
30
|
+
* own credential ({@link declareWithheldEnv}), which every policy withholds. */
|
|
31
|
+
export type SecretEnvFindingKind = "exact-name" | "name-rule" | "namespace-rule" | "prefix-rule" | "value-rule" | "own-credential";
|
|
32
|
+
/** One env entry withheld (deleted) from a child's environment: its name — never its value, so the record is
|
|
33
|
+
* loggable —, the rule that withheld it, how strong that claim is, and the minting module. */
|
|
32
34
|
export interface SecretEnvFinding {
|
|
33
35
|
key: string;
|
|
34
36
|
kind: SecretEnvFindingKind;
|
|
@@ -49,16 +51,39 @@ export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: Secret
|
|
|
49
51
|
/** How much of this process's environment a child spawned on this host inherits — the `inheritEnv` value. */
|
|
50
52
|
export type InheritEnvPolicy = "all" | "scrub" | string[];
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* Declare the names of this deployment's OWN credentials in this process's environment — the engine's or its host's:
|
|
55
|
+
* an approval bearer, a service token. From then on no child spawned on this host inherits one, whatever the policy:
|
|
56
|
+
* every spawn site reads its environment through {@link inheritedHostEnv}, which subtracts them from `"all"` and from
|
|
57
|
+
* the scrub's result (each one present is a finding of kind `own-credential`), and {@link inheritEnvPolicyProblem}
|
|
58
|
+
* refuses an allow list naming one. Compared as the platform compares variable names: exactly, but case-blind on
|
|
59
|
+
* Windows. A variable a host hands a child itself (a shell's `shellEnv`, a per-exec `env`, a language server's `env`, an MCP server entry's `env`)
|
|
60
|
+
* is its explicit grant and is not read against the list.
|
|
61
|
+
*
|
|
62
|
+
* Declared once, at start-up, before anything spawns; a later call REPLACES the list (a configuration reload), read
|
|
63
|
+
* from the next spawn on — a spawn whose environment is already read (one announcing its findings) keeps it. The
|
|
64
|
+
* list is copied. A malformed list — not an array, an entry that is no variable name, or a name the host hands every
|
|
65
|
+
* child anyway ({@link runtimeSuppliedEnv}) — throws and leaves the declaration as it was.
|
|
66
|
+
*/
|
|
67
|
+
export declare function declareWithheldEnv(names: readonly string[]): void;
|
|
68
|
+
/** Test seam: never called by production code (a host re-declares `[]`). */
|
|
69
|
+
export declare function __resetWithheldEnv(): void;
|
|
70
|
+
/**
|
|
71
|
+
* THE environment a child spawned ON THIS HOST inherits: the policy's arm, less the declared own credentials
|
|
72
|
+
* ({@link declareWithheldEnv}). The arms: `"scrub"` (and absent) is this process's environment through
|
|
73
|
+
* {@link scrubSecretEnv}; `"all"` is this process's environment as written; a `string[]` is exactly the named
|
|
74
|
+
* variables that are set, as written — a strict allow list, so a name it leaves out (`PATH` included) is not
|
|
75
|
+
* inherited. `findings` collects every entry withheld: the scrub's, and each declared own credential the arm held.
|
|
76
|
+
*
|
|
77
|
+
* A policy {@link inheritEnvPolicyProblem} refuses throws that reason HERE, at every spawn, read against the
|
|
78
|
+
* declaration as it stands: a value that is no policy at all (never read as the scrub), and an allow list naming a
|
|
79
|
+
* declared own credential — whenever the declaration was made, so the refusal at spawn time is the invariant. The
|
|
80
|
+
* spawn sites that take a policy (the tool shell's `NodeExecutionEnv` at construction, the oracle gate per gate) also
|
|
81
|
+
* ask first, so a host that declared before building them hears it at start-up.
|
|
59
82
|
*/
|
|
60
83
|
export declare function inheritedHostEnv(policy: InheritEnvPolicy | undefined, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
|
|
61
|
-
/** Why `value` is not an {@link InheritEnvPolicy} (absent counts as one — the scrub), or
|
|
84
|
+
/** Why `value` is not an {@link InheritEnvPolicy} this deployment can read (absent counts as one — the scrub), or
|
|
85
|
+
* `undefined` when it is: a malformed policy, or an allow list naming one of the declared own credentials
|
|
86
|
+
* ({@link declareWithheldEnv}, compared as it says). */
|
|
62
87
|
export declare function inheritEnvPolicyProblem(value: unknown): string | undefined;
|
|
63
88
|
/** Test seam: never called by production code. */
|
|
64
89
|
export declare function __resetSecretEnvScrubNoticeAnnouncement(): void;
|
|
@@ -66,7 +91,8 @@ export declare function __resetSecretEnvScrubNoticeAnnouncement(): void;
|
|
|
66
91
|
* The ONE disclosure point for a scrub's withheld keys. Routed through {@link deliverEngineNotice} — the
|
|
67
92
|
* engine's one notice throat — so a wired `onNotice` seat REPLACES the console line exactly as every
|
|
68
93
|
* other `EngineNotice` does, and an absent seat prints the same words `console.warn` always printed here.
|
|
69
|
-
* `detail.names` carries every withheld key.
|
|
70
|
-
*
|
|
94
|
+
* `detail.names` carries every withheld key; `detail.ownCredentials`, present only when one was withheld, the
|
|
95
|
+
* subset withheld as the deployment's declared own credentials (kind `own-credential`) rather than by the scrub.
|
|
96
|
+
* Once per PROCESS, never per call or per site; an EMPTY findings array is a no-op.
|
|
71
97
|
*/
|
|
72
98
|
export declare function announceSecretEnvScrub(onNotice: ((notice: EngineNotice) => void) | undefined, findings: readonly SecretEnvFinding[]): void;
|
package/dist/core/secret-env.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CREDENTIAL_VOCABULARY, envNameView, splitCamelCase } from "./credential-vocabulary.js";
|
|
2
|
+
import { DEFAULT_INHERITED_ENV_VARS } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2
3
|
import { deliverEngineNotice } from "./engine-notice.js";
|
|
3
4
|
const GIT_CONFIG_ENV_RE = /^GIT_CONFIG_(?:COUNT|PARAMETERS|(?:KEY|VALUE)_[0-9]+)$/;
|
|
4
5
|
const PROXY_ENV_RE = /^(?:(?:https?|ftp|all|no)_proxy|npm_config_(?:https?_)?proxy|npm_config_noproxy|yarn_proxy|(?:yarn|global_agent|docker|claude_code)_(?:https?|no)_proxy|cloudsdk_proxy_[a-z]+|electron_get_use_proxy)$/i;
|
|
@@ -93,6 +94,7 @@ const SECRET_ENV_KIND_CONFIDENCE = {
|
|
|
93
94
|
"exact-name": "high",
|
|
94
95
|
"prefix-rule": "high",
|
|
95
96
|
"value-rule": "high",
|
|
97
|
+
"own-credential": "high",
|
|
96
98
|
"name-rule": "medium",
|
|
97
99
|
"namespace-rule": "medium",
|
|
98
100
|
};
|
|
@@ -131,29 +133,91 @@ export function scrubSecretEnv(env, findings) {
|
|
|
131
133
|
}
|
|
132
134
|
return Object.fromEntries(kept);
|
|
133
135
|
}
|
|
136
|
+
function envNameKey(name) {
|
|
137
|
+
return process.platform === "win32" ? name.toUpperCase() : name;
|
|
138
|
+
}
|
|
139
|
+
const UNPAIRED_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
140
|
+
function variableNameListProblem(label, value) {
|
|
141
|
+
const bad = value.findIndex((k) => typeof k !== "string" || k.length === 0 || /[=\0\s]/.test(k) || UNPAIRED_SURROGATE_RE.test(k));
|
|
142
|
+
if (bad === -1)
|
|
143
|
+
return undefined;
|
|
144
|
+
const k = value[bad];
|
|
145
|
+
const why = typeof k !== "string" ? typeof k : k === "" ? "an empty string" : `a name holding ${k.includes("=") ? "\"=\"" : k.includes("\0") ? "a NUL" : /\s/.test(k) ? "whitespace" : "an unpaired surrogate"}`;
|
|
146
|
+
return `${label}[${bad}] is not a variable name (got ${why})`;
|
|
147
|
+
}
|
|
148
|
+
const WINDOWS_REQUIRED_ENV = Object.freeze(["HOMEDRIVE", "HOMEPATH", "LOGONSERVER", "PATH", "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERDOMAIN", "USERNAME", "USERPROFILE", "WINDIR"]);
|
|
149
|
+
function runtimeSuppliedEnv() {
|
|
150
|
+
return ["NODE_V8_COVERAGE", ...DEFAULT_INHERITED_ENV_VARS, ...(process.platform === "win32" ? WINDOWS_REQUIRED_ENV : [])];
|
|
151
|
+
}
|
|
152
|
+
const WITHHELD_ENV = Symbol.for("sema.core.withheldEnv");
|
|
153
|
+
const NOTHING_WITHHELD = Object.freeze([]);
|
|
154
|
+
const withheldEnvOf = () => {
|
|
155
|
+
const held = Reflect.get(globalThis, WITHHELD_ENV);
|
|
156
|
+
return Array.isArray(held) ? held : NOTHING_WITHHELD;
|
|
157
|
+
};
|
|
158
|
+
const setWithheldEnv = (names) => {
|
|
159
|
+
Reflect.set(globalThis, WITHHELD_ENV, Object.freeze([...names]));
|
|
160
|
+
};
|
|
161
|
+
export function declareWithheldEnv(names) {
|
|
162
|
+
const value = names;
|
|
163
|
+
if (!Array.isArray(value))
|
|
164
|
+
throw new Error(`declareWithheldEnv: the list must be an array of variable names (got ${typeof value === "string" ? JSON.stringify(value) : value === null ? "null" : typeof value})`);
|
|
165
|
+
const problem = variableNameListProblem("names", value);
|
|
166
|
+
if (problem !== undefined)
|
|
167
|
+
throw new Error(`declareWithheldEnv: ${problem}`);
|
|
168
|
+
const supplied = new Set(runtimeSuppliedEnv().map(envNameKey));
|
|
169
|
+
const unwithholdable = value.findIndex((k) => supplied.has(envNameKey(k)));
|
|
170
|
+
if (unwithholdable !== -1)
|
|
171
|
+
throw new Error(`declareWithheldEnv: names[${unwithholdable}] is ${value[unwithholdable]}, which a child is handed from this process's environment whatever the spawn passes (by Node, the MCP stdio transport or Windows) — it cannot be withheld`);
|
|
172
|
+
setWithheldEnv(value);
|
|
173
|
+
}
|
|
174
|
+
export function __resetWithheldEnv() {
|
|
175
|
+
setWithheldEnv([]);
|
|
176
|
+
}
|
|
177
|
+
function withholdOwnCredentials(inherited, findings) {
|
|
178
|
+
if (withheldEnvOf().length === 0)
|
|
179
|
+
return inherited;
|
|
180
|
+
const own = new Set(withheldEnvOf().map(envNameKey));
|
|
181
|
+
const kept = [];
|
|
182
|
+
for (const key of Object.keys(inherited)) {
|
|
183
|
+
if (!own.has(envNameKey(key)))
|
|
184
|
+
kept.push([key, inherited[key]]);
|
|
185
|
+
else
|
|
186
|
+
findings?.push({ key, kind: "own-credential", confidence: SECRET_ENV_KIND_CONFIDENCE["own-credential"], source: "secret-env" });
|
|
187
|
+
}
|
|
188
|
+
return Object.fromEntries(kept);
|
|
189
|
+
}
|
|
190
|
+
function policyShapeProblem(value) {
|
|
191
|
+
if (value === undefined || value === "all" || value === "scrub")
|
|
192
|
+
return undefined;
|
|
193
|
+
if (Array.isArray(value))
|
|
194
|
+
return variableNameListProblem("inheritEnv", value);
|
|
195
|
+
return `inheritEnv must be "scrub", "all" or a list of variable names (got ${typeof value === "string" ? JSON.stringify(value) : value === null ? "null" : typeof value})`;
|
|
196
|
+
}
|
|
134
197
|
export function inheritedHostEnv(policy, findings) {
|
|
198
|
+
const problem = inheritEnvPolicyProblem(policy);
|
|
199
|
+
if (problem !== undefined)
|
|
200
|
+
throw new Error(problem);
|
|
201
|
+
let arm;
|
|
135
202
|
if (policy === "all")
|
|
136
|
-
|
|
137
|
-
if (Array.isArray(policy)) {
|
|
138
|
-
|
|
203
|
+
arm = { ...process.env };
|
|
204
|
+
else if (Array.isArray(policy)) {
|
|
205
|
+
arm = {};
|
|
139
206
|
for (const k of policy)
|
|
140
207
|
if (process.env[k] !== undefined)
|
|
141
|
-
|
|
142
|
-
return picked;
|
|
208
|
+
arm[k] = process.env[k];
|
|
143
209
|
}
|
|
144
|
-
|
|
210
|
+
else
|
|
211
|
+
arm = scrubSecretEnv(process.env, findings);
|
|
212
|
+
return withholdOwnCredentials(arm, findings);
|
|
145
213
|
}
|
|
146
214
|
export function inheritEnvPolicyProblem(value) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const k = value[bad];
|
|
154
|
-
return `inheritEnv[${bad}] is not a variable name (got ${typeof k !== "string" ? typeof k : k === "" ? "an empty string" : `a name holding ${k.includes("=") ? "\"=\"" : "a NUL"}`})`;
|
|
155
|
-
}
|
|
156
|
-
return `inheritEnv must be "scrub", "all" or a list of variable names (got ${typeof value === "string" ? JSON.stringify(value) : value === null ? "null" : typeof value})`;
|
|
215
|
+
const shape = policyShapeProblem(value);
|
|
216
|
+
if (shape !== undefined || !Array.isArray(value) || withheldEnvOf().length === 0)
|
|
217
|
+
return shape;
|
|
218
|
+
const own = new Set(withheldEnvOf().map(envNameKey));
|
|
219
|
+
const named = value.findIndex((k) => own.has(envNameKey(k)));
|
|
220
|
+
return named === -1 ? undefined : `inheritEnv[${named}] names ${value[named]}, which is this deployment's own credential and cannot be inherited by a child (a configuration error: no retry succeeds until the policy or the declaration changes)`;
|
|
157
221
|
}
|
|
158
222
|
let secretEnvScrubNoticeAnnounced = false;
|
|
159
223
|
export function __resetSecretEnvScrubNoticeAnnouncement() {
|
|
@@ -164,9 +228,11 @@ export function announceSecretEnvScrub(onNotice, findings) {
|
|
|
164
228
|
return;
|
|
165
229
|
secretEnvScrubNoticeAnnounced = true;
|
|
166
230
|
const names = findings.map((f) => f.key);
|
|
231
|
+
const ownCredentials = findings.filter((f) => f.kind === "own-credential").map((f) => f.key);
|
|
232
|
+
const own = ownCredentials.length > 0 ? `; ${ownCredentials.length} of them this deployment's own credential(s) (${ownCredentials.join(", ")})` : "";
|
|
167
233
|
deliverEngineNotice(onNotice, {
|
|
168
234
|
code: "config.secret_env_scrubbed",
|
|
169
|
-
message: `secret_env_scrubbed: ${findings.length} env key(s) withheld from a spawned child's environment (${names.join(", ")}) — once per process; wire onNotice for the structured form`,
|
|
170
|
-
detail: { names },
|
|
235
|
+
message: `secret_env_scrubbed: ${findings.length} env key(s) withheld from a spawned child's environment (${names.join(", ")})${own} — once per process; wire onNotice for the structured form`,
|
|
236
|
+
detail: { names, ...(ownCredentials.length > 0 ? { ownCredentials } : {}) },
|
|
171
237
|
});
|
|
172
238
|
}
|
|
@@ -60,7 +60,7 @@ export function readSessionRulesShape(value) {
|
|
|
60
60
|
if (STORE_STAMPS.has(key) || v === undefined)
|
|
61
61
|
continue;
|
|
62
62
|
if (!Object.hasOwn(SESSION_RULE_BUCKETS, key)) {
|
|
63
|
-
return fault(key, "not a rule bucket", `session rules hold ${JSON.stringify(key)}, which is not a rule bucket — the buckets are ${BUCKETS.join(", ")}, and a shell command rule is a toolDeny / toolAllow entry written Bash(<command>) (Bash(rm:*)
|
|
63
|
+
return fault(key, "not a rule bucket", `session rules hold ${JSON.stringify(key)}, which is not a rule bucket — the buckets are ${BUCKETS.join(", ")}, and a shell command rule is a toolDeny / toolAllow entry written Bash(<command>) (Bash(rm:*) matches a command whose first word is rm)`);
|
|
64
64
|
}
|
|
65
65
|
const field = key;
|
|
66
66
|
const listed = Array.isArray(v) ? Array.from(v) : undefined;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* 任意命令)。它只能来自部署作者/可信代码路径;LLM 生成的 contract 直接喂 runSpec = 不受信输入获得
|
|
8
8
|
* 命令执行权(越红线),禁止。contract 里的 `spec` **文本**才是 DATA-tier(runSpec 恒围栏置入 objective)。
|
|
9
9
|
* 命令可信 ≠ 它跑的代码可信:门跑的测试会加载模型改过的代码,故门子进程的环境缺省经 `scrubSecretEnv`
|
|
10
|
-
* (`OracleGateSpec.inheritEnv`,与 tool shell 同一三态),凭据名只在部署显式列名 / `"all"`
|
|
10
|
+
* (`OracleGateSpec.inheritEnv`,与 tool shell 同一三态),凭据名只在部署显式列名 / `"all"` 时交给它;
|
|
11
|
+
* 部署声明的自有凭据名(`declareWithheldEnv`)在任一形下都不交。
|
|
11
12
|
*
|
|
12
13
|
* 🔴 **oracle 三值(fable-M6)**:每门结果 ∈ {green, red, error} —— 判据不满足 = red;基础设施故障
|
|
13
14
|
* (ENOENT/timeout/maxBuffer/parse 解析不出)= **error 非 red**。假红防线:error 绝不冒充"曾经红过"
|
|
@@ -56,8 +57,9 @@ export interface OracleGateSpec {
|
|
|
56
57
|
* way. Absent / `"scrub"`: the environment without its credential-shaped variables (`scrubSecretEnv`); the command
|
|
57
58
|
* runs code the model wrote, so the credentials are not handed to it by default. `"all"`: the whole environment.
|
|
58
59
|
* A list: exactly the named variables that are set (a strict allow list — `PATH` too, when the command is looked
|
|
59
|
-
* up on it).
|
|
60
|
-
* gate `error`.
|
|
60
|
+
* up on it). Under every form, the deployment's declared own credentials (`declareWithheldEnv`) are withheld, and a
|
|
61
|
+
* list naming one makes the gate `error`. Names only, never values: the contract is serializable data. A value of
|
|
62
|
+
* any other shape makes the gate `error`.
|
|
61
63
|
*/
|
|
62
64
|
inheritEnv?: "all" | "scrub" | string[];
|
|
63
65
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { inheritedHostEnv } from "../../core/secret-env.js";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
const DEFAULT_GRACE_MS = 3000;
|
|
@@ -145,7 +145,7 @@ function enumerateDescendantsSync(rootPid) {
|
|
|
145
145
|
let out;
|
|
146
146
|
try {
|
|
147
147
|
const result = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "etime="], {
|
|
148
|
-
env:
|
|
148
|
+
env: inheritedHostEnv("scrub"),
|
|
149
149
|
timeout: PS_ENUMERATION_TIMEOUT_MS,
|
|
150
150
|
encoding: "utf8",
|
|
151
151
|
windowsHide: true,
|
|
@@ -211,7 +211,7 @@ function parseEtimeSeconds(etime) {
|
|
|
211
211
|
function runTaskkill(args) {
|
|
212
212
|
try {
|
|
213
213
|
const child = spawn("taskkill", args, {
|
|
214
|
-
env:
|
|
214
|
+
env: inheritedHostEnv("scrub"),
|
|
215
215
|
stdio: "ignore",
|
|
216
216
|
detached: true,
|
|
217
217
|
windowsHide: true,
|
|
@@ -271,7 +271,7 @@ function tryMsysGroupKill(bashPath, pid) {
|
|
|
271
271
|
`[ -n "$g" ] && [ "$g" != "0" ] && kill -9 -"$g" 2>/dev/null; ` +
|
|
272
272
|
`done; exit 0`;
|
|
273
273
|
try {
|
|
274
|
-
const child = spawn(bashPath, ["-c", script], { env:
|
|
274
|
+
const child = spawn(bashPath, ["-c", script], { env: inheritedHostEnv("scrub"), stdio: "ignore", detached: true, windowsHide: true });
|
|
275
275
|
child.on("error", () => { });
|
|
276
276
|
return true;
|
|
277
277
|
}
|
|
@@ -88,6 +88,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
88
88
|
/** `ExecutionEnv.cdPath` = the `CDPATH` the shell child receives, read LIVE off the same merge each exec gets (never the
|
|
89
89
|
* engine process's own environment): its entries, `[]` when the child gets none. Residual: a per-exec `env.CDPATH`. */
|
|
90
90
|
get cdPath(): readonly string[];
|
|
91
|
+
/** The environment the next shell child would receive, for a READ (the scrub seat is a no-op: a read reports nothing).
|
|
92
|
+
* `undefined` when no child would be spawned at all — the policy is refused against the own credentials declared
|
|
93
|
+
* since construction, and every spawn throws that reason — so a reader sees "the child gets none", never a throw. */
|
|
94
|
+
private childEnvForRead;
|
|
91
95
|
private shellPath?;
|
|
92
96
|
private shellEnv?;
|
|
93
97
|
private inheritEnv;
|
|
@@ -108,7 +112,9 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
108
112
|
* fail-closed (drop secret-pattern keys, keep PATH/build/locale). `"all"` = full inherit (trusted env);
|
|
109
113
|
* `string[]` = strict allowlist. `shellEnv`/per-exec `env` always pass through. Any other value — an unknown
|
|
110
114
|
* word, a non-string, a list entry that is not a variable name — throws here (`inheritEnvPolicyProblem`'s
|
|
111
|
-
* reason), the same judgment the oracle gate's `inheritEnv` meets.
|
|
115
|
+
* reason), the same judgment the oracle gate's `inheritEnv` meets. Whatever the policy, the deployment's declared
|
|
116
|
+
* own credentials (`declareWithheldEnv`) are withheld from the inherited part; a list naming one throws here when
|
|
117
|
+
* the declaration came first, and every spawn refuses it (`spawn_error` / `spawn_failed`) whenever it came. */
|
|
112
118
|
inheritEnv?: "all" | "scrub" | string[];
|
|
113
119
|
/** design/105: inject a resident scheduler daemon backend (the shell daemon). Absent ⇒ scheduling INERT. */
|
|
114
120
|
scheduler?: SchedulerCapability;
|
|
@@ -125,9 +131,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
125
131
|
* deployments (their pre-suspend dispose becomes a no-op too — do not combine).
|
|
126
132
|
*/
|
|
127
133
|
retainBackgroundProcesses?: boolean;
|
|
128
|
-
/** The D1 scrub decision's observation seat: called with every NON-EMPTY finding set
|
|
129
|
-
* "scrub" leg
|
|
130
|
-
*
|
|
134
|
+
/** The D1 scrub decision's observation seat: called with every NON-EMPTY finding set withheld from a
|
|
135
|
+
* shell child — the default "scrub" leg's, and on any leg the declared own credentials the inherited part
|
|
136
|
+
* held (kind `own-credential`). Absent ⇒ a once-per-process console summary (loud enough to notice,
|
|
137
|
+
* quiet enough not to spam). An allow list withholds nothing else, so it reports nothing. */
|
|
131
138
|
onSecretEnvScrub?: (findings: readonly SecretEnvFinding[]) => void;
|
|
132
139
|
/** #1081 — the structured-notice seat for the SAME withheld-keys disclosure (`EngineNotice`,
|
|
133
140
|
* `code: "config.secret_env_scrubbed"`, `detail.names`); independent of `onSecretEnvScrub` (a raw
|
|
@@ -7,7 +7,7 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
|
|
9
9
|
import { killProcessTree, shutdownDebug } from "./kill-tree.js";
|
|
10
|
-
import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv
|
|
10
|
+
import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv } from "../../core/secret-env.js";
|
|
11
11
|
import { physicalPathOfAsync } from "../../core/physical-path.js";
|
|
12
12
|
import { posixModeBitsMeaningful } from "../../core/posix-mode-face.js";
|
|
13
13
|
import { walkSymlinkChain } from "./symlink-chain.js";
|
|
@@ -157,7 +157,7 @@ async function runCommand(command, args, timeoutMs) {
|
|
|
157
157
|
child = spawn(command, args, {
|
|
158
158
|
stdio: ["ignore", "pipe", "ignore"],
|
|
159
159
|
windowsHide: true,
|
|
160
|
-
env:
|
|
160
|
+
env: inheritedHostEnv("scrub"),
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
catch {
|
|
@@ -315,12 +315,17 @@ export function openSpoolPair(base) {
|
|
|
315
315
|
export class NodeExecutionEnv {
|
|
316
316
|
cwd;
|
|
317
317
|
get homeDir() {
|
|
318
|
-
return
|
|
318
|
+
return this.childEnvForRead()?.HOME;
|
|
319
319
|
}
|
|
320
320
|
get cdPath() {
|
|
321
|
-
const value =
|
|
321
|
+
const value = this.childEnvForRead()?.CDPATH;
|
|
322
322
|
return value === undefined || value === "" ? [] : value.split(":");
|
|
323
323
|
}
|
|
324
|
+
childEnvForRead() {
|
|
325
|
+
if (inheritEnvPolicyProblem(this.inheritEnv) !== undefined)
|
|
326
|
+
return undefined;
|
|
327
|
+
return getShellEnv(this.inheritEnv, this.shellEnv, undefined, () => { });
|
|
328
|
+
}
|
|
324
329
|
shellPath;
|
|
325
330
|
shellEnv;
|
|
326
331
|
inheritEnv;
|
|
@@ -21,7 +21,8 @@ export interface NodeLspManagerOptions {
|
|
|
21
21
|
/** Injectable spawn (tests supply a fake server; default = {@link defaultLspSpawn} over `child_process.spawn`). */
|
|
22
22
|
spawn?: LspSpawn;
|
|
23
23
|
/** The language server's environment for the default spawn, verbatim. Omitted ⇒ the host environment scrubbed of
|
|
24
|
-
* credential-shaped names (`
|
|
24
|
+
* credential-shaped names and of the declared own credentials (`inheritedHostEnv("scrub")`, a tool child's
|
|
25
|
+
* reading; `declareWithheldEnv`). Unread when {@link spawn} is supplied. */
|
|
25
26
|
env?: Record<string, string>;
|
|
26
27
|
/** #1081 — the withheld-keys notice sink, SAME shared once-per-process latch/code as the shell-exec
|
|
27
28
|
* site's `NodeExecutionEnv.onNotice`. Absent ⇒ console only. Unread when {@link spawn} is supplied. */
|
|
@@ -9,7 +9,7 @@ import { TransportLspSession } from "../../core/lsp-session.js";
|
|
|
9
9
|
import { StdioLspTransport } from "./stdio-lsp-transport.js";
|
|
10
10
|
import { MAX_LSP_FILE_BYTES, SharedAbortScope, settleOnAbort } from "../../core/lsp.js";
|
|
11
11
|
import { asThrownError, thrownText } from "../../core/thrown-value.js";
|
|
12
|
-
import { announceSecretEnvScrub,
|
|
12
|
+
import { announceSecretEnvScrub, inheritedHostEnv } from "../../core/secret-env.js";
|
|
13
13
|
export const DEFAULT_LSP_SERVERS = {
|
|
14
14
|
typescript: "typescript-language-server --stdio",
|
|
15
15
|
javascript: "typescript-language-server --stdio",
|
|
@@ -336,7 +336,7 @@ const lspSpawnWithEnv = (env, onNotice) => (command, args, cwd, signal) => new P
|
|
|
336
336
|
let scrubbedEnv = env;
|
|
337
337
|
if (scrubbedEnv == null) {
|
|
338
338
|
const findings = [];
|
|
339
|
-
scrubbedEnv =
|
|
339
|
+
scrubbedEnv = inheritedHostEnv("scrub", findings);
|
|
340
340
|
announceSecretEnvScrub(onNotice, findings);
|
|
341
341
|
}
|
|
342
342
|
child = spawn(command, args, { ...lspSpawnOptions(cwd), env: scrubbedEnv });
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type { RunInternals, ResolvedWorkspace } from "./core/runner/prepare-task
|
|
|
11
11
|
export type { RunnerSelfSeat } from "./core/runner/contracts.js";
|
|
12
12
|
export type { BudgetAxis } from "./core/runner/assemble-result.js";
|
|
13
13
|
export { terminalProjection } from "./core/runner/terminal-projection.js";
|
|
14
|
+
export { toolOutputBlockChars } from "./core/runner/tool-output-projection.js";
|
|
14
15
|
export { SESSION_LOG_DIGEST_SCHEME, sessionEntryDigest, sessionLogDigest, sessionLogDigestsComparable, } from "./engine/session/log-digest.js";
|
|
15
16
|
export type { ResumeTaskConfig } from "./core/runner/runtask.js";
|
|
16
17
|
export { defineTool } from "./core/tools.js";
|
|
@@ -98,6 +99,8 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
|
|
|
98
99
|
export { walkSymlinkChain, SYMLINK_CHAIN_MAX_HOPS, type SymlinkHopReader } from "./engine/execution-env/symlink-chain.js";
|
|
99
100
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
100
101
|
export type { SecretEnvFinding, SecretEnvFindingKind } from "./core/secret-env.js";
|
|
102
|
+
export { declareWithheldEnv, inheritedHostEnv, inheritEnvPolicyProblem, type InheritEnvPolicy } from "./core/secret-env.js";
|
|
103
|
+
export { ENGINE_CAPABILITIES, type EngineCapability } from "./core/engine-capabilities.js";
|
|
101
104
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
102
105
|
export { physicalPathOf, physicalPathOfExisting } from "./core/physical-path.js";
|
|
103
106
|
export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode, WriteExpectation, WriteReceipt, SymlinkChain, BoundFileObject } from "./internal/harness.js";
|
|
@@ -338,6 +341,8 @@ export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedacte
|
|
|
338
341
|
export type { Egress, EgressTier } from "./core/untrusted-egress.js";
|
|
339
342
|
export type { RedactionFinding, RedactionReport, RedactionConfidence, RedactionSource, SummarizableFinding, } from "./core/untrusted-egress.js";
|
|
340
343
|
export { summarizeRedactions } from "./core/untrusted-egress.js";
|
|
344
|
+
export { headerOwnershipCollision } from "./core/protocol-http-auth.js";
|
|
345
|
+
export type { ProtocolHttpAuth } from "./core/protocol-http-auth.js";
|
|
341
346
|
export { MemoryRosterStore, FileRosterStore, type RosterStore, type RosterEntry, type RosterAccess, type RosterGcOptions } from "./agents/roster-store.js";
|
|
342
347
|
export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, type ConfigKnob, type ConfigOverrideDeclaration, type ConfigProvenance, type EffectiveConfigField, } from "./config/catalog.js";
|
|
343
348
|
export { normalizeAgentName } from "./core/task-registry.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Runner, runTask } from "./core/runner/runtask.js";
|
|
2
2
|
export { resolveTaskLimits } from "./core/runner/prepare-task.js";
|
|
3
3
|
export { terminalProjection } from "./core/runner/terminal-projection.js";
|
|
4
|
+
export { toolOutputBlockChars } from "./core/runner/tool-output-projection.js";
|
|
4
5
|
export { SESSION_LOG_DIGEST_SCHEME, sessionEntryDigest, sessionLogDigest, sessionLogDigestsComparable, } from "./engine/session/log-digest.js";
|
|
5
6
|
export { defineTool } from "./core/tools.js";
|
|
6
7
|
export { SKILL_TOOL_NAME } from "./core/runner/synthetic-tools.js";
|
|
@@ -82,6 +83,8 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
|
|
|
82
83
|
export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
|
|
83
84
|
export { walkSymlinkChain, SYMLINK_CHAIN_MAX_HOPS } from "./engine/execution-env/symlink-chain.js";
|
|
84
85
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
86
|
+
export { declareWithheldEnv, inheritedHostEnv, inheritEnvPolicyProblem } from "./core/secret-env.js";
|
|
87
|
+
export { ENGINE_CAPABILITIES } from "./core/engine-capabilities.js";
|
|
85
88
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
86
89
|
export { physicalPathOf, physicalPathOfExisting } from "./core/physical-path.js";
|
|
87
90
|
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, canonicalPathIsAuthoritative, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
|
|
@@ -276,6 +279,7 @@ export { A2A_TASK_STATES, toA2ATaskState, fromA2ATaskState, } from "./core/a2a-t
|
|
|
276
279
|
export { InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, WorkflowJournalOversizeError, assertJournalEntryFits, } from "./core/workflow-journal-store.js";
|
|
277
280
|
export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedactedSummary } from "./core/untrusted-egress.js";
|
|
278
281
|
export { summarizeRedactions } from "./core/untrusted-egress.js";
|
|
282
|
+
export { headerOwnershipCollision } from "./core/protocol-http-auth.js";
|
|
279
283
|
export { MemoryRosterStore, FileRosterStore } from "./agents/roster-store.js";
|
|
280
284
|
export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, } from "./config/catalog.js";
|
|
281
285
|
export { normalizeAgentName } from "./core/task-registry.js";
|
|
@@ -5,7 +5,7 @@ import { hrtime } from "node:process";
|
|
|
5
5
|
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
6
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
-
import {
|
|
8
|
+
import { inheritedHostEnv } from "../../core/secret-env.js";
|
|
9
9
|
import { physicalPathOf, physicalPathOfExisting } from "../../core/physical-path.js";
|
|
10
10
|
import { thrownText } from "../../core/thrown-value.js";
|
|
11
11
|
const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
|
|
@@ -713,7 +713,7 @@ function probeStartTimesOnce(pid) {
|
|
|
713
713
|
encoding: "utf8",
|
|
714
714
|
timeout: START_TIME_PROBE_TIMEOUT_MS,
|
|
715
715
|
stdio: ["ignore", "pipe", "ignore"],
|
|
716
|
-
env: { ...
|
|
716
|
+
env: { ...inheritedHostEnv("scrub"), LC_ALL: "C", LANG: "C" },
|
|
717
717
|
});
|
|
718
718
|
}
|
|
719
719
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a BREAKING change: name every removed entry in the CHANGELOG's BREAKING lines (what was removed / who is hurt / what to read instead) and update this fixture in the SAME commit. Regenerate via REGEN in test/export-surface.test.ts.",
|
|
3
3
|
"_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
|
|
4
|
-
"count":
|
|
4
|
+
"count": 2679,
|
|
5
5
|
"exports": {
|
|
6
6
|
"A2ATaskState": "type",
|
|
7
7
|
"A2ATaskStateReversal": "type",
|
|
@@ -453,6 +453,7 @@
|
|
|
453
453
|
"EMPTY_USAGE_WINDOW_RECORD": "variable",
|
|
454
454
|
"ENGINE_AUTHORITY_ENVELOPE_TAGS": "variable",
|
|
455
455
|
"ENGINE_BUILTIN_TOOL_NAMES": "variable",
|
|
456
|
+
"ENGINE_CAPABILITIES": "variable",
|
|
456
457
|
"ENGINE_NOTICE_CODES": "variable",
|
|
457
458
|
"ENTER_PLAN_MODE_TOOL_NAME": "variable",
|
|
458
459
|
"ENV_LIFETIME_SUSPEND_MARGIN_MS": "variable",
|
|
@@ -475,6 +476,7 @@
|
|
|
475
476
|
"EgressTier": "type",
|
|
476
477
|
"Embedder": "interface",
|
|
477
478
|
"EngineAttestation": "interface",
|
|
479
|
+
"EngineCapability": "type",
|
|
478
480
|
"EngineNotice": "interface",
|
|
479
481
|
"EngineNoticeCode": "type",
|
|
480
482
|
"EntryCustodyReport": "interface",
|
|
@@ -637,6 +639,7 @@
|
|
|
637
639
|
"InMemoryWorkflowJournalStore": "class",
|
|
638
640
|
"InMemoryWorkflowRunStore": "class",
|
|
639
641
|
"InboundEntryFinding": "interface",
|
|
642
|
+
"InheritEnvPolicy": "type",
|
|
640
643
|
"Invariant": "interface",
|
|
641
644
|
"InvariantKind": "type",
|
|
642
645
|
"InvariantViolation": "interface",
|
|
@@ -1147,6 +1150,7 @@
|
|
|
1147
1150
|
"PromptSourceStateStore": "interface",
|
|
1148
1151
|
"PromptTextDeclaration": "interface",
|
|
1149
1152
|
"PromptTrust": "type",
|
|
1153
|
+
"ProtocolHttpAuth": "interface",
|
|
1150
1154
|
"ProtocolId": "type",
|
|
1151
1155
|
"ProtocolNamespace": "interface",
|
|
1152
1156
|
"ProviderCondition": "type",
|
|
@@ -2049,6 +2053,7 @@
|
|
|
2049
2053
|
"debitLedger": "function",
|
|
2050
2054
|
"decideAutoPromote": "function",
|
|
2051
2055
|
"decisionText": "function",
|
|
2056
|
+
"declareWithheldEnv": "function",
|
|
2052
2057
|
"declaredOffSpelling": "function",
|
|
2053
2058
|
"declaredPathTargetOf": "function",
|
|
2054
2059
|
"decodeCcPeerFrame": "function",
|
|
@@ -2181,7 +2186,10 @@
|
|
|
2181
2186
|
"hasScheduler": "function",
|
|
2182
2187
|
"hasSessionFork": "function",
|
|
2183
2188
|
"hasToolContract": "function",
|
|
2189
|
+
"headerOwnershipCollision": "function",
|
|
2184
2190
|
"htmlToText": "function",
|
|
2191
|
+
"inheritEnvPolicyProblem": "function",
|
|
2192
|
+
"inheritedHostEnv": "function",
|
|
2185
2193
|
"injectionLaneOf": "function",
|
|
2186
2194
|
"inlineUntrusted": "function",
|
|
2187
2195
|
"inputKeysOf": "function",
|
|
@@ -2629,6 +2637,7 @@
|
|
|
2629
2637
|
"toolCallFaceOf": "function",
|
|
2630
2638
|
"toolFaceProblem": "function",
|
|
2631
2639
|
"toolIdentityOf": "function",
|
|
2640
|
+
"toolOutputBlockChars": "variable",
|
|
2632
2641
|
"toolPolicyNameSets": "function",
|
|
2633
2642
|
"toolPolicyPathRules": "function",
|
|
2634
2643
|
"toolResultContentSegment": "function",
|
|
@@ -3125,6 +3134,7 @@
|
|
|
3125
3134
|
"EMPTY_USAGE_WINDOW_RECORD": "advanced",
|
|
3126
3135
|
"ENGINE_AUTHORITY_ENVELOPE_TAGS": "advanced",
|
|
3127
3136
|
"ENGINE_BUILTIN_TOOL_NAMES": "advanced",
|
|
3137
|
+
"ENGINE_CAPABILITIES": "advanced",
|
|
3128
3138
|
"ENGINE_NOTICE_CODES": "advanced",
|
|
3129
3139
|
"ENTER_PLAN_MODE_TOOL_NAME": "advanced",
|
|
3130
3140
|
"ENV_LIFETIME_SUSPEND_MARGIN_MS": "internal",
|
|
@@ -3147,6 +3157,7 @@
|
|
|
3147
3157
|
"EgressTier": "advanced",
|
|
3148
3158
|
"Embedder": "advanced",
|
|
3149
3159
|
"EngineAttestation": "advanced",
|
|
3160
|
+
"EngineCapability": "advanced",
|
|
3150
3161
|
"EngineNotice": "advanced",
|
|
3151
3162
|
"EngineNoticeCode": "advanced",
|
|
3152
3163
|
"EntryCustodyReport": "advanced",
|
|
@@ -3309,6 +3320,7 @@
|
|
|
3309
3320
|
"InMemoryWorkflowJournalStore": "advanced",
|
|
3310
3321
|
"InMemoryWorkflowRunStore": "advanced",
|
|
3311
3322
|
"InboundEntryFinding": "advanced",
|
|
3323
|
+
"InheritEnvPolicy": "advanced",
|
|
3312
3324
|
"Invariant": "advanced",
|
|
3313
3325
|
"InvariantKind": "advanced",
|
|
3314
3326
|
"InvariantViolation": "advanced",
|
|
@@ -3819,6 +3831,7 @@
|
|
|
3819
3831
|
"PromptSourceStateStore": "advanced",
|
|
3820
3832
|
"PromptTextDeclaration": "advanced",
|
|
3821
3833
|
"PromptTrust": "advanced",
|
|
3834
|
+
"ProtocolHttpAuth": "advanced",
|
|
3822
3835
|
"ProtocolId": "advanced",
|
|
3823
3836
|
"ProtocolNamespace": "advanced",
|
|
3824
3837
|
"ProviderCondition": "advanced",
|
|
@@ -4721,6 +4734,7 @@
|
|
|
4721
4734
|
"debitLedger": "advanced",
|
|
4722
4735
|
"decideAutoPromote": "advanced",
|
|
4723
4736
|
"decisionText": "advanced",
|
|
4737
|
+
"declareWithheldEnv": "advanced",
|
|
4724
4738
|
"declaredOffSpelling": "advanced",
|
|
4725
4739
|
"declaredPathTargetOf": "advanced",
|
|
4726
4740
|
"decodeCcPeerFrame": "advanced",
|
|
@@ -4853,7 +4867,10 @@
|
|
|
4853
4867
|
"hasScheduler": "advanced",
|
|
4854
4868
|
"hasSessionFork": "advanced",
|
|
4855
4869
|
"hasToolContract": "advanced",
|
|
4870
|
+
"headerOwnershipCollision": "advanced",
|
|
4856
4871
|
"htmlToText": "advanced",
|
|
4872
|
+
"inheritEnvPolicyProblem": "advanced",
|
|
4873
|
+
"inheritedHostEnv": "advanced",
|
|
4857
4874
|
"injectionLaneOf": "advanced",
|
|
4858
4875
|
"inlineUntrusted": "advanced",
|
|
4859
4876
|
"inputKeysOf": "advanced",
|
|
@@ -5301,6 +5318,7 @@
|
|
|
5301
5318
|
"toolCallFaceOf": "advanced",
|
|
5302
5319
|
"toolFaceProblem": "internal",
|
|
5303
5320
|
"toolIdentityOf": "advanced",
|
|
5321
|
+
"toolOutputBlockChars": "internal",
|
|
5304
5322
|
"toolPolicyNameSets": "advanced",
|
|
5305
5323
|
"toolPolicyPathRules": "advanced",
|
|
5306
5324
|
"toolResultContentSegment": "advanced",
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.51.1",
|
|
4
|
+
"from": "d990eb2fd73215c09bf254545bdab6f1080c0c98",
|
|
5
|
+
"to": "v7.51.1",
|
|
6
|
+
"added": [
|
|
7
|
+
{
|
|
8
|
+
"name": "ENGINE_CAPABILITIES",
|
|
9
|
+
"kind": "variable",
|
|
10
|
+
"tier": "advanced",
|
|
11
|
+
"at": [
|
|
12
|
+
"src/core/engine-capabilities.ts:13",
|
|
13
|
+
"src/index.ts:516"
|
|
14
|
+
],
|
|
15
|
+
"criteriaCovered": false,
|
|
16
|
+
"breaking": false
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "EngineCapability",
|
|
20
|
+
"kind": "type",
|
|
21
|
+
"tier": "advanced",
|
|
22
|
+
"at": [
|
|
23
|
+
"src/core/engine-capabilities.ts:16",
|
|
24
|
+
"src/index.ts:516"
|
|
25
|
+
],
|
|
26
|
+
"criteriaCovered": false,
|
|
27
|
+
"breaking": false
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "InheritEnvPolicy",
|
|
31
|
+
"kind": "type",
|
|
32
|
+
"tier": "advanced",
|
|
33
|
+
"at": [
|
|
34
|
+
"src/core/secret-env.ts:220",
|
|
35
|
+
"src/index.ts:515"
|
|
36
|
+
],
|
|
37
|
+
"criteriaCovered": false,
|
|
38
|
+
"breaking": false
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "ProtocolHttpAuth",
|
|
42
|
+
"kind": "interface",
|
|
43
|
+
"tier": "advanced",
|
|
44
|
+
"at": [
|
|
45
|
+
"src/core/protocol-http-auth.ts:18",
|
|
46
|
+
"src/index.ts:2985"
|
|
47
|
+
],
|
|
48
|
+
"criteriaCovered": false,
|
|
49
|
+
"breaking": false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "declareWithheldEnv",
|
|
53
|
+
"kind": "function",
|
|
54
|
+
"tier": "advanced",
|
|
55
|
+
"at": [
|
|
56
|
+
"src/core/secret-env.ts:273",
|
|
57
|
+
"src/index.ts:515"
|
|
58
|
+
],
|
|
59
|
+
"criteriaCovered": false,
|
|
60
|
+
"breaking": false
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "headerOwnershipCollision",
|
|
64
|
+
"kind": "function",
|
|
65
|
+
"tier": "advanced",
|
|
66
|
+
"at": [
|
|
67
|
+
"src/core/protocol-http-auth.ts:24",
|
|
68
|
+
"src/index.ts:2984"
|
|
69
|
+
],
|
|
70
|
+
"criteriaCovered": false,
|
|
71
|
+
"breaking": false
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "inheritEnvPolicyProblem",
|
|
75
|
+
"kind": "function",
|
|
76
|
+
"tier": "advanced",
|
|
77
|
+
"at": [
|
|
78
|
+
"src/core/secret-env.ts:336",
|
|
79
|
+
"src/index.ts:515"
|
|
80
|
+
],
|
|
81
|
+
"criteriaCovered": false,
|
|
82
|
+
"breaking": false
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "inheritedHostEnv",
|
|
86
|
+
"kind": "function",
|
|
87
|
+
"tier": "advanced",
|
|
88
|
+
"at": [
|
|
89
|
+
"src/core/secret-env.ts:321",
|
|
90
|
+
"src/index.ts:515"
|
|
91
|
+
],
|
|
92
|
+
"criteriaCovered": false,
|
|
93
|
+
"breaking": false
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "toolOutputBlockChars",
|
|
97
|
+
"kind": "variable",
|
|
98
|
+
"tier": "internal",
|
|
99
|
+
"at": [
|
|
100
|
+
"src/core/runner/tool-output-projection.ts:28",
|
|
101
|
+
"src/index.ts:34"
|
|
102
|
+
],
|
|
103
|
+
"criteriaCovered": false,
|
|
104
|
+
"breaking": false
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"removed": [],
|
|
108
|
+
"changed": [],
|
|
109
|
+
"narrowed": [],
|
|
110
|
+
"closedSetMembers": [
|
|
111
|
+
{
|
|
112
|
+
"set": "ENGINE_CAPABILITIES",
|
|
113
|
+
"at": [
|
|
114
|
+
"src/core/engine-capabilities.ts:13"
|
|
115
|
+
],
|
|
116
|
+
"newSet": true,
|
|
117
|
+
"added": [
|
|
118
|
+
"inherit_env.own_credentials_withheld"
|
|
119
|
+
],
|
|
120
|
+
"removed": [],
|
|
121
|
+
"breaking": false,
|
|
122
|
+
"criteriaCovered": false
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"set": "EngineCapability",
|
|
126
|
+
"at": [
|
|
127
|
+
"src/core/engine-capabilities.ts:16",
|
|
128
|
+
"src/index.ts:516"
|
|
129
|
+
],
|
|
130
|
+
"newSet": true,
|
|
131
|
+
"added": [
|
|
132
|
+
"inherit_env.own_credentials_withheld"
|
|
133
|
+
],
|
|
134
|
+
"removed": [],
|
|
135
|
+
"breaking": false,
|
|
136
|
+
"criteriaCovered": false
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"set": "SecretEnvFindingKind",
|
|
140
|
+
"at": [
|
|
141
|
+
"src/core/secret-env.ts:149"
|
|
142
|
+
],
|
|
143
|
+
"newSet": false,
|
|
144
|
+
"added": [
|
|
145
|
+
"own-credential"
|
|
146
|
+
],
|
|
147
|
+
"removed": [],
|
|
148
|
+
"breaking": false,
|
|
149
|
+
"criteriaCovered": false
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"closedSetsUnreadable": [],
|
|
153
|
+
"typeKeys": [],
|
|
154
|
+
"typeKeysUnreadableArms": [],
|
|
155
|
+
"typeKeysFlattenedCount": 234,
|
|
156
|
+
"signatures": [],
|
|
157
|
+
"declarationMoves": [
|
|
158
|
+
{
|
|
159
|
+
"name": "InheritEnvPolicy",
|
|
160
|
+
"kind": "type",
|
|
161
|
+
"onSurface": true,
|
|
162
|
+
"from": [
|
|
163
|
+
"src/core/secret-env.ts"
|
|
164
|
+
],
|
|
165
|
+
"to": [
|
|
166
|
+
"src/core/secret-env.ts",
|
|
167
|
+
"src/index.ts"
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "ProtocolHttpAuth",
|
|
172
|
+
"kind": "interface",
|
|
173
|
+
"onSurface": true,
|
|
174
|
+
"from": [
|
|
175
|
+
"src/core/protocol-http-auth.ts"
|
|
176
|
+
],
|
|
177
|
+
"to": [
|
|
178
|
+
"src/core/protocol-http-auth.ts",
|
|
179
|
+
"src/index.ts"
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "headerOwnershipCollision",
|
|
184
|
+
"kind": "function",
|
|
185
|
+
"onSurface": true,
|
|
186
|
+
"from": [
|
|
187
|
+
"src/core/protocol-http-auth.ts"
|
|
188
|
+
],
|
|
189
|
+
"to": [
|
|
190
|
+
"src/core/protocol-http-auth.ts",
|
|
191
|
+
"src/index.ts"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "inheritEnvPolicyProblem",
|
|
196
|
+
"kind": "function",
|
|
197
|
+
"onSurface": true,
|
|
198
|
+
"from": [
|
|
199
|
+
"src/core/secret-env.ts"
|
|
200
|
+
],
|
|
201
|
+
"to": [
|
|
202
|
+
"src/core/secret-env.ts",
|
|
203
|
+
"src/index.ts"
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "inheritedHostEnv",
|
|
208
|
+
"kind": "function",
|
|
209
|
+
"onSurface": true,
|
|
210
|
+
"from": [
|
|
211
|
+
"src/core/secret-env.ts"
|
|
212
|
+
],
|
|
213
|
+
"to": [
|
|
214
|
+
"src/core/secret-env.ts",
|
|
215
|
+
"src/index.ts"
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"mintMoves": [],
|
|
220
|
+
"criteriaObserves": [
|
|
221
|
+
{
|
|
222
|
+
"kind": "engineNoticeCode",
|
|
223
|
+
"name": "config.secret_env_scrubbed",
|
|
224
|
+
"assertions": [
|
|
225
|
+
"G1.b"
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"kind": "taskEventField",
|
|
230
|
+
"name": "tool_end.totalChars",
|
|
231
|
+
"assertions": [
|
|
232
|
+
"G2.a"
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"kind": "typeMember",
|
|
237
|
+
"name": "SecretEnvFinding.kind",
|
|
238
|
+
"assertions": [
|
|
239
|
+
"G1.a",
|
|
240
|
+
"G1.c"
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
],
|
|
244
|
+
"consumersHint": {
|
|
245
|
+
"cli": [
|
|
246
|
+
"InheritEnvPolicy",
|
|
247
|
+
"SecretEnvFindingKind"
|
|
248
|
+
],
|
|
249
|
+
"server": [
|
|
250
|
+
"InheritEnvPolicy",
|
|
251
|
+
"SecretEnvFindingKind"
|
|
252
|
+
],
|
|
253
|
+
"sdk": [],
|
|
254
|
+
"note": "scanned cli@80e061c0 server@3c0e175c sdk@304339d — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
|
|
255
|
+
},
|
|
256
|
+
"breakingCount": 0,
|
|
257
|
+
"breakingItems": [],
|
|
258
|
+
"toResolvedAt": "cedf0606eb8b5b488d394a01b5f0a632019d1692"
|
|
259
|
+
}
|