@sema-agent/server 7.35.1 → 7.36.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/README.zh-CN.md +1 -1
- package/USAGE.md +10 -1
- package/dist/boot/config-center.js +14 -4
- package/dist/boot/org-memory.d.ts +21 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/parked-revive-gate.d.ts +16 -1
- package/dist/boot/parked-revive-gate.js +33 -68
- package/dist/boot/runner-deps.d.ts +1 -1
- package/dist/boot/runner-deps.js +18 -3
- package/dist/boot/side-query-lane.d.ts +124 -0
- package/dist/boot/side-query-lane.js +158 -0
- package/dist/boot/webfetch-summarize-lane.d.ts +60 -0
- package/dist/boot/webfetch-summarize-lane.js +67 -0
- package/dist/brain.js +15 -1
- package/dist/config-types.d.ts +27 -2
- package/dist/config.js +13 -3
- package/dist/degenerate-instrument.d.ts +13 -1
- package/dist/degenerate-instrument.js +13 -1
- package/dist/http/active-run-conflict.js +16 -0
- package/dist/http/routes/a2a-serve.js +12 -2
- package/dist/http/routes/admin-drain.d.ts +17 -0
- package/dist/http/routes/admin-drain.js +7 -1
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/capabilities.js +11 -2
- package/dist/http/routes/rules.js +7 -4
- package/dist/http/routes/runs.js +55 -8
- package/dist/http/routes/sessions-list.js +1 -1
- package/dist/http/routes/sessions.js +4 -4
- package/dist/http/routes/side-query.js +6 -1
- package/dist/http/server.d.ts +18 -5
- package/dist/http/server.js +5 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +14 -0
- package/dist/main.js +17 -28
- package/dist/memory-posture.d.ts +14 -1
- package/dist/memory-posture.js +2 -0
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/parked-decide.d.ts +3 -1
- package/dist/parked-decide.js +34 -11
- package/dist/rules-consent.d.ts +20 -0
- package/dist/rules-consent.js +21 -0
- package/dist/security.d.ts +22 -0
- package/dist/security.js +24 -0
- package/dist/shared-memory-scope-authorizer.d.ts +36 -3
- package/dist/shared-memory-scope-authorizer.js +19 -4
- package/dist/store-live-probe.d.ts +26 -0
- package/dist/store-live-probe.js +60 -0
- package/dist/task-cwd.d.ts +16 -3
- package/dist/task-cwd.js +16 -3
- package/dist/tool-approval.d.ts +61 -2
- package/dist/tool-approval.js +159 -17
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@ import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
|
4
4
|
import { createLogger } from "../../observability/logger.js";
|
|
5
5
|
// 等待秒数的单一真源在车道:它同时被「放回认领时判这张票还撑不撑得过这段窗」用到(见那侧头注)。
|
|
6
6
|
// scope 的 wire 形也在车道(列举列 / 查询参数 / 删除体 / 游标四处同一份表示,见那侧头注)。
|
|
7
|
-
import { RULE_IMPORT_RETRY_AFTER_SEC, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
|
|
7
|
+
import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
|
|
8
8
|
const logger = createLogger();
|
|
9
9
|
export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
10
10
|
export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
|
|
@@ -45,12 +45,15 @@ const CcImportPrepareBodySchema = z
|
|
|
45
45
|
const CcImportRedeemBodySchema = z.object({ ticket: z.string().min(1).max(190) }).strict();
|
|
46
46
|
/** `DELETE /v1/rules` 的体。`.strict()` 同族理由:把 `scope` 拼错成 `scopes` 的客户端应该当场知道,
|
|
47
47
|
* 而不是拿到一个「删掉了 global 那条」的意外结果。`principal` = operator 越权域(缺席 = 删自己的)。
|
|
48
|
-
* 长度上限与 principal 入口上限同源(`PRINCIPAL_MAX_LENGTH = 190`,security.ts);`rule` 的 1024
|
|
49
|
-
*
|
|
48
|
+
* 长度上限与 principal 入口上限同源(`PRINCIPAL_MAX_LENGTH = 190`,security.ts);`rule` 的 1024 覆盖
|
|
49
|
+
* core 的 `MAX_RULE_TEXT_CHARS`(512,铸侧真上界)且留足余量。
|
|
50
|
+
* 🔴 `scope` 的上限**不再是本文件自己挑的数**(A-054.24):改由铸侧的尺派生 —— 见
|
|
51
|
+
* {@link MAX_RULE_SCOPE_CHARS} 顶注(此前独立写死 1088,而铸侧能落到 4096+,于是超限 root 的规则
|
|
52
|
+
* 「列得出、删不掉」)。撤销口的收窄若严于铸侧,等于造一条**不可撤的常驻放行规则**,方向是错的一侧。 */
|
|
50
53
|
const RuleRevokeBodySchema = z
|
|
51
54
|
.object({
|
|
52
55
|
rule: z.string().min(1).max(1024),
|
|
53
|
-
scope: z.string().min(1).max(
|
|
56
|
+
scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
|
|
54
57
|
principal: z.string().min(1).max(190).optional(),
|
|
55
58
|
})
|
|
56
59
|
.strict();
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -8,6 +8,7 @@ import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from ".
|
|
|
8
8
|
import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
|
|
9
9
|
import { fleetRunPublisher, fleetRunLabels } from "../../fleet/fleet-bus.js";
|
|
10
10
|
import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../../observability/cost-taxonomy.js";
|
|
11
|
+
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
11
12
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
12
13
|
import { createHash } from "node:crypto";
|
|
13
14
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
@@ -312,7 +313,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
312
313
|
const runMatch = req.method === "GET" ? RUN_ID_RE.exec(url) : null;
|
|
313
314
|
if (runMatch) {
|
|
314
315
|
if (!deps.runStore) {
|
|
315
|
-
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
316
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
316
317
|
return;
|
|
317
318
|
}
|
|
318
319
|
const taskId = runMatch[1];
|
|
@@ -404,7 +405,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
404
405
|
const cancelMatch = req.method === "POST" ? RUN_CANCEL_RE.exec(url) : null;
|
|
405
406
|
if (cancelMatch) {
|
|
406
407
|
if (!deps.runStore) {
|
|
407
|
-
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
408
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
408
409
|
return;
|
|
409
410
|
}
|
|
410
411
|
// Principal check BEFORE the lookup (parity with GET /v1/runs/:id): no 404-vs-401 existence oracle.
|
|
@@ -617,7 +618,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
617
618
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
618
619
|
return; // mutating + hits TiDB / runs the model
|
|
619
620
|
if (!deps.runStore) {
|
|
620
|
-
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
621
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
621
622
|
return;
|
|
622
623
|
}
|
|
623
624
|
// Per-tenant identity MUST come from gatedPrincipal (direct-door secure single point), NOT principalFrom:
|
|
@@ -862,7 +863,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
862
863
|
}
|
|
863
864
|
// per-session 串行(复审 HIGH:并发双终态 steer 的 tryPark→put 窗口双铸两个 pending park)。
|
|
864
865
|
const prior = wakeParkMints.get(run.sessionId) ?? Promise.resolve();
|
|
865
|
-
const mintResult = { parked: false, queueFull: false, duplicateKey: false };
|
|
866
|
+
const mintResult = { parked: false, queueFull: false, duplicateKey: false, noResumeCtx: false };
|
|
866
867
|
// wake park 腿的 queue_full:新铸的 task_done checkpoint 队列必空,所以只可能来自「锁内重查撞到
|
|
867
868
|
// 既有 pending」或「跨副本赢家」两条既有 park 上——两处都要如实报满,不许折成「不接受转向」。
|
|
868
869
|
const parkOnce = async (t, sc) => {
|
|
@@ -902,6 +903,25 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
902
903
|
const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
|
|
903
904
|
if (leafId === undefined || leafId === null)
|
|
904
905
|
return;
|
|
906
|
+
// 🔴 A-057.19 [CONFIRMED high](2026-08-19 三轴组复审,真机复现)—— **铸卡前先证明它赎得回**。
|
|
907
|
+
// 缺口:本支铸的 `task_done` park 只有一条出路 = `POST /v1/sessions/:id/wake`,而那条腿硬依赖
|
|
908
|
+
// `checkpoint_ctx`(server.ts 的 resumeWake:`getCtx` 为 null ⇒ 409
|
|
909
|
+
// `conflict.resume_context_unavailable`,无任何从 run 行重建的兜底);而 `reapCtx` 按**设计**在
|
|
910
|
+
// run 终态后把该行删掉(`run-store-sql.ts` 的 setTerminal 删 task_active ⇒ ctx 失去保护,
|
|
911
|
+
// reaper 每 tick 按 `REAP_RUN_STALE_SEC`(默认 120s)清理)。于是 steer-on-terminal 的真实用法
|
|
912
|
+
// (跑完隔几分钟补一句)几乎恒中:202「用 /wake 送」→ /wake 恒 409 → 消息永久搁浅在一张
|
|
913
|
+
// `GET /v1/approvals` 还照列、`/decide` 打它必 409(gate-kind 守卫)的卡上,直到 30 天
|
|
914
|
+
// terminal backstop 把它 expire 掉、消息静默蒸发。
|
|
915
|
+
// 处置照 #308 那条判例(「宁响亮拒不永久搁浅」):证不出 resume ctx 在 ⇒ **不铸**,当场诚实 409,
|
|
916
|
+
// 把真因(ctx 已随终态回收)与出路(重新提交任务)写进回体。读失败/超时同样算「证不出」——
|
|
917
|
+
// 方向 fail-closed:铸一张没人能消费的卡比拒绝一次 steer 伤得多(拒绝可重试,搁浅不可)。
|
|
918
|
+
// ⚠️ 射程只在**铸新卡**这一步:上面 reprobe 把消息 park 到**既有** pending 卡上的那条路不动
|
|
919
|
+
// (那张卡不是本腿造的,出路也未必是 /wake)。
|
|
920
|
+
const resumeCtx = await Promise.resolve(cs.getCtx(run.sessionId)).catch(() => null);
|
|
921
|
+
if (resumeCtx === null || resumeCtx === undefined) {
|
|
922
|
+
mintResult.noResumeCtx = true;
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
905
925
|
const cpScope = encodeCheckpointScope(run.owner); // 匿名提交哨兵,与 main.ts putCtx 的 principal ?? "_" 同域
|
|
906
926
|
const wakeToken = mintCheckpointToken();
|
|
907
927
|
await cs.put(wakeToken, {
|
|
@@ -916,6 +936,21 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
916
936
|
createdAt: Date.now(),
|
|
917
937
|
sourceTaskId: taskId,
|
|
918
938
|
});
|
|
939
|
+
// 🔴 A-057.19 第二半(codex 对抗复审 high,验真后采纳):上面那次 `getCtx` 与这次 `put` 之间
|
|
940
|
+
// 仍有一个 TOCTOU 窗 —— 窗内 `reapCtx` 完全可能把 ctx 删掉(它的三守卫此刻全部成立:run 已终态
|
|
941
|
+
// ⇒ 无 task_active、卡还没铸 ⇒ 无 pending checkpoint、提交早于 cutoff),于是我们照样铸出那张
|
|
942
|
+
// 赎不回的卡。**put 本身就是关窗动作**:`reapCtx` 的谓词逐字含
|
|
943
|
+
// `session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending')`
|
|
944
|
+
// (checkpoint-store-sql.ts 的 reapCtx / local-checkpoint-store.ts 双生同谓词),卡一落盘该 session
|
|
945
|
+
// 的 ctx 就被排除在回收之外。⇒ **put 之后再证一次**:还在 ⇒ 窗内没被删,从此也删不掉了;
|
|
946
|
+
// 不在 ⇒ 窗内正好被删,把刚铸的孤儿 expire 掉再诚实 409(绝不留一张赎不回的卡)。
|
|
947
|
+
// 读失败同样按「证不出」处置(与铸前那次同向 fail-closed)。
|
|
948
|
+
const ctxStillThere = await Promise.resolve(cs.getCtx(run.sessionId)).catch(() => null);
|
|
949
|
+
if (ctxStillThere === null || ctxStillThere === undefined) {
|
|
950
|
+
await cs.expire(wakeToken, cpScope).catch(() => undefined);
|
|
951
|
+
mintResult.noResumeCtx = true;
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
919
954
|
// 跨副本纵深:put 后复核赢家——若另一副本抢先铸了别的 pending,expire 我方孤儿并把消息
|
|
920
955
|
// park 到赢家上(收敛到单 park;店无序时收敛到店的稳定扫描序,残窗如实记 fast-follow)。
|
|
921
956
|
const winner = await cs.findPendingTokenBySession(run.sessionId).catch(() => null);
|
|
@@ -944,6 +979,12 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
944
979
|
sendDuplicateKey();
|
|
945
980
|
return;
|
|
946
981
|
}
|
|
982
|
+
// A-057.19:与 /wake 腿**同一个** errorCode —— 消费端对这条 session 的判断是同一件事(续跑材料没了),
|
|
983
|
+
// 两条腿给两个码只会让壳以为是两回事。
|
|
984
|
+
if (mintResult.noResumeCtx) {
|
|
985
|
+
sendError(res, 409, "conflict.resume_context_unavailable", "run already ended and this session's resume context is gone — a steer parked now could never be delivered by POST /v1/sessions/:id/wake; submit a new task instead");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
947
988
|
if (mintResult.parked) {
|
|
948
989
|
sendJson(res, 202, { taskId, status: run.status, delivery: "parked_for_wake", messageId, ...(priority ? { priority } : {}), note: "run already ended — steer parked on a task_done checkpoint; deliver it with POST /v1/sessions/:id/wake" });
|
|
949
990
|
return;
|
|
@@ -962,7 +1003,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
962
1003
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
963
1004
|
return; // mutating + runs the model (compaction summarizes)
|
|
964
1005
|
if (!deps.runStore) {
|
|
965
|
-
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
1006
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
966
1007
|
return;
|
|
967
1008
|
}
|
|
968
1009
|
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
@@ -1067,7 +1108,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
1067
1108
|
if (rateLimited(req, res))
|
|
1068
1109
|
return; // mutating, but runs no model (no quota gate — parity with cancel, not steer)
|
|
1069
1110
|
if (!deps.runStore) {
|
|
1070
|
-
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
1111
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
1071
1112
|
return;
|
|
1072
1113
|
}
|
|
1073
1114
|
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
@@ -1414,7 +1455,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1414
1455
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
1415
1456
|
return; // mutating + drives a model agent
|
|
1416
1457
|
if (!deps.runStore) {
|
|
1417
|
-
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg)");
|
|
1458
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
1418
1459
|
return;
|
|
1419
1460
|
}
|
|
1420
1461
|
// Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — `trusted`
|
|
@@ -1757,7 +1798,13 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1757
1798
|
const syncLiveFromDecidedRow = (row) => {
|
|
1758
1799
|
if (!decidedRowIsAuthoritative(row))
|
|
1759
1800
|
return false;
|
|
1760
|
-
deps.toolApproval?.notifyExternalDecision(row.askId, row.decision === "approve");
|
|
1801
|
+
const settled = deps.toolApproval?.notifyExternalDecision(row.askId, row.decision === "approve");
|
|
1802
|
+
// 🔴 A-054.15:**行派生**的 approve —— 第三形参 `updatedInput` 结构上取不到(行没有那一列),交出去的
|
|
1803
|
+
// 裸 `true` 会让 core 落回**原始未改写**实参执行。与 tool-approval.ts 的三个同形站点计同一个 tag
|
|
1804
|
+
// (选择性登记比不登记更坏);`settled > 0` 才算 —— 本地没有闭包被结算时什么都没执行,记了是假阳性。
|
|
1805
|
+
if (row.decision === "approve" && (settled?.settled ?? 0) > 0) {
|
|
1806
|
+
recordFailOpen("server.hitl.row-derived-approve-drops-updated-input", "runs:sync-live-from-decided-row");
|
|
1807
|
+
}
|
|
1761
1808
|
return true;
|
|
1762
1809
|
};
|
|
1763
1810
|
/**
|
|
@@ -27,7 +27,7 @@ async function handleSessionsListBody(req, res, url, ctx, miss) {
|
|
|
27
27
|
}
|
|
28
28
|
const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
|
|
29
29
|
if (!lister) {
|
|
30
|
-
sendError(res, 501, "capability.session_store_required", "session list requires a durable session store (DB_BACKEND=mysql|pg)");
|
|
30
|
+
sendError(res, 501, "capability.session_store_required", "session list requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
// [4198] 窄互认第二臂(列表面)—— 条件①③ 在此判,条件②(`owner === null`)由 store 的
|
|
@@ -257,7 +257,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
257
257
|
const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
|
|
258
258
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
259
259
|
if (!getLeafId || !ownerOf) {
|
|
260
|
-
sendError(res, 501, "capability.session_store_required", "session head probe requires a durable session store (DB_BACKEND=mysql|pg)");
|
|
260
|
+
sendError(res, 501, "capability.session_store_required", "session head probe requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
const headSession = decodeURIComponent(hm[1]);
|
|
@@ -310,7 +310,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
310
310
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
311
311
|
const watch = deps.sessionWatch;
|
|
312
312
|
if (!deps.sessionStorage?.getLeafId || !ownerOf || !watch) {
|
|
313
|
-
sendError(res, 501, "capability.session_store_required", "session events require a durable session store (DB_BACKEND=mysql|pg)");
|
|
313
|
+
sendError(res, 501, "capability.session_store_required", "session events require a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
316
316
|
const evSession = decodeURIComponent(em[1]);
|
|
@@ -579,7 +579,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
579
579
|
const fork = deps.sessionStorage?.fork?.bind(deps.sessionStorage);
|
|
580
580
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
581
581
|
if (!fork || !ownerOf) {
|
|
582
|
-
sendError(res, 501, "capability.session_store_required", "session fork requires a durable session store (DB_BACKEND=mysql|pg)");
|
|
582
|
+
sendError(res, 501, "capability.session_store_required", "session fork requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
583
583
|
return;
|
|
584
584
|
}
|
|
585
585
|
const scope = sessionOwnerScopeForWrite(req);
|
|
@@ -638,7 +638,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
638
638
|
const purge = deps.purgeSession;
|
|
639
639
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
640
640
|
if (!purge || !ownerOf) {
|
|
641
|
-
sendError(res, 501, "capability.session_store_required", "session delete requires a durable session store (DB_BACKEND=mysql|pg)");
|
|
641
|
+
sendError(res, 501, "capability.session_store_required", "session delete requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
642
642
|
return;
|
|
643
643
|
}
|
|
644
644
|
const scope = sessionOwnerScopeForWrite(req);
|
|
@@ -161,7 +161,12 @@ async function handleSideQueryBody(req, res, url, ctx, miss) {
|
|
|
161
161
|
signal: ac.signal,
|
|
162
162
|
};
|
|
163
163
|
try {
|
|
164
|
-
|
|
164
|
+
// #303:**不走** `deps.runner.sideQuery` —— core 的 SideQuerySpec 没有 key 座位,那条路恒把
|
|
165
|
+
// brain 构造时的网关 key 发到模型自己的 baseUrl(上游 401 + 凭据外泄形)。`deps.sideQuery` =
|
|
166
|
+
// boot/side-query-lane.ts 装配的执行席(真 runSideQuery + per-model key 注入 brain wrapper),
|
|
167
|
+
// 结果形逐字同 core 规格。poison(sealed key 不可解)在这里以异常上抛 ⇒ 落下面 500 臂(响亮),
|
|
168
|
+
// 绝不静默用网关 key 顶上。
|
|
169
|
+
const result = await deps.sideQuery(spec);
|
|
165
170
|
// 记账(codex R2 high→R3 收敛):走与 createTracer 同源装配的四路 sink seam(costQuota+指标+
|
|
166
171
|
// fleetUsage 批报+fleetLease 本地扣减)——sideQuery 不发 brain.call,tracer 链不经过这里。
|
|
167
172
|
// usageMissing/无 cost 面=0 记账,诚实缺席。
|
package/dist/http/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport } from "@sema-agent/core";
|
|
3
|
+
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type SideQuerySpec, type SideQueryResult } from "@sema-agent/core";
|
|
4
4
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
5
5
|
import type { ServiceConfig } from "../config-types.js";
|
|
6
6
|
import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
|
|
@@ -60,10 +60,19 @@ export interface RequestAuth {
|
|
|
60
60
|
* 入口摊平回平铺视图,server.ts 内的 ~700 处 `deps.x` 消费点原样保留。消费点迁移是后批的事——
|
|
61
61
|
* 一步到位需要同批改 ~700 处 src + ~800 处 test 键位,与「先立形、后搬家」相比没有额外收益。
|
|
62
62
|
*/
|
|
63
|
-
/** createHttpServer
|
|
64
|
-
*
|
|
63
|
+
/** createHttpServer 的**必填四件**——引擎、部署配置、请求→TaskSpec 的映射、side-query 执行席。
|
|
64
|
+
* 四者恒在场,故留在顶层(不进任何可选组):没有它们 server 根本起不来,包一层可选组只会把编译期
|
|
65
|
+
* 错误推迟到运行期(本仓 E-HIGH-1 病族的成因就是「可选键从未接线、编译期无声」)。 */
|
|
65
66
|
export interface ServiceCoreDeps {
|
|
66
67
|
runner: Runner;
|
|
68
|
+
/**
|
|
69
|
+
* #303:`POST /v1/side-query` 的 brain 出口。**不是** `runner.sideQuery` —— core 的 `SideQuerySpec`
|
|
70
|
+
* 结构性没有 key 座位,经 Runner 那条路走的 side-query 恒用 brain 构造时的网关 key 打**模型自己的**
|
|
71
|
+
* baseUrl(上游 401 + 凭据外泄形)。装配点 `boot/side-query-lane.ts` 给它一只 per-model key 注入
|
|
72
|
+
* 的 brain wrapper,models/roles 与 Runner 同源。
|
|
73
|
+
* 🔴 **必填**:可选形会让「装配点忘了接线」退化成静默走回旧路(安全轴上没有静默降级),必填让它编译红。
|
|
74
|
+
*/
|
|
75
|
+
sideQuery: (spec: SideQuerySpec) => Promise<SideQueryResult>;
|
|
67
76
|
config: ServiceConfig;
|
|
68
77
|
/**
|
|
69
78
|
* Map an inbound request body + resolved auth to a full TaskSpec. The SERVICE/deployment owns model
|
|
@@ -352,7 +361,9 @@ export interface ServiceSeamDeps {
|
|
|
352
361
|
* requiresParentConstraint 的 parked checkpoint 恒被 core pre-CAS 门诚实拒(不可赎回)。
|
|
353
362
|
* ⚠️ **async**(A-010.1):重建要按本行 principal 现解 entitlement——理由在 parked-decide.ts 的
|
|
354
363
|
* `rebuildInheritedGate` 头注。 */
|
|
355
|
-
parkedReviveInheritedGate?: (row: import("@sema-agent/core").BackgroundAgentRecord
|
|
364
|
+
parkedReviveInheritedGate?: (row: import("@sema-agent/core").BackgroundAgentRecord, identity: {
|
|
365
|
+
principal: string | undefined;
|
|
366
|
+
}) => Promise<unknown>;
|
|
356
367
|
}
|
|
357
368
|
/** **可观测面**:日志/指标/累加器。全体 fail-open——观测失败绝不改变业务面行为。 */
|
|
358
369
|
export interface ServiceObservabilityDeps {
|
|
@@ -458,7 +469,9 @@ export interface ServiceDeploymentDeps {
|
|
|
458
469
|
* 缺席(local/memory/探针关)= /health 形状不变;接线 = `storeProbe{live,ageMs,error?}` 恒在
|
|
459
470
|
* (「探过且活」与「没接线」机读可分),顶层告警键 `storeLive:false` 只在死时出现。status 恒 "ok"
|
|
460
471
|
* ——liveness≠readiness,DB 死不是进程死;摘流语义留给读键的编排器(与 durable/ready 同姿势,
|
|
461
|
-
* 披露不代裁)。钉:test/health-store-live.test.ts。
|
|
472
|
+
* 披露不代裁)。钉:test/health-store-live.test.ts。
|
|
473
|
+
* 🔴 #305:座席给的 `error` 是**驱动原始 message**(DSN/主机/账号常在其中),而 /health 在鉴权门
|
|
474
|
+
* 之前 ⇒ 路由把它过 `publicStoreProbeError` 换成闭集词才上线;原文只走日志轴。 */
|
|
462
475
|
storeLiveState?: () => {
|
|
463
476
|
live: boolean;
|
|
464
477
|
ageMs: number;
|
package/dist/http/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import { normalizeApproachNotice, validateTaskAgents } from "../spec-fields.js";
|
|
|
14
14
|
import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
15
15
|
import { runInBackground, evictIfConflict, stripCheckpointToken, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
16
16
|
import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
|
|
17
|
+
import { publicStoreProbeError } from "../store-live-probe.js"; // #305:免鉴权 /health 面的探针错误闭集词表
|
|
17
18
|
import { looksLikeJwt } from "@sema-agent/registry-core/api/auth-bridge";
|
|
18
19
|
import {} from "../orchestration/workflow-agent-steer.js";
|
|
19
20
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
@@ -498,7 +499,10 @@ export function createHttpServer(rawDeps) {
|
|
|
498
499
|
return {};
|
|
499
500
|
return {
|
|
500
501
|
...(probe.live ? {} : { storeLive: false }),
|
|
501
|
-
|
|
502
|
+
// #305:error 过闭集词表 —— 这一整个响应体在鉴权门**之前**发出,而座席给的是驱动原始
|
|
503
|
+
// message(DSN/主机/账号常在其中)。消毒钉在**这个 wire 装配点**而不是探针环里:座席是
|
|
504
|
+
// 可注入接缝,只有边界上的这一道对**任何**生产者成立(fail-closed 方向)。原文留日志轴。
|
|
505
|
+
storeProbe: { live: probe.live, ageMs: probe.ageMs, ...(probe.error !== undefined ? { error: publicStoreProbeError(probe.error) } : {}) },
|
|
502
506
|
};
|
|
503
507
|
})(),
|
|
504
508
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -21,4 +21,7 @@ export { createAuthorizer, principalFrom, HttpError, type AuthContext, type Owne
|
|
|
21
21
|
export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
|
|
22
22
|
export type { TaskRequestBody } from "./http/wire-types.js";
|
|
23
23
|
export { createHttpServer, type ServiceDeps, type RequestAuth, type FlatServiceDeps, type ServiceDepGroups, type ServiceCoreDeps, type ServiceStoreDeps, type ServiceCoordinatorDeps, type ServiceSeamDeps, type ServiceObservabilityDeps, type ServiceGovernanceDeps, type ServiceDeploymentDeps, type ServiceKnobDeps, } from "./http/server.js";
|
|
24
|
+
export { createSideQueryLane, createPerModelKeyBrain, type KeyResolver, type SideQueryLane, type SideQueryLaneCtx } from "./boot/side-query-lane.js";
|
|
25
|
+
export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-resolver.js";
|
|
26
|
+
export { isSealedKeyPoison, SealedKeyPoisonedError, type SealedKeyPoison, type SealedKeyPoisonReason } from "./sealed-key.js";
|
|
24
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -25,4 +25,18 @@ export { buildScenarios, selectScenario, } from "./capabilities/scenarios.js";
|
|
|
25
25
|
export { createAuthorizer, principalFrom, HttpError, } from "./security.js";
|
|
26
26
|
export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
|
|
27
27
|
export { createHttpServer, } from "./http/server.js";
|
|
28
|
+
// ── A-057.8:`ServiceCoreDeps.sideQuery` 是**必填**席,而它的装配链此前整条不在公面上 ──
|
|
29
|
+
// CHANGELOG 对自建 `ServiceDeps` 的嵌入方逐字指路「按 `createSideQueryLane` 装配即可」,但
|
|
30
|
+
// `package.json` 的 `exports` 只开 `.` / `./main` / `./package.json`(无通配、无 typesVersions),
|
|
31
|
+
// `files` 又只出 `dist` ⇒ 安装态里被指路的那个源文件读都读不到,深径 import 必
|
|
32
|
+
// `ERR_PACKAGE_PATH_NOT_EXPORTED`。嵌入方于是只剩「手抄一份 per-model key 解析 + poison 处理」,
|
|
33
|
+
// 而抄漏 poison 那半 = 拿共享网关账号去打别家上游,正是 #303 立案要修的凭据外泄病灶本身。
|
|
34
|
+
// 🔴 poison 的判别式(`isSealedKeyPoison` / `SealedKeyPoisonedError`)与工厂**同批**出面是刻意的:
|
|
35
|
+
// 只给工厂不给判据,嵌入方对「sealed key 解不开」就只能靠 message 猜,而猜错的方向恰好是静默回落
|
|
36
|
+
// 网关 key(安全轴上没有静默降级 —— CLAUDE.md #157)。
|
|
37
|
+
// 🔴 这五个值导出必须保持**值**形态(不许改成 `export type`):门 = `test/export-liveness-gate.test.ts`
|
|
38
|
+
// 的 gate:embedder-assembly-exports(表内符号缺一即红;runtime 类被 type-only 化同样红)。
|
|
39
|
+
export { createSideQueryLane, createPerModelKeyBrain } from "./boot/side-query-lane.js";
|
|
40
|
+
export { createKeyResolver, resolveModelApiKey } from "./key-resolver.js";
|
|
41
|
+
export { isSealedKeyPoison, SealedKeyPoisonedError } from "./sealed-key.js";
|
|
28
42
|
//# sourceMappingURL=index.js.map
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { Runner, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability,
|
|
4
|
+
import { Runner, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, probeSearchBackend, describeStaticWiring } from "@sema-agent/core";
|
|
5
5
|
import { createSessionTitler } from "./session-titler.js";
|
|
6
6
|
import { posIntEnv } from "./session-watch.js";
|
|
7
7
|
import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
|
|
@@ -51,6 +51,8 @@ import { openStores } from "./boot/stores.js";
|
|
|
51
51
|
import { runAdoptionBootScan } from "./boot/adoption.js";
|
|
52
52
|
import { auditDormantPermissionRules } from "./boot/permission-rules-audit.js";
|
|
53
53
|
import { createBudgetAndTracing } from "./boot/budget-tracing.js";
|
|
54
|
+
import { createSideQueryLane } from "./boot/side-query-lane.js";
|
|
55
|
+
import { createWebFetchSummarizeLane } from "./boot/webfetch-summarize-lane.js";
|
|
54
56
|
import { createRuleConsentLane } from "./rules-consent.js";
|
|
55
57
|
import { createExecutionEnv } from "./boot/execution-env.js";
|
|
56
58
|
import { createWorkflowOrchestration } from "./boot/workflow-orchestration.js";
|
|
@@ -261,7 +263,11 @@ async function main() {
|
|
|
261
263
|
const sharedMemoryStore = orgMemoryAdmission.directory !== undefined
|
|
262
264
|
? backend?.sharedMemoryStore?.(createSharedMemoryScopeAuthorizer({
|
|
263
265
|
directory: orgMemoryAdmission.directory,
|
|
264
|
-
|
|
266
|
+
// 🔴 A-057.40:递的是**取值口**不是值 —— `config.projects` 由 config-center 就地热应用,
|
|
267
|
+
// boot 期快照会让新登记的 org 库变成终局式空集、让已撤销的 scope 继续被放行(后者是
|
|
268
|
+
// fail-open,且 projects 不在 RESTART_SLICES 里 ⇒ /health 一句提示都没有)。多租户择净
|
|
269
|
+
// 仍留在这一行(那是部署形态判据,boot 期定死,不随热应用变)。
|
|
270
|
+
deploymentScopes: config.requirePrincipal === true ? () => [] : orgMemoryAdmission.deploymentMemoryScopesLive,
|
|
265
271
|
}))
|
|
266
272
|
: undefined;
|
|
267
273
|
if (sharedMemoryStore)
|
|
@@ -784,31 +790,9 @@ async function main() {
|
|
|
784
790
|
const ensureChildSessionDurable = checkpointStore && config.sessionBackend === "tidb" ? (sessionId) => ensureChildSessionDurableWithPromotion(sessionStore, subRunnerSessions, sessionId) : undefined;
|
|
785
791
|
// [1900]/[1904] WebFetch 摘要器:core 工厂 + summarize 角色模型(cheap tier;resolveTaskModel 自带
|
|
786
792
|
// 默认回落——与压缩线同族档位,不新造)。构造一次全场景共享;多租户形 WebFetch 整体被 filter,
|
|
787
|
-
// 本注入无效化由 scenarios
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
// 热应用 mutateInPlace 整表替换,boot 期急求值会把摘要器永久 pin 在占位/旧模型上(故障形态是
|
|
791
|
-
// 静默退回上下文放大 + 每次一发无效 LLM 调用)。
|
|
792
|
-
// ② **fail-soft**:core resolveTaskModel 在无可解析角色时 throw,这是全仓唯一 boot 期裸调用;
|
|
793
|
-
// center 发一张既无 summarize 又无 default 的 roles 表就会拒启。解析失败 ⇒ 本次不摘要
|
|
794
|
-
// (core 的 summarize 抛错是 fail-open:回退原文+note),绝不崩 boot。
|
|
795
|
-
// core 2.13.0:返回形放宽为 additive union(`string | { text, truncated? }`)—— 这里**原样转发**
|
|
796
|
-
// core summarizer 的返回值,不在本仓收窄成 string(收窄会把 core 的截断披露 `truncated` 吃掉,
|
|
797
|
-
// 让「内容被截断」这个事实在 fence 外消失)。
|
|
798
|
-
const webFetchSummarize = async (content, prompt, signal) => {
|
|
799
|
-
const model = (() => {
|
|
800
|
-
try {
|
|
801
|
-
return coreResolveTaskModel({ modelRole: "summarize" }, { models: config.models, roles: config.roles }).model;
|
|
802
|
-
}
|
|
803
|
-
catch (err) {
|
|
804
|
-
logger.warn("webfetch_summarizer_model_unresolved", { err: String(err), note: "this fetch falls back to raw page content" });
|
|
805
|
-
return undefined;
|
|
806
|
-
}
|
|
807
|
-
})();
|
|
808
|
-
if (!model)
|
|
809
|
-
throw new Error("summarize model unresolved for this deployment");
|
|
810
|
-
return createWebFetchSummarizer(brain, model)(content, prompt, signal);
|
|
811
|
-
};
|
|
793
|
+
// 本注入无效化由 scenarios 现行为保证。惰性解析 / fail-soft(E-MED-2/E-MED-3)与本席的 key plane
|
|
794
|
+
// (A-057.5:#303 的第四条直发面)都在 `boot/webfetch-summarize-lane.ts`,顶注写着全部判据与红先读数。
|
|
795
|
+
const webFetchSummarize = createWebFetchSummarizeLane({ brain, config, getKeyResolver: () => configCenter.getKeyResolver(), logger });
|
|
812
796
|
// design/158 S4:标注 `: ScenarioDeps` —— 无标注的字面量连自己的键名都不校验(打错/多写一个键
|
|
813
797
|
// 只是多一个没人读的属性,编译期无声),这是 1.304 落错家那一族的另一半土壤。
|
|
814
798
|
const scenarioDeps = {
|
|
@@ -1210,7 +1194,12 @@ async function main() {
|
|
|
1210
1194
|
});
|
|
1211
1195
|
// #196:执行点选路。resolveSpec 已按场景判别位登记 lane;`full` 恒回主 runner(修前行为逐字不变)。
|
|
1212
1196
|
const runnerFor = (spec) => pickHandsRunner(handsLanes.laneOf(spec), { full: runner, handsless: handslessRunner });
|
|
1213
|
-
|
|
1197
|
+
// #303:side-query 执行席 —— **不是** runner.sideQuery(core 的 SideQuerySpec 无 key 座位,那条路恒把
|
|
1198
|
+
// 网关 key 发到模型自己的 baseUrl)。key plane 第三面在此接线,取的是与另两面(任务面
|
|
1199
|
+
// boot/resolve-spec.ts、hook 面 hooks/hook-llm.ts)**同一只**活引用 getKeyResolver;brain 与
|
|
1200
|
+
// RunnerDeps.brain 同实例(wrapper 在外层包,主推理链零扰动)。
|
|
1201
|
+
const sideQuery = createSideQueryLane({ brain, config, getKeyResolver: () => configCenter.getKeyResolver() });
|
|
1202
|
+
const server = createHttpServer({ runner, runnerFor, sideQuery, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
|
|
1214
1203
|
// D-D SLA-timer: wire the server's deny-sweep into the reaper holder declared above (the reaper is defined
|
|
1215
1204
|
// before the server, so it calls through this late-bound reference).
|
|
1216
1205
|
runDenySweep = server.denyExpiredApprovals;
|
package/dist/memory-posture.d.ts
CHANGED
|
@@ -50,10 +50,23 @@ export interface MemoryPosture {
|
|
|
50
50
|
model: string;
|
|
51
51
|
dimensions: number;
|
|
52
52
|
} | null;
|
|
53
|
+
/**
|
|
54
|
+
* #299(core 5.45.0 design/324):本部署给委派臂设的**证据标准**(`MEMORY_DELEGATION_EVIDENCE`)。
|
|
55
|
+
*
|
|
56
|
+
* 🔴 `null` = **本部署没设这个键** ⇒ 引擎按它自己的缺省跑。**不是**「等于 static-face」—— 那句话是
|
|
57
|
+
* 关于 core 缺省的知识声明,本读面不替 core 宣布它(`vectorMode: null` 同一条诚实纪律:
|
|
58
|
+
* 「没有这个指示面」与「档位是词面」是两件事)。运维要知道「实际生效的是哪一档」,读这一位 +
|
|
59
|
+
* 实装 core 的契约,两件事分开摆。
|
|
60
|
+
*
|
|
61
|
+
* 受众 = **operator 面独占**:与 `darkReason`/`multiTenant`/embedder 身份同判(#252 件3 的分界),
|
|
62
|
+
* 刻意**不**进 `GET /v1/capabilities` 的窄投影 —— 证据标准回答的是「这台机器怎么判污染」,是运维
|
|
63
|
+
* 诊断料,不是任何持凭证调用方该读的位。
|
|
64
|
+
*/
|
|
65
|
+
delegationEvidence: "static-face" | "attested-only" | null;
|
|
53
66
|
}
|
|
54
67
|
/** 推导入参 = 装配现场的真事实(不在本函数里重算任何一件)。 */
|
|
55
68
|
export interface MemoryPostureFacts {
|
|
56
|
-
config: Pick<ServiceConfig, "memoryEngineEnabled" | "memoryEngineBackend" | "requirePrincipal" | "memoryEmbedder">;
|
|
69
|
+
config: Pick<ServiceConfig, "memoryEngineEnabled" | "memoryEngineBackend" | "requirePrincipal" | "memoryEmbedder" | "memoryDelegationEvidence">;
|
|
57
70
|
/** 引擎这一腿真的接上了吗(`memoryEngine !== undefined`)—— 装配结果。 */
|
|
58
71
|
engineWired: boolean;
|
|
59
72
|
/** 两支 SQL 后端从**实例 getter** 读到的档位;file 腿传 `undefined`(无此面)。 */
|
package/dist/memory-posture.js
CHANGED
|
@@ -23,6 +23,8 @@ export function buildMemoryPosture(facts) {
|
|
|
23
23
|
darkReason,
|
|
24
24
|
multiTenant: config.requirePrincipal === true,
|
|
25
25
|
embedder: embedder !== undefined ? { model: embedder.model, dimensions: embedder.dimensions } : null,
|
|
26
|
+
// #299:键缺席 ⇒ `null`(「本部署没设」),不折成 core 的缺省词 —— 见字段 JSDoc 的诚实纪律。
|
|
27
|
+
delegationEvidence: config.memoryDelegationEvidence ?? null,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
@@ -69,6 +69,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
69
69
|
readonly cls: "P-DEBT";
|
|
70
70
|
readonly note: "跨副本赎回 parked 审批时,祖先层在 park 时是 **auto-mode 武装**的,但那只分类器是祖先任务上的活闭包(绑着它自己的转写窗+brain),跨进程重建不出来 ⇒ 本仓交一只如实拒答的 decider,core 收到 `unavailable` 后**不产生任何自动裁决**、原样落到祖先冻结审批席那条链(本腿的席位又是无 ALS 的降级形 ⇒ 再 park 给人)。方向:分类器本会 allow 的改成问人(更严),本会 block 的也改成问人(**不是自动放行**,但比自动拒松一档)⇒ 记债不当合法兜底。计数 = 「丢了祖先分类器判决的继承 ask」次数。收口件二选一:core 把分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。";
|
|
71
71
|
};
|
|
72
|
+
readonly "server.hitl.row-derived-approve-drops-updated-input": {
|
|
73
|
+
readonly cls: "P-DEBT";
|
|
74
|
+
readonly note: "A-054.15:一条**持久 ask 行**上的 `DECIDED=approve` 被回放成裸 `true` 交给 core —— 而人当时可能是带 **ctrl+g 编辑**批的(`updatedInput`)。持久层没有该列(`AskRow`/`DecideAskInput` 皆无),行读不出「这次批准是否改写过实参」,于是 core 的 `d.updatedInput === undefined` 分支落回**原始未改写**实参执行:人批的是改写后的命令,真跑的是原命令 —— 一次**执行面宽于人所批准**的扩权,方向错的一侧,故记债不当合法兜底。四个同形站点全部计在本 tag 下(只在真把 `true` 交出去时计,不在读到行时计):`replayTerminalAskRow` 的三个消费点(`ensureAsk` 幂等回放 / `convergeFromDurableState` / 店报错臂的 `readDecidedForRecovery`)与 `runs.ts` 的 `syncLiveFromDecidedRow`(settled>0 才算)。⚠️ 计数是**上限**不是实数:分不出这次 approve 原本带没带编辑(能分出来就不用记债了),真丢的是其中带编辑的那一部分。补偿:回体在这条路上**省略** `updatedInputForwarded`(从不发 false = 成文的否定信号)。终局收口 = 给 ask 行加 `updated_input` 列(店语义级,须本模块属主立件),届时本 tag 同批销。";
|
|
75
|
+
};
|
|
72
76
|
readonly "server.e2b.orphan-skip-unknown": {
|
|
73
77
|
readonly cls: "F";
|
|
74
78
|
readonly note: "#242 E2B 孤儿回收对一只沙盒**判不了属主/活性**(metadata 缺 taskId 的病态形 / run 行状态词在 LIVE 与 TERMINAL 两表之外 / getRun 抛错)⇒ 恒不杀,本轮跳过。方向纪律:回收是清理型能力,缺席判据=不动作——误杀是杀活任务的手,漏杀只是钱;但每次跳过必须计数,否则「回收在跑却总有几只杀不掉」的病灶(店故障/新状态词未收编/别家部署共用 key)永不显形。";
|
|
@@ -93,6 +97,14 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
93
97
|
readonly cls: "F";
|
|
94
98
|
readonly note: "#193 车5(staleness-sweep P1-3):OTLP 周期导出失败(HTTP 非 2xx 或 fetch 拒绝)⇒ 丢弃本轮快照继续服务。导出本就 best-effort(collector 打嗝绝不影响 serving,方向不改),但此前唯一留痕是装配层 onError 的一条 warn——**metrics 轴零信号**,而 USAGE 力荐的 Prometheus-only 部署恰好只看 metrics ⇒ collector 持续宕机对运维完全不可见(遥测腿自盲)。放行的最坏后果 = 一段时间的指标断供;计数让「断供正在发生」本身成为一条指标。";
|
|
95
99
|
};
|
|
100
|
+
readonly "server.approvals.active-run-conflict-material-unavailable": {
|
|
101
|
+
readonly cls: "F";
|
|
102
|
+
readonly note: "A-057.47:`buildActiveRunConflict` 的材料装配段(getRun / turnActivity / peekPendingScope / findPendingTokenBySession / checkpoint `get`)任一失败 ⇒ 整段退化成裸 409(只剩 error + errorCode + activeTaskId),`activeTaskStatus` / `msSinceLastActivity` / `pendingGate`(kind·decidePath·governanceForced·checkpointId)全部蒸发。放行的最坏后果 = **展示/分诊**面缺料:壳画不出「这条 run 卡在哪道门、去哪儿决」,人退回 `POST /v1/runs/{activeTaskId}/cancel` 这条保底真路 —— 不参与任何门/CAS/resume 判定(执法面读的是 checkpoint blob 的 `get()`,不经本材料;与 census 第 21 行同一条判据),故 F 类。方向刻意不改:本材料是 best-effort 增强,让它抛会把一次 store 抖动变成整个 409 路径的 500(顶注第 11 行逐字成文「store 面任何失败都不得挡 409 本体」)。必须留痕的理由:本文件零 logger 席、pool 层无 per-query 错误日志、调用侧(tasks.ts / runs.ts / server.ts 五处)因本函数恒不 reject 结构上拿不到信号 ⇒ 「approval 出路材料系统性丢失」此前在遥测里与「一切正常」同形;滚动升级期 `checkpoint-store-sql.ts` 的 version-too-new throw 与 strict parseJson 也一并被吞在这里。";
|
|
103
|
+
};
|
|
104
|
+
readonly "server.model-key.off-route-model-uses-gateway-key": {
|
|
105
|
+
readonly cls: "P-DEBT";
|
|
106
|
+
readonly note: "A-057.59:一只 **baseUrl 指向别家主机**(非本部署 `gatewayBaseUrl`)的目录模型**没有** per-model key ⇒ `createPerModelKeyBrain` 不注入 `options.apiKey` ⇒ core 的 `options?.apiKey ?? config.apiKey` 回落**主网关 key**,而同一行下面的 `model.baseUrl || config.baseUrl` 仍把请求路由到那台外部主机 —— 这一次调用真把共享网关凭据发给了运维在 config-center 里指定的另一家。方向明知不对(hook 面对逐字同形的场景是 TRUE fail-closed:「refusing to send the prompt off-gateway」),之所以记债而不是当场收严:该缺席语义与**主推理链**(`dist/engine/harness/agent-harness.js` 的 `auth?.apiKey !== undefined` 臂)逐字同语义,只收严 wrapper 这两面会造成「同一只模型跑任务能用、问一句 401」,且「同账号多网关主机/多区域」是既有且正当的部署形、无旋钮可退。计数覆盖两面(side-query / WebFetch 摘要),`detail` = 模型名。⚠️ 判据需要参照系:调用方没给 `gatewayBaseUrl` ⇒ **不计**(宁可漏计不可错计),故计数是**下限**。终局收口 = 跨面收严件 #309(三面同批 + 旋钮 + 表态制),届时本 tag 同批销。";
|
|
107
|
+
};
|
|
96
108
|
};
|
|
97
109
|
/** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
|
|
98
110
|
export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
|
|
@@ -92,6 +92,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
92
92
|
cls: "P-DEBT",
|
|
93
93
|
note: "跨副本赎回 parked 审批时,祖先层在 park 时是 **auto-mode 武装**的,但那只分类器是祖先任务上的活闭包(绑着它自己的转写窗+brain),跨进程重建不出来 ⇒ 本仓交一只如实拒答的 decider,core 收到 `unavailable` 后**不产生任何自动裁决**、原样落到祖先冻结审批席那条链(本腿的席位又是无 ALS 的降级形 ⇒ 再 park 给人)。方向:分类器本会 allow 的改成问人(更严),本会 block 的也改成问人(**不是自动放行**,但比自动拒松一档)⇒ 记债不当合法兜底。计数 = 「丢了祖先分类器判决的继承 ask」次数。收口件二选一:core 把分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。",
|
|
94
94
|
},
|
|
95
|
+
"server.hitl.row-derived-approve-drops-updated-input": {
|
|
96
|
+
cls: "P-DEBT",
|
|
97
|
+
note: "A-054.15:一条**持久 ask 行**上的 `DECIDED=approve` 被回放成裸 `true` 交给 core —— 而人当时可能是带 **ctrl+g 编辑**批的(`updatedInput`)。持久层没有该列(`AskRow`/`DecideAskInput` 皆无),行读不出「这次批准是否改写过实参」,于是 core 的 `d.updatedInput === undefined` 分支落回**原始未改写**实参执行:人批的是改写后的命令,真跑的是原命令 —— 一次**执行面宽于人所批准**的扩权,方向错的一侧,故记债不当合法兜底。四个同形站点全部计在本 tag 下(只在真把 `true` 交出去时计,不在读到行时计):`replayTerminalAskRow` 的三个消费点(`ensureAsk` 幂等回放 / `convergeFromDurableState` / 店报错臂的 `readDecidedForRecovery`)与 `runs.ts` 的 `syncLiveFromDecidedRow`(settled>0 才算)。⚠️ 计数是**上限**不是实数:分不出这次 approve 原本带没带编辑(能分出来就不用记债了),真丢的是其中带编辑的那一部分。补偿:回体在这条路上**省略** `updatedInputForwarded`(从不发 false = 成文的否定信号)。终局收口 = 给 ask 行加 `updated_input` 列(店语义级,须本模块属主立件),届时本 tag 同批销。",
|
|
98
|
+
},
|
|
95
99
|
"server.e2b.orphan-skip-unknown": {
|
|
96
100
|
cls: "F",
|
|
97
101
|
note: "#242 E2B 孤儿回收对一只沙盒**判不了属主/活性**(metadata 缺 taskId 的病态形 / run 行状态词在 LIVE 与 TERMINAL 两表之外 / getRun 抛错)⇒ 恒不杀,本轮跳过。方向纪律:回收是清理型能力,缺席判据=不动作——误杀是杀活任务的手,漏杀只是钱;但每次跳过必须计数,否则「回收在跑却总有几只杀不掉」的病灶(店故障/新状态词未收编/别家部署共用 key)永不显形。",
|
|
@@ -116,6 +120,14 @@ export const FAIL_OPEN_TAGS = {
|
|
|
116
120
|
cls: "F",
|
|
117
121
|
note: "#193 车5(staleness-sweep P1-3):OTLP 周期导出失败(HTTP 非 2xx 或 fetch 拒绝)⇒ 丢弃本轮快照继续服务。导出本就 best-effort(collector 打嗝绝不影响 serving,方向不改),但此前唯一留痕是装配层 onError 的一条 warn——**metrics 轴零信号**,而 USAGE 力荐的 Prometheus-only 部署恰好只看 metrics ⇒ collector 持续宕机对运维完全不可见(遥测腿自盲)。放行的最坏后果 = 一段时间的指标断供;计数让「断供正在发生」本身成为一条指标。",
|
|
118
122
|
},
|
|
123
|
+
"server.approvals.active-run-conflict-material-unavailable": {
|
|
124
|
+
cls: "F",
|
|
125
|
+
note: "A-057.47:`buildActiveRunConflict` 的材料装配段(getRun / turnActivity / peekPendingScope / findPendingTokenBySession / checkpoint `get`)任一失败 ⇒ 整段退化成裸 409(只剩 error + errorCode + activeTaskId),`activeTaskStatus` / `msSinceLastActivity` / `pendingGate`(kind·decidePath·governanceForced·checkpointId)全部蒸发。放行的最坏后果 = **展示/分诊**面缺料:壳画不出「这条 run 卡在哪道门、去哪儿决」,人退回 `POST /v1/runs/{activeTaskId}/cancel` 这条保底真路 —— 不参与任何门/CAS/resume 判定(执法面读的是 checkpoint blob 的 `get()`,不经本材料;与 census 第 21 行同一条判据),故 F 类。方向刻意不改:本材料是 best-effort 增强,让它抛会把一次 store 抖动变成整个 409 路径的 500(顶注第 11 行逐字成文「store 面任何失败都不得挡 409 本体」)。必须留痕的理由:本文件零 logger 席、pool 层无 per-query 错误日志、调用侧(tasks.ts / runs.ts / server.ts 五处)因本函数恒不 reject 结构上拿不到信号 ⇒ 「approval 出路材料系统性丢失」此前在遥测里与「一切正常」同形;滚动升级期 `checkpoint-store-sql.ts` 的 version-too-new throw 与 strict parseJson 也一并被吞在这里。",
|
|
126
|
+
},
|
|
127
|
+
"server.model-key.off-route-model-uses-gateway-key": {
|
|
128
|
+
cls: "P-DEBT",
|
|
129
|
+
note: "A-057.59:一只 **baseUrl 指向别家主机**(非本部署 `gatewayBaseUrl`)的目录模型**没有** per-model key ⇒ `createPerModelKeyBrain` 不注入 `options.apiKey` ⇒ core 的 `options?.apiKey ?? config.apiKey` 回落**主网关 key**,而同一行下面的 `model.baseUrl || config.baseUrl` 仍把请求路由到那台外部主机 —— 这一次调用真把共享网关凭据发给了运维在 config-center 里指定的另一家。方向明知不对(hook 面对逐字同形的场景是 TRUE fail-closed:「refusing to send the prompt off-gateway」),之所以记债而不是当场收严:该缺席语义与**主推理链**(`dist/engine/harness/agent-harness.js` 的 `auth?.apiKey !== undefined` 臂)逐字同语义,只收严 wrapper 这两面会造成「同一只模型跑任务能用、问一句 401」,且「同账号多网关主机/多区域」是既有且正当的部署形、无旋钮可退。计数覆盖两面(side-query / WebFetch 摘要),`detail` = 模型名。⚠️ 判据需要参照系:调用方没给 `gatewayBaseUrl` ⇒ **不计**(宁可漏计不可错计),故计数是**下限**。终局收口 = 跨面收严件 #309(三面同批 + 旋钮 + 表态制),届时本 tag 同批销。",
|
|
130
|
+
},
|
|
119
131
|
};
|
|
120
132
|
/**
|
|
121
133
|
* 断流丢帧该记哪个 tag —— **按帧型分类**,纯函数(与写流的那条闭包解耦,才单测得动)。
|
package/dist/parked-decide.d.ts
CHANGED
|
@@ -52,7 +52,9 @@ export interface ParkedDecideDeps {
|
|
|
52
52
|
* `forceDurableGate` 项都要按**本行的 principal** 现解 entitlement(`RunnerDeps.runtimeCapsResolver`
|
|
53
53
|
* 本身就是 sync-or-async 的座),理由全在 `boot/parked-revive-gate.ts`。本腿一直就在 async 里,
|
|
54
54
|
* 「席位必须同步」是那个文件此前自设的约束、不是结构事实。 */
|
|
55
|
-
rebuildInheritedGate?: (row: BackgroundAgentRecord
|
|
55
|
+
rebuildInheritedGate?: (row: BackgroundAgentRecord, identity: {
|
|
56
|
+
principal: string | undefined;
|
|
57
|
+
}) => Promise<unknown>;
|
|
56
58
|
warn?: (event: string, fields: Record<string, unknown>) => void;
|
|
57
59
|
}
|
|
58
60
|
export interface ParkedDecideRequest {
|
package/dist/parked-decide.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
|
|
2
|
+
import { backgroundScopesForCheckpointScope, decodeCheckpointScope } from "./security.js";
|
|
2
3
|
/**
|
|
3
4
|
* pending checkpoint → parked bg 行匹配。
|
|
4
5
|
*
|
|
@@ -13,16 +14,34 @@ import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
|
|
|
13
14
|
* 该腿对已消失的 pending cp 有自己的 404/409 处置。
|
|
14
15
|
*/
|
|
15
16
|
export async function findParkedAgentForCheckpoint(agentStore, cp) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
// 🔴 A-057.44 [PARTIAL high](2026-08-19)—— **匿名部署上两套 scope 哨兵不同源**,分区查询恒落空。
|
|
18
|
+
// 病链:bg 行的 scope 写侧 = core `treeScope = reviveClaim?.row.scope ?? ctx.principal ??
|
|
19
|
+
// opts.background?.scope`,`ctx.principal` 在未开 REQUIRE_PRINCIPAL 的部署上恒 undefined ⇒ 落到本仓
|
|
20
|
+
// `capabilities/scenarios.ts` 填的 `"default"`;而 cp.scope 是 `encodeCheckpointScope(auth?.principal)`
|
|
21
|
+
// = `"_"`。店层是硬 `WHERE scope_key = ?`(MySQL/PG 双实现同形)⇒ 恒 miss ⇒ /decide 退回 legacy 腿 ⇒
|
|
22
|
+
// `getCtx(子代会话)` 必 null(全仓五个 putCtx 无一写 bg 子会话)⇒ 恒 409
|
|
23
|
+
// `conflict.resume_context_unavailable`,人的批准无处兑付、错误码还对真因有误导性。同一判别器被
|
|
24
|
+
// `server.ts` 的 deny-sweep `isParkedOwned` 复用,匿名下同样 miss ⇒ [1591] 那条静默跳过臂失效。
|
|
25
|
+
// 受害集 = REQUIRE_PRINCIPAL 未开 ∧ SQL 后端 ∧ DURABLE_APPROVAL=true。
|
|
26
|
+
//
|
|
27
|
+
// 归一**只能在读侧**(施工证伪了台账首选的「铸点改走 encodeCheckpointScope」——见
|
|
28
|
+
// `security.ts` 的 {@link backgroundScopesForCheckpointScope} 顶注:那个铸点的值同时喂进程内
|
|
29
|
+
// `defaultTaskRegistry`,而全仓读侧是一整族 `principal ?? "default"`,改铸点会把它们一起打瞎,
|
|
30
|
+
// 本车实测 subagent tail 建连当场 404)。所以这里按**别名集**查,别名表是单一属主。
|
|
31
|
+
for (const scope of backgroundScopesForCheckpointScope(cp.scope)) {
|
|
32
|
+
const parked = await agentStore.listByScope(scope, { status: "parked" });
|
|
33
|
+
for (const s of parked) {
|
|
34
|
+
if (s.sessionId !== cp.sessionId)
|
|
35
|
+
continue;
|
|
36
|
+
// ⚠️ 复核与下游(claim/rollback)都用**找到它的那个分区**,不是 cp.scope —— `decideParkedAgent`
|
|
37
|
+
// 之后取的是 `row.scope`/`row.owner` 双轴镜像([1588] Q3),故行自带的键就是权威。
|
|
38
|
+
const row = await agentStore.get(s.handle, scope);
|
|
39
|
+
if (row === null || row.status !== "parked")
|
|
40
|
+
continue;
|
|
41
|
+
if (row.parkedCheckpointToken !== cp.token)
|
|
42
|
+
continue;
|
|
43
|
+
return { handle: s.handle, row };
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
return undefined;
|
|
28
47
|
}
|
|
@@ -104,7 +123,11 @@ export async function decideParkedAgent(deps, req) {
|
|
|
104
123
|
// 都带着一个已认领的 claim 逃逸,行上的 `parkClaimId` 要等 stale-claim 清算窗(默认可达一小时)
|
|
105
124
|
// 才放开,这期间该审批**再决不动**。搬到 claim 前:此刻还没有任何东西需要回滚,抛错就是干净地抛错。
|
|
106
125
|
// (工厂本身只读 row 的 scope/handle/sessionId/rootSessionId —— claim 不改这四个字段,搬位零语义差。)
|
|
107
|
-
|
|
126
|
+
// 🔴 A-057.44 第二半(codex 对抗复审 medium):席位收的是**权威 principal**,不是行上那个有损的
|
|
127
|
+
// `row.scope`。checkpoint 的 scope 列由 `encodeCheckpointScope(auth?.principal)` 铸,与 core 喂给
|
|
128
|
+
// `runtimeCapsResolver` 的是同一个值;`row.scope` 在 `principal ?? "default"` 约定下匿名与真名叫
|
|
129
|
+
// `default` 的租户同形(匿名别名匹配让这一格第一次真的可达,见 findParkedAgentForCheckpoint 顶注)。
|
|
130
|
+
const inheritedGate = deps.rebuildInheritedGate ? await deps.rebuildInheritedGate(row, { principal: decodeCheckpointScope(req.scope) }) : undefined;
|
|
108
131
|
const stores = { agentStore: deps.agentStore, checkpointStore: deps.checkpointStore };
|
|
109
132
|
const claim = await deps.registry.claimParkedAgent(stores, match.handle, { scope: row.scope, owner: row.owner });
|
|
110
133
|
if (!claim.ok) {
|