@sema-agent/server 7.76.1 → 7.76.2
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/USAGE.md +47 -21
- package/dist/boot/engine-lease.d.ts +89 -11
- package/dist/boot/engine-lease.js +26 -11
- package/dist/boot/exit-last.d.ts +24 -0
- package/dist/boot/exit-last.js +46 -0
- package/dist/boot/shutdown.js +28 -4
- package/dist/config-catalog.js +3 -2
- package/dist/config-types.d.ts +23 -11
- package/dist/config.js +8 -8
- package/dist/http/principal-gate.d.ts +24 -1
- package/dist/http/principal-gate.js +16 -1
- package/dist/http/routes/a2a-serve.js +2 -2
- package/dist/http/routes/admin-config-refresh.js +2 -2
- package/dist/http/routes/admin-drain.js +2 -2
- package/dist/http/routes/admin-memory-optout.js +2 -2
- package/dist/http/routes/adoption.js +2 -2
- package/dist/http/routes/approvals-assistant.js +4 -4
- package/dist/http/routes/attachments.js +2 -2
- package/dist/http/routes/config-catalog.js +2 -2
- package/dist/http/routes/devices.js +2 -2
- package/dist/http/routes/fleet.js +2 -2
- package/dist/http/routes/leader.js +2 -2
- package/dist/http/routes/memory-bundle.js +2 -2
- package/dist/http/routes/memory-compliance.js +2 -2
- package/dist/http/routes/memory-consolidation.js +2 -2
- package/dist/http/routes/memory-origin.js +2 -2
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/notify-wake.js +3 -3
- package/dist/http/routes/observability.js +2 -2
- package/dist/http/routes/retention-ops.js +2 -2
- package/dist/http/routes/run-memory-capture.js +2 -2
- package/dist/http/routes/runs.js +13 -13
- package/dist/http/routes/sessions.js +7 -7
- package/dist/http/routes/side-query.js +2 -2
- package/dist/http/routes/workflows.js +3 -3
- package/dist/http/server.js +2 -2
- package/dist/parent-watch.d.ts +32 -18
- package/dist/parent-watch.js +12 -10
- package/dist/tool-approval.js +2 -2
- package/dist/trace/redact.d.ts +23 -1
- package/dist/trace/redact.js +45 -11
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { windowMessages, truncateMessageBlobs } from "../../audit.js";
|
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
6
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
7
7
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
8
|
-
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
8
|
+
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
9
9
|
const MCP_STATUS_TTL_MS = 5_000;
|
|
10
10
|
const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
|
|
11
11
|
const A2A_STATUS_TTL_MS = 5_000;
|
|
@@ -36,7 +36,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
36
36
|
if (im && req.method === "GET") {
|
|
37
37
|
const principal = gatedPrincipal(req, deps.config);
|
|
38
38
|
if (deps.config.requirePrincipal && !principal) {
|
|
39
|
-
|
|
39
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
sendJson(res, 200, {
|
|
@@ -52,7 +52,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
52
52
|
if (stm && req.method === "GET") {
|
|
53
53
|
const principal = gatedPrincipal(req, deps.config);
|
|
54
54
|
if (deps.config.requirePrincipal && !principal) {
|
|
55
|
-
|
|
55
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
sendJson(res, 200, {
|
|
@@ -72,7 +72,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
72
72
|
const mm = /^\/v1\/sessions\/([^/]+)\/mcp$/.exec(url);
|
|
73
73
|
if (mm && req.method === "GET") {
|
|
74
74
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
75
|
-
|
|
75
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
|
|
@@ -122,7 +122,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
122
122
|
const am = /^\/v1\/sessions\/([^/]+)\/a2a$/.exec(url);
|
|
123
123
|
if (am && req.method === "GET") {
|
|
124
124
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
125
|
-
|
|
125
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
const specs = a2aForScenario(deps.config.a2aPeers, deps.config.defaultScenario);
|
|
@@ -420,7 +420,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
422
422
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
423
|
-
|
|
423
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
424
424
|
return;
|
|
425
425
|
}
|
|
426
426
|
if (!deps.sessionAudit) {
|
|
@@ -480,7 +480,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
480
480
|
return;
|
|
481
481
|
}
|
|
482
482
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
483
|
-
|
|
483
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
484
484
|
return;
|
|
485
485
|
}
|
|
486
486
|
if (!deps.sessionAudit) {
|
|
@@ -3,7 +3,7 @@ import { HttpError } from "../../security.js";
|
|
|
3
3
|
import { isModelAllowlisted } from "../../model-select.js";
|
|
4
4
|
import { redactSecrets } from "../../trace/redact.js";
|
|
5
5
|
import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
|
|
6
|
-
import { gatedPrincipal } from "../principal-gate.js";
|
|
6
|
+
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
7
7
|
const CORE_PREFLIGHT_VALIDATION_PREFIXES = ["Unknown model ref ", "No model for role "];
|
|
8
8
|
export function isCorePreflightValidationError(message) {
|
|
9
9
|
return CORE_PREFLIGHT_VALIDATION_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
@@ -43,7 +43,7 @@ async function handleSideQueryBody(req, res, url, ctx, miss) {
|
|
|
43
43
|
return;
|
|
44
44
|
const principal = gatedPrincipal(req, deps.config);
|
|
45
45
|
if (deps.config.requirePrincipal && !principal) {
|
|
46
|
-
|
|
46
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
let body;
|
|
@@ -9,7 +9,7 @@ import { redactErrorMessage, redactHead } from "../../observability/run-terminal
|
|
|
9
9
|
const JOURNAL_PROJECTION_MEMO_CAP = 1024;
|
|
10
10
|
const journalProjectionMemo = new Map();
|
|
11
11
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
12
|
-
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
12
|
+
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
13
13
|
const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/;
|
|
14
14
|
const WORKFLOW_AGENT_STEER_RE = /^\/v1\/workflows\/([^/]+)\/agents\/([^/]+)\/steer$/;
|
|
15
15
|
export async function handleWorkflows(req, res, url, ctx) {
|
|
@@ -29,7 +29,7 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
|
|
|
29
29
|
if (wfMatch) {
|
|
30
30
|
const principal = gatedPrincipal(req, deps.config);
|
|
31
31
|
if (deps.config.requirePrincipal && !principal) {
|
|
32
|
-
|
|
32
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
if (!deps.workflowRunStore) {
|
|
@@ -163,7 +163,7 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
|
|
|
163
163
|
}
|
|
164
164
|
const principal = gatedPrincipal(req, deps.config);
|
|
165
165
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
166
|
-
|
|
166
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
const wfRunId = safeDecode(wfSteerMatch[1]);
|
package/dist/http/server.js
CHANGED
|
@@ -85,7 +85,7 @@ export { cascadeConfig };
|
|
|
85
85
|
import { handleImages, createImagesLocal, coarseStatusForState, errorCodeForExit } from "./routes/images.js";
|
|
86
86
|
export { coarseStatusForState, errorCodeForExit };
|
|
87
87
|
import { sendJson, sendError, httpErrorCode, msg, isSseResponse } from "./send.js";
|
|
88
|
-
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator } from "./principal-gate.js";
|
|
88
|
+
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "./principal-gate.js";
|
|
89
89
|
import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
|
|
90
90
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
|
|
91
91
|
export { explicitOperatorOk, isOperator };
|
|
@@ -2331,7 +2331,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2331
2331
|
function runOwnerOk(req, res, owner, notFoundForm = "run") {
|
|
2332
2332
|
const principal = gatedPrincipal(req, deps.config);
|
|
2333
2333
|
if (deps.config.requirePrincipal && !principal) {
|
|
2334
|
-
|
|
2334
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
2335
2335
|
return false;
|
|
2336
2336
|
}
|
|
2337
2337
|
const legacyShellOwner = !deps.config.requirePrincipal && owner === "anon:shell-live" && (principal === undefined || principal === null);
|
package/dist/parent-watch.d.ts
CHANGED
|
@@ -9,23 +9,31 @@
|
|
|
9
9
|
* (core `FileStorageBackend` 的 pidfile,失效判定是 `process.kill(pid,0)` ⇒ **活孤儿是合法持锁人**),
|
|
10
10
|
* 于是此后每一只新壳起自己的引擎都撞锁 fatal「another instance (pid N) owns this data dir」—— 用户看到
|
|
11
11
|
* 的是「壳崩过一次以后就再也起不来了」。持锁 pid 不存在时 core 已自动剪陈旧锁,缺的只有「父死、子活」
|
|
12
|
-
* 这一形。所以引擎给自己装一只**自查**腿:定期问一句"生我的那个还在吗"
|
|
12
|
+
* 这一形。所以引擎给自己装一只**自查**腿:定期问一句"生我的那个还在吗"。
|
|
13
|
+
*
|
|
14
|
+
* ## 本腿**不**做什么(S-306,硬 breaking)
|
|
15
|
+
*
|
|
16
|
+
* 它**不判死、不 drain、不退进程**。父没了只说明这台引擎成了**孤儿**,而「孤儿该不该退」是**租约**
|
|
17
|
+
* 的问题:还有没有人附着、有没有在飞的 turn、静默够不够久 —— 那是 `boot/engine-lease.ts` 那一条腿,
|
|
18
|
+
* 孤儿态只是把它的窗从 `ENGINE_LINGER_MS` 换成 `ENGINE_ORPHAN_LINGER_MS`(默认 2s)。
|
|
19
|
+
* 旧出口(父死**即** drain,单拍、零附着感知)在「A 起引擎、B 按 engine.port 复用、A 正常退出」这一形
|
|
20
|
+
* 下会在 B 的 turn 中间把引擎拆掉 —— 自退律从此**只有一条**,本腿退化成它的一个输入。
|
|
13
21
|
*
|
|
14
22
|
* ## 机制(逐条都是契约)
|
|
15
23
|
*
|
|
16
24
|
* 1. **opt-in**:`SEMA_PARENT_PID` 缺席 ⇒ 整件不装配(`boot/shutdown.ts` 连 `createParentWatch` 都不调)。
|
|
17
25
|
* 存量部署逐字零变化 —— systemd / launchd / 容器托管的引擎本来就 reparent 到 1,自退是事故。
|
|
18
26
|
* 坏值(非正整数 / 超 int32)⇒ **启动期拒**,在 `config.ts` 的 `parentPidEnv()`。
|
|
19
|
-
* 2.
|
|
27
|
+
* 2. **判据 = 两支合取口一句,任一成立即标孤儿**,每 {@link PARENT_WATCH_INTERVAL_MS} 一拍:
|
|
20
28
|
*
|
|
21
29
|
* · **强身份支 `reparented`**:`process.ppid` 不再是装配那一刻的那个值。生我的进程没了,内核就把
|
|
22
30
|
* 我过继给 init(或容器里的 subreaper)—— 这是**内核对亲子关系的直答**,不依赖 pid 的身份。
|
|
23
31
|
* 判据刻意**不写成 `ppid === 1`**:① 收养者未必是 1(PID 1 是 subreaper 的容器里不是);
|
|
24
32
|
* ② 壳本身就是 pid 1 的容器形里(`SEMA_PARENT_PID=1`),`=== 1` 会在**开机第一拍**把一台健康
|
|
25
|
-
*
|
|
33
|
+
* 引擎标成孤儿。「不再是生我的那个」两种形都答得对,而且不需要任何特判。
|
|
26
34
|
* 🔴 **前提**(codex 复审 R1 [high],验真后修):这一支只有在「被监视的 pid 就是生我的那个」时
|
|
27
35
|
* 才等价于「它没了」。壳经一层 wrapper spawn 引擎却仍传**自己**的 pid 时,引擎的直接父是
|
|
28
|
-
* wrapper —— wrapper
|
|
36
|
+
* wrapper —— wrapper 先退会让这一支把一台**壳还活着**的健康引擎标成孤儿(于是它的租约窗被无故收短)。
|
|
29
37
|
* 前提在**装配那一刻**就能一次判完(`ppidAtArm === pid`),所以它是**装配期的开关**而不是每拍
|
|
30
38
|
* 的特判:不成立 ⇒ 强身份支整条不装,只留弱身份支(它在那一形下语义仍然正确),并且 armed 那行
|
|
31
39
|
* 改打 **warn** —— 覆盖面被降级了不许静默。
|
|
@@ -37,16 +45,16 @@
|
|
|
37
45
|
*
|
|
38
46
|
* 强身份支**先判**:它成立时这一拍连探活都不发 —— 弱判据没有资格给一个已经被过继走的进程改口
|
|
39
47
|
* (僵尸父那一形里 `kill(pid,0)` 恰恰恒成功)。
|
|
40
|
-
* 3. **单拍即判,不去抖**。[ref] 定稿是「15s
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* 4.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
48
|
+
* 3. **单拍即判,不去抖**。[ref] 定稿是「15s 一拍、连续两拍才判定」(上界 30s),S-292 整条退役:
|
|
49
|
+
* 部署面的判据是「壳挂了以后数据根要在秒级重新可用」,而 `ESRCH` / `ppid` 都不是会抖的读数(两者
|
|
50
|
+
* 都是内核的直答,不是采样值)。去抖买不到确定性,只买到 15s 的延迟和一个 30s 长的 pid-复用窗口。
|
|
51
|
+
* 4. **命中一次即停探**(父不会复活),出口是 {@link ParentWatchDeps.onOrphaned} —— 装配层据此记下
|
|
52
|
+
* 「孤儿了 + 是哪一支判的」,租约腿下一拍读到它就换窗。本模块自己**从不** drain、**从不**
|
|
53
|
+
* `process.exit`。已有停机属主(`isStopped`)时站下不重入(一次正在进行的优雅停机不必再被标孤儿)。
|
|
46
54
|
* 5. **interval 必须 unref**:监视腿不得成为"进程本该退出却退不掉"的理由(与 drain tick 刻意不 unref
|
|
47
55
|
* 的取向相反 —— 那一只 tick 就是停机驱动,这一只只是观察者)。
|
|
48
|
-
* 6. **日志分档**([ref] 响亮律):装配 = `info
|
|
49
|
-
*
|
|
56
|
+
* 6. **日志分档**([ref] 响亮律):装配 = `info`;标孤儿与探活自坏 = `warn`(非计划内的生命周期事件、
|
|
57
|
+
* 以及"这条腿此刻是失效的",都不许混在 info 流里)。档位是**闭集** {@link ParentWatchLogLevel},装配层用
|
|
50
58
|
* `Record<ParentWatchLogLevel, …>` 派发 ⇒ 新增一档是编译错,不会静默落进 info。
|
|
51
59
|
*
|
|
52
60
|
* ## 已知残余(成文,现版不收窄)
|
|
@@ -64,8 +72,9 @@
|
|
|
64
72
|
export declare const PARENT_WATCH_INTERVAL_MS = 2000;
|
|
65
73
|
/** 日志档位闭集。装配层按 `Record<ParentWatchLogLevel, …>` 派发 —— 新增一档必须在那里显式安置(编译错)。 */
|
|
66
74
|
export type ParentWatchLogLevel = "info" | "warn";
|
|
67
|
-
/**
|
|
68
|
-
|
|
75
|
+
/** 成孤儿的缘由闭集(上 `parent_watch_orphaned` 的 `reason` 键,运维按它区分两支;也是租约自退留痕
|
|
76
|
+
* `exit-last.json` 与 `engine_lease_auto_exit` 事件里 `reason` 的孤儿那两个词)。 */
|
|
77
|
+
export type ParentOrphanReason = "reparented" | "esrch";
|
|
69
78
|
export interface ParentWatchDeps {
|
|
70
79
|
/** 被监视的父 pid(`config.parentPid`,已在 config 层校验为正整数)。 */
|
|
71
80
|
pid: number;
|
|
@@ -75,11 +84,16 @@ export interface ParentWatchDeps {
|
|
|
75
84
|
/** 亲子关系 seam:缺省 = `process.ppid`(**活读**,不是 boot 时的快照 —— 实测过继后当场变)。
|
|
76
85
|
* 存在的理由与 `probe` 相同:reparent 在进程内测试里没有确定性的触发法。 */
|
|
77
86
|
readPpid?: () => number;
|
|
78
|
-
/** 已有停机属主(`closing || draining`)
|
|
79
|
-
*
|
|
87
|
+
/** 已有停机属主(`closing || draining`)时监视腿站下:停机已经在走了,再标一次孤儿改变不了任何事,
|
|
88
|
+
* 只会给一次计划内的停机添一条误导的 warn。 */
|
|
80
89
|
isStopped: () => boolean;
|
|
81
|
-
/**
|
|
82
|
-
|
|
90
|
+
/**
|
|
91
|
+
* **唯一出口**(S-306):父不在了 ⇒ 把这台引擎标成孤儿。**至多调用一次**(命中即停探)。
|
|
92
|
+
*
|
|
93
|
+
* 🔴 它**不是** drain:退不退由 `boot/engine-lease.ts` 的租约律按「无附着 ∧ 无在飞 ∧ 静默满
|
|
94
|
+
* `orphanLingerMs`」判。装配层拿到 reason 之后要做的只有一件事 —— 记下孤儿态(时刻 + 缘由)。
|
|
95
|
+
*/
|
|
96
|
+
onOrphaned: (reason: ParentOrphanReason) => void;
|
|
83
97
|
log: (level: ParentWatchLogLevel, event: string, fields: Record<string, unknown>) => void;
|
|
84
98
|
}
|
|
85
99
|
/** 装配并**立刻**起监视(装配即 `parent_watch_armed`)。返回停机链用的 `stop()`。 */
|
package/dist/parent-watch.js
CHANGED
|
@@ -12,11 +12,11 @@ export function createParentWatch(deps) {
|
|
|
12
12
|
directParent,
|
|
13
13
|
intervalMs: PARENT_WATCH_INTERVAL_MS,
|
|
14
14
|
note: directParent
|
|
15
|
-
? "engine watches this pid; it
|
|
16
|
-
"
|
|
17
|
-
"Unset SEMA_PARENT_PID to disable."
|
|
15
|
+
? "engine watches this pid; it marks ITSELF an orphan as soon as EITHER the engine is reparented away from " +
|
|
16
|
+
"it OR process.kill(pid,0) reports ESRCH (EPERM = alive). Being orphaned does NOT shut the engine down — " +
|
|
17
|
+
"it only shortens the attachment-lease window to ENGINE_ORPHAN_LINGER_MS. Unset SEMA_PARENT_PID to disable."
|
|
18
18
|
: "SEMA_PARENT_PID is NOT this engine's direct parent (ppid differs at arm time) — a wrapper process sits " +
|
|
19
|
-
"in between. The reparent arm is therefore NOT armed (it would
|
|
19
|
+
"in between. The reparent arm is therefore NOT armed (it would orphan a healthy engine when the wrapper " +
|
|
20
20
|
"exits while the watched process is still alive); only the process.kill(pid,0)/ESRCH arm is active, so a " +
|
|
21
21
|
"watched parent that exits WITHOUT being reaped (zombie) will not be detected. Spawn the engine directly " +
|
|
22
22
|
"from the process whose pid you pass.",
|
|
@@ -30,7 +30,7 @@ export function createParentWatch(deps) {
|
|
|
30
30
|
}
|
|
31
31
|
const ppid = readPpid();
|
|
32
32
|
if (directParent && ppid !== ppidAtArm) {
|
|
33
|
-
|
|
33
|
+
markOrphaned("reparented", { ppidAtArm, ppid });
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
@@ -39,7 +39,7 @@ export function createParentWatch(deps) {
|
|
|
39
39
|
catch (err) {
|
|
40
40
|
const code = err?.code;
|
|
41
41
|
if (code === "ESRCH") {
|
|
42
|
-
|
|
42
|
+
markOrphaned("esrch", { ppid });
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
if (code !== "EPERM" && !probeErrorReported) {
|
|
@@ -56,15 +56,17 @@ export function createParentWatch(deps) {
|
|
|
56
56
|
}
|
|
57
57
|
}, PARENT_WATCH_INTERVAL_MS);
|
|
58
58
|
timer.unref?.();
|
|
59
|
-
function
|
|
59
|
+
function markOrphaned(reason, fields) {
|
|
60
60
|
stop();
|
|
61
|
-
deps.log("warn", "
|
|
61
|
+
deps.log("warn", "parent_watch_orphaned", {
|
|
62
62
|
pid: deps.pid,
|
|
63
63
|
reason,
|
|
64
64
|
...fields,
|
|
65
|
-
note: "the parent that spawned this engine is gone —
|
|
65
|
+
note: "the parent that spawned this engine is gone — this engine is now an ORPHAN. It does NOT shut down here: " +
|
|
66
|
+
"it stays up while anything is attached or in flight, and self-exits via the SIGTERM graceful drain once " +
|
|
67
|
+
"the attachment lease has been idle for ENGINE_ORPHAN_LINGER_MS (see engine_lease_auto_exit).",
|
|
66
68
|
});
|
|
67
|
-
deps.
|
|
69
|
+
deps.onOrphaned(reason);
|
|
68
70
|
}
|
|
69
71
|
function stop() {
|
|
70
72
|
if (stopped)
|
package/dist/tool-approval.js
CHANGED
|
@@ -2,7 +2,7 @@ import { contentKindOf } from "./approval-content-kind.js";
|
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { uuidv7, MAX_RULE_TEXT_CHARS, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
|
|
4
4
|
import { precheckCardRuleText } from "./rules-consent.js";
|
|
5
|
-
import {
|
|
5
|
+
import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
|
|
6
6
|
import { hasBidiControls } from "./text-bidi.js";
|
|
7
7
|
import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerprint.js";
|
|
8
8
|
import { createLogger } from "./observability/logger.js";
|
|
@@ -32,7 +32,7 @@ const DEFAULT_PARK_TOMBSTONE_TTL_MS = 60 * 60_000;
|
|
|
32
32
|
const DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX = 128;
|
|
33
33
|
const PARKING_RETRY_AFTER_MS = 250;
|
|
34
34
|
function ruleMaterialCarriesCredentialShape(command) {
|
|
35
|
-
return
|
|
35
|
+
return redactSecretsWithArms(command, { observe: false }).arms.has("full");
|
|
36
36
|
}
|
|
37
37
|
function durableRuleMaterial(material) {
|
|
38
38
|
if (material === undefined)
|
package/dist/trace/redact.d.ts
CHANGED
|
@@ -69,7 +69,9 @@ export declare function resetPpkScanBytesForTest(): void;
|
|
|
69
69
|
* core [ref] 落地后这一支**趋零是证据、不归零不是证明**。别把它当截断事实的度量用。 */
|
|
70
70
|
export type RedactionArm = "full" | "fragment";
|
|
71
71
|
export interface RedactionHitMeta {
|
|
72
|
-
/**
|
|
72
|
+
/** 命中臂的**判别力档位**(闭集两词,S-303):`"fragment"` = {@link FRAGMENT_SECRET_PATTERNS} 那一族
|
|
73
|
+
* 「短前缀 + 长度/熵阈值」的弱证据臂;`"full"` = 其余全部(结构锚 / 词法关键字 / 定长裸形)。
|
|
74
|
+
* 判据 = 表归属,不是 token 的 `:partial` 后缀 —— 两条轴的分家逐字见 {@link armOfToken}。 */
|
|
73
75
|
arm: RedactionArm;
|
|
74
76
|
}
|
|
75
77
|
export declare function setRedactionObserver(fn: ((patternLabel: string, count: number, meta: RedactionHitMeta) => void) | undefined): void;
|
|
@@ -77,6 +79,26 @@ export declare function redactSecrets(text: string, opts?: {
|
|
|
77
79
|
observe?: boolean;
|
|
78
80
|
json?: boolean;
|
|
79
81
|
}): string;
|
|
82
|
+
/**
|
|
83
|
+
* 脱敏 + **这一遍命中了哪些档位的臂**({@link RedactionArm} 闭集两词)—— 与 {@link redactSecrets} 同一只
|
|
84
|
+
* 脱敏器、同一张表、同一遍扫描的另一个出口。
|
|
85
|
+
*
|
|
86
|
+
* 🔴 **为什么要这个出口**(S-303):下游有一类消费方问的不是「脱完长什么样」而是「这段文本里有没有
|
|
87
|
+
* **强证据**的凭据形」——`tool-approval.ts` 的规则素材闸是头一个。旧形只能拿「字节变了没有」当谓词,
|
|
88
|
+
* 于是弱证据臂(短前缀 + 阈值启发式)的一次多脱就等于一次「素材整只扣下」,用户拿不到常驻放行。
|
|
89
|
+
* 出口开在脱敏器**自己**身上是硬约束:全仓只该有一个「什么算凭据」的属主,消费方**不许**按 token 字面
|
|
90
|
+
* (`includes(":partial»")`)或另跑一遍正则去反推 —— 那就是第二只脱敏器。
|
|
91
|
+
*
|
|
92
|
+
* `arms` 是**这一遍真命中的档位集合**:空集 = 一条臂都没命中(文本可能仍被归一化剥了 `\p{Cf}`,
|
|
93
|
+
* 所以「`text !== 入参`」与「`arms` 非空」不是一回事,后者才是凭据形的证据)。
|
|
94
|
+
*/
|
|
95
|
+
export declare function redactSecretsWithArms(text: string, opts?: {
|
|
96
|
+
observe?: boolean;
|
|
97
|
+
json?: boolean;
|
|
98
|
+
}): {
|
|
99
|
+
text: string;
|
|
100
|
+
arms: ReadonlySet<RedactionArm>;
|
|
101
|
+
};
|
|
80
102
|
/**
|
|
81
103
|
* 🔴 环 + 深度预算(2026-07-25 补)。原实现是纯递归、**既不记已访问节点也不限深度**,而它的调用位置是
|
|
82
104
|
* **最不可信的边界**:`toolStart`/`toolEnd` 把工具的 `args`/`output`/`structured` 直接喂进来。后果两条:
|
package/dist/trace/redact.js
CHANGED
|
@@ -761,7 +761,20 @@ function redactionTokenEndAt(s, p, limit) {
|
|
|
761
761
|
return 0;
|
|
762
762
|
}
|
|
763
763
|
const labelOfToken = (token) => token.replace(/[«»]/g, "").replace(/^redacted:/, "");
|
|
764
|
-
const
|
|
764
|
+
const FRAGMENT_ARM_TOKENS = new Set(FRAGMENT_SECRET_PATTERNS.map(([, repl]) => repl));
|
|
765
|
+
{
|
|
766
|
+
const partialSuffix = ":partial»";
|
|
767
|
+
const hasPartialToken = (repl) => (repl.match(/«[^»]*»/g) ?? [repl]).some((t) => t.endsWith(partialSuffix));
|
|
768
|
+
for (const [, repl] of FRAGMENT_SECRET_PATTERNS) {
|
|
769
|
+
if (!hasPartialToken(repl))
|
|
770
|
+
throw new Error(`redact: fragment-arm replacement ${JSON.stringify(repl)} must end with ${partialSuffix}`);
|
|
771
|
+
}
|
|
772
|
+
for (const [, repl] of [...STRUCTURAL_SECRET_PATTERNS, ...KEYWORD_SECRET_PATTERNS, ...SHAPE_SECRET_PATTERNS]) {
|
|
773
|
+
if (hasPartialToken(repl))
|
|
774
|
+
throw new Error(`redact: strong-arm replacement ${JSON.stringify(repl)} must not end with ${partialSuffix}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const armOfToken = (repl) => (FRAGMENT_ARM_TOKENS.has(repl) ? "fragment" : "full");
|
|
765
778
|
const NO_HITS = [];
|
|
766
779
|
const singleHit = (label, arm, r) => r.count > 0 ? { out: r.out, hits: [{ label, count: r.count, arm }] } : { out: r.out, hits: NO_HITS };
|
|
767
780
|
const regexRule = ([re, repl]) => {
|
|
@@ -1021,10 +1034,24 @@ const armPoolRule = (() => {
|
|
|
1021
1034
|
const out = applySpans(text, kept).out;
|
|
1022
1035
|
if (!wantCount || kept.length === 0)
|
|
1023
1036
|
return { out, hits: NO_HITS };
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1037
|
+
const strong = spans.filter((sp) => armOfToken(sp.repl) === "full").sort((a, b) => a.start - b.start);
|
|
1038
|
+
const byArmedToken = new Map();
|
|
1039
|
+
let si = 0;
|
|
1040
|
+
let strongMaxEnd = -1;
|
|
1041
|
+
for (const sp of kept) {
|
|
1042
|
+
while (si < strong.length && strong[si].start < sp.end) {
|
|
1043
|
+
strongMaxEnd = Math.max(strongMaxEnd, strong[si].end);
|
|
1044
|
+
si += 1;
|
|
1045
|
+
}
|
|
1046
|
+
const arm = strongMaxEnd > sp.start ? "full" : armOfToken(sp.repl);
|
|
1047
|
+
const key = `${sp.repl}\u0000${arm}`;
|
|
1048
|
+
const cur = byArmedToken.get(key);
|
|
1049
|
+
if (cur === undefined)
|
|
1050
|
+
byArmedToken.set(key, { token: sp.repl, arm, count: 1 });
|
|
1051
|
+
else
|
|
1052
|
+
cur.count += 1;
|
|
1053
|
+
}
|
|
1054
|
+
return { out, hits: [...byArmedToken.values()].map(({ token, arm, count }) => ({ label: labelOfToken(token), count, arm })) };
|
|
1028
1055
|
},
|
|
1029
1056
|
};
|
|
1030
1057
|
})();
|
|
@@ -1063,13 +1090,20 @@ function reportHits(hits) {
|
|
|
1063
1090
|
}
|
|
1064
1091
|
}
|
|
1065
1092
|
}
|
|
1066
|
-
|
|
1067
|
-
const wantCount = onRedaction !== undefined &&
|
|
1068
|
-
const
|
|
1069
|
-
const r = applyRules(normalizeForRedaction(text), wantCount, json, opts?.observe ?? true);
|
|
1093
|
+
function runRedaction(text, observe, json, wantHits) {
|
|
1094
|
+
const wantCount = wantHits || (onRedaction !== undefined && observe);
|
|
1095
|
+
const r = applyRules(normalizeForRedaction(text), wantCount, json, observe);
|
|
1070
1096
|
forgetEscapedSeparatorTails();
|
|
1071
|
-
|
|
1072
|
-
|
|
1097
|
+
if (observe)
|
|
1098
|
+
reportHits(r.hits);
|
|
1099
|
+
return { text: r.out, hits: r.hits };
|
|
1100
|
+
}
|
|
1101
|
+
export function redactSecrets(text, opts) {
|
|
1102
|
+
return runRedaction(text, opts?.observe ?? true, opts?.json === true, false).text;
|
|
1103
|
+
}
|
|
1104
|
+
export function redactSecretsWithArms(text, opts) {
|
|
1105
|
+
const r = runRedaction(text, opts?.observe ?? true, opts?.json === true, true);
|
|
1106
|
+
return { text: r.text, arms: new Set(r.hits.map((h) => h.arm)) };
|
|
1073
1107
|
}
|
|
1074
1108
|
const REDACT_MAX_DEPTH = 64;
|
|
1075
1109
|
export function redactDeep(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.76.
|
|
3
|
+
"version": "7.76.2",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|