@sema-agent/core 7.33.7 → 7.33.8
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 +18 -0
- package/dist/core/secret-env.d.ts +16 -2
- package/dist/core/secret-env.js +25 -1
- package/dist/core/spec-contract.d.ts +15 -0
- package/dist/core/spec-contract.js +19 -10
- package/dist/engine/execution-env/kill-tree.js +4 -1
- package/dist/engine/execution-env/node-execution-env.js +23 -34
- package/dist/orchestration/run-spec.d.ts +4 -0
- package/dist/orchestration/run-spec.js +4 -3
- package/dist/stores/file/fs-atomic.js +2 -1
- package/package.json +3 -1
- package/wire-changes/core@7.33.8.json +165 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 7.33.8 — 2026-09-28
|
|
2
|
+
|
|
3
|
+
### Fix — every child process the engine starts gets its environment through the credential scrub; a `runSpec` oracle gate's command no longer inherits the engine's environment (#1181; security; behavior narrowing; +0 runtime exports; new optional type members `OracleGateSpec.inheritEnv`, `RunSpecOptions.onNotice`, `runOracle` options `onNotice`) (@server)
|
|
4
|
+
|
|
5
|
+
- **Before.** A `runSpec` / `runOracle` gate ran its command through `execFile` with no `env`, so the child inherited the engine's whole environment — every credential in it. The command is the deployment's, but the tests it runs load code the model wrote: that code could read the credentials, write them to a file and read them back on the next turn (measured on 7.33.7: a gate printing two planted credential-shaped variables printed both values). The engine's own helpers — the tree kill's `ps` / `taskkill` / Git Bash group kill, shell discovery (`which bash` / `where bash.exe`) and the store lock's holder probe (`ps -p`) — also inherited the whole environment; each is looked up on PATH, so a PATH entry the model can write to (a workspace `node_modules/.bin`, which `npm run` prepends) made a planted `ps` read the credentials on every timed-out tool shell's tree kill (reproduced).
|
|
6
|
+
- **Now.** One rule, machine-kept: a child's environment comes through the scrub. The oracle gate reads the tool shell's three states, per gate: `OracleGateSpec.inheritEnv` absent or `"scrub"` — the engine's environment without its credential-shaped variables (`scrubSecretEnv`); `"all"` — the whole environment; a list — exactly the named variables that are set (a strict allow list: name `PATH` too when the command is looked up on it). Names only, never values — the contract is serializable data. A malformed `inheritEnv` (another word, a non-string, an empty name, a name holding `=` or a NUL) makes that gate `error` and starts nothing; a command the OS refuses outright (a NUL in an argument) is an `error` gate instead of a rejected `runOracle`. The withheld names reach the shared once-per-process `config.secret_env_scrubbed` notice through the new `onNotice` on `runOracle`'s options and on `RunSpecOptions` (absent ⇒ the console line). The helpers pass the scrubbed environment and announce nothing. The tool shell reads the same three-state helper (`inheritedHostEnv`, src/core/secret-env.ts), behaviour unchanged; the language server keeps its own `env` / scrub; a stdio MCP server already received only the SDK's allow list (`HOME LOGNAME PATH SHELL TERM USER`; the Windows list) plus its declared `env`, now pinned by a real child. `gate:spawn-sites` (static tier) keeps the table: every call of a process-starting import under `src/` is a row — `scrub` (it passes an `env` and its file reaches the scrub) or `exempt` with the reason (only the MCP allow list and example code remain) — and each runtime dependency's process starts are reviewed at its pinned version; a new site, an unreadable shape (`require`, `import()`, a re-export, the binding used as a value) or an unreviewed dependency version is red. Residual (KL-1181): at `import("sharp")` on Linux, sharp's libc probe (`detect-libc`) may run `getconf …; ldd --version` through `execSync` with the engine's environment when the interpreter, `/usr/bin/ldd` and `process.report` all fail to answer — a dependency's own spawn, which core cannot hand an environment.
|
|
7
|
+
- **Who needs it.** Deployments that run `runSpec` / `runOracle` with credentials in the engine's environment; any deployment whose PATH holds a directory the model can write to.
|
|
8
|
+
- **Who is hurt / compensation.** A deployment whose oracle tests need a credential-shaped variable (`DATABASE_URL`, `*_TOKEN`, `*_API_KEY`, …): the test now runs without it and the gate may go red or `error`. Put it back per gate — `inheritEnv: ["PATH", "HOME", "DATABASE_URL", …]` (strict: list everything the command needs) or `inheritEnv: "all"` (the previous behaviour). A `runSpec` whose preflight is the first scrub in the process takes the once-per-process notice: wire `RunSpecOptions.onNotice` to the same sink as the exec env's `onNotice`, or the line goes to the console. A PATH-looked-up helper no longer sees credential-shaped variables (none of them needs one).
|
|
9
|
+
- Pins: `test/backlog1181-spawn-site-env.test.ts` — the oracle gate through real children (default / `"scrub"` / `"all"` / allow list, nine malformed shapes starting nothing, a NUL argument, the notice, the public `runSpec` preflight + stop gate + final run), the shared helper and its validator, a planted `ps` on PATH through a timed-out `NodeExecutionEnv` shell and through `processFingerprint`, the stdio MCP control row; `test/backlog1181-shell-discovery-env.test.ts` — `/bin/bash` read as absent, the public `getShellConfig()` asks a planted `which` that reads no credential; `gate:spawn-sites` green on the shipped tree and red on each mutation (the 7.33.7 oracle site, a lost scrub call, a new site in a registered file / a namespace import / a stdio transport, a moved count, `require` / `import()` / re-export / value / destructure / shorthand / local export / default export, the table's own stale / reasonless / unknown-word / duplicate rows, a new / bumped / removed dependency).
|
|
10
|
+
|
|
11
|
+
### Fix — a credential entry of `GIT_CONFIG_PARAMETERS` is withheld wherever it sits in the list (#1183; security; 7.33.6 regression; behavior narrowing; +0 exports, +0 wire keys, `SecretEnvFindingKind` unchanged) (@server)
|
|
12
|
+
|
|
13
|
+
- **Before.** The variable's entries were judged with `entries.some(gitConfigEntryCarriesCredential)`: `some` passes each entry's index as the second argument, and that function's second parameter is its recursion bound (`depth`, command lines read only below 3, since 7.33.6). From the fourth entry on, a credential-carrying command line (`credential.helper=!sh -c '…password…'`, a `core.sshCommand` with a password) was not read and the variable reached the tool shell and the language server — git reads every entry.
|
|
14
|
+
- **Now.** Each entry is judged from depth 0, whatever its position (checked through the real git binary for positions 1–6, before and after the scrub). A clean list of the same length is handed on as before. `gate:point-free-callbacks` (static tier): a function handed by name to `some` / `every` / `map` / `filter` / `find` / `findIndex` / `findLast` / `findLastIndex` / `forEach` / `flatMap` / `Array.from` (dot or element-access spelling) may declare one parameter (`reduce` / `reduceRight`: two); more — or a rest parameter — is red unless an allow row says why the extra argument is wanted (the 155 point-free sites in `src/` today: one was this defect, none allowed).
|
|
15
|
+
- **Who needs it.** Hosts that hand git configuration through `GIT_CONFIG_PARAMETERS` (`git -c` in a parent process, CI wrappers).
|
|
16
|
+
- **Who is hurt / compensation.** None beyond the variable's existing reading: a list whose fourth or later entry carries a credential is now withheld whole, as the first three always were.
|
|
17
|
+
- Pins: `test/backlog1183-point-free-callbacks.test.ts` — the reported helper at positions 1–6 through real git; a clean six-entry list kept; the gate green on the shipped tree, red on the unwrapped call in the real `secret-env.ts`, on a defaulted / plain second parameter, a rest parameter, `parseInt`, a method reference, a three-parameter reducer, `Array.from`'s mapFn and the element-access spellings `x?.['some'](f)` / `Array['from'](xs, f)`; quiet on the wrapped form, `Boolean` / `String`, a two-parameter reducer, an inline `(e, i)` arrow, a non-callback `find(id)`; the allow list's silence, stale row and reasonless row.
|
|
18
|
+
|
|
1
19
|
## 7.33.7 — 2026-09-28
|
|
2
20
|
|
|
3
21
|
### Fix — the OpenTelemetry OTLP exporter's header lists are withheld from the tool shell and language-server children by name (#1180; security; behavior narrowing; +0 exports, +0 wire keys, `SecretEnvFindingKind` unchanged) (@server)
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* 2. The public export points at a STABLE first-party path that SURVIVES the planned de-vendoring refactor —
|
|
14
14
|
* when NodeExecutionEnv is naturalized out of `src/vendor/`, this module does not move and nothing rebinds.
|
|
15
15
|
*
|
|
16
|
-
* The `inheritEnv` THREE-STATE
|
|
17
|
-
*
|
|
16
|
+
* The `inheritEnv` THREE-STATE policy (`"all"` opt-out / `"scrub"` default / `string[]` allowlist) is read ONCE, by
|
|
17
|
+
* {@link inheritedHostEnv}, for every engine spawn site that takes it (the tool shell, a `runSpec` oracle gate). A
|
|
18
|
+
* downstream adapter composes its own child env; only the secret PATTERN is shared with it.
|
|
18
19
|
*/
|
|
19
20
|
import type { RedactionConfidence, RedactionSource } from "./arg-summary.js";
|
|
20
21
|
import { type EngineNotice } from "./engine-notice.js";
|
|
@@ -62,6 +63,19 @@ export declare function isSecretEnvKey(key: string, value?: string, env?: NodeJS
|
|
|
62
63
|
* to this process's own platform, the answer for a child spawned on this host; a host whose child runs elsewhere (a
|
|
63
64
|
* remote or sandboxed execution environment) passes that environment's platform. */
|
|
64
65
|
export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: SecretEnvFinding[], platform?: NodeJS.Platform): NodeJS.ProcessEnv;
|
|
66
|
+
/** How much of this process's environment a child spawned on this host inherits — the `inheritEnv` value. */
|
|
67
|
+
export type InheritEnvPolicy = "all" | "scrub" | string[];
|
|
68
|
+
/**
|
|
69
|
+
* The environment a child spawned ON THIS HOST inherits, by an {@link InheritEnvPolicy}: `"scrub"` (and absent) is
|
|
70
|
+
* this process's environment through {@link scrubSecretEnv} for this platform; `"all"` is this process's environment
|
|
71
|
+
* verbatim (the explicit opt-out); a `string[]` is exactly the named variables that are set, verbatim — a strict allow
|
|
72
|
+
* list, so a name it leaves out (`PATH` included) is not inherited. `findings` collects what the scrub withheld, and
|
|
73
|
+
* only the `"scrub"` arm adds to it. Any other value reads as `"scrub"`: a spawn site that must refuse a malformed
|
|
74
|
+
* policy asks {@link inheritEnvPolicyProblem} first.
|
|
75
|
+
*/
|
|
76
|
+
export declare function inheritedHostEnv(policy: InheritEnvPolicy | undefined, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
|
|
77
|
+
/** Why `value` is not an {@link InheritEnvPolicy} (absent counts as one — the scrub), or `undefined` when it is. */
|
|
78
|
+
export declare function inheritEnvPolicyProblem(value: unknown): string | undefined;
|
|
65
79
|
/** Test seam: never called by production code. */
|
|
66
80
|
export declare function __resetSecretEnvScrubNoticeAnnouncement(): void;
|
|
67
81
|
/**
|
package/dist/core/secret-env.js
CHANGED
|
@@ -469,7 +469,7 @@ export function readGitConfigParameters(text) {
|
|
|
469
469
|
}
|
|
470
470
|
function gitConfigVariableCarriesCredential(upperName, value) {
|
|
471
471
|
if (upperName === "GIT_CONFIG_PARAMETERS")
|
|
472
|
-
return readGitConfigParameters(value)?.some(gitConfigEntryCarriesCredential) ?? true;
|
|
472
|
+
return readGitConfigParameters(value)?.some((entry) => gitConfigEntryCarriesCredential(entry)) ?? true;
|
|
473
473
|
const key = PROXY_ENV_NAMES.has(upperName) ? "http.proxy" : GIT_COMMAND_ENV_KEYS.get(upperName);
|
|
474
474
|
return key !== undefined && gitConfigEntryCarriesCredential({ key, value });
|
|
475
475
|
}
|
|
@@ -608,6 +608,30 @@ export function scrubSecretEnv(env, findings, platform = process.platform) {
|
|
|
608
608
|
}
|
|
609
609
|
return out;
|
|
610
610
|
}
|
|
611
|
+
export function inheritedHostEnv(policy, findings) {
|
|
612
|
+
if (policy === "all")
|
|
613
|
+
return { ...process.env };
|
|
614
|
+
if (Array.isArray(policy)) {
|
|
615
|
+
const picked = {};
|
|
616
|
+
for (const k of policy)
|
|
617
|
+
if (process.env[k] !== undefined)
|
|
618
|
+
picked[k] = process.env[k];
|
|
619
|
+
return picked;
|
|
620
|
+
}
|
|
621
|
+
return scrubSecretEnv(process.env, findings, process.platform);
|
|
622
|
+
}
|
|
623
|
+
export function inheritEnvPolicyProblem(value) {
|
|
624
|
+
if (value === undefined || value === "all" || value === "scrub")
|
|
625
|
+
return undefined;
|
|
626
|
+
if (Array.isArray(value)) {
|
|
627
|
+
const bad = value.findIndex((k) => typeof k !== "string" || k.length === 0 || /[=\0]/.test(k));
|
|
628
|
+
if (bad === -1)
|
|
629
|
+
return undefined;
|
|
630
|
+
const k = value[bad];
|
|
631
|
+
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"}`})`;
|
|
632
|
+
}
|
|
633
|
+
return `inheritEnv must be "scrub", "all" or a list of variable names (got ${typeof value === "string" ? JSON.stringify(value) : value === null ? "null" : typeof value})`;
|
|
634
|
+
}
|
|
611
635
|
let secretEnvScrubNoticeAnnounced = false;
|
|
612
636
|
export function __resetSecretEnvScrubNoticeAnnouncement() {
|
|
613
637
|
secretEnvScrubNoticeAnnounced = false;
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
* 🔴 **信任声明(fable-M3)**:SpecContract 是 **CODE-tier** 输入 —— `oracle[].cmd` 会被亲跑(execFile,
|
|
7
7
|
* 任意命令)。它只能来自部署作者/可信代码路径;LLM 生成的 contract 直接喂 runSpec = 不受信输入获得
|
|
8
8
|
* 命令执行权(越红线),禁止。contract 里的 `spec` **文本**才是 DATA-tier(runSpec 恒围栏置入 objective)。
|
|
9
|
+
* 命令可信 ≠ 它跑的代码可信:门跑的测试会加载模型改过的代码,故门子进程的环境缺省经 `scrubSecretEnv`
|
|
10
|
+
* (`OracleGateSpec.inheritEnv`,与 tool shell 同一三态),凭据名只在部署显式列名 / `"all"` 时交给它。
|
|
9
11
|
*
|
|
10
12
|
* 🔴 **oracle 三值(fable-M6)**:每门结果 ∈ {green, red, error} —— 判据不满足 = red;基础设施故障
|
|
11
13
|
* (ENOENT/timeout/maxBuffer/parse 解析不出)= **error 非 red**。假红防线:error 绝不冒充"曾经红过"
|
|
12
14
|
* (design/73 红线② 的机器化 —— `oracleHadRedRun` 只由真 red 观察置位)。
|
|
13
15
|
*/
|
|
16
|
+
import type { EngineNotice } from "./engine-notice.js";
|
|
14
17
|
/** Declarative green criteria for one oracle gate. Absent ⇒ `exitCode === 0`. All declared criteria AND. */
|
|
15
18
|
export interface OracleGreenSpec {
|
|
16
19
|
/** Expected exit code. Default 0 when no `parse` is declared; with `parse`, exit code is only
|
|
@@ -48,6 +51,15 @@ export interface OracleGateSpec {
|
|
|
48
51
|
/** Per-gate timeout. Default 60s. */
|
|
49
52
|
timeoutMs?: number;
|
|
50
53
|
green?: OracleGreenSpec;
|
|
54
|
+
/**
|
|
55
|
+
* How much of the engine's environment the gate's command inherits — the tool shell's `inheritEnv`, read the same
|
|
56
|
+
* way. Absent / `"scrub"`: the environment without its credential-shaped variables (`scrubSecretEnv`); the command
|
|
57
|
+
* runs code the model wrote, so the credentials are not handed to it by default. `"all"`: the whole environment.
|
|
58
|
+
* A list: exactly the named variables that are set (a strict allow list — `PATH` too, when the command is looked
|
|
59
|
+
* up on it). Names only, never values: the contract is serializable data. A value of any other shape makes the
|
|
60
|
+
* gate `error`.
|
|
61
|
+
*/
|
|
62
|
+
inheritEnv?: "all" | "scrub" | string[];
|
|
51
63
|
}
|
|
52
64
|
/** design/125 D1 — the machine-readable task contract (CODE-tier input; see module doc). */
|
|
53
65
|
export interface SpecContract {
|
|
@@ -113,6 +125,9 @@ export interface OracleRunReport {
|
|
|
113
125
|
export declare function runOracle(gates: OracleGateSpec[], cwdRoot: string, opts?: {
|
|
114
126
|
baseline?: OracleBaseline;
|
|
115
127
|
signal?: AbortSignal;
|
|
128
|
+
/** Receives the once-per-process `config.secret_env_scrubbed` notice when a gate's scrub withholds names (the
|
|
129
|
+
* tool shell's seat and latch); absent ⇒ the console line. */
|
|
130
|
+
onNotice?: (notice: EngineNotice) => void;
|
|
116
131
|
}): Promise<OracleRunReport>;
|
|
117
132
|
/** Extract the per-gate baseline counts from a (preflight) report — only gates whose parse succeeded. */
|
|
118
133
|
export declare function captureBaseline(report: OracleRunReport): OracleBaseline;
|
|
@@ -2,20 +2,15 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { lstatSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
5
|
+
import { announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv } from "./secret-env.js";
|
|
5
6
|
import { physicalPathOf } from "./physical-path.js";
|
|
6
7
|
const DEFAULT_GATE_TIMEOUT_MS = 60_000;
|
|
7
8
|
const GATE_MAX_BUFFER = 16 * 1024 * 1024;
|
|
8
9
|
const OUTPUT_TAIL_CHARS = 1500;
|
|
9
10
|
const REGEX_SCAN_CAP = 2 * 1024 * 1024;
|
|
10
|
-
function execGate(gate, cwd, signal) {
|
|
11
|
+
function execGate(gate, cwd, env, signal) {
|
|
11
12
|
return new Promise((resolvePromise) => {
|
|
12
|
-
|
|
13
|
-
cwd,
|
|
14
|
-
timeout: gate.timeoutMs ?? DEFAULT_GATE_TIMEOUT_MS,
|
|
15
|
-
maxBuffer: GATE_MAX_BUFFER,
|
|
16
|
-
encoding: "utf8",
|
|
17
|
-
signal,
|
|
18
|
-
}, (err, stdout, stderr) => {
|
|
13
|
+
const settle = (err, stdout, stderr) => {
|
|
19
14
|
const output = (stdout ?? "") + (stderr ?? "");
|
|
20
15
|
if (!err)
|
|
21
16
|
return resolvePromise({ exitCode: 0, output });
|
|
@@ -26,7 +21,13 @@ function execGate(gate, cwd, signal) {
|
|
|
26
21
|
return resolvePromise({ exitCode: null, output, infraError: `timeout/killed (${e.signal ?? "timeout"})` });
|
|
27
22
|
}
|
|
28
23
|
return resolvePromise({ exitCode: null, output, infraError: String(e.code ?? e.message ?? "spawn error") });
|
|
29
|
-
}
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
execFile(gate.cmd[0], gate.cmd.slice(1), { cwd, env, timeout: gate.timeoutMs ?? DEFAULT_GATE_TIMEOUT_MS, maxBuffer: GATE_MAX_BUFFER, encoding: "utf8", signal }, settle);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
settle(e instanceof Error ? e : new Error(String(e)));
|
|
30
|
+
}
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
function safeTest(re, output) {
|
|
@@ -132,6 +133,11 @@ export async function runOracle(gates, cwdRoot, opts) {
|
|
|
132
133
|
results.push(err(gate, "gate cmd is empty — no executable to run"));
|
|
133
134
|
continue;
|
|
134
135
|
}
|
|
136
|
+
const envProblem = inheritEnvPolicyProblem(gate.inheritEnv);
|
|
137
|
+
if (envProblem !== undefined) {
|
|
138
|
+
results.push(err(gate, `gate ${envProblem}`));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
135
141
|
let cwd = root;
|
|
136
142
|
if (gate.cwd !== undefined) {
|
|
137
143
|
const candidate = resolve(root, gate.cwd);
|
|
@@ -151,7 +157,10 @@ export async function runOracle(gates, cwdRoot, opts) {
|
|
|
151
157
|
}
|
|
152
158
|
cwd = candidate;
|
|
153
159
|
}
|
|
154
|
-
const
|
|
160
|
+
const withheld = [];
|
|
161
|
+
const env = inheritedHostEnv(gate.inheritEnv, withheld);
|
|
162
|
+
announceSecretEnvScrub(opts?.onNotice, withheld);
|
|
163
|
+
const exec = await execGate(gate, cwd, env, opts?.signal);
|
|
155
164
|
results.push(evaluateGate(gate, exec, opts?.baseline));
|
|
156
165
|
}
|
|
157
166
|
const verdict = results.some((r) => r.verdict === "error")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { scrubSecretEnv } from "../../core/secret-env.js";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
const DEFAULT_GRACE_MS = 3000;
|
|
@@ -144,6 +145,7 @@ function enumerateDescendantsSync(rootPid) {
|
|
|
144
145
|
let out;
|
|
145
146
|
try {
|
|
146
147
|
const result = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "etime="], {
|
|
148
|
+
env: scrubSecretEnv(process.env),
|
|
147
149
|
timeout: PS_ENUMERATION_TIMEOUT_MS,
|
|
148
150
|
encoding: "utf8",
|
|
149
151
|
windowsHide: true,
|
|
@@ -209,6 +211,7 @@ function parseEtimeSeconds(etime) {
|
|
|
209
211
|
function runTaskkill(args) {
|
|
210
212
|
try {
|
|
211
213
|
const child = spawn("taskkill", args, {
|
|
214
|
+
env: scrubSecretEnv(process.env),
|
|
212
215
|
stdio: "ignore",
|
|
213
216
|
detached: true,
|
|
214
217
|
windowsHide: true,
|
|
@@ -268,7 +271,7 @@ function tryMsysGroupKill(bashPath, pid) {
|
|
|
268
271
|
`[ -n "$g" ] && [ "$g" != "0" ] && kill -9 -"$g" 2>/dev/null; ` +
|
|
269
272
|
`done; exit 0`;
|
|
270
273
|
try {
|
|
271
|
-
const child = spawn(bashPath, ["-c", script], { stdio: "ignore", detached: true, windowsHide: true });
|
|
274
|
+
const child = spawn(bashPath, ["-c", script], { env: scrubSecretEnv(process.env), stdio: "ignore", detached: true, windowsHide: true });
|
|
272
275
|
child.on("error", () => { });
|
|
273
276
|
return true;
|
|
274
277
|
}
|
|
@@ -7,7 +7,7 @@ import { dirname, isAbsolute, join, resolve, sep } 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, scrubSecretEnv } from "../../core/secret-env.js";
|
|
10
|
+
import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritedHostEnv, scrubSecretEnv } from "../../core/secret-env.js";
|
|
11
11
|
import { physicalPathOfAsync } from "../../core/physical-path.js";
|
|
12
12
|
import { isUncSpelling } from "../../tools/fs/safety.js";
|
|
13
13
|
import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
|
|
@@ -119,6 +119,7 @@ async function runCommand(command, args, timeoutMs) {
|
|
|
119
119
|
child = spawn(command, args, {
|
|
120
120
|
stdio: ["ignore", "pipe", "ignore"],
|
|
121
121
|
windowsHide: true,
|
|
122
|
+
env: scrubSecretEnv(process.env),
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
125
|
catch {
|
|
@@ -203,44 +204,32 @@ export function __resetSecretScrubAnnouncement() {
|
|
|
203
204
|
__resetSecretEnvScrubNoticeAnnouncement();
|
|
204
205
|
}
|
|
205
206
|
function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub, onNotice) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else {
|
|
217
|
-
const scrubbed = [];
|
|
218
|
-
inherited = scrubSecretEnv(process.env, scrubbed, process.platform);
|
|
219
|
-
const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
|
|
220
|
-
if (findings.length > 0) {
|
|
221
|
-
if (typeof onScrub === "function") {
|
|
222
|
-
try {
|
|
223
|
-
const r = onScrub(findings);
|
|
224
|
-
if (typeof r?.then === "function") {
|
|
225
|
-
r.then(undefined, () => {
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
207
|
+
const scrubbed = [];
|
|
208
|
+
const inherited = inheritedHostEnv(inheritEnv, scrubbed);
|
|
209
|
+
const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
|
|
210
|
+
if (findings.length > 0) {
|
|
211
|
+
if (typeof onScrub === "function") {
|
|
212
|
+
try {
|
|
213
|
+
const r = onScrub(findings);
|
|
214
|
+
if (typeof r?.then === "function") {
|
|
215
|
+
r.then(undefined, () => {
|
|
216
|
+
});
|
|
230
217
|
}
|
|
231
218
|
}
|
|
232
|
-
|
|
233
|
-
secretScrubSeatDefectAnnounced = true;
|
|
234
|
-
try {
|
|
235
|
-
console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
}
|
|
219
|
+
catch {
|
|
239
220
|
}
|
|
240
|
-
|
|
241
|
-
|
|
221
|
+
}
|
|
222
|
+
else if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
|
|
223
|
+
secretScrubSeatDefectAnnounced = true;
|
|
224
|
+
try {
|
|
225
|
+
console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
242
228
|
}
|
|
243
229
|
}
|
|
230
|
+
if (!(typeof onScrub === "function" && onNotice === undefined)) {
|
|
231
|
+
announceSecretEnvScrub(onNotice, findings);
|
|
232
|
+
}
|
|
244
233
|
}
|
|
245
234
|
return {
|
|
246
235
|
...inherited,
|
|
@@ -29,6 +29,7 @@ import type { RunnerSelfSeat } from "../core/runner/contracts.js";
|
|
|
29
29
|
import type { TaskResult, TaskSpec } from "../core/types.js";
|
|
30
30
|
import { type ToolPolicy } from "../core/tool-policy.js";
|
|
31
31
|
import type { TaskOutcome } from "../core/task-outcome.js";
|
|
32
|
+
import type { EngineNotice } from "../core/engine-notice.js";
|
|
32
33
|
import { type OracleBaseline, type OracleGateResult, type OracleVerdict, type SpecContract } from "../core/spec-contract.js";
|
|
33
34
|
export interface RunSpecOptions {
|
|
34
35
|
/** Workcopy root — the containment root for frozenPaths, oracle cwd, and `spec.path` resolution. */
|
|
@@ -36,6 +37,9 @@ export interface RunSpecOptions {
|
|
|
36
37
|
/** Per-run base spec (model/tools/toolPolicy/hooks/limits/…). `objective` is owned by runSpec (the
|
|
37
38
|
* spec framing); `toolPolicy` and `hooks` are COMPOSED with runSpec's, never replaced. */
|
|
38
39
|
taskSpec: Omit<TaskSpec, "objective">;
|
|
40
|
+
/** Receives the oracle gates' once-per-process `config.secret_env_scrubbed` notice (`runOracle`'s `onNotice`);
|
|
41
|
+
* absent ⇒ the console line. */
|
|
42
|
+
onNotice?: (notice: EngineNotice) => void;
|
|
39
43
|
}
|
|
40
44
|
export interface RunSpecOracleReport {
|
|
41
45
|
/** Terminal adjudication (the authoritative bit — the stop gate only advises mid-run). */
|
|
@@ -80,7 +80,8 @@ export async function runSpec(runner, contract, opts) {
|
|
|
80
80
|
const startedAt = Date.now();
|
|
81
81
|
const frozenResolved = resolveFrozenPaths(rootDir, contract.frozenPaths);
|
|
82
82
|
const snapshot = snapshotFrozenPaths(frozenResolved);
|
|
83
|
-
const
|
|
83
|
+
const noticeSeat = opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {};
|
|
84
|
+
const preflight = await runOracle(contract.oracle, rootDir, { signal: opts.taskSpec.signal, ...noticeSeat });
|
|
84
85
|
const baseline = captureBaseline(preflight);
|
|
85
86
|
let oracleHadRedRun = preflight.verdict === "red";
|
|
86
87
|
let oracleRuns = 1;
|
|
@@ -137,7 +138,7 @@ export async function runSpec(runner, contract, opts) {
|
|
|
137
138
|
guardRestores += restoreFrozenPaths(snapshot, rootDir);
|
|
138
139
|
if (!gateAbandoned) {
|
|
139
140
|
const gateSignal = ctx.signal !== undefined && opts.taskSpec.signal !== undefined ? AbortSignal.any([ctx.signal, opts.taskSpec.signal]) : (ctx.signal ?? opts.taskSpec.signal);
|
|
140
|
-
const report = await runOracle(contract.oracle, rootDir, { baseline, ...(gateSignal !== undefined ? { signal: gateSignal } : {}) });
|
|
141
|
+
const report = await runOracle(contract.oracle, rootDir, { baseline, ...(gateSignal !== undefined ? { signal: gateSignal } : {}), ...noticeSeat });
|
|
141
142
|
if (ctx.signal?.aborted === true)
|
|
142
143
|
return undefined;
|
|
143
144
|
oracleRuns++;
|
|
@@ -175,7 +176,7 @@ export async function runSpec(runner, contract, opts) {
|
|
|
175
176
|
hooks,
|
|
176
177
|
});
|
|
177
178
|
guardRestores += restoreFrozenPaths(snapshot, rootDir);
|
|
178
|
-
const final = await runOracle(contract.oracle, rootDir, { baseline });
|
|
179
|
+
const final = await runOracle(contract.oracle, rootDir, { baseline, ...noticeSeat });
|
|
179
180
|
oracleRuns++;
|
|
180
181
|
if (final.verdict === "red") {
|
|
181
182
|
oracleHadRedRun = true;
|
|
@@ -4,6 +4,7 @@ import { hrtime } from "node:process";
|
|
|
4
4
|
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { scrubSecretEnv } from "../../core/secret-env.js";
|
|
7
8
|
import { physicalPathOf, physicalPathOfExisting } from "../../core/physical-path.js";
|
|
8
9
|
import { thrownText } from "../../core/thrown-value.js";
|
|
9
10
|
const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
|
|
@@ -637,7 +638,7 @@ function probeStartTimesOnce(pid) {
|
|
|
637
638
|
encoding: "utf8",
|
|
638
639
|
timeout: START_TIME_PROBE_TIMEOUT_MS,
|
|
639
640
|
stdio: ["ignore", "pipe", "ignore"],
|
|
640
|
-
env: { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
641
|
+
env: { ...scrubSecretEnv(process.env), LC_ALL: "C", LANG: "C" },
|
|
641
642
|
});
|
|
642
643
|
}
|
|
643
644
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "7.33.
|
|
3
|
+
"version": "7.33.8",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -86,6 +86,8 @@
|
|
|
86
86
|
"gate:tool-docs": "node scripts/gen-tool-spec-fields.mjs --check",
|
|
87
87
|
"gate:backlog-ledger": "node scripts/verify-backlog-ledger.mjs",
|
|
88
88
|
"gate:child-clamp-seats": "node scripts/verify-child-clamp-seats.mjs",
|
|
89
|
+
"gate:spawn-sites": "node scripts/verify-spawn-sites.mjs",
|
|
90
|
+
"gate:point-free-callbacks": "node scripts/verify-point-free-callbacks.mjs",
|
|
89
91
|
"gate:docs-tool-names": "node scripts/verify-docs-tool-names.mjs",
|
|
90
92
|
"gate:comment-ratio": "vitest run test/comment-ratio-ratchet.test.ts",
|
|
91
93
|
"gate:negative-controls": "vitest run test/gate-negative-controls.test.ts",
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.33.8",
|
|
4
|
+
"from": "2dbd7b86789e1e5385ca2ebf13db6de617b33669",
|
|
5
|
+
"to": "v7.33.8",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [
|
|
10
|
+
{
|
|
11
|
+
"id": "F1",
|
|
12
|
+
"source": "behaviorFlips.summary",
|
|
13
|
+
"text": "#1181 (security, narrowing): a `runSpec` / `runOracle` gate's command is spawned with the engine's environment through the credential scrub; `OracleGateSpec.inheritEnv` (\"scrub\" default / \"all\" / allow list, names only) puts names back per gate; a malformed `inheritEnv` or a command the OS refuses outright is an `error` gate; the withheld names reach the shared once-per-process `config.secret_env_scrubbed` notice through `runOracle`'s and `RunSpecOptions`' new optional `onNotice`."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "F2",
|
|
17
|
+
"source": "behaviorFlips.summary",
|
|
18
|
+
"text": "#1181 (security, narrowing): the engine's helpers looked up on PATH — the tree kill's `ps` / `taskkill` / Git Bash group kill, shell discovery (`which bash` / `where bash.exe`) and the lock-holder probe (`ps -p`) — spawn with the scrubbed environment and announce nothing."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "F3",
|
|
22
|
+
"source": "behaviorFlips.summary",
|
|
23
|
+
"text": "#1183 (security, narrowing; 7.33.6 regression): every `GIT_CONFIG_PARAMETERS` entry is judged from depth 0 — a credential-carrying command line in the fourth or a later entry withholds the variable, as the first three did (`SecretEnvFinding.kind` = `family-rule`)."
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"closedSetMembers": [
|
|
27
|
+
{
|
|
28
|
+
"set": "InheritEnvPolicy",
|
|
29
|
+
"at": [
|
|
30
|
+
"src/core/secret-env.ts:941"
|
|
31
|
+
],
|
|
32
|
+
"newSet": true,
|
|
33
|
+
"added": [
|
|
34
|
+
"all",
|
|
35
|
+
"scrub"
|
|
36
|
+
],
|
|
37
|
+
"removed": [],
|
|
38
|
+
"breaking": false,
|
|
39
|
+
"criteriaCovered": false
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"closedSetsUnreadable": [],
|
|
43
|
+
"typeKeys": [
|
|
44
|
+
{
|
|
45
|
+
"type": "OracleGateSpec",
|
|
46
|
+
"at": [
|
|
47
|
+
"src/core/spec-contract.ts:54"
|
|
48
|
+
],
|
|
49
|
+
"added": [
|
|
50
|
+
"inheritEnv"
|
|
51
|
+
],
|
|
52
|
+
"addedDetail": [
|
|
53
|
+
{
|
|
54
|
+
"key": "inheritEnv",
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"addedRequired": [],
|
|
59
|
+
"removed": [],
|
|
60
|
+
"breaking": false,
|
|
61
|
+
"criteriaCovered": true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "RunSpecOptions",
|
|
65
|
+
"at": [
|
|
66
|
+
"src/orchestration/run-spec.ts:56"
|
|
67
|
+
],
|
|
68
|
+
"added": [
|
|
69
|
+
"onNotice"
|
|
70
|
+
],
|
|
71
|
+
"addedDetail": [
|
|
72
|
+
{
|
|
73
|
+
"key": "onNotice",
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"addedRequired": [],
|
|
78
|
+
"removed": [],
|
|
79
|
+
"breaking": false,
|
|
80
|
+
"criteriaCovered": true
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"typeKeysUnreadableArms": [],
|
|
84
|
+
"typeKeysFlattenedCount": 217,
|
|
85
|
+
"declarationMoves": [],
|
|
86
|
+
"mintMoves": [],
|
|
87
|
+
"criteriaObserves": [
|
|
88
|
+
{
|
|
89
|
+
"kind": "engineNoticeCode",
|
|
90
|
+
"name": "config.secret_env_scrubbed",
|
|
91
|
+
"assertions": [
|
|
92
|
+
"G1.c"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"kind": "typeMember",
|
|
97
|
+
"name": "ExecutionError.code",
|
|
98
|
+
"assertions": [
|
|
99
|
+
"G2.a"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"kind": "typeMember",
|
|
104
|
+
"name": "McpServerSpec.transport",
|
|
105
|
+
"assertions": [
|
|
106
|
+
"G2.b",
|
|
107
|
+
"G2.c"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"kind": "typeMember",
|
|
112
|
+
"name": "OracleGateResult.reason",
|
|
113
|
+
"assertions": [
|
|
114
|
+
"G1.b"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"kind": "typeMember",
|
|
119
|
+
"name": "OracleGateResult.verdict",
|
|
120
|
+
"assertions": [
|
|
121
|
+
"G1.a",
|
|
122
|
+
"G1.b"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"kind": "typeMember",
|
|
127
|
+
"name": "OracleGateSpec.inheritEnv",
|
|
128
|
+
"assertions": [
|
|
129
|
+
"G1.a",
|
|
130
|
+
"G2.c"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"kind": "typeMember",
|
|
135
|
+
"name": "RunSpecOptions.onNotice",
|
|
136
|
+
"assertions": [
|
|
137
|
+
"G1.c"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"kind": "typeMember",
|
|
142
|
+
"name": "SecretEnvFinding.key",
|
|
143
|
+
"assertions": [
|
|
144
|
+
"G3.a"
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"kind": "typeMember",
|
|
149
|
+
"name": "SecretEnvFinding.kind",
|
|
150
|
+
"assertions": [
|
|
151
|
+
"G3.a",
|
|
152
|
+
"G3.b"
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"consumersHint": {
|
|
157
|
+
"cli": [],
|
|
158
|
+
"server": [],
|
|
159
|
+
"sdk": [],
|
|
160
|
+
"note": "scanned cli@4e593722 server@108404d4 sdk@8c379ef — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it. Ledger files (**/docs/core-consumption/**, **/docs/wire-consumption/**, **/*LEDGER*.json) are not evidence of use — server: 2 name(s) matched ONLY in bookkeeping files and were set aside (OracleGateSpec, RunSpecOptions)"
|
|
161
|
+
},
|
|
162
|
+
"breakingCount": 0,
|
|
163
|
+
"breakingItems": [],
|
|
164
|
+
"toResolvedAt": "128ed39a3b20bb0a28352a156cb229538261551e"
|
|
165
|
+
}
|