@sema-agent/server 7.101.1 → 7.103.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +84 -0
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/USAGE.md +156 -9
- package/dist/approval-card.d.ts +35 -2
- package/dist/approval-card.js +19 -1
- package/dist/background-session-reap.d.ts +58 -0
- package/dist/background-session-reap.js +90 -0
- package/dist/bake-runner/main.d.ts +21 -3
- package/dist/bake-runner/main.js +18 -11
- package/dist/boot/config-center.d.ts +21 -4
- package/dist/boot/config-center.js +62 -19
- package/dist/boot/exit-boundary.js +17 -0
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.js +3 -4
- package/dist/boot/shutdown.d.ts +2 -1
- package/dist/boot/shutdown.js +33 -12
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -1
- package/dist/boot/stage-07-capability-layer.js +6 -4
- package/dist/boot/stage-08-reapers.d.ts +1 -1
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +13 -4
- package/dist/brain.d.ts +28 -8
- package/dist/brain.js +20 -4
- package/dist/capabilities/builtin-tools.d.ts +14 -1
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +9 -3
- package/dist/capabilities/center-plugins.js +5 -10
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/center-credential-renewer.d.ts +76 -0
- package/dist/center-credential-renewer.js +359 -0
- package/dist/center-credential.d.ts +76 -22
- package/dist/center-credential.js +119 -43
- package/dist/config-catalog.js +17 -8
- package/dist/config-center/apply-effective.d.ts +16 -0
- package/dist/config-center/apply-effective.js +26 -21
- package/dist/config-center/center-request.d.ts +13 -0
- package/dist/config-center/center-request.js +23 -3
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/http-client.js +2 -5
- package/dist/config-center/mcp-revocation.d.ts +2 -2
- package/dist/config-center/mcp-revocation.js +1 -1
- package/dist/config-center/read-warnings.d.ts +19 -48
- package/dist/config-center/read-warnings.js +7 -49
- package/dist/config-center/restart-signal.d.ts +8 -21
- package/dist/config-center/restart-signal.js +2 -12
- package/dist/config-center/types.d.ts +4 -2
- package/dist/config-provider.js +1 -48
- package/dist/config-types.d.ts +107 -17
- package/dist/config-types.js +7 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +91 -29
- package/dist/device-store.d.ts +1 -1
- package/dist/env-name-allowlist-knobs.d.ts +21 -7
- package/dist/env-name-allowlist-knobs.js +7 -2
- package/dist/fleet-client.d.ts +27 -7
- package/dist/fleet-client.js +43 -12
- package/dist/fleet-lease.d.ts +1 -1
- package/dist/host-lsp-manager.d.ts +4 -3
- package/dist/host-lsp-manager.js +2 -6
- package/dist/http/approval-sweeps.js +5 -20
- package/dist/http/dispatch.js +24 -14
- package/dist/http/readiness.d.ts +87 -0
- package/dist/http/readiness.js +54 -0
- package/dist/http/resume-legs.js +5 -4
- package/dist/http/route-table.d.ts +7 -0
- package/dist/http/route-table.js +6 -0
- package/dist/http/routes/a2a-serve.js +6 -14
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/background.d.ts +80 -0
- package/dist/http/routes/background.js +94 -0
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/server.d.ts +25 -8
- package/dist/http/server.js +2 -2
- package/dist/leader/wire.d.ts +8 -1
- package/dist/leader/wire.js +4 -3
- package/dist/model-entry-refusal.d.ts +42 -19
- package/dist/model-entry-refusal.js +34 -19
- package/dist/model-route-endpoint.d.ts +10 -0
- package/dist/model-route-endpoint.js +6 -2
- package/dist/observability/fail-open.d.ts +2 -2
- package/dist/observability/fail-open.js +2 -2
- package/dist/observability/secret-env-scrub.d.ts +12 -0
- package/dist/observability/secret-env-scrub.js +12 -0
- package/dist/outbound-dispatcher.d.ts +125 -0
- package/dist/outbound-dispatcher.js +211 -0
- package/dist/parked-decide.d.ts +11 -5
- package/dist/parked-decide.js +4 -1
- package/dist/parked-revive-surface.d.ts +47 -0
- package/dist/parked-revive-surface.js +20 -0
- package/dist/plugins/background-shell-support.d.ts +20 -1
- package/dist/plugins/background-shell-support.js +26 -0
- package/dist/plugins/host-platform.d.ts +16 -1
- package/dist/plugins/host-platform.js +11 -0
- package/dist/plugins/remote-env-host.d.ts +14 -1
- package/dist/plugins/remote-env-host.js +15 -10
- package/dist/project-memory.js +3 -6
- package/dist/run-local.js +2 -0
- package/dist/server-secret-env.d.ts +97 -24
- package/dist/server-secret-env.js +37 -1
- package/dist/tool-approval.d.ts +30 -1
- package/dist/tool-approval.js +31 -8
- package/dist/trace/core-keyset-guard.d.ts +9 -4
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.js +13 -0
- package/dist/trace/redact.d.ts +11 -0
- package/dist/trace/redact.js +1 -0
- package/package.json +4 -3
|
@@ -7,11 +7,10 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
|
|
|
7
7
|
import { StringDecoder } from "node:string_decoder";
|
|
8
8
|
import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
|
|
9
9
|
import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
10
|
-
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
|
|
10
|
+
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
|
|
11
11
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
12
12
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
|
|
13
|
-
import {
|
|
14
|
-
import { allowlistedEnv, scrubServerSecrets } from "../server-secret-env.js";
|
|
13
|
+
import { allowlistedEnv, hostSubprocessEnv } from "../server-secret-env.js";
|
|
15
14
|
import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
|
|
16
15
|
const PROVIDER = "host";
|
|
17
16
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -21,7 +20,7 @@ const HOST_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("HOST_BG_DEFAULT_TIMEOUT_SEC", 300,
|
|
|
21
20
|
const HOST_BG_MAX_TIMEOUT_SEC = numEnvOr("HOST_BG_MAX_TIMEOUT_SEC", 1800, HOST_BG_DEFAULT_TIMEOUT_SEC);
|
|
22
21
|
const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024);
|
|
23
22
|
const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_READ_CAP);
|
|
24
|
-
const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
|
|
23
|
+
export const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
|
|
25
24
|
const HOST_BG_MEM_CAP = numEnvOr("HOST_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
|
|
26
25
|
const ok = (value) => ({ ok: true, value });
|
|
27
26
|
const UNC_SPELLING = /^[\\/]{2}[^\\/]+[\\/]+[^\\/]/;
|
|
@@ -253,7 +252,7 @@ export class RemoteHostExecutionEnv {
|
|
|
253
252
|
await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
254
253
|
throw new BackgroundShellError("spawn_failed", "host background spawn produced no pid");
|
|
255
254
|
}
|
|
256
|
-
return { pid, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
|
|
255
|
+
return { pid, child, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
|
|
257
256
|
});
|
|
258
257
|
},
|
|
259
258
|
async read(state) {
|
|
@@ -334,6 +333,11 @@ export class RemoteHostExecutionEnv {
|
|
|
334
333
|
if (state.dir)
|
|
335
334
|
await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { });
|
|
336
335
|
},
|
|
336
|
+
killNow(state) {
|
|
337
|
+
if (state.child.exitCode !== null || state.child.signalCode !== null)
|
|
338
|
+
return "already_gone";
|
|
339
|
+
return hardKillGroupNow(state.pid);
|
|
340
|
+
},
|
|
337
341
|
};
|
|
338
342
|
}
|
|
339
343
|
spawnBackground(command, options) {
|
|
@@ -362,7 +366,7 @@ export class RemoteHostExecutionEnv {
|
|
|
362
366
|
child.off("close", onClose);
|
|
363
367
|
child.off("error", onErrEvt);
|
|
364
368
|
};
|
|
365
|
-
return { pid, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
|
|
369
|
+
return { pid, child, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
|
|
366
370
|
});
|
|
367
371
|
return adopted?.shellId;
|
|
368
372
|
}
|
|
@@ -372,6 +376,9 @@ export class RemoteHostExecutionEnv {
|
|
|
372
376
|
killBackground(shellId) {
|
|
373
377
|
return this.bgManager.kill(shellId);
|
|
374
378
|
}
|
|
379
|
+
killBackgroundNow(shellId) {
|
|
380
|
+
return this._bgManager?.killNow(shellId) ?? "already_gone";
|
|
381
|
+
}
|
|
375
382
|
disposeBackgroundShells(opts) {
|
|
376
383
|
return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
|
|
377
384
|
}
|
|
@@ -782,7 +789,7 @@ export class RemoteHostExecutionEnv {
|
|
|
782
789
|
child.off("close", onClose);
|
|
783
790
|
child.off("error", onErrEvt);
|
|
784
791
|
};
|
|
785
|
-
return { pid, dir: spool.dir, outPath: spool.outPath, errPath: spool.errPath, stdoutCursor: cursors.out, stderrCursor: cursors.err, detachCleanup, ...(fgRemovedBytes > 0 ? { preSpoolBase: fgRemovedBytes, pendingDropped: fgRemovedBytes } : {}) };
|
|
792
|
+
return { pid, child, dir: spool.dir, outPath: spool.outPath, errPath: spool.errPath, stdoutCursor: cursors.out, stderrCursor: cursors.err, detachCleanup, ...(fgRemovedBytes > 0 ? { preSpoolBase: fgRemovedBytes, pendingDropped: fgRemovedBytes } : {}) };
|
|
786
793
|
});
|
|
787
794
|
return adopted?.shellId;
|
|
788
795
|
}
|
|
@@ -1309,9 +1316,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1309
1316
|
base = allowlistedEnv(mode, process.env);
|
|
1310
1317
|
}
|
|
1311
1318
|
else {
|
|
1312
|
-
|
|
1313
|
-
base = scrubServerSecrets(process.env, findings);
|
|
1314
|
-
recordSecretEnvScrub("server.remote-env-host.inherit-scrub", findings, []);
|
|
1319
|
+
base = hostSubprocessEnv({ site: "server.remote-env-host.inherit-scrub", allow: [] });
|
|
1315
1320
|
}
|
|
1316
1321
|
if (this.cfg.env)
|
|
1317
1322
|
Object.assign(base, this.cfg.env);
|
package/dist/project-memory.js
CHANGED
|
@@ -3,8 +3,7 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { createHmac, randomBytes } from "node:crypto";
|
|
4
4
|
import hostPath, { isAbsolute, join } from "node:path";
|
|
5
5
|
import { announceInstructionSource } from "./instruction-source-notice.js";
|
|
6
|
-
import {
|
|
7
|
-
import { scrubServerSecrets } from "./server-secret-env.js";
|
|
6
|
+
import { gitSubprocessEnv } from "./server-secret-env.js";
|
|
8
7
|
import { redactSecrets } from "./trace/redact.js";
|
|
9
8
|
const INSTRUCTION_FILE_CHAIN = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
|
|
10
9
|
const INSTRUCTION_READ_CAP = 24 * 1024;
|
|
@@ -72,11 +71,9 @@ async function readChainMember(root, rel, maxBytes, truncatedFlag) {
|
|
|
72
71
|
}
|
|
73
72
|
function git(root, args, maxBytes) {
|
|
74
73
|
const safeArgs = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args];
|
|
75
|
-
const
|
|
76
|
-
const scrubbedEnv = scrubServerSecrets(process.env, scrubFindings);
|
|
77
|
-
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings, []);
|
|
74
|
+
const env = gitSubprocessEnv({ site: "server.project-memory.git-subprocess", isolateConfig: true });
|
|
78
75
|
return new Promise((res) => {
|
|
79
|
-
execFile("git", safeArgs, { cwd: root, timeout: 5000, maxBuffer: maxBytes, env
|
|
76
|
+
execFile("git", safeArgs, { cwd: root, timeout: 5000, maxBuffer: maxBytes, env }, (err, stdout, stderr) => {
|
|
80
77
|
if (!err)
|
|
81
78
|
return res({ out: stdout.trim() });
|
|
82
79
|
const e = err;
|
package/dist/run-local.js
CHANGED
|
@@ -52,6 +52,7 @@ import { createMetrics } from "./observability/metrics.js";
|
|
|
52
52
|
import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
|
|
53
53
|
import { createHostLspManager } from "./host-lsp-manager.js";
|
|
54
54
|
import { buildDisplaySafeUrl } from "./trace/redact.js";
|
|
55
|
+
import { installOutboundDispatcher } from "./outbound-dispatcher.js";
|
|
55
56
|
export function parseArgs(argv) {
|
|
56
57
|
let objective;
|
|
57
58
|
let root;
|
|
@@ -267,6 +268,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
267
268
|
const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
|
|
268
269
|
installSecretEnvScrubRecorder({ logger, metrics });
|
|
269
270
|
logConfigDiagnostics(logger);
|
|
271
|
+
installOutboundDispatcher(logger);
|
|
270
272
|
const configProvider = createConfigProvider({
|
|
271
273
|
provider: config.configProvider ?? "local",
|
|
272
274
|
...(config.configLocalDir ? { localDir: config.configLocalDir } : { localDir: root }),
|
|
@@ -1,40 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 7.99.0 合并复审 F7(web-admin [ref] 实测;安全轴;DEBTS S-684)—— 子进程 env
|
|
2
|
+
* 7.99.0 合并复审 F7(web-admin [ref] 实测;安全轴;DEBTS S-684)—— 子进程 env 交出去之前的**本仓**密钥剥除;7.103.0(车OT,
|
|
3
|
+
* S-684 残余按类收口)起也是本包**宿主子进程 env 的唯一组合点**。
|
|
3
4
|
*
|
|
4
5
|
* ## 病
|
|
5
|
-
*
|
|
6
|
+
* 本仓的子进程腿 —— 模型可驱动的 host shell(`inheritEnv:"scrub"` 缺省腿,`plugins/remote-env-host.ts`)与
|
|
6
7
|
* project-memory 的 git 子进程(`project-memory.ts`)—— 此前只调 core 的 `scrubSecretEnv`。它按键名**形状**判
|
|
7
8
|
* (`*_KEY` / `*_TOKEN` / `*_SECRET` …),而本仓自己的凭据键里有形状规则认不出的:`SERVICE_AUTH_TOKENS`(复数,
|
|
8
9
|
* per-system 凭据表)、`APPROVAL_HMAC_KEYS`(直连门 HMAC 密钥集)、`OTEL_EXPORTER_OTLP_HEADERS`(常载鉴权头)⇒
|
|
9
|
-
* 原样进子进程,模型选的一句 `printenv` 就读得到。
|
|
10
|
+
* 原样进子进程,模型选的一句 `printenv` 就读得到。S-684 残余(7.103.0):另外三条宿主腿连 core 的形状剥都没过 —— leader 推送的
|
|
11
|
+
* git(`leader/wire.ts`)、center-plugins 克隆的 git(`capabilities/center-plugins.ts`)、bake-runner 交给 build.sh 与主机命令的
|
|
12
|
+
* env(`bake-runner/main.ts`,连 runner 自己的 `BAKE_RUNNER_TOKEN`)。
|
|
10
13
|
*
|
|
11
14
|
* ## 规则(一句)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* 本包任何进程在宿主上 spawn 的子进程:env = {@link hostSubprocessEnv} = {@link scrubServerSecrets}(process.env) ∪ 部署方点名放回的键
|
|
16
|
+
* (只在运维真可能要传东西的腿上有旋钮),剥掉的键经一个登记过的 `SecretEnvScrubSite` 留痕;git 腿再叠 git 自己的两件
|
|
17
|
+
* ({@link gitSubprocessEnv})。各腿只调这两只 —— 不许再有内联拼装(类级门在 `test/secret-env-scrub-host-legs.test.ts`)。
|
|
18
|
+
*
|
|
19
|
+
* {@link scrubServerSecrets} = core `scrubSecretEnv`(按形)∘ 本仓按名两臂:
|
|
20
|
+
* · 目录臂 —— 配置目录(`config-catalog.ts`)里 `type:"secret"` 的每一行,从目录机器派生,不手抄第二份名单;
|
|
21
|
+
* · 名单型旋钮臂 —— `ENV_NAME_ALLOWLIST_KNOBS` 的每一只(`env-name-allowlist-knobs.ts`,表键派生):名单**原文**可能夹着一枚被错展开进来
|
|
22
|
+
* 的令牌,按凭据类对待。服务侧三只(`LSP_ENV_ALLOW` S-699 / `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` S-713)同时是目录 secret 行,
|
|
23
|
+
* 目录臂先认;本臂接住的是**不在服务目录里**的那一只 —— bake-runner 的 `BAKE_ENV_ALLOW`(它不是服务进程的配置)。
|
|
15
24
|
* core 形状规则的缺口由 core 源头修 —— 7.30.1 #1072 已补 `KEYS|TOKENS|PASSWORDS`(S-616 提货),目录臂独担的只剩
|
|
16
|
-
* `OTEL_EXPORTER_OTLP_HEADERS`(`*_HEADERS` 不在形状规则射程,是边界不是缺陷票)
|
|
17
|
-
*
|
|
18
|
-
* 认它们)。本层不改 core 的判据,也不自造
|
|
19
|
-
* 第二份形状规则。目录里加一个 secret 型键,三条腿当场就剥它。
|
|
25
|
+
* `OTEL_EXPORTER_OTLP_HEADERS`(`*_HEADERS` 不在形状规则射程,是边界不是缺陷票)与名单型旋钮(形状规则按名判,本来就不该认它们)。
|
|
26
|
+
* 本层不改 core 的判据,也不自造第二份形状规则。目录里加一个 secret 型键 / 叶表加一只名单型旋钮,每条腿当场就剥它。
|
|
20
27
|
*
|
|
21
28
|
* ## 不变的面
|
|
22
29
|
* `inheritEnv:"all"`(显式全继承)与显式 allowlist 两形不经本函数,行为逐字不变;调用方显式传入的 env
|
|
23
30
|
* (`cfg.env` / 单次命令的 `env`)照旧原样覆盖在剥除结果之上。
|
|
24
31
|
*
|
|
25
|
-
* ## 点名放行(S-699 / S-713
|
|
26
|
-
* {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env
|
|
27
|
-
* host shell 的 `inheritEnv:[names]`(`plugins/remote-env-host.ts
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* ## 点名放行(S-699 / S-713,一只谓词各处用)
|
|
33
|
+
* {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env **原样**交给子进程」的唯一实现。调用:
|
|
34
|
+
* host shell 的 `inheritEnv:[names]`(`plugins/remote-env-host.ts`,hermetic 形)、{@link hostSubprocessEnv}(剥后放回形:本地语言服务器的
|
|
35
|
+
* `LSP_ENV_ALLOW`、bake-runner 的 `BAKE_ENV_ALLOW`)、沙箱带外 env 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(`config.ts`,S-713 起)。
|
|
36
|
+
* 名单型旋钮的**名单**另由 `config.ts` 的同一只解析器 `parseEnvNameAllowlist` 判形(形错拒启 / 源里没有的名按序号 warn),本函数只管取值。
|
|
37
|
+
* 「点名」在各处同义:点到的键原样带值(空串值照带),密钥形也照过(点名 = 部署方显式承担);源里没有的名不产生条目。
|
|
31
38
|
* 沙箱两腿不继承本进程环境:进沙箱的本进程值只有点到的键(与 host shell 的 hermetic 形同一侧)。
|
|
32
|
-
* host shell
|
|
33
|
-
* `NodeExecutionEnv` 同名选项同义,没点到的一律不继承)
|
|
34
|
-
* (没点到的照缺省剥,`PATH` / `HOME` 这类非密钥键照常在 —— hermetic
|
|
35
|
-
*
|
|
39
|
+
* host shell 与「剥后放回」形**只在没点到的键上**不同,这是有意保留的差别:host shell 的 `string[]` 形是 hermetic 白名单(与 core
|
|
40
|
+
* `NodeExecutionEnv` 同名选项同义,没点到的一律不继承);语言服务器 / bake-runner 在 {@link scrubServerSecrets} 的结果之上放回点名键
|
|
41
|
+
* (没点到的照缺省剥,`PATH` / `HOME` 这类非密钥键照常在 —— hermetic 形会让子进程当场找不到自己的工具链)。
|
|
42
|
+
* 两形都不放宽对方:把 host shell 改成「剥后放回」= 在模型可驱动的 shell 上放宽,把后者改成 hermetic = 功能当场失灵。
|
|
36
43
|
*/
|
|
37
44
|
import { type SecretEnvFinding } from "@sema-agent/core";
|
|
45
|
+
import { type SecretEnvScrubSite } from "./observability/secret-env-scrub.js";
|
|
38
46
|
/** 目录派生臂的一条剥除记录(形同 core 的 `SecretEnvFinding`:键名 / 规则 / 置信度 / 来源;值永不携带)。
|
|
39
47
|
* `source` 用本仓自己的词 `server-config-catalog`,与 core 的 `secret-env` 分开 —— 读观测面的人要分得清
|
|
40
48
|
* 「形状规则剥的」与「本仓目录点名剥的」(后者多出来的那部分正是 core 形状规则的缺口)。 */
|
|
@@ -44,16 +52,81 @@ export interface CatalogSecretEnvFinding {
|
|
|
44
52
|
readonly confidence: "high";
|
|
45
53
|
readonly source: "server-config-catalog";
|
|
46
54
|
}
|
|
47
|
-
/**
|
|
48
|
-
export
|
|
55
|
+
/** 名单型旋钮臂的一条剥除记录(形同上;`source` 用自己的词 —— 读观测面的人要分得清「目录点名剥的」与「名单型旋钮原文按凭据类剥的」)。 */
|
|
56
|
+
export interface NameListKnobEnvFinding {
|
|
57
|
+
readonly key: string;
|
|
58
|
+
readonly kind: "name-list-knob";
|
|
59
|
+
readonly confidence: "high";
|
|
60
|
+
readonly source: "server-name-list-knob";
|
|
61
|
+
}
|
|
62
|
+
/** 随残组扣下的 git 配置组成员(codex r1 C1;见 {@link hostSubprocessEnv} 的组不变量)。它本身不是凭据 —— 是因为同组的某个成员被剥掉、
|
|
63
|
+
* 整组已不成形而一起扣下的;`kind` 单列一桶,读观测面的人分得清「剥的」与「随组扣下的」。 */
|
|
64
|
+
export interface GitConfigGroupEnvFinding {
|
|
65
|
+
readonly key: string;
|
|
66
|
+
readonly kind: "git-config-group";
|
|
67
|
+
readonly confidence: "high";
|
|
68
|
+
readonly source: "server-git-config-group";
|
|
69
|
+
}
|
|
70
|
+
/** 本仓子进程腿的扣下记录:core 的形状臂 ∪ 本仓的目录臂 ∪ 名单型旋钮臂 ∪ 随残组扣下的 git 配置组成员。 */
|
|
71
|
+
export type ServerSecretEnvFinding = SecretEnvFinding | CatalogSecretEnvFinding | NameListKnobEnvFinding | GitConfigGroupEnvFinding;
|
|
49
72
|
/** 配置目录里 `type:"secret"` 的 env 名(大写 —— Windows 的 env 名不分大小写,按大写比才不会漏)。 */
|
|
50
73
|
export declare const CATALOG_SECRET_ENV_NAMES: ReadonlySet<string>;
|
|
51
74
|
/**
|
|
52
|
-
* 先交 core
|
|
75
|
+
* 先交 core 按形剥,再按目录名、名单型旋钮名剥它漏下的。`findings` 是调用方持有的收集器(交给 `recordSecretEnvScrub` 留痕):
|
|
53
76
|
* 形状臂剥掉的记 core 的 finding;目录臂只记**形状臂漏下、被目录点名剥掉**的键 —— 于是既有的形状臂计数不变,
|
|
54
|
-
* 目录臂的计数恰好就是 core
|
|
77
|
+
* 目录臂的计数恰好就是 core 形状规则的缺口读数;名单型旋钮臂只记前两臂都漏下的(今天 = 不在服务目录里的 `BAKE_ENV_ALLOW`)。
|
|
78
|
+
* 各腿不直接调它 —— 经 {@link hostSubprocessEnv}(剥 + 放回 + 留痕一只口)。
|
|
55
79
|
*/
|
|
56
80
|
export declare function scrubServerSecrets(env: NodeJS.ProcessEnv, findings: ServerSecretEnvFinding[]): NodeJS.ProcessEnv;
|
|
81
|
+
/**
|
|
82
|
+
* 🔴 **宿主子进程 env 的唯一组合**(S-684 残余按类收口,7.103.0):{@link scrubServerSecrets}(process.env) ∪ `allow` 点到的键
|
|
83
|
+
* (经 {@link allowlistedEnv} 从原 env 原样放回),**最终没进子进程**的剥除键经 `site` 留痕(点名放回的键不是剥除,不计数、不披露)。
|
|
84
|
+
*
|
|
85
|
+
* 六条宿主腿都只经它(直接或经 {@link gitSubprocessEnv}):host shell 缺省 scrub 腿、project-memory / center-plugins / leader 三条 git 腿、
|
|
86
|
+
* 本地语言服务器、bake-runner 的每一只子进程组。`allow` 只在**有部署级出路**的现场非空(今天 = `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`,
|
|
87
|
+
* 它们的现场在 `SECRET_ENV_SCRUB_SITES` 里登记了 `remedy`,披露行因此是 warn + 放行名单 + 出路句);其余现场传 `[]`。
|
|
88
|
+
* 返回 `NodeJS.ProcessEnv` 形(值可能是 `undefined` 的键本来就不会进子进程);调用方显式要交的键(`LC_ALL`、`cfg.env` …)叠在它之上。
|
|
89
|
+
* 调用时机 = 每次要交 env 时现算一次(`process.env` 在本仓是 boot 期输入,各次同值;计数因此是「每组合一次」)。
|
|
90
|
+
*
|
|
91
|
+
* 🔴 **组不变量(codex r1 C1 [high])**:逐键剥除不许把一组**按组生效**的 env 剥成残组。今天的实例是 git 的 env 配置组
|
|
92
|
+
* (`GIT_CONFIG_COUNT` + `GIT_CONFIG_KEY_<i>` / `GIT_CONFIG_VALUE_<i>`):core 剥掉某个 `GIT_CONFIG_KEY_<i>`、COUNT 与 VALUE 照留 ⇒ 子进程里的
|
|
93
|
+
* git 起手就 `missing config key`、退 128(任何读配置的子命令,不只要用那条配置的)。core ≤7.33.0 形状规则的 `KEY(_<数字>)?` 后缀剥**每一个**
|
|
94
|
+
* `GIT_CONFIG_KEY_<i>`;core 7.33.1 #1129 起该族具名豁免,残组今天从**两侧**来:KEY 侧的值筛(值里是带 userinfo 的 URL —— 令牌
|
|
95
|
+
* `url.<base>.insteadOf`)剥 KEY;core 7.33.2 起 VALUE 侧的值族表(`GIT_CONFIG_VALUE_<i>` 是 `Authorization:` / `Proxy-Authorization:` /
|
|
96
|
+
* `Cookie:` 头、带 userinfo 的 URL,或 proxy 地址键下的 `user[:secret]@host` ⇒ `family-rule`)剥 VALUE。两侧的 URL userinfo 判据自 core 7.33.2
|
|
97
|
+
* 起就是**同一只**(7.33.2 = 正则 `URL_USERINFO_RE`:`//` 到 `@` 之间无 `/` `?` `#`);core 7.33.3 #1140 只改这一只判据本身,剥得更多(`://` 后到
|
|
98
|
+
* 第一个 `@` 的段:无 `/` `?` `#`,或第一个 `/` `?` `#` 之前有 `:`(段首 `[…]` 里的 `:` 不计)—— 密码里未转义的 `/` 也算;带端口的路径 `@` 过剥,
|
|
99
|
+
* core KL-1129b);core 7.33.4 #1146 再改它、不再解析 authority:值里有 `:/`、其后任意位置有 `@` 或 `%40` 即剥,proxy 地址键下值里有 `@` / `%40` 即剥
|
|
100
|
+
* —— 路径里的 `@`(`@scope` 包路径)与 scp 绝对路径带 `@`(`host:/repo@v2.git`)一并多剥(fail-closed,KL-1129b)。本不变量对哪一侧剥残的组都照样
|
|
101
|
+
* 成立(车OR / 车PA / 车PB / 车PC 提货翻钉,零改码)。
|
|
102
|
+
* 规则:交出去的组要么**源里有的成员一个不少**、要么**整组都不给**(值同扣 —— VALUE 里可能正是鉴权头);随组扣下的成员记
|
|
103
|
+
* `kind: "git-config-group"`。本层只**多扣**、从不放回 core 判掉的键(core 的判据不在这里改;`GIT_CONFIG_KEY_<i>` 是不是凭据是 core
|
|
104
|
+
* 形状规则的事,见 [ref] 登记);部署方点名放回全部 `GIT_CONFIG_KEY_<i>` 与 `GIT_CONFIG_VALUE_<i>`(有放行面的现场;core 7.33.2 起两边都可能被剥,
|
|
105
|
+
* 只放回 KEY 仍是残组)⇒ 组完整,原样交出。源里本就残缺的组不是本层
|
|
106
|
+
* 剥残的,不碰(git 对它照样报错 —— 那是部署方自己的配置)。
|
|
107
|
+
*/
|
|
108
|
+
export declare function hostSubprocessEnv(o: {
|
|
109
|
+
site: SecretEnvScrubSite;
|
|
110
|
+
allow: readonly string[];
|
|
111
|
+
}): NodeJS.ProcessEnv;
|
|
112
|
+
/** git 子进程腿的现场(闭集里名字带 `.git-` 的那几只 —— 编译期筛;它们都没有点名放行面)。 */
|
|
113
|
+
export type GitSubprocessSite = Extract<SecretEnvScrubSite, `${string}.git-${string}`>;
|
|
114
|
+
/**
|
|
115
|
+
* 宿主 **git 子进程**的 env:{@link hostSubprocessEnv}(site, 不放回) + `GIT_TERMINAL_PROMPT=0`(恒:凭据缺席 ⇒ 响亮失败,绝不挂在交互
|
|
116
|
+
* prompt 上 —— 服务进程没有终端)+(`isolateConfig` ⇒ `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM` = `/dev/null`:宿主全局 / 系统 git 配置
|
|
117
|
+
* (insteadOf / credential helper / hooksPath / fsmonitor …)一概不继承)。
|
|
118
|
+
*
|
|
119
|
+
* `isolateConfig` 按腿定(三条腿各自的理由在调用点):project-memory 与 center-plugins 隔离(读仓 / 克隆不需要宿主身份);leader 推送
|
|
120
|
+
* **不**隔离 —— durable remote 的写凭据属于控制面宿主(「Coordinator's creds」),可能由宿主 git 配置里的 credential helper / insteadOf
|
|
121
|
+
* 提供,隔离会把它们一起切掉。不隔离 ≠ 不剥:helper 同样只看得到剥后的 env(读 `*_TOKEN` 形 env 的 helper 因此读不到,出路见 CHANGELOG)。
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ 已知边界(不在本函数修):Apple Git 另有一层 Xcode 自带配置(`…/Xcode.app/…/git-core/gitconfig`,内含 `credential.helper=osxkeychain`),
|
|
124
|
+
* `GIT_CONFIG_SYSTEM` 盖不住、`GIT_CONFIG_NOSYSTEM=1` 才盖得住 —— 只在 macOS 开发机上,文件属 root(不可被模型种),见 [ref] 登记。
|
|
125
|
+
*/
|
|
126
|
+
export declare function gitSubprocessEnv(o: {
|
|
127
|
+
site: GitSubprocessSite;
|
|
128
|
+
isolateConfig: boolean;
|
|
129
|
+
}): NodeJS.ProcessEnv;
|
|
57
130
|
/**
|
|
58
131
|
* S-699 —— **点名放行**的唯一谓词:从 `env` 里把 `names` 点到的键**原样**取出(值逐字,空串照带;源里没有的名不产生
|
|
59
132
|
* 条目)。密钥形的键点到了也照取 —— 点名即部署方显式承担;要不要在这之上再叠别的基底,是调用方的事(见文件头「点名放行」节)。
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { scrubSecretEnv } from "@sema-agent/core";
|
|
2
2
|
import { CONFIG_CATALOG } from "./config-catalog.js";
|
|
3
|
+
import { ENV_NAME_ALLOWLIST_KNOB_NAMES } from "./env-name-allowlist-knobs.js";
|
|
4
|
+
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
3
5
|
export const CATALOG_SECRET_ENV_NAMES = new Set(CONFIG_CATALOG.filter((spec) => spec.type === "secret").map((spec) => spec.name.toUpperCase()));
|
|
4
6
|
export function scrubServerSecrets(env, findings) {
|
|
5
7
|
const shapeFindings = [];
|
|
@@ -7,14 +9,48 @@ export function scrubServerSecrets(env, findings) {
|
|
|
7
9
|
findings.push(...shapeFindings);
|
|
8
10
|
const out = {};
|
|
9
11
|
for (const key of Object.keys(shaped)) {
|
|
10
|
-
|
|
12
|
+
const upper = key.toUpperCase();
|
|
13
|
+
if (CATALOG_SECRET_ENV_NAMES.has(upper)) {
|
|
11
14
|
findings.push({ key, kind: "config-catalog", confidence: "high", source: "server-config-catalog" });
|
|
12
15
|
continue;
|
|
13
16
|
}
|
|
17
|
+
if (ENV_NAME_ALLOWLIST_KNOB_NAMES.has(upper)) {
|
|
18
|
+
findings.push({ key, kind: "name-list-knob", confidence: "high", source: "server-name-list-knob" });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
14
21
|
out[key] = shaped[key];
|
|
15
22
|
}
|
|
16
23
|
return out;
|
|
17
24
|
}
|
|
25
|
+
export function hostSubprocessEnv(o) {
|
|
26
|
+
const scrubFindings = [];
|
|
27
|
+
const composed = { ...scrubServerSecrets(process.env, scrubFindings), ...allowlistedEnv(o.allow, process.env) };
|
|
28
|
+
const stripped = scrubFindings.filter((f) => composed[f.key] === undefined);
|
|
29
|
+
const grouped = withholdBrokenGitConfigGroup(process.env, composed).map((key) => ({ key, kind: "git-config-group", confidence: "high", source: "server-git-config-group" }));
|
|
30
|
+
recordSecretEnvScrub(o.site, [...stripped, ...grouped], o.allow);
|
|
31
|
+
return composed;
|
|
32
|
+
}
|
|
33
|
+
const GIT_CONFIG_GROUP_MEMBER_RE = /^GIT_CONFIG_(?:COUNT|(?:KEY|VALUE)_\d+)$/;
|
|
34
|
+
function withholdBrokenGitConfigGroup(source, composed) {
|
|
35
|
+
const isMember = (k) => GIT_CONFIG_GROUP_MEMBER_RE.test(k.toUpperCase());
|
|
36
|
+
const sourceMembers = new Set(Object.keys(source).filter(isMember).map((k) => k.toUpperCase()));
|
|
37
|
+
if (!sourceMembers.has("GIT_CONFIG_COUNT"))
|
|
38
|
+
return [];
|
|
39
|
+
const composedMembers = Object.keys(composed).filter(isMember);
|
|
40
|
+
const present = new Set(composedMembers.map((k) => k.toUpperCase()));
|
|
41
|
+
if ([...sourceMembers].every((m) => present.has(m)))
|
|
42
|
+
return [];
|
|
43
|
+
for (const k of composedMembers)
|
|
44
|
+
delete composed[k];
|
|
45
|
+
return composedMembers;
|
|
46
|
+
}
|
|
47
|
+
export function gitSubprocessEnv(o) {
|
|
48
|
+
return {
|
|
49
|
+
...hostSubprocessEnv({ site: o.site, allow: [] }),
|
|
50
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
51
|
+
...(o.isolateConfig ? { GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
18
54
|
export function allowlistedEnv(names, env) {
|
|
19
55
|
const out = {};
|
|
20
56
|
for (const name of names) {
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HostDecidedBy, HostSettledBy } from "./host-decision.js";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
3
|
+
import { type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type PersistedRuleMandate, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
4
4
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
5
5
|
import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
6
6
|
import { type ApprovalAskStore, type AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
@@ -244,6 +244,16 @@ export interface ToolApprovalFrame {
|
|
|
244
244
|
* 席照常答得了」说的是**这一次调用的应答者**,本仓不改它。
|
|
245
245
|
*/
|
|
246
246
|
mandated?: true;
|
|
247
|
+
/**
|
|
248
|
+
* **S-721(core 7.32.0 #1092;ADDITIVE,`"tool_approval"` only)** —— 强制位**站在哪一个词上**:core `PERSISTED_RULE_MANDATES`
|
|
249
|
+
* 六词之一(`operator_always` / `tool_marks` / `probe_mandate` / `probe_unanswered` / `write_protection` /
|
|
250
|
+
* `write_protection_unresolved`,词义属主在 core)。只在 `mandated: true` 的**整因**是那一个词时在场;另一扇门(治理 / hook /
|
|
251
|
+
* 祖先)或不止一个理由 ⇒ 缺席 —— **缺席从不命名一个词**,也不等于「不强制」(读 `mandated`)。值经 `approval-card.ts` 的
|
|
252
|
+
* `readClosedWordSeat(req, "mandate")` 窄读(闭集筛 = core `isPersistedRuleMandate`,本仓不抄词表;集外词读作缺席 + 计一次)。echo-only:
|
|
253
|
+
* 本仓不按词分支。耐久面同一个词:运维队列行(`parkRowFacts`,park 行孪生座)与 inbox 行(core `CheckpointSummary.mandate`)。
|
|
254
|
+
* 与 `mandated` 同一条:`card_json` 卡面不带本键(`ApprovalCardSchema` `.strict()`)。
|
|
255
|
+
*/
|
|
256
|
+
mandate?: PersistedRuleMandate;
|
|
247
257
|
/**
|
|
248
258
|
* **S-125③/[ref]**(core 7.5.0;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**出身**:
|
|
249
259
|
* 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS` **11 员**,core 7.14.0 现值:content_question /
|
|
@@ -263,6 +273,12 @@ export interface ToolApprovalFrame {
|
|
|
263
273
|
* 🔴 **不枚举词表**(`string` 而不是本仓自铸的联合):词表单一属主在 core,在这里抄一份会把 core 加的
|
|
264
274
|
* 新词吞成缺席 —— 与 `wiring_manifest.autoMode.reason` 的透传纪律逐字同规。缺席 = 老引擎 / 非 ask 路径。
|
|
265
275
|
*
|
|
276
|
+
* 同一个词的其余四面(S-755 起全部在场):`livePending` 行顶层 / `live_pending` 帧(条目铸定时与本键**同一次**读定);运维队列行
|
|
277
|
+
* (`GET /v1/approvals` 的 `pending[]` + `/stream` 的 `pending` 帧,读 park 行孪生座 `PendingAction.tool_approval.origin`,经
|
|
278
|
+
* `approval-card.ts` 的 `readClosedWordSeat(…, "origin")` —— 那一面读的是持久层原字节、中间没有 core 投影,所以亲手调 core 的
|
|
279
|
+
* `isAskOrigin`);inbox 行(core `CheckpointSummary.origin`,core 投影处已筛)。本键读的是引擎在门上刚盖好的值(盖章点即 core
|
|
280
|
+
* 的筛点),所以只过「非空串」形门、不再筛第二遍 —— 对 core 盖得出的每一个词,五面逐字同值。
|
|
281
|
+
*
|
|
266
282
|
* ⚠️ 与 {@link governanceForced} **不是**一回事,别互相顶替:本键答「哪一类权威提的问」,那一键答
|
|
267
283
|
* 「**本部署运维治理层**是不是这只 ask 的门」。`origin: "policy"` 覆盖的是「任何部署 ToolPolicy 的
|
|
268
284
|
* ask」,治理策略产的与普通策略产的在这一个词上同形 ⇒ 顶替不了(逐字论证见 `governance-ask-marks.ts` 顶注)。
|
|
@@ -530,6 +546,13 @@ export interface LivePendingRow {
|
|
|
530
546
|
* 值 = 条目上那一位(`askBroadcast` 铸条目时取自这一次过闸的 `req.mandated`,与 {@link frame} 上的同名键同源)。此前只在
|
|
531
547
|
* `frame` 里:同一个 `GET /v1/approvals` 响应的 durable `pending[]` 行顶层带它(S-663 二段),live 行却要进帧里读。 */
|
|
532
548
|
mandated?: true;
|
|
549
|
+
/** S-721(core 7.32.0 #1092):强制位站在哪一个词上 —— 与 `mandated` 同层同形(在场逐字,缺席不编词),值 = 条目上那一个词
|
|
550
|
+
* (与 {@link frame} 上同名键同源;帧型该键顶注是全文)。 */
|
|
551
|
+
mandate?: PersistedRuleMandate;
|
|
552
|
+
/** S-755(cli L-695 / [ref]):ask 的**出身词**(谁提的这一问,core `ASK_ORIGINS` 闭集一词)—— 与 `mandate` 同层同形(在场逐字,
|
|
553
|
+
* 缺席不编词),值 = 条目上那一个词(铸条目那一次与 {@link frame} 上同名键**同一次**读定;帧型该键顶注是全文)。同一个
|
|
554
|
+
* `GET /v1/approvals` 响应里 durable `pending[]` 行的 `origin` 也在顶层(park 行孪生座),两族行同一种读法。 */
|
|
555
|
+
origin?: string;
|
|
533
556
|
/** 发起者为委派子代(originTaskId 在场)。 */
|
|
534
557
|
fromSubagent?: true;
|
|
535
558
|
/** [ref] C2(additive):委派子代的 **canonical taskId**。值域精确成文(codex R1-F3 问询后按 core dist
|
|
@@ -695,6 +718,11 @@ export declare const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOffe
|
|
|
695
718
|
*/
|
|
696
719
|
export declare const SETTLED_BY_MUST_BE_STRING = "settledBy must be a string \u2014 the settlement source is one word from the engine's closed set, and this layer judges only the SHAPE (which word it is, the engine decides)";
|
|
697
720
|
export declare const SETTLED_BY_DENY_ONLY = "settledBy is only meaningful with decision \"deny\" \u2014 only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door's own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision";
|
|
721
|
+
/** S-720(core 7.32.0 #1093)—— `readRootGrant` 只随 `decision:"allow"`(CC 卡的第三选项「Yes, and add directory」= 放行这一次
|
|
722
|
+
* + 加目录;`allow_session` 不是那一个选项,`deny` 上它没有意义)。裁决之前响亮拒,不静默丢。 */
|
|
723
|
+
export declare const READ_ROOT_GRANT_ALLOW_ONLY = "readRootGrant is only meaningful with decision \"allow\" \u2014 it is the card's \"Yes, and add directory\" answer (allow this call and add the card's readRootCandidate.dir to this run's read directories)";
|
|
724
|
+
/** S-720:`readRootGrant` 的形 —— 卡上 `readRootCandidate.dir` 的原串(非空,≤ core `READ_ROOT_CANDIDATE_DIR_MAX`;超限整只拒,不截:截过的目录是另一个目录)。 */
|
|
725
|
+
export declare const READ_ROOT_GRANT_SHAPE = "readRootGrant must be the card's readRootCandidate.dir sent back byte for byte \u2014 a non-empty string of at most 1024 characters";
|
|
698
726
|
export declare const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope \u2014 the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
|
|
699
727
|
/** Validate the respond body — the closed three-choice enum (rationale in the module header). */
|
|
700
728
|
export declare function parseToolApprovalResponse(body: unknown): {
|
|
@@ -704,6 +732,7 @@ export declare function parseToolApprovalResponse(body: unknown): {
|
|
|
704
732
|
persistRule?: ParsedPersistRule;
|
|
705
733
|
note?: string;
|
|
706
734
|
settledBy?: string;
|
|
735
|
+
readRootGrant?: string;
|
|
707
736
|
} | {
|
|
708
737
|
ok: false;
|
|
709
738
|
error: string;
|
package/dist/tool-approval.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { contentKindOf } from "./approval-content-kind.js";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import { uuidv7, MAX_RULE_TEXT_CHARS, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
|
|
3
|
+
import { uuidv7, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
|
|
4
4
|
import { precheckCardRuleText } from "./rules-consent.js";
|
|
5
5
|
import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
|
|
6
6
|
import { hasBidiControls } from "./text-bidi.js";
|
|
@@ -8,7 +8,7 @@ import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerpri
|
|
|
8
8
|
import { createLogger } from "./observability/logger.js";
|
|
9
9
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
10
10
|
import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
11
|
-
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
|
|
11
|
+
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
|
|
12
12
|
import { POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
|
|
13
13
|
import { settleByPolicy } from "./approval-policy-settlement.js";
|
|
14
14
|
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
@@ -111,6 +111,8 @@ function respondSettledBy(parsed) {
|
|
|
111
111
|
return parsed.settledBy;
|
|
112
112
|
}
|
|
113
113
|
export const SETTLED_BY_DENY_ONLY = 'settledBy is only meaningful with decision "deny" — only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door\'s own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision';
|
|
114
|
+
export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with decision "allow" — it is the card\'s "Yes, and add directory" answer (allow this call and add the card\'s readRootCandidate.dir to this run\'s read directories)';
|
|
115
|
+
export const READ_ROOT_GRANT_SHAPE = `readRootGrant must be the card's readRootCandidate.dir sent back byte for byte — a non-empty string of at most ${READ_ROOT_CANDIDATE_DIR_MAX} characters`;
|
|
114
116
|
export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
|
|
115
117
|
export function parseToolApprovalResponse(body) {
|
|
116
118
|
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
@@ -161,6 +163,12 @@ export function parseToolApprovalResponse(body) {
|
|
|
161
163
|
if (rawSettledBy !== undefined && typeof rawSettledBy !== "string")
|
|
162
164
|
return { ok: false, error: SETTLED_BY_MUST_BE_STRING };
|
|
163
165
|
const settledBy = rawSettledBy;
|
|
166
|
+
const rawGrant = body.readRootGrant;
|
|
167
|
+
if (rawGrant !== undefined && d !== "allow")
|
|
168
|
+
return { ok: false, error: READ_ROOT_GRANT_ALLOW_ONLY };
|
|
169
|
+
if (rawGrant !== undefined && (typeof rawGrant !== "string" || rawGrant === "" || rawGrant.length > READ_ROOT_CANDIDATE_DIR_MAX))
|
|
170
|
+
return { ok: false, error: READ_ROOT_GRANT_SHAPE };
|
|
171
|
+
const readRootGrant = rawGrant;
|
|
164
172
|
return {
|
|
165
173
|
ok: true,
|
|
166
174
|
value: d,
|
|
@@ -168,6 +176,7 @@ export function parseToolApprovalResponse(body) {
|
|
|
168
176
|
...(persistRule !== undefined ? { persistRule } : {}),
|
|
169
177
|
...(note !== undefined ? { note } : {}),
|
|
170
178
|
...(rawSettledBy !== undefined ? { settledBy } : {}),
|
|
179
|
+
...(readRootGrant !== undefined ? { readRootGrant } : {}),
|
|
171
180
|
};
|
|
172
181
|
}
|
|
173
182
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
@@ -938,6 +947,8 @@ export class ToolApprovalCoordinator {
|
|
|
938
947
|
const bounded = boundArgs(req.args);
|
|
939
948
|
let effectiveGovernanceForced = governanceForced;
|
|
940
949
|
let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
|
|
950
|
+
const mandateWord = readClosedWordSeat(req, "mandate");
|
|
951
|
+
const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
|
|
941
952
|
const frame = {
|
|
942
953
|
type: "tool_approval",
|
|
943
954
|
approvalId: id,
|
|
@@ -963,7 +974,8 @@ export class ToolApprovalCoordinator {
|
|
|
963
974
|
...(governanceForced ? { governanceForced: true } : {}),
|
|
964
975
|
...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
965
976
|
...(req.mandated === true ? { mandated: true } : {}),
|
|
966
|
-
...(
|
|
977
|
+
...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
|
|
978
|
+
...(originWord !== undefined ? { origin: originWord } : {}),
|
|
967
979
|
...(typeof req.persistedRuleShadowed === "string" && req.persistedRuleShadowed !== ""
|
|
968
980
|
? { persistedRuleShadowed: redactSecrets(req.persistedRuleShadowed).slice(0, MAX_RULE_TEXT_CHARS) }
|
|
969
981
|
: {}),
|
|
@@ -1170,11 +1182,17 @@ export class ToolApprovalCoordinator {
|
|
|
1170
1182
|
return unattended === "unavailable" ? unattended : shapeDeny(unattended);
|
|
1171
1183
|
}
|
|
1172
1184
|
if (settled.allowed) {
|
|
1173
|
-
|
|
1185
|
+
if (settled.updatedInput === undefined && settled.readRootGrant === undefined)
|
|
1186
|
+
return true;
|
|
1187
|
+
return {
|
|
1188
|
+
allow: true,
|
|
1189
|
+
...(settled.updatedInput !== undefined ? { updatedInput: settled.updatedInput } : {}),
|
|
1190
|
+
...(settled.readRootGrant !== undefined ? { readRootGrant: settled.readRootGrant } : {}),
|
|
1191
|
+
};
|
|
1174
1192
|
}
|
|
1175
1193
|
return shapeDeny(false);
|
|
1176
1194
|
};
|
|
1177
|
-
const settle = (allowed, outcome, updatedInput, hostSettledBy, denyReason) => {
|
|
1195
|
+
const settle = (allowed, outcome, updatedInput, hostSettledBy, denyReason, readRootGrant) => {
|
|
1178
1196
|
if (done)
|
|
1179
1197
|
return;
|
|
1180
1198
|
done = true;
|
|
@@ -1208,7 +1226,7 @@ export class ToolApprovalCoordinator {
|
|
|
1208
1226
|
...(hostSettledBy !== undefined ? { settledBy: hostSettledBy } : {}),
|
|
1209
1227
|
settledAtMs: Date.now(),
|
|
1210
1228
|
});
|
|
1211
|
-
resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}) });
|
|
1229
|
+
resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}), ...(allowed && readRootGrant !== undefined ? { readRootGrant } : {}) });
|
|
1212
1230
|
};
|
|
1213
1231
|
const broadcastClaimTruth = (read) => {
|
|
1214
1232
|
if (askId === undefined)
|
|
@@ -1483,6 +1501,8 @@ export class ToolApprovalCoordinator {
|
|
|
1483
1501
|
...(effectiveGovernanceForced ? { governanceForced: true } : {}),
|
|
1484
1502
|
...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
1485
1503
|
...(req.mandated === true ? { mandated: true } : {}),
|
|
1504
|
+
...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
|
|
1505
|
+
...(originWord !== undefined ? { origin: originWord } : {}),
|
|
1486
1506
|
...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
|
|
1487
1507
|
};
|
|
1488
1508
|
selfEntry = pendingEntry;
|
|
@@ -2051,7 +2071,7 @@ export class ToolApprovalCoordinator {
|
|
|
2051
2071
|
approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
|
|
2052
2072
|
const result = this.finishRespond(id, entry, parsed, {
|
|
2053
2073
|
noteRecorded: noteLanded,
|
|
2054
|
-
...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow) } : {}),
|
|
2074
|
+
...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow), settleOutcomeIsThisRequest: ambiguousConfirmedRow === undefined } : {}),
|
|
2055
2075
|
...(ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
|
|
2056
2076
|
});
|
|
2057
2077
|
if (winnerRow !== undefined)
|
|
@@ -2077,7 +2097,8 @@ export class ToolApprovalCoordinator {
|
|
|
2077
2097
|
}
|
|
2078
2098
|
const allowed = parsed.value !== "deny";
|
|
2079
2099
|
const settleArgs = opts?.settleOutcome !== undefined ? settleArgsOfDecided(opts.settleOutcome) : { allowed, updatedInput: parsed.updatedInput, settledBy: respondSettledBy(parsed) };
|
|
2080
|
-
|
|
2100
|
+
const readRootGrant = opts?.settleOutcome === undefined || opts.settleOutcomeIsThisRequest === true ? parsed.readRootGrant : undefined;
|
|
2101
|
+
entry.settle(settleArgs.allowed, settleArgs.allowed ? "allowed" : "denied", settleArgs.updatedInput, settleArgs.settledBy, settleArgs.allowed ? undefined : parsed.note, readRootGrant);
|
|
2081
2102
|
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|
|
2082
2103
|
const noteRecorded = opts?.noteRecorded ?? false;
|
|
2083
2104
|
return {
|
|
@@ -2107,6 +2128,8 @@ export class ToolApprovalCoordinator {
|
|
|
2107
2128
|
...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
|
|
2108
2129
|
...(e.governanceForced ? { governanceForced: true } : {}),
|
|
2109
2130
|
...(e.mandated ? { mandated: true } : {}),
|
|
2131
|
+
...(e.mandate !== undefined ? { mandate: e.mandate } : {}),
|
|
2132
|
+
...(e.origin !== undefined ? { origin: e.origin } : {}),
|
|
2110
2133
|
...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
|
|
2111
2134
|
...(contentKind !== undefined ? { contentKind } : {}),
|
|
2112
2135
|
frame: e.frame.approvalId === id ? e.frame : { ...e.frame, approvalId: id },
|
|
@@ -43,7 +43,7 @@ type _GuardBgNotifReverse = AssertAllKeysHandled<Exclude<BgNotifProjected | BgNo
|
|
|
43
43
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
|
|
44
44
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
45
45
|
type _GuardRosterReverse = AssertAllKeysHandled<Exclude<RosterProjected, keyof RosterEntry>>;
|
|
46
|
-
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate" | "mandated";
|
|
46
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate" | "mandated" | "mandate";
|
|
47
47
|
type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
|
|
48
48
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
49
49
|
type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
|
|
@@ -59,7 +59,7 @@ type _GuardMailboxReverse = AssertAllKeysHandled<Exclude<MailboxProjected, keyof
|
|
|
59
59
|
type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
|
|
60
60
|
type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
|
|
61
61
|
type _GuardMailboxAppendReverse = AssertAllKeysHandled<Exclude<MailboxAppendProjected, keyof MailboxAppendMessage>>;
|
|
62
|
-
type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "writeProtection" | "teamRole";
|
|
62
|
+
type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "hands" | "writeProtection" | "teamRole";
|
|
63
63
|
type WiringSectionExcluded = "governance" | "configFingerprint" | "autoConsolidation" | "readDeny" | "peerPresence" | "fileHistory";
|
|
64
64
|
type _GuardWiringSections = AssertAllKeysHandled<Exclude<keyof WiringManifest, WiringSectionProjected | WiringSectionExcluded>>;
|
|
65
65
|
type _GuardWiringSectionsReverse = AssertAllKeysHandled<Exclude<WiringSectionProjected | WiringSectionExcluded, keyof WiringManifest>>;
|
|
@@ -104,6 +104,9 @@ type _GuardWiringMcpLivenessReverse = AssertAllKeysHandled<Exclude<WiringMcpLive
|
|
|
104
104
|
type WiringRulesHandled = "storeWired" | "syncWired" | "orgGoverned";
|
|
105
105
|
type _GuardWiringRules = AssertAllKeysHandled<Exclude<keyof WiringSection<"permissionRules">, WiringRulesHandled>>;
|
|
106
106
|
type _GuardWiringRulesReverse = AssertAllKeysHandled<Exclude<WiringRulesHandled, keyof WiringSection<"permissionRules">>>;
|
|
107
|
+
type WiringHandsHandled = "mounted" | "reason";
|
|
108
|
+
type _GuardWiringHands = AssertAllKeysHandled<Exclude<keyof WiringSection<"hands">, WiringHandsHandled>>;
|
|
109
|
+
type _GuardWiringHandsReverse = AssertAllKeysHandled<Exclude<WiringHandsHandled, keyof WiringSection<"hands">>>;
|
|
107
110
|
type WiringWriteProtectionHandled = "targetView";
|
|
108
111
|
type _GuardWiringWriteProtection = AssertAllKeysHandled<Exclude<keyof WiringSection<"writeProtection">, WiringWriteProtectionHandled>>;
|
|
109
112
|
type _GuardWiringWriteProtectionReverse = AssertAllKeysHandled<Exclude<WiringWriteProtectionHandled, keyof WiringSection<"writeProtection">>>;
|
|
@@ -130,7 +133,7 @@ type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTrace
|
|
|
130
133
|
/** `T` 非 `never` 时收敛到 `never`(空集 ⇒ 残留 `"EMPTY"` ⇒ tsc 红)。 */
|
|
131
134
|
type AssertNonEmpty<T> = [T] extends [never] ? "EMPTY" : never;
|
|
132
135
|
type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<PermissionTraceKind>>;
|
|
133
|
-
type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "mandated";
|
|
136
|
+
type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "mandated" | "mandate";
|
|
134
137
|
type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
|
|
135
138
|
type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
|
|
136
139
|
type _GuardInboxSummaryReverse = AssertAllKeysHandled<Exclude<SummaryProjected | SummaryExcluded, keyof CheckpointSummary>>;
|
|
@@ -138,7 +141,7 @@ type ParkArmKeys = keyof Extract<PendingAction, {
|
|
|
138
141
|
kind: "tool_approval";
|
|
139
142
|
}>;
|
|
140
143
|
type ParkArmQueueProjected = "toolName" | "toolCallId" | "boundInputHash" | "ruleOffers" | "hasBidiControls" | keyof import("../approval-card.js").ParkRowFacts;
|
|
141
|
-
type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "
|
|
144
|
+
type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "ruleStoreUnreadable";
|
|
142
145
|
type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
|
|
143
146
|
type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
|
|
144
147
|
type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
|
|
@@ -236,6 +239,8 @@ export type CoreKeysetGuards = [
|
|
|
236
239
|
_GuardWiringSections,
|
|
237
240
|
_GuardWiringWriteProtection,
|
|
238
241
|
_GuardWiringWriteProtectionReverse,
|
|
242
|
+
_GuardWiringHands,
|
|
243
|
+
_GuardWiringHandsReverse,
|
|
239
244
|
_GuardWiringReadDeny,
|
|
240
245
|
_GuardWiringReadDenyReverse,
|
|
241
246
|
_GuardWiringAutoConsolidation,
|