@sema-agent/server 7.10.0 → 7.12.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.md +1 -1
- package/USAGE.md +56 -0
- package/dist/adoption/plan.d.ts +152 -0
- package/dist/adoption/plan.js +513 -0
- package/dist/adoption/runner.d.ts +54 -0
- package/dist/adoption/runner.js +505 -0
- package/dist/adoption/sql.d.ts +76 -0
- package/dist/adoption/sql.js +106 -0
- package/dist/adoption/wire.d.ts +250 -0
- package/dist/adoption/wire.js +153 -0
- package/dist/approval-card.d.ts +24 -0
- package/dist/approval-card.js +32 -0
- package/dist/auth-keys.d.ts +28 -4
- package/dist/auth-keys.js +60 -15
- package/dist/boot/adoption.d.ts +30 -0
- package/dist/boot/adoption.js +57 -0
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/parked-revive-gate.d.ts +56 -7
- package/dist/boot/parked-revive-gate.js +177 -8
- package/dist/boot/permission-rules-audit.d.ts +49 -0
- package/dist/boot/permission-rules-audit.js +57 -0
- package/dist/boot/resolve-spec.js +43 -12
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/budget.js +22 -0
- package/dist/config-types.d.ts +24 -1
- package/dist/config.d.ts +28 -2
- package/dist/config.js +348 -75
- package/dist/governance-ask-marks.js +8 -2
- package/dist/http/route-ctx.d.ts +6 -3
- package/dist/http/routes/adoption.d.ts +26 -0
- package/dist/http/routes/adoption.js +120 -0
- package/dist/http/routes/approvals-assistant.js +2 -1
- package/dist/http/routes/capabilities.js +49 -2
- package/dist/http/routes/rules.d.ts +35 -0
- package/dist/http/routes/rules.js +293 -0
- package/dist/http/routes/shared-memory.d.ts +31 -0
- package/dist/http/routes/shared-memory.js +181 -0
- package/dist/http/routes/trace-usage.js +139 -3
- package/dist/http/server.d.ts +23 -3
- package/dist/http/server.js +123 -3
- package/dist/http/wire-types.d.ts +48 -0
- package/dist/main.js +70 -3
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/tool-trace.d.ts +5 -1
- package/dist/observability/tool-trace.js +33 -6
- package/dist/parked-decide.d.ts +13 -3
- package/dist/parked-decide.js +10 -1
- package/dist/plugins/adoption-log-sql.d.ts +191 -0
- package/dist/plugins/adoption-log-sql.js +273 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -0
- package/dist/plugins/checkpoint-store-sql.js +11 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +8 -0
- package/dist/plugins/permission-rule-store-file.d.ts +83 -0
- package/dist/plugins/permission-rule-store-file.js +371 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +249 -0
- package/dist/plugins/permission-rule-store-sql.js +828 -0
- package/dist/plugins/pg-pool.js +37 -0
- package/dist/plugins/session-policy-store-sql.d.ts +6 -0
- package/dist/plugins/session-policy-store-sql.js +7 -1
- package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
- package/dist/plugins/shared-memory-store-sql.js +516 -0
- package/dist/plugins/store-backend.d.ts +38 -2
- package/dist/plugins/store-backend.js +69 -6
- package/dist/plugins/tidb-pool.js +47 -0
- package/dist/rules-consent.d.ts +194 -0
- package/dist/rules-consent.js +240 -0
- package/dist/run-local.js +120 -13
- package/dist/runtime-governance.js +9 -3
- package/dist/shared-memory-scope-authorizer.d.ts +29 -0
- package/dist/shared-memory-scope-authorizer.js +17 -0
- package/dist/task-settings.d.ts +44 -0
- package/dist/task-settings.js +57 -1
- package/dist/tool-approval.d.ts +60 -0
- package/dist/tool-approval.js +223 -25
- package/dist/trace/core-keyset-guard.d.ts +15 -4
- package/dist/trace/project.d.ts +20 -2
- package/dist/trace/project.js +25 -4
- package/package.json +3 -3
package/dist/http/server.js
CHANGED
|
@@ -30,7 +30,10 @@ import { clampVerifyRounds, verifyRoundsFromBody } from "./verify-rounds.js"; //
|
|
|
30
30
|
import { handleCapabilities } from "./routes/capabilities.js";
|
|
31
31
|
import { handleObservability } from "./routes/observability.js";
|
|
32
32
|
import { handleDiagnostics } from "./routes/diagnostics.js";
|
|
33
|
+
import { handleAdoption } from "./routes/adoption.js";
|
|
33
34
|
import { handleMemoryPolicy } from "./routes/memory-policy.js";
|
|
35
|
+
import { handleSharedMemory } from "./routes/shared-memory.js";
|
|
36
|
+
import { handleRules } from "./routes/rules.js";
|
|
34
37
|
import { handleSessionsList } from "./routes/sessions-list.js";
|
|
35
38
|
import { handleSessions, createSessionsLocal } from "./routes/sessions.js";
|
|
36
39
|
import { handleSessionSync, createSessionSyncLocal } from "./routes/session-sync.js";
|
|
@@ -147,7 +150,10 @@ const ROUTE_DOMAINS = [
|
|
|
147
150
|
handleImages,
|
|
148
151
|
handleApprovalsAssistant,
|
|
149
152
|
handleObservability,
|
|
153
|
+
handleAdoption,
|
|
150
154
|
handleMemoryPolicy,
|
|
155
|
+
handleSharedMemory,
|
|
156
|
+
handleRules,
|
|
151
157
|
handleSessionsList,
|
|
152
158
|
handleSessions,
|
|
153
159
|
handleSessionSync,
|
|
@@ -598,9 +604,20 @@ export function createHttpServer(rawDeps) {
|
|
|
598
604
|
// #154 件三:装配自证只读面(routes/diagnostics.ts):GET /v1/diagnostics/wiring(operator-only)。
|
|
599
605
|
if (await handleDiagnostics(req, res, url, ctx))
|
|
600
606
|
return;
|
|
607
|
+
// design/183 §4.3:收编面(routes/adoption.ts,operator lane;POST /v1/adoption + GET /v1/adoption/:id)。
|
|
608
|
+
if (await handleAdoption(req, res, url, ctx))
|
|
609
|
+
return;
|
|
601
610
|
// design/158 A9:memory 导出/同步 + /v1/policy 只读面(routes/memory-policy.ts)。
|
|
602
611
|
if (await handleMemoryPolicy(req, res, url, ctx))
|
|
603
612
|
return;
|
|
613
|
+
// #154 车二:CC settings 导入两口(lane=principal,billable=false)。
|
|
614
|
+
if (await handleRules(req, res, url, ctx))
|
|
615
|
+
return;
|
|
616
|
+
// design/177 共享记忆库只读面(routes/shared-memory.ts)。🔴 域头的合取式**就是**挂载条件:供给面
|
|
617
|
+
// 与 org 折叠面双在场才有这个域,缺一个则请求落到本函数尾的全局 404(诚实缺席,不是 501)。
|
|
618
|
+
// `capabilities.sharedMemory` 读同一个合取式 ——「说 yes ⟺ 面真能用」因此是结构性的,不靠人盯。
|
|
619
|
+
if (deps.sharedMemoryStore && deps.orgMemoryDirectory && (await handleSharedMemory(req, res, url, ctx)))
|
|
620
|
+
return;
|
|
604
621
|
// design/158 A9:会话/发件链接列表面(routes/sessions-list.ts)。
|
|
605
622
|
if (await handleSessionsList(req, res, url, ctx))
|
|
606
623
|
return;
|
|
@@ -1065,6 +1082,13 @@ export function createHttpServer(rawDeps) {
|
|
|
1065
1082
|
sendError(res, 400, "request.field_invalid", "interactiveTools must be a boolean");
|
|
1066
1083
|
return null;
|
|
1067
1084
|
}
|
|
1085
|
+
// [1909]⑧ oneShot(core 5.23.0):boolean fail-loud,与它的姊妹旋钮 interactiveTools 逐字同姿势 ——
|
|
1086
|
+
// 两者说的是同一件事的两半(这次提交是不是 `-p` 一次性形)。非 boolean 静默丢会让调用方以为已经声明
|
|
1087
|
+
// 了一次性、模型却照旧被告知「结束回合等通知」= 后台结果丢失,正是本键存在的那个失败形。
|
|
1088
|
+
if (body.oneShot !== undefined && typeof body.oneShot !== "boolean") {
|
|
1089
|
+
sendError(res, 400, "request.field_invalid", "oneShot must be a boolean");
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1068
1092
|
// [854]② retainBackgroundProcesses:boolean fail-loud(邻居姿势 —— 非 boolean 静默变「不驻留」正是
|
|
1069
1093
|
// silent-drop 病灶);单用户闸/多租忽略在 resolveSpec(retainBackgroundProcessesFromBody)。
|
|
1070
1094
|
if (body.retainBackgroundProcesses !== undefined && typeof body.retainBackgroundProcesses !== "boolean") {
|
|
@@ -1254,7 +1278,15 @@ export function createHttpServer(rawDeps) {
|
|
|
1254
1278
|
* rebuild the taskConfig via the same `resolveSpec` path (from the sessionId-keyed checkpoint_ctx), then
|
|
1255
1279
|
* `runner.resume`. The capability token never leaves the service. Updates the parked run row to its new state.
|
|
1256
1280
|
*/
|
|
1257
|
-
async function resumeCheckpoint(sessionId, decision, reason,
|
|
1281
|
+
async function resumeCheckpoint(sessionId, decision, reason,
|
|
1282
|
+
// core `ApprovalSettledBy`(5.23.0):**这次结算的出处**,由调用方命名。REQUIRED —— core 的字段
|
|
1283
|
+
// 文档把义务钉在结算点上(「Fill it at every settlement site」),并说明集中推导正是「窗到期被报成
|
|
1284
|
+
// 另一个人拒绝」的成因。本函数有两个调用方且函数内**无从分辨**(sweep 与人为 decide 在这里唯一
|
|
1285
|
+
// 的差别是 `req` 缺席,那是「有没有 HTTP 请求」不是「谁结算的」),所以只能穿参。
|
|
1286
|
+
// ⚠️ core 的相容规则:`decision:"approve"` 只接受 `"human"` 或缺席 —— 「没人答所以它跑了」不是
|
|
1287
|
+
// 任何事实的记录。今天唯一传非 `"human"` 的调用方(SLA sweep)恒 deny,故不冲突;将来若有一个
|
|
1288
|
+
// 会 approve 的非人调用方,它该传的是**缺席**而不是一个自造的词。
|
|
1289
|
+
settledBy, req, // absent on the internal D-D SLA deny-sweep (resolveSpec rebuilds auth from the checkpoint, not req)
|
|
1258
1290
|
answer, binding, onResumeCommitted) {
|
|
1259
1291
|
const cs = deps.checkpointStore;
|
|
1260
1292
|
// No scope filter here BY DESIGN (council): an operator may view/decide ANY tenant's pending approval.
|
|
@@ -1371,7 +1403,9 @@ export function createHttpServer(rawDeps) {
|
|
|
1371
1403
|
...(deps.logger ? { warn: (event, fields) => deps.logger?.warn?.(event, fields) } : {}),
|
|
1372
1404
|
}, {
|
|
1373
1405
|
token, scope: cp.scope, sessionId: cp.sessionId, pendingAction: cp.pendingAction,
|
|
1374
|
-
|
|
1406
|
+
// #204 件6③:赎回腿是**另一个**结算点,出处同样穿参(它自己不猜)。今天走到这里的恒是人为
|
|
1407
|
+
// /decide —— 上面那道门写着 `req !== undefined`,而 sweep 恰是 req 缺席的那条腿。
|
|
1408
|
+
decision, settledBy, ...(reason !== undefined ? { reason } : {}),
|
|
1375
1409
|
...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
|
|
1376
1410
|
// remember 的 grant 闭包(onResumeCommitted 的唯一现役来源)在 parked 腿以行的 root/host
|
|
1377
1411
|
// 会话为锚透传([1591] 候裁② server 修——落子代会话则探针键永不相交,见 grantOnCommit 注);
|
|
@@ -1509,6 +1543,8 @@ export function createHttpServer(rawDeps) {
|
|
|
1509
1543
|
...(decision === "approve" && binding?.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
|
|
1510
1544
|
...(reason ? { reason } : {}),
|
|
1511
1545
|
...(answer !== undefined ? { answer } : {}),
|
|
1546
|
+
// #204 件6:结算出处逐字上 outcome(见参数顶注)。core 只接受它自己那三个词,server 不派生、不缺省。
|
|
1547
|
+
settledBy,
|
|
1512
1548
|
};
|
|
1513
1549
|
// Developer-mode verify (1.44) survives suspend/resume (core `resumeWithVerification`, design/51 P1-b): a
|
|
1514
1550
|
// task submitted with verify:true that hit an F4 gate must STILL be gated by the adversarial verifier on
|
|
@@ -2096,6 +2132,60 @@ export function createHttpServer(rawDeps) {
|
|
|
2096
2132
|
}
|
|
2097
2133
|
catch (e) {
|
|
2098
2134
|
if (e instanceof CheckpointError) {
|
|
2135
|
+
// ── A-010.2:`checkpoint.resume_aborted` = 取消终局(既非 terminal-failed,也**不是** retriable)──
|
|
2136
|
+
// core 只在 `taskConfig.signal.aborted === true` 时铸这个码(四个铸造点:pre-CAS 入口 / 编辑复裁
|
|
2137
|
+
// 竞速两处 / consumed 后 / reopen 成功后),而本腿递进去的 `signal` 只有 `cancelCtrl.signal` ——
|
|
2138
|
+
// 其唯一 abort 源是取消(inflightRuns 句柄 + 心跳轮询的持久取消旗)。所以收到该码 ⟺ 这条 run
|
|
2139
|
+
// 被取消了,四形的正确落点因此**同一个**:按取消结算(下面那道 pending/consumed 的判别式,在
|
|
2140
|
+
// 「该做什么」这个问题上是无差别的,故不做)。
|
|
2141
|
+
// · 归下面的 terminal 支(修前现状)= 200 的取消出路(本 catch 更下面的 `cancelCtrl.signal.aborted`
|
|
2142
|
+
// 分支)被本 CheckpointError 分支**遮蔽**:运维拿到 409 + core 裸报文,行虽落 failed 但
|
|
2143
|
+
// `errorCode` 不是 `cancelled` 且 `setTerminal` 连 TaskResult 都不写(null),pre-CAS 三形的
|
|
2144
|
+
// 卡还留 pending = 行终局 + 卡 pending 的孤儿对。
|
|
2145
|
+
// · 归 retriable(把该码加进下面的闭集)= 把一条**用户已取消**的 run 重新 park 成 `suspended`;
|
|
2146
|
+
// 下一次 /decide 的 markResuming 会重置取消旗并把活真跑起来 —— 取消被静默打败,执行车道回归。
|
|
2147
|
+
// ⛔ 修法刻意是「只给该码开一条臂」而不是把 `cancelCtrl.signal.aborted` 分支上提到本分支之前:
|
|
2148
|
+
// 同在 abort 期铸的 `checkpoint.reopen_failed` 两形(store 拒 reopen = 审批已终局消耗;reopen
|
|
2149
|
+
// 在飞失败 = 卡的状态从这里无法证明)带的信息比 "cancelled" 多,上提会把它们折没。
|
|
2150
|
+
if (e.code === "checkpoint.resume_aborted") {
|
|
2151
|
+
// 序与 /v1/runs/:id/cancel 的 cancelSuspended 逐字同向:**先**按 cancel 语义结算卡,**再**驱行
|
|
2152
|
+
// 终局(行终局会释放 task_active;反序会开一个「卡还 pending 而行已死」的窗)。cancel 的卡结算
|
|
2153
|
+
// 语义是 expire(≈ deny 的 reaper 终局),不是 resolve —— 取消意味着 run 死掉,deny-RESUME 会
|
|
2154
|
+
// 把拒绝喂回模型烧 token。consumed 那一形(core 3894)在这里天然是 no-op:CAS 只翻 pending。
|
|
2155
|
+
// ⚠️ 与 `/v1/runs/:id/cancel` 的 `cancelSuspended` **刻意分道**的一点(codex 对抗复审 R2-高2
|
|
2156
|
+
// 的处置):那只在 CAS 输掉时会改答 409,因为它站在腿**外面**——输掉意味着「可能有一条 decide
|
|
2157
|
+
// 腿正在跑」,它没资格驱行终局。本臂站在腿**里面**:markResuming 已经赢过、这条 run 归我们,
|
|
2158
|
+
// 而本码的语义是「它被取消了」。此刻 CAS 输掉只可能是卡已离开 pending(自己那次 resume 消费掉
|
|
2159
|
+
// 的 consumed 形,或 reaper 先收了)——两种都意味着**没有活审批留在外面**,行照驱终局才是对的:
|
|
2160
|
+
// 不驱才会把会话锁一直攥着(这正是 [868] 那条事故)。故不改结果面,但**必须留痕**。
|
|
2161
|
+
const cs2 = deps.checkpointStore;
|
|
2162
|
+
if (cs2) {
|
|
2163
|
+
try {
|
|
2164
|
+
const live = await cs2.get(token);
|
|
2165
|
+
if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
|
|
2166
|
+
// CAS 输了:卡在我们读它与写它之间被别人(自己的 resume / reaper)settle 了。行为面无变化,
|
|
2167
|
+
// 但「取消时这张卡到底是谁收的」不留痕就永远查不出来。
|
|
2168
|
+
deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
catch (cancelSettleErr) {
|
|
2172
|
+
// 卡结算是尽力而为:行终局才是释放锁的权威写,残留的 pending 卡另有 reaper 车道兜底。
|
|
2173
|
+
// 但**必须留痕** —— 静默吞掉的话「取消之后卡为什么还在」在遥测里没有任何线索。
|
|
2174
|
+
deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
|
|
2175
|
+
taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// `claimedRow &&` 与下面那道分支**同一个**守卫,不是多余的:驱行终局是有资格才做的写(只有
|
|
2180
|
+
// 抢到这条 run 的腿才有资格),而「本码只可能在 markResuming 之后铸」这件事是当前控制流的
|
|
2181
|
+
// 巧合、不是结构保证。守卫写出来,这条臂就不依赖那个巧合。
|
|
2182
|
+
if (claimedRow && taskId && deps.runStore) {
|
|
2183
|
+
const c = { taskId, sessionId, status: "failed", errorCode: "cancelled", errorMessage: e.message, stats: { turns: 0, tokens: 0 } };
|
|
2184
|
+
await deps.runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null).catch(() => undefined);
|
|
2185
|
+
}
|
|
2186
|
+
settleFleet("failed"); // MF-Fleet (#7): 取消结算终局 → 行从 fleet 摘除
|
|
2187
|
+
return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
|
|
2188
|
+
}
|
|
2099
2189
|
// We flipped the row to `running` (claimedRow). Two CheckpointError classes diverge here (D-1, core 1.101):
|
|
2100
2190
|
// • TERMINAL (already_resolved / not_found / gate_mismatch / unsupported_version): the checkpoint is
|
|
2101
2191
|
// consumed or gone — drive the row terminal now (releasing task_active) instead of leaving a zombie
|
|
@@ -2110,6 +2200,13 @@ export function createHttpServer(rawDeps) {
|
|
|
2110
2200
|
e.code === "checkpoint.reopened_concurrently" ||
|
|
2111
2201
|
e.code === "resume.parent_constraint_missing" || // 历史复审轴A #4(1.254):core 新增两码同为
|
|
2112
2202
|
e.code === "resume.parent_constraint_mismatch" || // PRE-CAS(checkpoint 保持 pending)——按本块 doctrine 归 retriable;当前 /decide 面只 resume 顶层 checkpoint 应不可达,前瞻补齐防嵌套 resume 面开放时踩中
|
|
2203
|
+
// core 5.22.0(BREAKING「编辑复裁前移 pre-CAS」)闭集加员:审批人**改写**入参时,继承祖先链
|
|
2204
|
+
// 的冻结投影先静态复裁 —— 判 deny ⇒ constraint_rejected;链上有 opaque 层/声明 live remainder
|
|
2205
|
+
// ⇒ constraint_unprojectable。两者与上面两码同族:PRE-CAS、checkpoint 留 pending 且**同 token
|
|
2206
|
+
// 仍可再决**(core 明写 fresh-redecision 语义:照原样批 / deny / 改别的都还点得动)。归 terminal
|
|
2207
|
+
// 会释放 task_active 并孤儿化那条仍 pending 的卡 —— 一张还能点的卡当场变死件。
|
|
2208
|
+
e.code === "resume.constraint_rejected" ||
|
|
2209
|
+
e.code === "resume.constraint_unprojectable" ||
|
|
2113
2210
|
e.code.startsWith("wake."); // design/144:wake 拒绝(gate_pending/nothing_to_deliver)全是 PRE-CAS,checkpoint 保持 pending——驱 terminal 会孤儿化仍在等的 park
|
|
2114
2211
|
if (claimedRow && taskId && deps.runStore) {
|
|
2115
2212
|
// Re-park on the SAME gate family the outcome targeted: a plan_review retry must re-park `needs_review`
|
|
@@ -2689,7 +2786,9 @@ export function createHttpServer(rawDeps) {
|
|
|
2689
2786
|
parkedSkipped += 1;
|
|
2690
2787
|
continue;
|
|
2691
2788
|
}
|
|
2692
|
-
|
|
2789
|
+
// #204 件6②:出处 = `"timeout"`(窗到期,**没有人**答)。此前这里与人为 /decide 共用一个不带
|
|
2790
|
+
// 出处的构造,于是下游把一次自动拒渲染成「有人拒绝了你」。
|
|
2791
|
+
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", "timeout").then((r) => { if (r.status >= 400)
|
|
2693
2792
|
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, body: r.body }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: e instanceof Error ? e.message : String(e) }));
|
|
2694
2793
|
}
|
|
2695
2794
|
if (parkedSkipped > 0)
|
|
@@ -2770,12 +2869,19 @@ const ROUTE_LABEL_PATTERNS = [
|
|
|
2770
2869
|
// #151 车4:durable 回决口。必须排在 `/v1/tasks/:id/$sub` **之前**?—— 不必(那条只认三个字面动词),
|
|
2771
2870
|
// 但仍按「具体先于泛化」的表序纪律放在它前面,免得日后 $sub 放宽成 `[^/]+` 时静默吞掉本条。
|
|
2772
2871
|
[/^\/v1\/tasks\/[^/]+\/asks\/[^/]+\/decision$/, "/v1/tasks/:id/asks/:askId/decision"],
|
|
2872
|
+
// [3321] tool-results 读面:同样排在 `/v1/tasks/:id/$sub` 之前(具体先于泛化的表序纪律;$sub 那条
|
|
2873
|
+
// 只认三个字面动词,今日不冲突,但它日放宽成 `[^/]+` 时本条不会被静默吞掉)。
|
|
2874
|
+
[/^\/v1\/tasks\/[^/]+\/tool-results\/[^/]+$/, "/v1/tasks/:id/tool-results/:ref"],
|
|
2773
2875
|
[/^\/v1\/tasks\/[^/]+\/(turns|stream|artifacts)$/, "/v1/tasks/:id/$sub"],
|
|
2774
2876
|
[/^\/v1\/leader\/[^/]+$/, "/v1/leader/:id"],
|
|
2775
2877
|
[/^\/v1\/attachments\/[^/]+$/, "/v1/attachments/:id"],
|
|
2776
2878
|
[/^\/v1\/capabilities\/scenarios\/[^/]+$/, "/v1/capabilities/scenarios/:name"],
|
|
2879
|
+
[/^\/v1\/adoption\/[^/]+$/, "/v1/adoption/:id"],
|
|
2777
2880
|
[/^\/v1\/memory\/sync\/[^/]+$/, "/v1/memory/sync/:scope"],
|
|
2778
2881
|
[/^\/v1\/usage\/(summary|series|breakdown)$/, "/v1/usage/$sub"],
|
|
2882
|
+
// design/177 共享记忆库只读面。两条各自成桶(列举是窗口读、正文是单文档读,延迟分布不同族)。
|
|
2883
|
+
[/^\/v1\/shared-memory\/stores\/[^/]+\/documents$/, "/v1/shared-memory/stores/:store/documents"],
|
|
2884
|
+
[/^\/v1\/shared-memory\/stores\/[^/]+\/document$/, "/v1/shared-memory/stores/:store/document"],
|
|
2779
2885
|
];
|
|
2780
2886
|
const ROUTE_LABEL_LITERALS = new Set([
|
|
2781
2887
|
"/health", "/metrics", "/metrics/summary", "/metrics/plan-cache",
|
|
@@ -2789,8 +2895,22 @@ const ROUTE_LABEL_LITERALS = new Set([
|
|
|
2789
2895
|
// 名册门的枚举器当时只认 `url === …`,整条路由对门隐形(枚举器已一并补上)。
|
|
2790
2896
|
"/v1/fleet/stream",
|
|
2791
2897
|
"/v1/memory/export", "/v1/sendfile-links",
|
|
2898
|
+
// design/177:共享记忆库清单面(具名两条走上面的模式表)。
|
|
2899
|
+
"/v1/shared-memory/stores",
|
|
2792
2900
|
// #154:装配自证读面(operator-only)。低频但**每次都在排障现场被打**,落进 `other` 桶等于排障时看不见。
|
|
2793
2901
|
"/v1/diagnostics/wiring",
|
|
2902
|
+
// design/183:收编发起口(operator-only,一次性部署级动作)。低频、但它的 duration 与失败率是
|
|
2903
|
+
// 迁移现场唯一的机器信号,落进 `other` 桶就看不见了。
|
|
2904
|
+
"/v1/adoption",
|
|
2905
|
+
// #154 车二:CC settings 导入两口。字面量(无 id 段)⇒ 进 LITERALS 表而不是模式表。
|
|
2906
|
+
// 🔴 写**字面量**而不是引 `RULES_CC_IMPORT_*_PATH` 常量:名册门与 billable 申明门都是**扫源码文本**
|
|
2907
|
+
// 的(它们正则抓本表里的双引号词),引常量会让两条新路由对两道门隐形 —— 那不是「过了门」,是绕过门。
|
|
2908
|
+
// ⚠️ 本段注释里也不许出现方括号后紧跟右圆括号的字符对:billable 门的表捕获正则是**非贪婪**的,
|
|
2909
|
+
// 那两个字符会让它在这里提前收尾,后面的字面量整段落在门的视野外(写这条注时真的踩到过)。
|
|
2910
|
+
"/v1/rules/cc-import/prepare", "/v1/rules/cc-import/redeem",
|
|
2911
|
+
// #203 §2:撤销面两口(GET 列举 / DELETE 撤销)共用一个字面路径。同样写字面量而不是引 `RULES_PATH`
|
|
2912
|
+
// 常量,理由与上一条逐字相同(名册门与 billable 申明门都扫源码文本)。
|
|
2913
|
+
"/v1/rules",
|
|
2794
2914
|
]);
|
|
2795
2915
|
/** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
|
|
2796
2916
|
* [#104] 字面量**先于**模式:此前模式先查,五条精确路由被形状桶吞掉(`/v1/approvals/stream`
|
|
@@ -10,6 +10,14 @@ import type { AgentDefinition, McpServerSpec } from "@sema-agent/core";
|
|
|
10
10
|
export interface TaskRequestBody {
|
|
11
11
|
objective: string;
|
|
12
12
|
sessionId?: string;
|
|
13
|
+
/** Which deployment SCENARIO assembles this task's tools/prompt/skills (the keys of the worker's scenario
|
|
14
|
+
* table — e.g. "default"/"code-review"/"scan"/"team"). An INTENT bounded by the deployment's own table AND,
|
|
15
|
+
* on a governed worker, by the principal's center-resolved allowlist: an unknown name is a typed 400
|
|
16
|
+
* `scenario_unknown`, a known-but-not-allowed one a typed 400 `scenario_not_allowed`; absent ⇒ the ruling's
|
|
17
|
+
* assigned scenario, else `DEFAULT_SCENARIO` (consumed at `boot/resolve-spec.ts` via `gateScenarioRequest`).
|
|
18
|
+
* ⚠️ scenario-SPECIFIC keys (`repo`/`lenses`/`rounds`, read through `ScenarioRequest`'s index signature) are
|
|
19
|
+
* deliberately NOT declared here — they belong to one scenario each, not to the shared submit surface. */
|
|
20
|
+
scenario?: string;
|
|
13
21
|
/** dispatch-gateway failover 幂等第二级:caller-minted 任务 id(uuidv7)。同 owner 重放同 id ⇒ 幂等重放;
|
|
14
22
|
* 异 owner ⇒ 409 `conflict.run_exists`;非 uuidv7 ⇒ 400 `request.id_invalid`(routes/runs.ts:192-208 真验收)。
|
|
15
23
|
* [2400] TR-16 连带:此键此前只活在 runs.ts 读点,导出类型漏declared——CAPS-OPS-13 同族第 9 键。 */
|
|
@@ -76,6 +84,14 @@ export interface TaskRequestBody {
|
|
|
76
84
|
* a rung fails the gate (default = it didn't complete). Default off; mutually exclusive with `verify`;
|
|
77
85
|
* not on /v1/tasks/stream. ⚠️ each rung is a cold re-run — use for read-only / idempotent tasks. */
|
|
78
86
|
cascade?: boolean;
|
|
87
|
+
/** Multi-lens review council (the `code-review` scenario's expensive tier) — `council:true` fans the review out
|
|
88
|
+
* to N parallel lenses + an arbiter instead of the lead reviewing in-line; `debate:true` additionally runs the
|
|
89
|
+
* L2 peer-debate rounds. Read on TWO seams and both matter: the scenario builder mounts `run_council`
|
|
90
|
+
* (`capabilities/scenarios.ts` `codeReview`), and `boot/resolve-spec.ts` treats either flag as an EXPLICIT team
|
|
91
|
+
* declaration — it widens the task's wall-clock tenancy budget (`resolveTaskLimits`) and suppresses the value
|
|
92
|
+
* router's auto-escalation (`explicitTeam`). Only literal `true` counts on both seams. */
|
|
93
|
+
council?: boolean;
|
|
94
|
+
debate?: boolean;
|
|
79
95
|
/** Work-view correlation id (optional): groups the runs of one logical task across both client doors
|
|
80
96
|
* (MCP façade / portal) so a fragmented set of runs reads as one task. Opaque to the engine — only
|
|
81
97
|
* persisted on the run row + filterable via `GET /v1/tasks?jobId=`. ≤64 chars (matches the column). */
|
|
@@ -102,6 +118,18 @@ export interface TaskRequestBody {
|
|
|
102
118
|
* surfaces the validated object as `TaskResult.structuredOutput` (an invalid submit retries, then fails with
|
|
103
119
|
* `output.invalid`). A plain JSON-schema OBJECT (shape + size validated here; deep validity is core's). */
|
|
104
120
|
outputSchema?: Record<string, unknown>;
|
|
121
|
+
/** Retry budget for an INVALID `submit_output` against `outputSchema` (core `TaskSpec.outputRetries`) — the
|
|
122
|
+
* caller-facing other half of the structured-output pair. Narrow acceptance in `boot/resolve-spec.ts`:
|
|
123
|
+
* finite, ≥1, floored, capped at 10; anything else ⇒ key omitted (core's own default). Meaningless without
|
|
124
|
+
* `outputSchema` (core ignores it there). */
|
|
125
|
+
outputRetries?: number;
|
|
126
|
+
/** Within-task compaction tuning (core design/145 `TaskSpec.compaction`). ⚠️ ONLY `clampTolerance` is a caller
|
|
127
|
+
* knob — the rest of core's compaction object is an OPERATOR axis and is deliberately not on the wire, so this
|
|
128
|
+
* type is narrower than core's field on purpose. `boot/resolve-spec.ts` accepts a finite number in [0,1] and
|
|
129
|
+
* omits the whole `compaction` key otherwise. Pairs with `compactionModel` (which gear compacts). */
|
|
130
|
+
compaction?: {
|
|
131
|
+
clampTolerance?: number;
|
|
132
|
+
};
|
|
105
133
|
/** Reasoning-effort selection (CC `/effort` picker, shell-host contract E7): a neutral effort tier mapped to
|
|
106
134
|
* core's `TaskSpec.thinking` (`ThinkingLevel`). Accepted set = core's tiers (off/minimal/low/medium/high/xhigh/max);
|
|
107
135
|
* the picker's advertised default set is `/v1/models` `supportedEffortLevels`. A provided-but-unknown value is a
|
|
@@ -275,6 +303,26 @@ export interface TaskRequestBody {
|
|
|
275
303
|
* (a caller can't point a shared/cloud worker at an arbitrary host path). Validated absolute (prepareSpec 400s a
|
|
276
304
|
* relative cwd — it would silently resolve against the SERVICE process cwd). */
|
|
277
305
|
cwd?: string;
|
|
306
|
+
/** design/119 (CC `--add-dir`, core `TaskSpec.additionalDirectories`): extra host dirs the FILE tools may reach
|
|
307
|
+
* beyond the containment root. 🔒 Gated exactly like `cwd`/`shellEnv` — honored ONLY on the single-user host
|
|
308
|
+
* lane (`task-cwd.ts` `cwdHonored`); off that lane `boot/resolve-spec.ts` drops them with a loud
|
|
309
|
+
* `task_additional_directories_ignored` (never silently). SHAPE is fail-loud at submit (absolute host paths,
|
|
310
|
+
* no `..` segments, ≤ MAX_ADDITIONAL_DIRS entries) — separate from whether the lane honors them.
|
|
311
|
+
* `additionalReadDirectories` (core 5.11.0) is the same door with a READ-only semantic: it widens the read
|
|
312
|
+
* containment (classify auto-allow + read_file/grep) and never the write face. Rides the persisted body onto
|
|
313
|
+
* resume legs. */
|
|
314
|
+
additionalDirectories?: string[];
|
|
315
|
+
additionalReadDirectories?: string[];
|
|
316
|
+
/** [1909]⑧/[1910]/[1911] (core 5.23.0 `TaskSpec.oneShot`): this SUBMISSION is one-shot — no later turn exists
|
|
317
|
+
* in which an async background notification could land (the archetypal case is a headless `sema -p` whose
|
|
318
|
+
* process exits when the turn ends). PER-REQUEST on purpose: "does this submission expect to be continued" is
|
|
319
|
+
* a property of the submission, not of the connection it arrived on. core consumes it as GUIDANCE ONLY (the
|
|
320
|
+
* `RunWorkflow` / delegation receipts tell the model to block-wait via `TaskOutput({block:true})` instead of
|
|
321
|
+
* "end your turn, you will be notified" — the latter is actively wrong here and loses background results); it
|
|
322
|
+
* grants nothing, so there is no tenancy gate. Sibling of `interactiveTools` (the same `-p` posture) and passed
|
|
323
|
+
* through the same way: a boolean rides, absent/garbage ⇒ key omitted (core's default = interactive). Rides the
|
|
324
|
+
* persisted body onto resume legs. */
|
|
325
|
+
oneShot?: boolean;
|
|
278
326
|
/** [R3] Caller-supplied per-request MCP servers (the TOC client's local `.mcp.json`), aligned to core
|
|
279
327
|
* `McpServerSpec`. 🔒 honored on any SINGLE-USER deployment (task-mcp.ts `mcpInjectionHonored` = `requirePrincipal!==true`)
|
|
280
328
|
* — the requester is the super-admin of their OWN worker (CC-parity), on ANY execution lane (the stdio MCP runs on the
|
package/dist/main.js
CHANGED
|
@@ -43,13 +43,17 @@ import { createResolveSpec } from "./boot/resolve-spec.js";
|
|
|
43
43
|
import { createParkedReviveInheritedGate } from "./boot/parked-revive-gate.js";
|
|
44
44
|
import { startReapers } from "./boot/reapers.js";
|
|
45
45
|
import { openStores } from "./boot/stores.js";
|
|
46
|
+
import { runAdoptionBootScan } from "./boot/adoption.js";
|
|
47
|
+
import { auditDormantPermissionRules } from "./boot/permission-rules-audit.js";
|
|
46
48
|
import { createBudgetAndTracing } from "./boot/budget-tracing.js";
|
|
49
|
+
import { createRuleConsentLane } from "./rules-consent.js";
|
|
47
50
|
import { createExecutionEnv } from "./boot/execution-env.js";
|
|
48
51
|
import { createWorkflowOrchestration } from "./boot/workflow-orchestration.js";
|
|
49
52
|
import { createLiveCoordinators } from "./boot/coordinators.js";
|
|
50
53
|
import { createRuntimeCaps } from "./boot/runtime-caps.js";
|
|
51
|
-
import { createRunnerDeps, createSharedRunnerDeps } from "./boot/runner-deps.js";
|
|
54
|
+
import { createRunnerDeps, createRunnerDepsOnAsk, createSharedRunnerDeps } from "./boot/runner-deps.js";
|
|
52
55
|
import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
|
|
56
|
+
import { createSharedMemoryScopeAuthorizer } from "./shared-memory-scope-authorizer.js";
|
|
53
57
|
import { createSessionFaces } from "./boot/session-faces.js";
|
|
54
58
|
import { createLeaderFace } from "./boot/leader.js";
|
|
55
59
|
import { assertStaticWiringConsistent } from "./http/routes/diagnostics.js";
|
|
@@ -168,9 +172,27 @@ async function main() {
|
|
|
168
172
|
// design/158 A10:持久层装配搬到 src/boot/stores.ts(逐字)。⚠️ 该段就地归一 `config.sessionBackend`
|
|
169
173
|
// 且承载三条 fail-loud 拒启断言 —— 位置即契约,理由见该文件头注。
|
|
170
174
|
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, } = await openStores({ config, logger, metrics, localRoot });
|
|
175
|
+
// design/183 I6(server 同族):**每副本 boot 必查**收编日志的在飞行 —— 续跑或响亮留痕,禁静默跳过。
|
|
176
|
+
// 位置:store 开完之后(要 backend)、任何路由装配之前(半迁移状态绝不带进服务期)。判据与「为什么
|
|
177
|
+
// 是续跑而不是拒启」见 boot/adoption.ts 顶注。
|
|
178
|
+
const adoptionBoot = await runAdoptionBootScan({ backend, logger });
|
|
179
|
+
if (adoptionBoot.scanned > 0 || adoptionBoot.error !== undefined) {
|
|
180
|
+
logger.info("adoption_boot_scan", { ...adoptionBoot });
|
|
181
|
+
}
|
|
171
182
|
// design/158 A10:计费/追踪/预算装配搬到 src/boot/budget-tracing.ts(逐字;tracer 与 side-query 同 sink 实例的
|
|
172
183
|
// 「同段构造」契约见该文件头注)。
|
|
173
184
|
const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileSnapshotStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
|
|
185
|
+
// #154 车二:持久化权限规则店(core 5.18.0 design/179 + 5.22.0 design/182)。三面一束 —— 规则桶
|
|
186
|
+
// provider 上 `RunnerDeps.permissionRuleStore`(引擎据它铸 ruleSuggestions + 把 manifest 的
|
|
187
|
+
// `permissionRules.storeWired` 报成真),审批记录 + 导入票喂同意车道(HTTP 两口的属主)。
|
|
188
|
+
// 缺席(local 车道 / 无 backend)⇒ 诚实缺席:storeWired:false、帧上零候选、两口 501。
|
|
189
|
+
// 🔴 总开关在最前(codex round7 [high] 一):关 ⇒ 整条车道根本不装配(店/车道/帧键/两口一起消失)。
|
|
190
|
+
const permissionRuleStores = config.permissionRulesEnabled && backend ? backend.permissionRule() : undefined;
|
|
191
|
+
const ruleConsent = permissionRuleStores ? createRuleConsentLane(permissionRuleStores) : undefined;
|
|
192
|
+
// #203 §3(设计稿 v2 F4 残余):默认 ON 会把**既有**规则桶一并唤醒 —— 在启动日志里把它说出来。
|
|
193
|
+
// 三条判据(店缺席 / 零桶 / 运维显式表过态 ⇒ 都不打行)与失败方向(数不出来只 warn,绝不拒启、
|
|
194
|
+
// 也绝不编一个 0)逐字见 `boot/permission-rules-audit.ts`。
|
|
195
|
+
await auditDormantPermissionRules({ stores: permissionRuleStores, logger, explicit: config.permissionRulesEnabledExplicit });
|
|
174
196
|
// design/158 A10:执行环境装配搬到 src/boot/execution-env.ts(逐字)。
|
|
175
197
|
// ⚠️ 工厂装饰顺序=行为(scratchpad → worktree → SendUserFile 登记 → 附件物化最外层),见该文件头注。
|
|
176
198
|
const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
|
|
@@ -189,11 +211,25 @@ async function main() {
|
|
|
189
211
|
const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowRecoverOpts, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
|
|
190
212
|
// design/158 A10:活体协调器 + SendUserFile 工具面搬到 src/boot/coordinators.ts(逐字;durableEnabled 的
|
|
191
213
|
// 「必须早于 runnerDeps 求值」次序契约见该文件头注)。
|
|
192
|
-
const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
|
|
214
|
+
const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs, ruleConsent });
|
|
193
215
|
// design/158 A10:per-principal caps 段搬到 src/boot/runtime-caps.ts(逐字)。
|
|
194
216
|
const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
|
|
195
217
|
// design/170 件A(#148 件3③):org 记忆准入装配(目录源三态选择+C12 能力探测,坏配置在此拒启动)。
|
|
196
218
|
const orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
|
|
219
|
+
// design/177 —— org 共享记忆库(memory_list/memory_read + `/v1/shared-memory/*` 只读面)。
|
|
220
|
+
// 🔴 铸它的合取式是**唯一**的挂载条件,模型面与 HTTP 面共用:
|
|
221
|
+
// ① SQL 供给面在场(`backend.sharedMemoryStore` —— local 车道诚实缺席,理由在 store-backend.ts);
|
|
222
|
+
// ② org 折叠面在场(目录源)—— 没有成员性判据的共享读面只能全放或全拒,两个都比"这个面不存在"差。
|
|
223
|
+
// deployment-origin scope 只在**无租户边界**的部署里授予:多租户下一条部署级声明会同时授予每一个
|
|
224
|
+
// principal,那是跨租户读而不是配置便利(org-memory.ts 的 N2 同判)。择净在这一行,授权模块只忠实使用。
|
|
225
|
+
const sharedMemoryStore = orgMemoryAdmission.directory !== undefined
|
|
226
|
+
? backend?.sharedMemoryStore?.(createSharedMemoryScopeAuthorizer({
|
|
227
|
+
directory: orgMemoryAdmission.directory,
|
|
228
|
+
deploymentScopes: config.requirePrincipal === true ? [] : orgMemoryAdmission.deploymentMemoryScopes,
|
|
229
|
+
}))
|
|
230
|
+
: undefined;
|
|
231
|
+
if (sharedMemoryStore)
|
|
232
|
+
logger.info("shared_memory_stores_enabled", { backend: backend?.kind });
|
|
197
233
|
// 🔴 codex 复审(#196 finding-2b):`toolResultStore` 在**无 backend** 形下是 undefined,而 core 的
|
|
198
234
|
// Runner 构造函数会在缺席时**每只各自私建**一份 `RunnerSharedToolResultStore`(runtask.js
|
|
199
235
|
// `if (!this.deps.toolResultStore)`)。本部署有多只 Runner(主 / subRunner / hookAgent / #196 的两只无手
|
|
@@ -209,8 +245,10 @@ async function main() {
|
|
|
209
245
|
config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
|
|
210
246
|
pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
|
|
211
247
|
memorySyncRunner, toolResultStore: runnerOffloadStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
|
|
248
|
+
permissionRuleStore: permissionRuleStores?.provider,
|
|
212
249
|
executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
|
|
213
250
|
workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
|
|
251
|
+
sharedMemoryStores: sharedMemoryStore ? sharedMemoryStore : undefined,
|
|
214
252
|
getRunStore: () => runStore,
|
|
215
253
|
});
|
|
216
254
|
const runner = new Runner(runnerDeps);
|
|
@@ -380,6 +418,7 @@ async function main() {
|
|
|
380
418
|
sessionPolicyStore,
|
|
381
419
|
usageWindowStore,
|
|
382
420
|
orgMemoryAdmission,
|
|
421
|
+
sharedMemoryStores: sharedMemoryStore ? sharedMemoryStore : undefined, // design/177:与主 runner 同实例
|
|
383
422
|
}),
|
|
384
423
|
// ── 以下为 subRunner 差异键(不在共享基座;逐个有因)──────────────────────────────────────
|
|
385
424
|
sessionStore: subRunnerSessions, // 子代转录=私有短 TTL fork 路由店,生命周期异于宿主 durable 店
|
|
@@ -679,8 +718,27 @@ async function main() {
|
|
|
679
718
|
// 「部署 ⊇ 操作员」两层完整链(design/181 件二收编;实现与全部理由在 boot/parked-revive-gate.ts,
|
|
680
719
|
// 提出去的唯一理由是 main.ts 顶层 `void main()` 让那条腿的运行期语义在原地一格都钉不住)。
|
|
681
720
|
// 构造条件逐字保持:裸 Agent 工具在场 ∧ 部署真开了 durable 审批。
|
|
721
|
+
// `approverSeat`:与 `RunnerDeps.onAsk` **同一个具名工厂**(单一属主,禁在此处手搓等价闭包)。core
|
|
722
|
+
// 5.22.0 起链条目的 `durableMandate` 位进了摘要,而该位的判据正是这只席位在不在场——两处若不同源,
|
|
723
|
+
// 赎回腿算出的摘要与 park 记的对不上,跨副本赎回整条腿被 pre-CAS 拒(见 parked-revive-gate.ts
|
|
724
|
+
// `mandatePostureOf`)。此处新铸的转发闭包与 runnerDeps 那只行为逐字相同(都只转 `toolApproval.ask`),
|
|
725
|
+
// 摘要只看在场性;仅 core 的 hook 席位去重按函数身份判,那一侧的身份未命中是**多筛一次**(core 自述
|
|
726
|
+
// 的保守方向),不是漏筛。
|
|
727
|
+
// A-010.1 同族的另外两位(`resolveRuntimeCaps` / `autoModeSeatMounted`):5.22.0 的摘要卷进的是**三**位
|
|
728
|
+
// 决议链元数据,其中 `autoModeArmed` 与 `durableMandate` 的 `forceDurableGate` 项都由 per-principal 的
|
|
729
|
+
// `RuntimeCaps` 决定。两处必须与 core 看到的**同一只**事实源接线,否则同样是「park 记 true 而赎回腿供
|
|
730
|
+
// false ⇒ 摘要恒不匹配 ⇒ 这批 principal 的卡永不可赎」:
|
|
731
|
+
// · `resolveRuntimeCaps` = 递给 `RunnerDeps.runtimeCapsResolver` 的同一只(单一属主 boot/runtime-caps.ts);
|
|
732
|
+
// · `autoModeSeatMounted` = **读** runnerDeps 上那一位,不写字面 `true` —— core 的武装式是
|
|
733
|
+
// `runtimeCaps.autoMode === true ∧ deps.autoMode !== undefined`,第二个半场的真值只有 runnerDeps 知道,
|
|
734
|
+
// 在此处手抄 `true` 会在 runner-deps 那边改成有条件挂载的那一天静默错供一位。
|
|
682
735
|
const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
|
|
683
|
-
? createParkedReviveInheritedGate({
|
|
736
|
+
? createParkedReviveInheritedGate({
|
|
737
|
+
config, question, approvalExemptionStore, logger, localRoot,
|
|
738
|
+
approverSeat: createRunnerDepsOnAsk(toolApproval),
|
|
739
|
+
...(runnerDeps.runtimeCapsResolver ? { resolveRuntimeCaps: runnerDeps.runtimeCapsResolver } : {}),
|
|
740
|
+
autoModeSeatMounted: runnerDeps.autoMode !== undefined,
|
|
741
|
+
})
|
|
684
742
|
: undefined;
|
|
685
743
|
// 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
|
|
686
744
|
// center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
|
|
@@ -782,11 +840,17 @@ async function main() {
|
|
|
782
840
|
approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined, // decide remember="session" + list/revoke
|
|
783
841
|
checkpointStore,
|
|
784
842
|
sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined, // E6 operator session-rule store (PUT/GET /v1/sessions/:id/policy)
|
|
843
|
+
ruleConsent, // #154 车二:CC settings 导入两口的属主(缺席 ⇒ 两口 501)
|
|
785
844
|
// E19 cap: core's gate-split (1.134.0) REMOVED the isRemoteExecutionEnv skip — core now snapshots each
|
|
786
845
|
// completed turn + restores on resumeAt for ANY ExecutionEnv when fileSnapshotStore is wired (captureManifest/
|
|
787
846
|
// applyManifest run over any env's FileSystem ops; a 30s timeout bounds a slow remote walk). So rewind works for
|
|
788
847
|
// host/e2b/k8s/ssh/adb/local-docker AND the in-process worker → advertise `rewindFiles` whenever the store is wired.
|
|
789
848
|
fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
|
|
849
|
+
// [3321] tool-results 读面(GET /v1/tasks/:id/tool-results/:ref)的数据源。传的是 **durable** 的那一只
|
|
850
|
+
// (`toolResultStore`,present ⇔ 有 store backend),**不是** Runner 侧的内存兜底 `runnerOffloadStore`:
|
|
851
|
+
// 无 backend 部署里那只是进程内的、跨副本读不到的,把它接上读面只会让调用方读到「有时有有时无」。
|
|
852
|
+
// undefined ⇒ 路由 404 同形(理由见 ServiceStoreDeps.toolResultStore 的头注)。
|
|
853
|
+
toolResultStore,
|
|
790
854
|
taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined, // D-1 上传/取回/删除三动词面
|
|
791
855
|
// design/153 件3d(/decide parked 赎回腿):durable bg 行店 + boot 裸 Agent 工具,与 RunnerDeps/
|
|
792
856
|
// scenarioDeps 同实例(claim/expire/consumeParkedFlip 作用于同一行)。任一缺席=分支不存在。
|
|
@@ -855,6 +919,9 @@ async function main() {
|
|
|
855
919
|
// (`createOrgMemoryAdmissionWiring` 的产物),两面因此共享 TTL 缓存/退避窗/gen 高水位 —— 一个进程
|
|
856
920
|
// 对「谁属于 org:acme」只有一个答案。缺席(无 center 且无 env 表)⇒ 策略面 `org:` 键仍 operator-only。
|
|
857
921
|
orgMemoryDirectory: orgMemoryAdmission.directory,
|
|
922
|
+
// design/177:HTTP 只读面与模型面共用**同一个** provider 实例 —— 两面看到的库集合按定义一致,
|
|
923
|
+
// 不可能出现「壳能读到模型读不到的库」。缺席 ⇒ `/v1/shared-memory/*` 整域不挂载(见 server.ts 域头)。
|
|
924
|
+
sharedMemoryStore: sharedMemoryStore ? sharedMemoryStore : undefined,
|
|
858
925
|
// sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
|
|
859
926
|
// 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
|
|
860
927
|
sessionMirrorRuling: principalCaps
|
|
@@ -41,6 +41,18 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
41
41
|
readonly cls: "F";
|
|
42
42
|
readonly note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。";
|
|
43
43
|
};
|
|
44
|
+
readonly "server.rules.rejected-prepare-row-left": {
|
|
45
|
+
readonly cls: "F";
|
|
46
|
+
readonly note: "被拒的 CC 导入 prepare(候选超帽)未能收掉 core 已落盘的那条 pending 审批记录。放行的最坏后果 = 共享库里留一条**永远没人要的** pending 行(零权限影响:没有票就兑不动它,而且它连确认都没过)。不留痕就没人知道清理面在漏,故记 F 类;真解是给 permission_rule_approval / permission_rule_ticket 加保留期清扫腿(已列后续件)。";
|
|
47
|
+
};
|
|
48
|
+
readonly "server.rules.ticket-claim-release-failed": {
|
|
49
|
+
readonly cls: "F";
|
|
50
|
+
readonly note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。";
|
|
51
|
+
};
|
|
52
|
+
readonly "server.parked-revive.ancestor-classifier-unreachable": {
|
|
53
|
+
readonly cls: "P-DEBT";
|
|
54
|
+
readonly note: "跨副本赎回 parked 审批时,祖先层在 park 时是 **auto-mode 武装**的,但那只分类器是祖先任务上的活闭包(绑着它自己的转写窗+brain),跨进程重建不出来 ⇒ 本仓交一只如实拒答的 decider,core 收到 `unavailable` 后**不产生任何自动裁决**、原样落到祖先冻结审批席那条链(本腿的席位又是无 ALS 的降级形 ⇒ 再 park 给人)。方向:分类器本会 allow 的改成问人(更严),本会 block 的也改成问人(**不是自动放行**,但比自动拒松一档)⇒ 记债不当合法兜底。计数 = 「丢了祖先分类器判决的继承 ask」次数。收口件二选一:core 把分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。";
|
|
55
|
+
};
|
|
44
56
|
readonly "server.fleet.subscriber-callback-threw": {
|
|
45
57
|
readonly cls: "F";
|
|
46
58
|
readonly note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。";
|
|
@@ -64,6 +64,18 @@ export const FAIL_OPEN_TAGS = {
|
|
|
64
64
|
cls: "F",
|
|
65
65
|
note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。",
|
|
66
66
|
},
|
|
67
|
+
"server.rules.rejected-prepare-row-left": {
|
|
68
|
+
cls: "F",
|
|
69
|
+
note: "被拒的 CC 导入 prepare(候选超帽)未能收掉 core 已落盘的那条 pending 审批记录。放行的最坏后果 = 共享库里留一条**永远没人要的** pending 行(零权限影响:没有票就兑不动它,而且它连确认都没过)。不留痕就没人知道清理面在漏,故记 F 类;真解是给 permission_rule_approval / permission_rule_ticket 加保留期清扫腿(已列后续件)。",
|
|
70
|
+
},
|
|
71
|
+
"server.rules.ticket-claim-release-failed": {
|
|
72
|
+
cls: "F",
|
|
73
|
+
note: "CC 规则导入票的**认领回滚**失败(认领之后的某一步没成 ⇒ 本该把认领放回去,而这次放回本身也抛了)。放行的最坏后果 = 这张票留在已认领态、属主这一轮不能重试 —— 恰好等于加认领回滚**之前**的行为,不是新损失;票随 TTL 自然消失,属主重走一次 prepare 即可拿新票(导入按 core 的设计幂等:同一条规则再兑付只是同一个 dot 的重放)。方向上没有任何权限被放宽(规则**没有**落地才走到这条臂),故 F 类;必须留痕,否则「票为什么突然不能用了」在遥测里没有任何痕迹。",
|
|
74
|
+
},
|
|
75
|
+
"server.parked-revive.ancestor-classifier-unreachable": {
|
|
76
|
+
cls: "P-DEBT",
|
|
77
|
+
note: "跨副本赎回 parked 审批时,祖先层在 park 时是 **auto-mode 武装**的,但那只分类器是祖先任务上的活闭包(绑着它自己的转写窗+brain),跨进程重建不出来 ⇒ 本仓交一只如实拒答的 decider,core 收到 `unavailable` 后**不产生任何自动裁决**、原样落到祖先冻结审批席那条链(本腿的席位又是无 ALS 的降级形 ⇒ 再 park 给人)。方向:分类器本会 allow 的改成问人(更严),本会 block 的也改成问人(**不是自动放行**,但比自动拒松一档)⇒ 记债不当合法兜底。计数 = 「丢了祖先分类器判决的继承 ask」次数。收口件二选一:core 把分类器判据持久进链条目,或让祖先 decider 有可跨进程重建的形。",
|
|
78
|
+
},
|
|
67
79
|
"server.fleet.subscriber-callback-threw": {
|
|
68
80
|
cls: "F",
|
|
69
81
|
note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。",
|
|
@@ -336,7 +336,8 @@ export function createMetrics() {
|
|
|
336
336
|
m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
|
|
337
337
|
// 收账批(2026-07-08):这 15 个计数器一直只有 inc 没注册 — 在 auto-register 落地前
|
|
338
338
|
// 整批从未上过 /metrics(inc 曾对未注册名静默 no-op)。补显式注册拿正经 HELP 文案。
|
|
339
|
-
m.counter("permission_denied_total", "Tool-gate denials by source (
|
|
339
|
+
m.counter("permission_denied_total", "Tool-gate denials by source (core's PermissionDeniedSource, incl. org governance; 'other' = a word this build does not know; goal B4 always-on deny meter)");
|
|
340
|
+
m.counter("permission_rule_events_total", "Persisted allow-rule seam events by event (persisted_rule_allowed = a rule authorized a call with nobody asked; rule_store_unreadable = the store failed to read so the call was adjudicated with ZERO rules)");
|
|
340
341
|
m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
|
|
341
342
|
m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
|
|
342
343
|
m.counter("auth_bridge_verify_total", "Registry SSO JWT verifications (auth-bridge), by outcome");
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { Hooks } from "@sema-agent/core";
|
|
1
|
+
import type { Hooks, PermissionDeniedSource } from "@sema-agent/core";
|
|
2
2
|
import type { Logger } from "./logger.js";
|
|
3
3
|
import type { Metrics } from "./metrics.js";
|
|
4
|
+
/** 表内 ⇒ 收窄成 `PermissionDeniedSource`(类型守卫)。计量点用它判「这个词是不是本 build 认得的真词」。
|
|
5
|
+
* 导出是为了让运行期锚(`test/tool-trace.test.ts`,从装树 core 的 `.d.ts` 解析真词表)**复用同一个谓词**
|
|
6
|
+
* —— 测试自己另写一份判据 = 又一张手抄表,正是本文件刚消灭的那个病。 */
|
|
7
|
+
export declare function isKnownDenySource(v: unknown): v is PermissionDeniedSource;
|
|
4
8
|
/**
|
|
5
9
|
* ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{source}` — how often the
|
|
6
10
|
* adjudicate chain DENIES a tool call, by gate source. Deliberately SEPARATE from {@link createToolTracer}:
|
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import { redactSecrets } from "../trace/redact.js";
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The gate sources core's `PermissionDeniedPayload.source` names. Treated as an OPEN enum on the WIRE
|
|
4
|
+
* (a core newer than this build may send a word this table has never heard of — that must not explode
|
|
5
|
+
* the Prometheus label set), but as a CLOSED SET against the core we compile with.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 core 5.23.0([3372] 提货批件②):这张表**曾是手抄的**(`new Set([...五个字面量])`,与 core 的类型
|
|
8
|
+
* 零编译期联系),于是 core 先加 `classifier`、又在 5.23.0(design/182 §7)加 `org` 时,两个**真词**都
|
|
9
|
+
* 被静默折进了 `other`。那不是「少一个标签」——`permission_denied_total{source="other"}` 的告警语义是
|
|
10
|
+
* 「引擎发了本 build 不认识的词,去对表」,真词混进来就把这个信号读废了,而 `org`(组织治理层的拒)
|
|
11
|
+
* 恰恰是运维最该单独看见的一类。
|
|
12
|
+
*
|
|
13
|
+
* ⇒ 表改为 `Record<PermissionDeniedSource, true>` 穷举:**core 增删一个词,本表编译期先红**(闭集词表
|
|
14
|
+
* 禁手抄、必须从 core 类型推导的成文纪律)。运行期配对锚在 `test/tool-trace.test.ts`(从装树 core 的
|
|
15
|
+
* `.d.ts` 解析联合词表逐词断言),因为 vitest 走 esbuild 不查类型 —— 两道锚缺一不可。
|
|
16
|
+
*/
|
|
17
|
+
const KNOWN_DENY_SOURCE_TABLE = {
|
|
18
|
+
policy: true,
|
|
19
|
+
hook: true,
|
|
20
|
+
safety: true,
|
|
21
|
+
shellGate: true,
|
|
22
|
+
planMode: true,
|
|
23
|
+
classifier: true,
|
|
24
|
+
org: true,
|
|
25
|
+
};
|
|
26
|
+
/** 表内 ⇒ 收窄成 `PermissionDeniedSource`(类型守卫)。计量点用它判「这个词是不是本 build 认得的真词」。
|
|
27
|
+
* 导出是为了让运行期锚(`test/tool-trace.test.ts`,从装树 core 的 `.d.ts` 解析真词表)**复用同一个谓词**
|
|
28
|
+
* —— 测试自己另写一份判据 = 又一张手抄表,正是本文件刚消灭的那个病。 */
|
|
29
|
+
export function isKnownDenySource(v) {
|
|
30
|
+
return typeof v === "string" && Object.hasOwn(KNOWN_DENY_SOURCE_TABLE, v);
|
|
31
|
+
}
|
|
6
32
|
/**
|
|
7
33
|
* ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{source}` — how often the
|
|
8
34
|
* adjudicate chain DENIES a tool call, by gate source. Deliberately SEPARATE from {@link createToolTracer}:
|
|
@@ -15,7 +41,7 @@ const KNOWN_DENY_SOURCES = new Set(["policy", "hook", "safety", "shellGate", "pl
|
|
|
15
41
|
export function createPermissionDeniedMeter(metrics) {
|
|
16
42
|
return {
|
|
17
43
|
permissionDenied(payload) {
|
|
18
|
-
metrics.inc("permission_denied_total", { source:
|
|
44
|
+
metrics.inc("permission_denied_total", { source: isKnownDenySource(payload.source) ? payload.source : "other" });
|
|
19
45
|
},
|
|
20
46
|
};
|
|
21
47
|
}
|
|
@@ -82,7 +108,8 @@ export function createToolTracer(logger) {
|
|
|
82
108
|
},
|
|
83
109
|
// core 1.257: the gate's DENY short-circuit never reaches postToolUse* (the call
|
|
84
110
|
// doesn't execute), so denied calls were INVISIBLE to this trace — the exact observability hole
|
|
85
|
-
// asked about. `source` = core's adjudicate-chain enum (
|
|
111
|
+
// asked about. `source` = core's adjudicate-chain enum (the closed set is `KNOWN_DENY_SOURCE_TABLE`
|
|
112
|
+
// above — deliberately NOT re-listed here, a second hand-written copy is how the meter drifted); the
|
|
86
113
|
// deny `reason` can quote the adjudicated args (an approver's text, a policy message), so redact+clip
|
|
87
114
|
// like every other line. Same `tool_trace` key so existing grep workflows see denies in sequence.
|
|
88
115
|
permissionDenied(payload) {
|