@sema-agent/server 7.101.0 → 7.102.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 +23 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +58 -4
- package/dist/approval-card.d.ts +35 -2
- package/dist/approval-card.js +19 -1
- package/dist/background-session-reap.d.ts +55 -0
- package/dist/background-session-reap.js +90 -0
- package/dist/boot/config-center.d.ts +16 -4
- package/dist/boot/config-center.js +44 -15
- 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-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 +9 -3
- package/dist/capabilities/builtin-tools.d.ts +14 -1
- package/dist/capabilities/builtin-tools.js +3 -0
- 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 +9 -4
- package/dist/config-center/apply-effective.js +5 -12
- 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/read-warnings.d.ts +19 -48
- package/dist/config-center/read-warnings.js +7 -49
- package/dist/config-center/restart-signal.d.ts +8 -11
- package/dist/config-center/types.d.ts +4 -2
- package/dist/config-provider.js +1 -48
- package/dist/config-types.d.ts +77 -5
- package/dist/config-types.js +5 -0
- package/dist/config.js +49 -13
- package/dist/device-store.d.ts +1 -1
- package/dist/fleet/fleet-bus.d.ts +30 -4
- package/dist/fleet/fleet-bus.js +20 -4
- package/dist/fleet/fleet-reconciler.d.ts +29 -6
- package/dist/fleet/fleet-reconciler.js +109 -79
- package/dist/fleet-client.d.ts +21 -7
- package/dist/fleet-client.js +41 -12
- package/dist/fleet-lease.d.ts +1 -1
- package/dist/http/approval-sweeps.js +5 -20
- package/dist/http/dispatch.js +20 -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 +21 -8
- package/dist/http/server.js +2 -2
- package/dist/model-entry-refusal.d.ts +11 -10
- package/dist/model-entry-refusal.js +13 -10
- package/dist/observability/fail-open.d.ts +6 -2
- package/dist/observability/fail-open.js +6 -2
- 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 +11 -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 +18 -5
- package/dist/tool-approval.d.ts +39 -1
- package/dist/tool-approval.js +37 -8
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/package.json +3 -3
|
@@ -50,6 +50,7 @@ function entryInvalidRefusal(id, provider, fields, issues) {
|
|
|
50
50
|
"deployment's catalog on its own (the other entries keep serving) instead of being served with a value nothing validated; fix the entry to bring it back",
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
const MALFORMED_CARRIER_NOT_A_LIST = "(droppedIdentities is not a list)";
|
|
53
54
|
export function judgeCatalogEntries(enabled, ctx, dropped) {
|
|
54
55
|
const usable = [];
|
|
55
56
|
const refused = {};
|
|
@@ -60,20 +61,22 @@ export function judgeCatalogEntries(enabled, ctx, dropped) {
|
|
|
60
61
|
else
|
|
61
62
|
refused[String(m.name)] = r;
|
|
62
63
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
64
|
+
const rows = dropped === undefined ? [] : Array.isArray(dropped) ? dropped : undefined;
|
|
65
|
+
if (rows === undefined)
|
|
66
|
+
return { usable, refused, malformed: [MALFORMED_CARRIER_NOT_A_LIST] };
|
|
67
|
+
const malformed = [];
|
|
68
|
+
for (const d of rows) {
|
|
65
69
|
const e = (typeof d === "object" && d !== null ? d : {});
|
|
66
|
-
const where = typeof e.entry === "string" ? e.entry : "(unknown coordinate)";
|
|
67
70
|
if (typeof e.name !== "string") {
|
|
68
|
-
|
|
71
|
+
malformed.push(typeof e.entry === "string" ? e.entry : "(unknown coordinate)");
|
|
69
72
|
continue;
|
|
70
73
|
}
|
|
71
74
|
if (Object.hasOwn(refused, e.name))
|
|
72
75
|
continue;
|
|
73
76
|
const fields = Array.isArray(e.fields) ? e.fields.filter((f) => typeof f === "string") : [];
|
|
74
|
-
refused[e.name] = entryInvalidRefusal(typeof e.id === "string" ? e.id : "",
|
|
77
|
+
refused[e.name] = entryInvalidRefusal(typeof e.id === "string" ? e.id : "", "", fields, typeof e.issues === "string" ? e.issues : fields.join(", "));
|
|
75
78
|
}
|
|
76
|
-
return { usable, refused,
|
|
79
|
+
return { usable, refused, malformed };
|
|
77
80
|
}
|
|
78
81
|
export function refusalEventOf(reason) {
|
|
79
82
|
switch (reason) {
|
|
@@ -149,9 +152,9 @@ export function refusedNameMintedRefusal(view) {
|
|
|
149
152
|
"an explicit pick of that name (or a role bound to it) would silently run on the other entry's endpoint and account; " +
|
|
150
153
|
"rename the unavailable entry, fix it (see its own warn), or rebind / drop the tier binding that mints the name.");
|
|
151
154
|
}
|
|
152
|
-
export function
|
|
153
|
-
return (`
|
|
154
|
-
"
|
|
155
|
-
"the
|
|
155
|
+
export function malformedDroppedCarrierRefusal(entries) {
|
|
156
|
+
return (`the dropped-entry identity carrier of this candidate is malformed [${REFUSED_NAME_MINTED_CODE}]: ${entries.join(", ")} — ` +
|
|
157
|
+
"every row the settings-schema read writes there carries a string name, so this carrier was edited outside the read legs (e.g. a hand-edited last-known-good file); " +
|
|
158
|
+
"the candidate cannot prove the tier/alias expansion does not re-mint the unnamed entries, so its model plane is refused — restore or delete that carrier (a fresh read of the source re-mints it).");
|
|
156
159
|
}
|
|
157
160
|
//# sourceMappingURL=model-entry-refusal.js.map
|
|
@@ -123,7 +123,7 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
123
123
|
};
|
|
124
124
|
readonly "server.approvals.park-row-blob-unreadable": {
|
|
125
125
|
readonly cls: "F";
|
|
126
|
-
readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
|
|
126
|
+
readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)/ `mandate`(S-721)/ `origin`(S-755)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
|
|
127
127
|
};
|
|
128
128
|
readonly "server.sql-json.checkpoint.pending-steer-queue-lossy": {
|
|
129
129
|
readonly cls: "F";
|
|
@@ -173,6 +173,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
173
173
|
readonly cls: "F";
|
|
174
174
|
readonly note: "A-032 P1-④:`runStatusToFleet` 收到**词表外**的 run 状态词(闭集签名之外的唯一来路 = `rowToRun` 把无约束的 status 文本列裸 cast,滚动升级里更新的副本写的新词被旧副本读到)⇒ 折成 `idle`。放行的最坏后果 = 该行在 fleet 面板上**永久滞留**(removal 集 {completed,failed,killed} 收不到 idle),纯展示面、不影响 run 本身与任何执法判据,故 F 类。方向刻意不改(把 miss 折成终局会把一条还在跑的任务从面板上摘掉,误摘比多一行更伤)。留痕是承重的:2026-06-28 on-box e2e 逮到的 `blocked` 泄漏当年只能靠人眼在面板上发现——计数让下一次词表漂移在遥测里当场显形。编译期一侧已由闭集入参 + `never` 臂执法。";
|
|
175
175
|
};
|
|
176
|
+
readonly "server.fleet.agent-status-unmapped": {
|
|
177
|
+
readonly cls: "F";
|
|
178
|
+
readonly note: "S-751:fleet 对账腿问 bg registry「这只子代还活着吗」时,读到**词表外**的 `BackgroundAgentRecord.status`(闭集之外的唯一来路 = SQL 孪生把 status 文本列裸 cast 成联合,滚动升级里更新的 core 写下的新词被本副本读到)⇒ 按「属主作保」处置:子代行**留在面板上**。放行的最坏后果 = 一条其实已终局的子代行多留在面板上(它的终态通知仍会结算它;与 #261 之前「子代行永远不走」同形),纯展示面、不影响子代本身与任何执法判据,故 F 类。方向刻意不折成「死」:把不认识的词读成死会把一只正在跑的后台子代从面板上摘掉,而那正是 S-751 报案的症状(误摘比多一行更伤,与 `server.fleet.run-status-unmapped` 同一条判据)。编译期一侧由 `bgAgentVouches` 的穷举 switch + `never` 臂执法;留痕让词表漂移在遥测里当场显形。";
|
|
179
|
+
};
|
|
176
180
|
readonly "server.run-store.stale-claim-quarantine-unremoved": {
|
|
177
181
|
readonly cls: "F";
|
|
178
182
|
readonly note: "#213 陈旧 claim 接管:claim 已 rename 进 tmp/ 隔离名(接管本体已完成、正确性不受影响),但隔离件 rmSync 失败留在 tmp。放行的最坏后果 = tmp 里多一个死文件(不在 activeDir,hydrate 永不把它当 claim 复活);故 F 类。必须留痕:反复删不掉 = tmp 权限/fs 病灶,静默吞掉会让 tmp 无声膨胀。";
|
|
@@ -283,7 +287,7 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
283
287
|
};
|
|
284
288
|
readonly "server.approval-card.closed-word-out-of-set": {
|
|
285
289
|
readonly cls: "F";
|
|
286
|
-
readonly note: "S-249(`approval-card.ts`
|
|
290
|
+
readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天两个成员:`readRuleOffersAbsence`,与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
|
|
287
291
|
};
|
|
288
292
|
readonly "server.pg-pool.idle-client-error": {
|
|
289
293
|
readonly cls: "F";
|
|
@@ -114,7 +114,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
114
114
|
},
|
|
115
115
|
"server.approvals.park-row-blob-unreadable": {
|
|
116
116
|
cls: "F",
|
|
117
|
-
note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。",
|
|
117
|
+
note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)/ `mandate`(S-721)/ `origin`(S-755)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。",
|
|
118
118
|
},
|
|
119
119
|
"server.sql-json.checkpoint.pending-steer-queue-lossy": {
|
|
120
120
|
cls: "F",
|
|
@@ -164,6 +164,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
164
164
|
cls: "F",
|
|
165
165
|
note: "A-032 P1-④:`runStatusToFleet` 收到**词表外**的 run 状态词(闭集签名之外的唯一来路 = `rowToRun` 把无约束的 status 文本列裸 cast,滚动升级里更新的副本写的新词被旧副本读到)⇒ 折成 `idle`。放行的最坏后果 = 该行在 fleet 面板上**永久滞留**(removal 集 {completed,failed,killed} 收不到 idle),纯展示面、不影响 run 本身与任何执法判据,故 F 类。方向刻意不改(把 miss 折成终局会把一条还在跑的任务从面板上摘掉,误摘比多一行更伤)。留痕是承重的:2026-06-28 on-box e2e 逮到的 `blocked` 泄漏当年只能靠人眼在面板上发现——计数让下一次词表漂移在遥测里当场显形。编译期一侧已由闭集入参 + `never` 臂执法。",
|
|
166
166
|
},
|
|
167
|
+
"server.fleet.agent-status-unmapped": {
|
|
168
|
+
cls: "F",
|
|
169
|
+
note: "S-751:fleet 对账腿问 bg registry「这只子代还活着吗」时,读到**词表外**的 `BackgroundAgentRecord.status`(闭集之外的唯一来路 = SQL 孪生把 status 文本列裸 cast 成联合,滚动升级里更新的 core 写下的新词被本副本读到)⇒ 按「属主作保」处置:子代行**留在面板上**。放行的最坏后果 = 一条其实已终局的子代行多留在面板上(它的终态通知仍会结算它;与 #261 之前「子代行永远不走」同形),纯展示面、不影响子代本身与任何执法判据,故 F 类。方向刻意不折成「死」:把不认识的词读成死会把一只正在跑的后台子代从面板上摘掉,而那正是 S-751 报案的症状(误摘比多一行更伤,与 `server.fleet.run-status-unmapped` 同一条判据)。编译期一侧由 `bgAgentVouches` 的穷举 switch + `never` 臂执法;留痕让词表漂移在遥测里当场显形。",
|
|
170
|
+
},
|
|
167
171
|
"server.run-store.stale-claim-quarantine-unremoved": {
|
|
168
172
|
cls: "F",
|
|
169
173
|
note: "#213 陈旧 claim 接管:claim 已 rename 进 tmp/ 隔离名(接管本体已完成、正确性不受影响),但隔离件 rmSync 失败留在 tmp。放行的最坏后果 = tmp 里多一个死文件(不在 activeDir,hydrate 永不把它当 claim 复活);故 F 类。必须留痕:反复删不掉 = tmp 权限/fs 病灶,静默吞掉会让 tmp 无声膨胀。",
|
|
@@ -256,7 +260,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
256
260
|
"server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
|
|
257
261
|
"server.approval-card.closed-word-out-of-set": {
|
|
258
262
|
cls: "F",
|
|
259
|
-
note: "S-249(`approval-card.ts`
|
|
263
|
+
note: "S-249(`approval-card.ts` 的边界窄读族;今天两个成员:`readRuleOffersAbsence`,与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
|
|
260
264
|
},
|
|
261
265
|
"server.pg-pool.idle-client-error": {
|
|
262
266
|
cls: "F",
|
package/dist/parked-decide.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* 行永 parked、生命周期分叉)。
|
|
10
10
|
*/
|
|
11
11
|
import type { HostDecision } from "./host-decision.js";
|
|
12
|
-
import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, ResumeOutcome, TaskRegistry,
|
|
12
|
+
import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, ResumeOutcome, TaskRegistry, WorkflowRun, WorkflowRunStore } from "@sema-agent/core";
|
|
13
|
+
import type { ParkedReviveSurfaceFactory } from "./parked-revive-surface.js";
|
|
14
|
+
import type { RebuiltInheritedGate } from "./boot/parked-revive-gate.js";
|
|
13
15
|
import { type WorkflowRunParkKey, type WorkflowAgentSessionIndex } from "./orchestration/workflow-agent-session-index.js";
|
|
14
16
|
export interface ParkedAgentMatch {
|
|
15
17
|
handle: string;
|
|
@@ -112,9 +114,13 @@ export interface ParkedDecideDeps {
|
|
|
112
114
|
checkpointStore: Pick<CheckpointStore, "get">;
|
|
113
115
|
/** 与 reaper/scenarios 同一个 registry(claim/rollback 的 CAS 与 in-memory handle 收敛在同处)。 */
|
|
114
116
|
registry: Pick<TaskRegistry, "claimParkedAgent" | "rollbackParkedClaim">;
|
|
115
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
|
|
117
|
+
/** #1110(core 7.32.0):按**行**解析赎回面 —— 驱动哪一只 Agent 工具(生产装配,与 spawn 同源:subRunner / bg opts /
|
|
118
|
+
* checkpointStore 同实例,[ref] 设计稿 §4),以及与它同出一张列表的继承座 `mountsCallerTool`(一层行才有;属主与全部判据
|
|
119
|
+
* 在 `parked-revive-surface.ts`)。修前是 boot 期的一只裸 Agent 工具、从不交座 ⇒ 7.32.0 起停在继承工具上的 allow 恒 422。 */
|
|
120
|
+
reviveSurface: ParkedReviveSurfaceFactory;
|
|
121
|
+
/** #1110 codex r1 [high]:会话属主面(`sessionStorage.ownerOf`)—— 赎回腿在 claim 前读**根会话现在**的属主交给赎回面(属主换人 / 会话
|
|
122
|
+
* 已不在 ⇒ 不交座)。缺席 = 本部署无属主面 ⇒ 读数缺席 ⇒ 不交座(fail-closed)。读抛 ⇒ 原样上抛(claim 之前,没有东西要回滚)。 */
|
|
123
|
+
sessionOwnerOf?: (sessionId: string) => Promise<string | null | undefined>;
|
|
118
124
|
/** revive args 的 `subagent_type` 成员性判定集([ref] Q1 修正:非 roster 成员必须省略,否则
|
|
119
125
|
* unknown subagent_type 拒;字面比对 general-purpose 在 roster 撞名时会错省)。缺省空集=恒省略。 */
|
|
120
126
|
knownAgentTypes?: ReadonlySet<string>;
|
|
@@ -130,7 +136,7 @@ export interface ParkedDecideDeps {
|
|
|
130
136
|
* 「席位必须同步」是那个文件此前自设的约束、不是结构事实。 */
|
|
131
137
|
rebuildInheritedGate?: (row: BackgroundAgentRecord, identity: {
|
|
132
138
|
principal: string | undefined;
|
|
133
|
-
}) => Promise<
|
|
139
|
+
}) => Promise<RebuiltInheritedGate | undefined>;
|
|
134
140
|
warn?: (event: string, fields: Record<string, unknown>) => void;
|
|
135
141
|
}
|
|
136
142
|
export interface ParkedDecideRequest {
|
package/dist/parked-decide.js
CHANGED
|
@@ -149,6 +149,8 @@ export async function decideParkedAgent(deps, req, match) {
|
|
|
149
149
|
return { status: 409, body: { error: "parked row carries no agent name — the revive path requires a named agent (row damaged?)", errorCode: "decide.row_unrevivable", taskId: match.handle } };
|
|
150
150
|
}
|
|
151
151
|
const inheritedGate = deps.rebuildInheritedGate ? await deps.rebuildInheritedGate(row, { principal: decodeCheckpointScope(req.scope) }) : undefined;
|
|
152
|
+
const rootOwner = row.rootSessionId !== undefined && deps.sessionOwnerOf ? await deps.sessionOwnerOf(row.rootSessionId) : undefined;
|
|
153
|
+
const surface = deps.reviveSurface(row, { principal: decodeCheckpointScope(req.scope), ...(rootOwner !== undefined ? { rootOwner } : {}) });
|
|
152
154
|
const stores = { agentStore: deps.agentStore, checkpointStore: deps.checkpointStore };
|
|
153
155
|
const claim = await deps.registry.claimParkedAgent(stores, match.handle, { scope: row.scope, owner: row.owner });
|
|
154
156
|
if (!claim.ok) {
|
|
@@ -180,6 +182,7 @@ export async function decideParkedAgent(deps, req, match) {
|
|
|
180
182
|
const outcome = buildParkedResumeOutcome(req);
|
|
181
183
|
const ctx = {
|
|
182
184
|
toolCallId: `drv-${ticket.claimId}`,
|
|
185
|
+
...(surface.mountsCallerTool !== undefined ? { mountsCallerTool: surface.mountsCallerTool } : {}),
|
|
183
186
|
reviveClaim: {
|
|
184
187
|
row: { ...ticket.row, parkClaimId: ticket.claimId },
|
|
185
188
|
rev: ticket.reservedRev,
|
|
@@ -211,7 +214,7 @@ export async function decideParkedAgent(deps, req, match) {
|
|
|
211
214
|
defaultSubagentTailBus.forgetHandle(match.handle);
|
|
212
215
|
let receipt;
|
|
213
216
|
try {
|
|
214
|
-
receipt = await
|
|
217
|
+
receipt = await surface.reviveTool.execute(mintReviveArgs(row, deps.knownAgentTypes ?? new Set()), ctx);
|
|
215
218
|
}
|
|
216
219
|
catch (e) {
|
|
217
220
|
const disp = await rollback("execute_threw");
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1110(core 7.32.0)—— `/decide` parked 赎回腿的**赎回面**:驱动哪一只 Agent 工具,以及交不交继承座
|
|
3
|
+
* `ToolExecuteContext.mountsCallerTool`。
|
|
4
|
+
*
|
|
5
|
+
* core 7.32.0 起 Agent 子代按**身份**继承场景层工具(bundle 交给 Agent 的 `inheritedTools` ∩ 父 run 的 `spec.tools`),由宿主
|
|
6
|
+
* **自己**驱动 `execute` 的一方(本腿)必须交座,否则 fail-closed:复活的子代什么都不继承,而停在一只**继承工具**上的 `allow` 在消费
|
|
7
|
+
* 审批之前被拒(`parked_resume.startup_failed` ⇒ `parked-decide.ts` 的 422 臂,审批留 pending;`deny` 照常结)。
|
|
8
|
+
*
|
|
9
|
+
* 规则(core [ref] / unreleased-1110 原话,一条):**座建在那条行的根会话解析出来的同一张列表上,赎回工具取自同一张列表**。
|
|
10
|
+
* · 列表 = 内建 default 场景工厂按行的根会话重解一次(`defaultScenario({}, principal, { sessionId: root })`)—— 与生产 resolve-spec
|
|
11
|
+
* 给那个会话的 run 组 `spec.tools` 的同一只工厂、同一个会话分区(任务清单店 `taskListStoreFor(root)` 与领队那一张是同一张;
|
|
12
|
+
* 多租户的 WebFetch 过滤同样生效 ⇒ 子代同样拿不到)。座 = `(t) => tools.includes(t)`(身份判:同名副本不算)。
|
|
13
|
+
* · **禁**用 boot 期单例填座:那只 bundle 没有会话,它的任务清单是一张私有表 —— 填了座,每个会话 / 租户的复活队友都会共用它。
|
|
14
|
+
* · **根会话现在还属于这张卡的租户**才填座(codex r1 [high]):任务清单店只按会话 id 分区,而会话 id 由调用方自选、删后可被别人重领 ——
|
|
15
|
+
* 根会话删了(或换了主人)之后赎回一张旧卡,按 id 重解出来的是**新主人**的清单。属主读数由赎回腿在 claim 前读(`identity.rootOwner`,
|
|
16
|
+
* 读自会话属主面;缺席 = 会话已不在 / 读不出),与卡的权威 principal 逐字相等才填;否则不交座(fail-closed:子代不继承,继承工具上的
|
|
17
|
+
* allow 回 422、审批待决,deny 照结)。根删除时把子孙的 parked 卡一并围栏 / 过期 = 另车(会话化身轴,[ref] 同族)。
|
|
18
|
+
* · 只对**一层**行填座(`rootSessionId` 在场且 = `parentSessionId`:派它的 run 就是根会话的 run,列表正是那个 run 的挂载)。更深的行
|
|
19
|
+
* (孙代:派它的是一个子代,它的挂载是继承裁剪后的子集,行上读不出来)不填 —— 用 boot 期那只裸 Agent、无座 = core 的 fail-closed 臂;
|
|
20
|
+
* 行上带继承组成是 core #1112(7.32.x)的半场,到货再改读行。
|
|
21
|
+
* 近似如实记:重解的列表是**部署缺省**那一份,读不回那一 run 的逐请求差异(`settings.webSearch` 后端、center overlay 场景)——
|
|
22
|
+
* 座只判 bundle 实例的成员性,继承面是 `[...web, ...scaffolds]`,那两项差异不改变谁在面内。
|
|
23
|
+
*/
|
|
24
|
+
import { type BackgroundAgentRecord, type ToolSpec } from "@sema-agent/core";
|
|
25
|
+
import type { Scenario } from "./capabilities/scenarios.js";
|
|
26
|
+
/** 一次赎回用的面:驱动的 Agent 工具 + (一层行时)与它同出一张列表的继承座。 */
|
|
27
|
+
export interface ParkedReviveSurface {
|
|
28
|
+
readonly reviveTool: ToolSpec;
|
|
29
|
+
/** 缺席 = 不交座(core fail-closed:复活的子代不继承任何场景层工具)。 */
|
|
30
|
+
readonly mountsCallerTool?: (tool: ToolSpec) => boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 按行解析赎回面的入参身份:`principal` = 权威 principal(取 checkpoint 的 scope 列,不取行上有损的 `row.scope`);`rootOwner` = 行的根会话
|
|
34
|
+
* **现在**的属主(赎回腿在 claim 前读会话属主面;`null` = 匿名会话,缺席 = 会话已不在 / 读不出 / 本部署无属主面)。
|
|
35
|
+
*/
|
|
36
|
+
export interface ParkedReviveSurfaceIdentity {
|
|
37
|
+
readonly principal: string | undefined;
|
|
38
|
+
readonly rootOwner?: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** 按行解析赎回面。 */
|
|
41
|
+
export type ParkedReviveSurfaceFactory = (row: BackgroundAgentRecord, identity: ParkedReviveSurfaceIdentity) => ParkedReviveSurface;
|
|
42
|
+
/**
|
|
43
|
+
* 赎回面工厂。入参 = **内建** default 场景工厂(stage-07 在 center overlay 盖名之前取的那一只;赎回要的是与 spawn 同源的生产装配)。
|
|
44
|
+
* 内建 bundle 里没有 Agent 工具 ⇒ `undefined`(park 本就不可达,赎回腿不接线)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function createParkedReviveSurface(defaultScenario: Scenario): ParkedReviveSurfaceFactory | undefined;
|
|
47
|
+
//# sourceMappingURL=parked-revive-surface.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
|
|
2
|
+
const agentOf = (tools) => tools.find((t) => t.name === DEFAULT_SUBAGENT_TOOL_NAME);
|
|
3
|
+
export function createParkedReviveSurface(defaultScenario) {
|
|
4
|
+
const bootTool = agentOf(defaultScenario({}).tools);
|
|
5
|
+
if (bootTool === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
return (row, identity) => {
|
|
8
|
+
const root = row.rootSessionId;
|
|
9
|
+
if (root === undefined || row.parentSessionId !== root)
|
|
10
|
+
return { reviveTool: bootTool };
|
|
11
|
+
if (identity.rootOwner === undefined || (identity.rootOwner ?? undefined) !== identity.principal)
|
|
12
|
+
return { reviveTool: bootTool };
|
|
13
|
+
const tools = defaultScenario({}, identity.principal, { sessionId: root }).tools;
|
|
14
|
+
const reviveTool = agentOf(tools);
|
|
15
|
+
if (reviveTool === undefined)
|
|
16
|
+
return { reviveTool: bootTool };
|
|
17
|
+
return { reviveTool, mountsCallerTool: (t) => tools.includes(t) };
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=parked-revive-surface.js.map
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* re-read by byte cursor on each poll (path ①). Both are expressed through {@link BackgroundShellDriver}.
|
|
22
22
|
*/
|
|
23
23
|
import { BackgroundShellError } from "@sema-agent/core";
|
|
24
|
-
import type { BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions, Result } from "@sema-agent/core";
|
|
24
|
+
import type { BackgroundHardKillOutcome, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions, Result } from "@sema-agent/core";
|
|
25
25
|
import { StringDecoder } from "node:string_decoder";
|
|
26
26
|
/** Per-端 background capability bounds, surfaced as `backgroundCapabilities` on the env ([ref] §3.1/§3.6).
|
|
27
27
|
*
|
|
@@ -105,6 +105,13 @@ export interface BackgroundShellDriver<S> {
|
|
|
105
105
|
kill(state: S): Promise<void>;
|
|
106
106
|
/** Best-effort cleanup of ONE shell's transport resources (kill + remove temp dirs/handles). Should not throw. */
|
|
107
107
|
disposeOne(state: S): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* S-745(core 7.32.0 #1107)—— **可选**的同步硬收一枪(`BackgroundShellCapability.killBackgroundNow` 的 driver 半场)。
|
|
110
|
+
* 只有**本机有进程组可发**的 driver 实现它(今天只有 host 车道);远端 / 沙箱车道留空 ⇒ 座位根本不挂出来,core 对那些行
|
|
111
|
+
* 答 `no_pgid` 并派普通 kill。契约(core `docs/background-process-lifetime.md`):同步(无 TERM、无定时器、无 await)、
|
|
112
|
+
* 幂等、永不抛、只对**证明得了还是自己的** pid 发信号(观察到退出 ⇒ `already_gone`,一枪都不发)。
|
|
113
|
+
*/
|
|
114
|
+
killNow?(state: S): BackgroundHardKillOutcome;
|
|
108
115
|
}
|
|
109
116
|
export declare class BackgroundShellManager<S> {
|
|
110
117
|
private readonly driver;
|
|
@@ -154,6 +161,18 @@ export declare class BackgroundShellManager<S> {
|
|
|
154
161
|
private register;
|
|
155
162
|
poll(shellId: BackgroundShellId): Promise<Result<BackgroundPoll, BackgroundShellError>>;
|
|
156
163
|
kill(shellId: BackgroundShellId): Promise<Result<void, BackgroundShellError>>;
|
|
164
|
+
/**
|
|
165
|
+
* S-745 —— 座位 `killBackgroundNow(shellId)` 的 manager 半场:**同步**、永不抛。
|
|
166
|
+
* · 不是本 env 的 shell(未知 / 伪造 / 已被驱逐的终态行)⇒ `already_gone`(core 契约「or not its shell」);
|
|
167
|
+
* · driver 不实现 {@link BackgroundShellDriver.killNow} ⇒ `no_pgid`(没有同步硬收可落;实现座位的 env 才会走到这里);
|
|
168
|
+
* · driver 答出三词之外 ⇒ `no_pgid`(没有落地,可重试)。driver 的座位按契约**永不抛**(host 的那只由构造保证:只读子进程句柄
|
|
169
|
+
* + `hardKillGroupNow` 自己吞 `kill` 的拒因);万一抛了,本函数**不吞** —— 异常原样上抛给 core 的注册表,由它读成 `no_pgid` 并记
|
|
170
|
+
* `SEMA_DEBUG_SHUTDOWN` 取证行(core 契约:「A seat that throws … reads no_pgid」),不在这里再写一只静默兜底;
|
|
171
|
+
* · 落地 `killed` 且条目还在 `running` ⇒ 条目结算 `killed` + 清 BG 超时墙(与 {@link kill} 的记账同一形:这一枪是**我们**
|
|
172
|
+
* 打的,之后迟到的 `close` 不再把它改判成 `exited`)。条目状态**不参与**判「该不该发」:软梯子会先把条目翻成 `killed`
|
|
173
|
+
* 再等宽限(屏蔽 TERM 的组此刻还活着),所以「该不该发」只由 driver 读子进程句柄判。
|
|
174
|
+
*/
|
|
175
|
+
killNow(shellId: BackgroundShellId): BackgroundHardKillOutcome;
|
|
157
176
|
/** Kill + clean up EVERY shell. Best-effort, MUST NOT throw, idempotent ([ref] §3.7).
|
|
158
177
|
*
|
|
159
178
|
* core `except` 契约(background-shell.d.ts:core runtask 尾调 `{except: keepAlive}`):
|
|
@@ -216,6 +216,32 @@ export class BackgroundShellManager {
|
|
|
216
216
|
}
|
|
217
217
|
return ok(undefined);
|
|
218
218
|
}
|
|
219
|
+
killNow(shellId) {
|
|
220
|
+
const entry = this.shells.get(shellId);
|
|
221
|
+
if (!entry)
|
|
222
|
+
return "already_gone";
|
|
223
|
+
const seat = this.driver.killNow;
|
|
224
|
+
if (seat === undefined)
|
|
225
|
+
return "no_pgid";
|
|
226
|
+
const outcome = seat.call(this.driver, entry.state);
|
|
227
|
+
switch (outcome) {
|
|
228
|
+
case "killed":
|
|
229
|
+
if (entry.status === "running") {
|
|
230
|
+
entry.status = "killed";
|
|
231
|
+
if (entry.timer)
|
|
232
|
+
clearTimeout(entry.timer);
|
|
233
|
+
}
|
|
234
|
+
return "killed";
|
|
235
|
+
case "already_gone":
|
|
236
|
+
case "no_pgid":
|
|
237
|
+
return outcome;
|
|
238
|
+
default: {
|
|
239
|
+
const unreachable = outcome;
|
|
240
|
+
void unreachable;
|
|
241
|
+
return "no_pgid";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
219
245
|
async dispose(opts) {
|
|
220
246
|
const keep = opts?.except?.length ? new Set(opts.except) : undefined;
|
|
221
247
|
const doomed = [];
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* structural tests only on mac/Linux; behavior-level bite happens on the first Windows-runner green.
|
|
24
24
|
*/
|
|
25
25
|
import type { ChildProcess } from "node:child_process";
|
|
26
|
-
import { killProcessTree } from "@sema-agent/core";
|
|
26
|
+
import { killProcessTree, type BackgroundHardKillOutcome } from "@sema-agent/core";
|
|
27
27
|
export declare const IS_WIN32: boolean;
|
|
28
28
|
export interface HostShell {
|
|
29
29
|
shell: string;
|
|
@@ -61,6 +61,16 @@ export declare function spawnGroupOptions(): {
|
|
|
61
61
|
* win32 = core `signalProcessTree(pid,"SIGKILL")` → `taskkill /F /T` (never throws — fire-and-forget spawn).
|
|
62
62
|
*/
|
|
63
63
|
export declare function killTreeHard(pid: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* S-745(core 7.32.0 #1107)—— **同步组硬收的一枪**:后台 driver 的 `killBackgroundNow` 座位用它(梯子的「组 SIGKILL」那一级,
|
|
66
|
+
* 不经 TERM、不挂定时器)。读数是 core 座位闭集里的三词,判据只看 `kill` 系统调用自己的结局:
|
|
67
|
+
* · POSIX:`kill(-pid, SIGKILL)` 落地 ⇒ `killed`;`ESRCH`(这个组已经没有进程)⇒ `already_gone`;其余拒因(`EPERM` 一族)⇒
|
|
68
|
+
* `no_pgid`(没有落地,调用方可重试;core 随即派普通 kill);
|
|
69
|
+
* · win32 ⇒ 恒 `no_pgid`(core 契约:win32 无同步硬收 —— 没有进程组,树杀要一个延后的发现步,同步 `taskkill` 会杀掉启动器而
|
|
70
|
+
* 留下 Git Bash 的作业组、却自称 `killed`)。
|
|
71
|
+
* 「这个 pid 还是不是我的」由**调用方**先判(观察到退出 ⇒ 不许再发,pid 可能已经是别人的):本函数只管打出去的那一枪。永不抛。
|
|
72
|
+
*/
|
|
73
|
+
export declare function hardKillGroupNow(pid: number): BackgroundHardKillOutcome;
|
|
64
74
|
/**
|
|
65
75
|
* SOFT kill (design D2 — the bg KillShell/timeout-wall first rung; the caller owns the grace→hard escalation
|
|
66
76
|
* timer, unchanged). POSIX = `process.kill(-pid, "SIGTERM")` byte-identical (throws on dead group — the caller
|
|
@@ -46,6 +46,17 @@ export function killTreeHard(pid) {
|
|
|
46
46
|
}
|
|
47
47
|
process.kill(-pid, "SIGKILL");
|
|
48
48
|
}
|
|
49
|
+
export function hardKillGroupNow(pid) {
|
|
50
|
+
if (IS_WIN32)
|
|
51
|
+
return "no_pgid";
|
|
52
|
+
try {
|
|
53
|
+
process.kill(-pid, "SIGKILL");
|
|
54
|
+
return "killed";
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return err.code === "ESRCH" ? "already_gone" : "no_pgid";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
49
60
|
export function killTreeSoft(pid) {
|
|
50
61
|
if (IS_WIN32) {
|
|
51
62
|
signalProcessTree(pid, "SIGTERM");
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
|
|
1
|
+
import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
|
|
2
2
|
/** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
|
|
3
3
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
4
|
+
/** SIGTERM → grace → SIGKILL (no host isolation). 🔴 宽限的**唯一属主**:本 driver 的 `kill` 梯子用它排升级定时器;会话级收敛
|
|
5
|
+
* (`background-session-reap.ts#reapSessionBackgroundHard`,DELETE 与停止口)在软面之后、硬面之前**等同一只**(车OL O1 —— 硬面不许把
|
|
6
|
+
* 这把梯子的用户面宽限取消),不另抄数。启动期读一次;`0` 被 `numEnvOr` 读作缺席(回落 1000),要几乎不等写 `1`。 */
|
|
7
|
+
export declare const HOST_BG_KILL_GRACE_MS: number;
|
|
4
8
|
/** Construction config for {@link RemoteHostExecutionEnv}. */
|
|
5
9
|
export interface HostEnvConfig {
|
|
6
10
|
/**
|
|
@@ -242,6 +246,15 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
242
246
|
private adoptRunningExec;
|
|
243
247
|
pollBackground(shellId: BackgroundShellId): Promise<Result<BackgroundPoll, BackgroundShellError>>;
|
|
244
248
|
killBackground(shellId: BackgroundShellId): Promise<Result<void, BackgroundShellError>>;
|
|
249
|
+
/**
|
|
250
|
+
* S-745(core 7.32.0 #1107)—— `BackgroundShellCapability.killBackgroundNow` 座位:退出边界 / 会话收敛的**同步硬收**面经它对
|
|
251
|
+
* 本 env 的一行发**一枪组 SIGKILL**(pid 不出 env,调用方只握不透明的 shellId)。同步、永不抛、幂等;答 `killed` /
|
|
252
|
+
* `already_gone` / `no_pgid`(win32 恒 `no_pgid`)。从没起过 shell 的 env(`_bgManager` 未建)⇒ 这个 id 不是本 env 的 ⇒
|
|
253
|
+
* `already_gone`;**不**顺手新建 manager(拆除路径上不造东西,与 `disposeBackgroundShells` 同一条)。
|
|
254
|
+
* 本仓只有 host 车道实现它:e2b / k8s·kata / local-docker / ssh / device / adb 没有本机进程组可发,留空 ⇒ core 每行 `no_pgid`
|
|
255
|
+
* 并派普通 kill(车道矩阵钉:`test/background-hard-kill-s745.test.ts`)。
|
|
256
|
+
*/
|
|
257
|
+
killBackgroundNow(shellId: BackgroundShellId): BackgroundHardKillOutcome;
|
|
245
258
|
disposeBackgroundShells(opts?: {
|
|
246
259
|
except?: readonly BackgroundShellId[];
|
|
247
260
|
}): Promise<void>;
|
|
@@ -7,7 +7,7 @@ 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
13
|
import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
|
|
@@ -21,7 +21,7 @@ const HOST_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("HOST_BG_DEFAULT_TIMEOUT_SEC", 300,
|
|
|
21
21
|
const HOST_BG_MAX_TIMEOUT_SEC = numEnvOr("HOST_BG_MAX_TIMEOUT_SEC", 1800, HOST_BG_DEFAULT_TIMEOUT_SEC);
|
|
22
22
|
const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024);
|
|
23
23
|
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);
|
|
24
|
+
export const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
|
|
25
25
|
const HOST_BG_MEM_CAP = numEnvOr("HOST_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
|
|
26
26
|
const ok = (value) => ({ ok: true, value });
|
|
27
27
|
const UNC_SPELLING = /^[\\/]{2}[^\\/]+[\\/]+[^\\/]/;
|
|
@@ -253,7 +253,7 @@ export class RemoteHostExecutionEnv {
|
|
|
253
253
|
await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
254
254
|
throw new BackgroundShellError("spawn_failed", "host background spawn produced no pid");
|
|
255
255
|
}
|
|
256
|
-
return { pid, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
|
|
256
|
+
return { pid, child, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
|
|
257
257
|
});
|
|
258
258
|
},
|
|
259
259
|
async read(state) {
|
|
@@ -334,6 +334,16 @@ export class RemoteHostExecutionEnv {
|
|
|
334
334
|
if (state.dir)
|
|
335
335
|
await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { });
|
|
336
336
|
},
|
|
337
|
+
killNow(state) {
|
|
338
|
+
if (state.hardKilled === true)
|
|
339
|
+
return "already_gone";
|
|
340
|
+
if (state.child.exitCode !== null || state.child.signalCode !== null)
|
|
341
|
+
return "already_gone";
|
|
342
|
+
const outcome = hardKillGroupNow(state.pid);
|
|
343
|
+
if (outcome === "killed")
|
|
344
|
+
state.hardKilled = true;
|
|
345
|
+
return outcome;
|
|
346
|
+
},
|
|
337
347
|
};
|
|
338
348
|
}
|
|
339
349
|
spawnBackground(command, options) {
|
|
@@ -362,7 +372,7 @@ export class RemoteHostExecutionEnv {
|
|
|
362
372
|
child.off("close", onClose);
|
|
363
373
|
child.off("error", onErrEvt);
|
|
364
374
|
};
|
|
365
|
-
return { pid, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
|
|
375
|
+
return { pid, child, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
|
|
366
376
|
});
|
|
367
377
|
return adopted?.shellId;
|
|
368
378
|
}
|
|
@@ -372,6 +382,9 @@ export class RemoteHostExecutionEnv {
|
|
|
372
382
|
killBackground(shellId) {
|
|
373
383
|
return this.bgManager.kill(shellId);
|
|
374
384
|
}
|
|
385
|
+
killBackgroundNow(shellId) {
|
|
386
|
+
return this._bgManager?.killNow(shellId) ?? "already_gone";
|
|
387
|
+
}
|
|
375
388
|
disposeBackgroundShells(opts) {
|
|
376
389
|
return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
|
|
377
390
|
}
|
|
@@ -782,7 +795,7 @@ export class RemoteHostExecutionEnv {
|
|
|
782
795
|
child.off("close", onClose);
|
|
783
796
|
child.off("error", onErrEvt);
|
|
784
797
|
};
|
|
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 } : {}) };
|
|
798
|
+
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
799
|
});
|
|
787
800
|
return adopted?.shellId;
|
|
788
801
|
}
|
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;
|
|
@@ -1588,6 +1617,15 @@ export declare class ToolApprovalCoordinator {
|
|
|
1588
1617
|
* 宁少列不超范围;`"__none__"` 未鉴权哨兵自然恒空)。行的 `approvalId` 就是 `respond()` 收的 id
|
|
1589
1618
|
* (同源可达,[ref] 判据);`expiresAtMs` 照抄登记时单点铸定的窗死线([ref] 三键同一个数)。 */
|
|
1590
1619
|
listLivePending(scope: string | undefined): LivePendingRow[];
|
|
1620
|
+
/**
|
|
1621
|
+
* S-751:某只委派子代此刻有没有**待决**的活卡 —— fleet 对账腿「子代活性由属主说了算」的属主②读口(另一只是
|
|
1622
|
+
* bg registry 账)。子代卡在一张没人答的卡上时当然不出帧,只看「静默」就会把它当孤儿扫下面板(报案的真形)。
|
|
1623
|
+
* 只答是非、不交出 Map(与 `isRunLiveOnThisReplica` 同姿势);纯读,零锁零结算语义。
|
|
1624
|
+
* 键 = ask 发起者域(`originTaskId` = core `AskRequest.sourceTaskId` = 子代自己的 core session id),**不是**
|
|
1625
|
+
* fleet 行 id —— 换算在 `FleetEventBus.childOwnerKeys`。只看本副本的内存登记:子代跑在本副本,它的活卡就登记在
|
|
1626
|
+
* 本副本;耐久 park 的卡由 bg registry 的 `parked` 态作保(见 fleet-reconciler.ts `bgAgentVouches`)。
|
|
1627
|
+
*/
|
|
1628
|
+
hasLivePendingFrom(originTaskId: string): boolean;
|
|
1591
1629
|
/**
|
|
1592
1630
|
* 🔴 [ref](案A)追记 F4 —— **跨副本决议的执行通知**(悬挂行的一次终态轮询)。
|
|
1593
1631
|
*
|