@sema-agent/server 7.117.0 → 7.118.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.
Files changed (98) hide show
  1. package/MIGRATION.md +97 -0
  2. package/USAGE.md +37 -18
  3. package/dist/approval-card.d.ts +40 -10
  4. package/dist/approval-card.js +11 -1
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/parked-revive-gate.d.ts +1 -7
  8. package/dist/boot/parked-revive-gate.js +2 -12
  9. package/dist/boot/resolve-spec.js +8 -13
  10. package/dist/boot/runner-deps.js +2 -2
  11. package/dist/boot/stage-07-capability-layer.js +2 -2
  12. package/dist/capabilities/hands-lane.d.ts +25 -0
  13. package/dist/capabilities/hands-lane.js +1 -1
  14. package/dist/capabilities/mcp-probe.d.ts +2 -2
  15. package/dist/config.d.ts +14 -18
  16. package/dist/config.js +13 -24
  17. package/dist/deployment-governance.d.ts +2 -7
  18. package/dist/deployment-governance.js +3 -5
  19. package/dist/host-decision.d.ts +83 -25
  20. package/dist/host-decision.js +31 -1
  21. package/dist/http/admission.js +7 -1
  22. package/dist/http/approval-sweeps.js +1 -1
  23. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  24. package/dist/http/mcp-dial-deadline.js +21 -0
  25. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  26. package/dist/http/memory-compliance-failure-reply.js +17 -8
  27. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  28. package/dist/http/memory-store-refusal-reply.js +16 -5
  29. package/dist/http/resume-legs.js +7 -3
  30. package/dist/http/routes/approvals-assistant.js +21 -24
  31. package/dist/http/routes/capabilities.js +2 -0
  32. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  33. package/dist/http/routes/mcp-reconnect.js +6 -1
  34. package/dist/http/routes/memory-bundle.js +1 -1
  35. package/dist/http/routes/memory-compliance.js +1 -1
  36. package/dist/http/routes/memory-origin.js +1 -1
  37. package/dist/http/routes/runs.js +41 -33
  38. package/dist/http/routes/sessions.d.ts +1 -1
  39. package/dist/http/routes/sessions.js +3 -6
  40. package/dist/http/routes/workflows.js +2 -0
  41. package/dist/http/wire-types.d.ts +6 -1
  42. package/dist/memory-store-refusal.d.ts +42 -17
  43. package/dist/memory-store-refusal.js +25 -6
  44. package/dist/model-unreachable-notice.d.ts +1 -1
  45. package/dist/namespaced-rule-name.d.ts +45 -41
  46. package/dist/namespaced-rule-name.js +36 -22
  47. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  48. package/dist/observability/corrupt-read-seat.js +1 -0
  49. package/dist/observability/fail-open.d.ts +14 -2
  50. package/dist/observability/fail-open.js +14 -2
  51. package/dist/operator-ask.d.ts +4 -1
  52. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  53. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  54. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  55. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  56. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  57. package/dist/orchestration/workflow-notify-journal.js +7 -2
  58. package/dist/outbound-dispatcher.d.ts +29 -20
  59. package/dist/outbound-dispatcher.js +105 -16
  60. package/dist/parked-decide.d.ts +43 -48
  61. package/dist/parked-decide.js +54 -16
  62. package/dist/permission-rule-vocab.d.ts +4 -3
  63. package/dist/permission-rule-vocab.js +1 -1
  64. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  65. package/dist/plugins/approval-ask-store-sql.js +0 -3
  66. package/dist/plugins/one-time-migrations.d.ts +5 -0
  67. package/dist/plugins/one-time-migrations.js +110 -4
  68. package/dist/plugins/pg-pool.js +1 -1
  69. package/dist/plugins/tidb-pool.js +1 -1
  70. package/dist/plugins/web-search.d.ts +20 -1
  71. package/dist/plugins/web-search.js +39 -10
  72. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  73. package/dist/plugins/workflow-run-store-sql.js +5 -7
  74. package/dist/request-key-closure.js +1 -1
  75. package/dist/request-value-closure.d.ts +1 -1
  76. package/dist/rule-entry-list.d.ts +35 -46
  77. package/dist/rule-entry-list.js +17 -25
  78. package/dist/rules-consent.js +3 -0
  79. package/dist/run-local.js +1 -0
  80. package/dist/runs.d.ts +1 -1
  81. package/dist/session-policy-wire.d.ts +13 -12
  82. package/dist/session-policy-wire.js +11 -6
  83. package/dist/spec-fields.d.ts +7 -9
  84. package/dist/spec-fields.js +4 -7
  85. package/dist/standing-answer-grant.d.ts +14 -2
  86. package/dist/standing-answer-grant.js +8 -1
  87. package/dist/task-settings.js +8 -4
  88. package/dist/tool-approval.d.ts +53 -112
  89. package/dist/tool-approval.js +75 -55
  90. package/dist/trace/core-keyset-guard.d.ts +5 -5
  91. package/dist/trace/self-minted-wire.d.ts +16 -2
  92. package/dist/trace/self-minted-wire.js +7 -0
  93. package/dist/user-input-ledger.d.ts +24 -12
  94. package/dist/user-input-ledger.js +33 -14
  95. package/package.json +2 -2
  96. package/scripts/drain-check-local.mjs +74 -3
  97. package/scripts/drain-check.mysql.sql +43 -17
  98. package/scripts/drain-check.pg.sql +46 -20
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * **宿主的决策事实** —— 一条耐久 park 被结算时,本服务能如实陈述的那半件事:谁结束了这次等待,以及
3
- * 审批通道自报的归属。一个叶子模块(只依赖 `@sema-agent/core`),四个消费点共用一个定义。
3
+ * 审批通道自报的归属,以及决定体上陈述「谁决定的」那一只键(`decidedBy`)的闭集、读法与映射。一个叶子模块
4
+ * (运行期只依赖 `@sema-agent/core`;另一处 import 是纯类型的编译期门),全部决定口共用一个定义。
4
5
  *
5
6
  * ## 为什么是「事实」而不是「结算词」(core 7.6.0 / [ref] S6-A,BREAKING)
6
7
  *
@@ -17,14 +18,12 @@
17
18
  import { type AskOutcome, type ResumeOutcome } from "@sema-agent/core";
18
19
  /**
19
20
  * 谁结束了这次等待,以及通道自报的归属。
20
- * · `decidedBy: "person"` —— 运维经审批通道做的决定(两条 HTTP `/decide` 腿);
21
+ * · `decidedBy: "person"` —— 一个人经审批通道做的决定(决定体缺席 `decidedBy` 即此);
21
22
  * · `decidedBy: "sla_timeout"` —— 宿主自己的 SLA 扫在行的 deadline 上判的(FACET A;**只有** sweep 能这么说,
22
- * 而 sweep 结构上不能 `allow`)。
23
- * · `decidedBy: "policy"`(core 7.26.2 / #1016)—— **这台部署自己的策略**拒的:答话的是机器不是人。调用方
24
- * 经决裁体的 `settledBy` 自报;core 据它给模型铸一句**策略句**(而不是「用户不同意,停下来等他」),
25
- * 并把结算记成 `policy_refused`(`who.party:"policy"`,core 7.39.0 #1265 起;此前 `"none"`)。`allow` 侧:core 7.39.0 起引擎收策略的
26
- * 放行(`policy_allowed`),但本仓的两条 decide 口**不开**这一形(`tool-approval.ts` 的 `SETTLED_BY_ONLY_ON_DENY` 与店列的同一条拒仍在 ——
27
- * 耐久车道上的机器放行是一件没裁过的新能力,不在 7.113.0 提货批内,候登)。
23
+ * 而 sweep 结构上不能 `allow`;wire 上永不受理,见 {@link APPROVAL_DECIDED_BY})。
24
+ * · `decidedBy: "policy"` —— **这台部署自己的策略**答的:答话的是机器不是人,`allow` 与 `deny` 两向都可以。
25
+ * 调用方经决定体的 `decidedBy` 陈述;core 据它铸 `policy_allowed` / `policy_refused`(`who.party:"policy"`),
26
+ * 拒的那一向给模型一句**策略句**(而不是「用户不同意,停下来等他」)。
28
27
  * · `approver` —— 通道自报的结算方标识串。**缺席永不读成**「没人批」或「有人批」,只意味着通道没报名字。
29
28
  */
30
29
  /**
@@ -33,9 +32,9 @@ import { type AskOutcome, type ResumeOutcome } from "@sema-agent/core";
33
32
  * · **铸**(本仓自己观察到的事实):只有 `"timeout"` —— 同步 `onAsk` 腿的等待窗归**宿主**所有(引擎不为
34
33
  * 一个它没排程的回调起表),所以「本仓的 TTL 窗走完了、没人答」这件事引擎结构上观察不到,不自报就
35
34
  * 永远被记成「一个人拒绝了」;
36
- * · **转**(调用方在回决体里自报的事实,core 7.26.2 / #1016 起):`settledBy:"policy"` = 答这张卡的是
37
- * **这台部署自己的策略**,不是人。本仓**零判据、原样过境**:词的拒绝逻辑归引擎(`resolveAsk` 对
38
- * 自报词表外的值是响亮 fail-closed deny),本仓抄一份词表的那天 core 加词就会被静默 400 掉。
35
+ * · **转**(调用方在决定体里陈述的事实):决定体的 `decidedBy:"policy"` 经 {@link liveSettledBy} 落成这一格的
36
+ * `"policy"` = 答这张卡的是**这台部署自己的策略**,不是人。wire 上的词只有 {@link APPROVAL_DECIDED_BY} 两个,
37
+ * 映射只有那一只函数 —— 这一格的「转」不再是一个原样过境的串。
39
38
  * 逐词(词表属主恒是 core):
40
39
  * · `"human"` —— 与「不铸这个键」逐字同义(core 缺席即按人处理);
41
40
  * · `"aborted"` —— core **明令拒收**宿主自报(那是引擎自家 fail-closed 端的词,宿主自报等于把自己的
@@ -62,13 +61,78 @@ export type HostDecision = Extract<ResumeOutcome, {
62
61
  * 编译期先红)。今天 = `"person"` | `"sla_timeout"` | `"policy"`。 */
63
62
  export type HostDecidedBy = HostDecision["decidedBy"];
64
63
  /**
65
- * 一次 decide 的事实,**一处成形**(两条 HTTP decide 腿 + 活卡 PARKED 赎回席共用)。
64
+ * ══ 决定体的出处位 `decidedBy`:一只闭集、一只读法、一只映射 ═══════════════════════════════════════════════
66
65
  *
67
- * `decidedBy` = 调用方**陈述**的决策者(缺席 ⇒ `"person"`,= 本面自古以来的形:一条不说话的 decide 就是
68
- * 一个人按的)。🔴 **本仓零判据**:词表的属主是 core,不认识的词由它 pre-CAS 响亮拒
69
- * (`checkpoint.invalid_outcome`,`hostDecision.decidedBy … is outside the closed set`)。在这里再判一次
70
- * 就是同一语义面的第二个判官(源头修复纪律 [ref]),而且本仓抄一份词表的那天 core 加第四个词就会被
71
- * 静默 400 掉 —— 那正是 #1016 的 `"policy"` 加进来时本可以发生的事。
66
+ * 规则一句话:**一个审批决定用一只键说是谁做的** —— `decidedBy ∈ {"person","policy"}`,缺席 = `"person"`;读它的只有
67
+ * {@link readDecidedBy}(三条工具审批决定口共读:live `POST /v1/tool-approvals/:id/respond`、耐久 ask 账
68
+ * `POST /v1/tasks/:id/asks/:askId/decision`、耐久 `POST /v1/approvals/:sessionId/decide`),把它交给引擎的只有
69
+ * {@link liveSettledBy}(同步座 `AskOutcome.settledBy`)与 {@link hostDecisionFacts}(停驻座 `hostDecision.decidedBy`)。
70
+ * · 词与决定动词**正交**:策略可以放行也可以拒(core 7.39.0 起引擎铸 `policy_allowed` / `policy_refused`)。唯一的例外是
71
+ * 引擎自己的那一条 —— 一张要人的卡(`requiresRealApproval`)不归策略放行,见 {@link policyCannotAllow}。
72
+ * · `"sla_timeout"` **永不从 wire 收**:那是「等待到点、没人答」,只有本服务自己的截止扫说得出来(`approval-sweeps.ts`
73
+ * 直接铸 `{decidedBy:"sla_timeout"}`,不经本读法)。调用方说它 = 替服务端伪证一件只有服务端看得见的事。
74
+ * · `plan_review` 不读这只键:引擎的 plan_review 结局形没有决定者位(core 的结局规整会把别的键整只重建掉),收下它就是
75
+ * 一句「已记」的空话。那一门照兄弟门字段那一族拒(`approvals-assistant.ts` 的回决门族派生),候 core 给座。
76
+ *
77
+ * 词表的来源:core 包根**没有**导出这只词表的运行期常量(只有类型 `ResumeOutcome[...]["hostDecision"]["decidedBy"]`),所以
78
+ * 本仓一只闭集常量 + 两道编译期门**双向**咬着 core 的型:core 加 / 退一个词 ⇒ 这里先红,不会在 wire 上静默留一个两个座都已经
79
+ * 不收的词,也不会把 core 新开的词静默 400 掉。core 导出词表之日,本常量改为直接镜像那一只(请托在案)。
80
+ */
81
+ export declare const APPROVAL_DECIDED_BY: readonly ["person", "policy"];
82
+ /** wire 上的出处词(决定体 `decidedBy` 的值域)。 */
83
+ export type ApprovalDecidedBy = (typeof APPROVAL_DECIDED_BY)[number];
84
+ /** 闭集外的拒句(三条决定口同一句;码按各口既有约定,见 {@link readDecidedBy})。 */
85
+ export declare const DECIDED_BY_OUTSIDE_CLOSED_SET = "decidedBy must be \"person\" or \"policy\" when present \u2014 who made this decision: a person, or this deployment's own policy (absent = \"person\"); \"sla_timeout\" is never accepted from a caller (only this server's own deadline sweep says a wait ended with nobody answering)";
86
+ /** 直连门上的拒句(与 `remember` / `updatedInput` / `checkpointToken` 同族:键不在决策证明的签名载荷里)。 */
87
+ export declare const DECIDED_BY_NOT_IN_PROOF = "decidedBy is not supported on a direct-door worker (not covered by the decision proof)";
88
+ /** 一张要人的卡上策略的放行(三条决定口同一句;判据 {@link policyCannotAllow})。 */
89
+ export declare const POLICY_CANNOT_ALLOW_PERSON_ASK = "decidedBy \"policy\" cannot allow this ask \u2014 it demands a person's approval (risk.requiresRealApproval), and a policy's answer is not a person's; answer it as a person, or refuse it as the policy";
90
+ /**
91
+ * 退役键 `settledBy`(7.93.3–7.117.x 的「只配 deny」那一位)的拒句。耐久两口由各自的键闭集 / strict 体拒;live `/respond` 的解析不是
92
+ * 键闭集的(未知键一律忽略),而这只键不是随便一个未知键:它是调用方在**明确陈述**「这是策略答的」—— 忽略它就是把缺席律(person)
93
+ * 套在一个说了话的体上(旧壳的策略拒记成人拒;一张要人的卡上旧形的放行被读成人的放行,绕过 {@link policyCannotAllow})。所以那条口
94
+ * 在裁决之前响亮拒它,与同一只 parser 里 `scope` 的拒同一条先例;那只 parser 收成键闭集之日,这一臂并进闭集门。
95
+ */
96
+ export declare const SETTLED_BY_RETIRED = "settledBy is retired \u2014 send decidedBy: \"person\" | \"policy\" (who made this decision; absent = \"person\"); ignoring it would record a policy's answer as a person's";
97
+ /** {@link readDecidedBy} 的判决(调用点做穷举 `switch` + `never` 兜底,各按自己的码约定发出)。 */
98
+ export type DecidedByReading = {
99
+ kind: "accepted";
100
+ decidedBy: ApprovalDecidedBy;
101
+ } | {
102
+ kind: "not_in_proof";
103
+ } | {
104
+ kind: "outside_closed_set";
105
+ };
106
+ /**
107
+ * 决定体 `decidedBy` 的**唯一读法**。次序即契约:
108
+ * ① 缺席 ⇒ `"person"`(缺席律:一条不说话的决定就是一个人按的 —— 与修前逐字同形);
109
+ * ② 直连门在场且键在场 ⇒ `not_in_proof`(不论值:它不在签名载荷里,能改线上字节的一方可以把人翻成策略;
110
+ * 第二参给字面 `false` 的调用点 = 那条口没有决策证明,live `/respond` 即此);
111
+ * ③ 闭集内 ⇒ 那个词;其余一切(`"sla_timeout"`、结算词、别的串、非串、`null`)⇒ `outside_closed_set`。
112
+ */
113
+ export declare function readDecidedBy(raw: unknown, directDoorActive: false): Exclude<DecidedByReading, {
114
+ kind: "not_in_proof";
115
+ }>;
116
+ export declare function readDecidedBy(raw: unknown, directDoorActive: boolean): DecidedByReading;
117
+ /**
118
+ * **同步座的映射**(`AskOutcome.settledBy`;ask 账行的 `settled_by` 列存的也是这一格):`person` ⇒ 不铸键(引擎缺席即按人读,
119
+ * 与修前逐字同形 —— 人的放行仍是裸 `true`),`policy` ⇒ `"policy"`。总函数,无 cast。
120
+ */
121
+ export declare function liveSettledBy(decidedBy: ApprovalDecidedBy): Extract<HostSettledBy, "policy"> | undefined;
122
+ /**
123
+ * 引擎自己的那一条例外:**一张要人的卡不归策略放行**(core `policyCannotAnswer` / `unjudgedAllowRefused` —— 同步座上
124
+ * `req.requiresRealApproval === true` 时策略的 allow 被拒成 `blanket_allow_refused`)。本仓**只在自己的副作用先于引擎落地的
125
+ * 两条口**上于裁决之前说这一句(live `/respond`:会话放行 / 常驻规则 / 一条按 approve 回放的 DECIDED 行;耐久 ask 账:一条
126
+ * 宣称 approve 的 DECIDED 行),读的是引擎读的同一个事实(卡上的 `requiresRealApproval`)。耐久 `/decide` 不判:引擎在 CAS 之前
127
+ * 就拒(`checkpoint.invalid_outcome`,卡留 pending),本口的副作用全在引擎受理之后 —— 在那里再判一次就是第二个判官。
128
+ * core 包根没有导出这只谓词(请托在案);导出之日本函数改为直接调它。
129
+ */
130
+ export declare function policyCannotAllow(decidedBy: ApprovalDecidedBy, allow: boolean, demandsAPerson: boolean): boolean;
131
+ /**
132
+ * 一次 decide 的**停驻座事实**,一处成形(耐久 `/decide` 与活卡 PARKED 赎回席共用;截止扫直接铸 `{decidedBy:"sla_timeout"}`)。
133
+ *
134
+ * `decidedBy` = 已经过 {@link readDecidedBy} 的 wire 词:两个词在停驻座上**就是它自己**(`person` / `policy`,
135
+ * 两只词表的交集由上面的编译期门证),所以这里没有 cast,也没有缺席律(缺席已在读法里落成 `"person"`)。
72
136
  *
73
137
  * `principal` = 那条腿已经验过身份的决策者(缺席 = 匿名档的部署,通道确实没有名字可报)。
74
138
  * 归属串走 core 的 `screenApproverAttribution` **先筛后带**:core 对形不合的归属是 pre-CAS 响亮拒,
@@ -76,14 +140,8 @@ export type HostDecidedBy = HostDecision["decidedBy"];
76
140
  * 被改造过的串):归属是转录,不是可以「修一修」的东西 —— 修出来的名字既不是通道报的那个,也不是
77
141
  * 「通道没报」,两头都不真。
78
142
  *
79
- * 🔴 归属**不按词分岔**:core 7.38.x 的 `policy_refused` 结算记录上没有归属席(`who.party:"none"`),7.39.0 #1265 起有了
80
- * (`who.party:"policy"`,`approver?`)—— 无论 core 的记录怎么写,**本服务观察到的事实**不变:送出这次请求的那把已验证身份是真的,本仓的
143
+ * 🔴 归属**不按词分岔**:无论 core 的记录怎么写,**本服务观察到的事实**不变:送出这次请求的那把已验证身份是真的,本仓的
81
144
  * 迟到结算账本(`approval-ask-audit-store.ts`)记的就是它。按词删一格 = 给账本挖一个只在某个词上出现的洞。
82
- *
83
- * 🔴 **入参是 `string`,不是 `HostDecidedBy` —— 这一格刻意让类型说真话**(wire 边界强类型宪法):两条
84
- * decide 腿能证得出的只有「调用方给了个串」(形门在各自的口上),**证不出**它是词表成员;把参数声明成
85
- * 闭集就是让类型替一件本层从没做过的检查背书。⇒ 收 `string`,在**这一处**(本座的边界)做唯一一次 cast,
86
- * 而那次 cast 说的是「我把一个未判词的串放到只收词的座上,判词的是座的属主(core,pre-CAS 响亮拒)」。
87
145
  */
88
- export declare function hostDecisionFacts(decidedBy: string | undefined, principal: string | undefined): HostDecision;
146
+ export declare function hostDecisionFacts(decidedBy: ApprovalDecidedBy, principal: string | undefined): HostDecision;
89
147
  //# sourceMappingURL=host-decision.d.ts.map
@@ -1,6 +1,36 @@
1
1
  import { screenApproverAttribution } from "@sema-agent/core";
2
+ export const APPROVAL_DECIDED_BY = ["person", "policy"];
3
+ export const DECIDED_BY_OUTSIDE_CLOSED_SET = 'decidedBy must be "person" or "policy" when present — who made this decision: a person, or this deployment\'s own policy (absent = "person"); "sla_timeout" is never accepted from a caller (only this server\'s own deadline sweep says a wait ended with nobody answering)';
4
+ export const DECIDED_BY_NOT_IN_PROOF = "decidedBy is not supported on a direct-door worker (not covered by the decision proof)";
5
+ export const POLICY_CANNOT_ALLOW_PERSON_ASK = 'decidedBy "policy" cannot allow this ask — it demands a person\'s approval (risk.requiresRealApproval), and a policy\'s answer is not a person\'s; answer it as a person, or refuse it as the policy';
6
+ export const SETTLED_BY_RETIRED = 'settledBy is retired — send decidedBy: "person" | "policy" (who made this decision; absent = "person"); ignoring it would record a policy\'s answer as a person\'s';
7
+ function isApprovalDecidedBy(v) {
8
+ return APPROVAL_DECIDED_BY.includes(v);
9
+ }
10
+ export function readDecidedBy(raw, directDoorActive) {
11
+ if (raw === undefined)
12
+ return { kind: "accepted", decidedBy: "person" };
13
+ if (directDoorActive)
14
+ return { kind: "not_in_proof" };
15
+ return isApprovalDecidedBy(raw) ? { kind: "accepted", decidedBy: raw } : { kind: "outside_closed_set" };
16
+ }
17
+ export function liveSettledBy(decidedBy) {
18
+ switch (decidedBy) {
19
+ case "person":
20
+ return undefined;
21
+ case "policy":
22
+ return "policy";
23
+ default: {
24
+ const unreachable = decidedBy;
25
+ throw new Error(`liveSettledBy: unknown decidedBy ${String(unreachable)}`);
26
+ }
27
+ }
28
+ }
29
+ export function policyCannotAllow(decidedBy, allow, demandsAPerson) {
30
+ return decidedBy === "policy" && allow && demandsAPerson;
31
+ }
2
32
  export function hostDecisionFacts(decidedBy, principal) {
3
33
  const screened = screenApproverAttribution(principal);
4
- return { decidedBy: (decidedBy ?? "person"), ...(screened.approver !== undefined ? { approver: screened.approver } : {}) };
34
+ return { decidedBy, ...(screened.approver !== undefined ? { approver: screened.approver } : {}) };
5
35
  }
6
36
  //# sourceMappingURL=host-decision.js.map
@@ -1,4 +1,4 @@
1
- import { MCP_IMAGE_MAX_BASE64, isThinkingLevel, expandTiers } from "@sema-agent/core";
1
+ import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isThinkingLevel, expandTiers } from "@sema-agent/core";
2
2
  import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError } from "../security.js";
@@ -14,6 +14,7 @@ import { verifyRoundsFromBody } from "./verify-rounds.js";
14
14
  import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
15
15
  import { resolveDevicePlacementIntent } from "./device-placement.js";
16
16
  import { sendError, httpErrorCode } from "./send.js";
17
+ const APPROVAL_REACH_WORDS = APPROVAL_REACH.map((word) => JSON.stringify(word)).join(" / ");
17
18
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
18
19
  export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
19
20
  export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
@@ -470,6 +471,11 @@ export function createAdmission(ctx) {
470
471
  sendError(res, 400, "request.field_invalid", "interactiveTools must be a boolean");
471
472
  return null;
472
473
  }
474
+ const approvalReach = body.approvalReach;
475
+ if (approvalReach !== undefined && !isApprovalReach(approvalReach)) {
476
+ sendError(res, 400, "request.field_invalid", `approvalReach must be ${APPROVAL_REACH_WORDS} when present (the run declares that nobody on it can be asked to approve; omit the key otherwise)`);
477
+ return null;
478
+ }
473
479
  if (body.oneShot !== undefined && typeof body.oneShot !== "boolean") {
474
480
  sendError(res, 400, "request.field_invalid", "oneShot must be a boolean");
475
481
  return null;
@@ -150,7 +150,7 @@ export function createApprovalSweeps(ctx) {
150
150
  ...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
151
151
  ...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
152
152
  };
153
- const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, hostDecisionFacts(req.settledBy, req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
153
+ const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, hostDecisionFacts(req.decidedBy, req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
154
154
  return { status: out.status, body: out.body };
155
155
  }
156
156
  return { denyExpiredApprovals, redeemParkedAsk };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * S-431 —— 一条 HTTP 请求愿意等**一次远端 MCP 拨号**多久:**一只数、一只 helper**,两口共读。
3
+ *
4
+ * 同一类动作(连一台远端 MCP 服务器 + 列它的工具)在本仓有两张 HTTP 面:
5
+ * · `GET /v1/sessions/:id/mcp` 面板(`routes/sessions.ts`)—— 物化部署缺省场景的申报,到点答 200 `degraded:true`;
6
+ * · `POST /v1/sessions/:id/mcp/reconnect`(`routes/mcp-reconnect.ts`)—— 在飞 run 上重拨一台,到点答 503。
7
+ * 修前面板有期限、reconnect 是裸 `await`(S-431):同一件事两个读法,一台拨不通 / 半开的远端能把后者挂很久。
8
+ * 两口各自**到点答什么**不同(面板是只读的部署面,有一个诚实的空形;reconnect 是一次交易,没有结果就没有
9
+ * 一个能当 `outcome` 的词)—— 但**等多久**是一个事实,所以只有一个数。
10
+ *
11
+ * 🔴 **这是常量,不是旋钮**:没有 env、不进 config-catalog。要调窗就改这一处,两口一起动。
12
+ * (底层拨号自己的上界是 core 的启动期钟 —— env `MCP_TIMEOUT`,未设落 SDK 缺省 —— 与本数无关:本数只界
13
+ * **HTTP 应答**,不界拨号;见 {@link raceDeadline}。)
14
+ *
15
+ * helper 收的是期限**参数**而不是自己读上面那个常量:调用点把 {@link MCP_DIAL_DEADLINE_MS} 显式交下来,
16
+ * `test/mcp-reconnect-deadline.test.ts` 的静态钉就是「每个远端 MCP 拨号调用点都把它交给 `raceDeadline`」。
17
+ */
18
+ /** 一次远端 MCP 拨号的 HTTP 应答期限(毫秒)。面板与 reconnect 共读;**全仓只有这一处写它**。 */
19
+ export declare const MCP_DIAL_DEADLINE_MS = 10000;
20
+ /** `work` 没在期限内落定。**具名型**:调用腿据它分诊,不比对错误文案。 */
21
+ export declare class DeadlineExceeded extends Error {
22
+ readonly boundMs: number;
23
+ constructor(boundMs: number);
24
+ }
25
+ /**
26
+ * 让**应答**不再等 `work`:`work` 先落定就原样交出(resolve / reject 都不变形),期限先到就 reject
27
+ * {@link DeadlineExceeded}。
28
+ *
29
+ * 🔴 **只界应答,不取消 `work`**:本函数不持有任何取消把手 —— core 的 `TaskStream.reconnectMcpServer` 与
30
+ * `materializeMcpTools` 都不收 `AbortSignal`(d.ts 逐字),所以期限到点后底层拨号**继续跑**,落定时
31
+ * 才释放它占的连接 / 子进程。调用点必须把这句话交代给调用方(reconnect 的 503 文案、契约 §G.7c)。
32
+ * 迟到的落定(尤其迟到的 reject)挂一条吞掉,免得没人 await 它时变成未处理拒绝。
33
+ */
34
+ export declare function raceDeadline<T>(work: Promise<T>, deadlineMs: number): Promise<T>;
35
+ //# sourceMappingURL=mcp-dial-deadline.d.ts.map
@@ -0,0 +1,21 @@
1
+ export const MCP_DIAL_DEADLINE_MS = 10_000;
2
+ export class DeadlineExceeded extends Error {
3
+ boundMs;
4
+ constructor(boundMs) {
5
+ super(`did not settle within ${boundMs}ms`);
6
+ this.boundMs = boundMs;
7
+ this.name = "DeadlineExceeded";
8
+ }
9
+ }
10
+ export function raceDeadline(work, deadlineMs) {
11
+ let timer;
12
+ const deadline = new Promise((_resolve, reject) => {
13
+ timer = setTimeout(() => reject(new DeadlineExceeded(deadlineMs)), deadlineMs);
14
+ });
15
+ work.catch(() => undefined);
16
+ return Promise.race([work, deadline]).finally(() => {
17
+ if (timer !== undefined)
18
+ clearTimeout(timer);
19
+ });
20
+ }
21
+ //# sourceMappingURL=mcp-dial-deadline.js.map
@@ -10,21 +10,24 @@
10
10
  *
11
11
  * ## 规则(三层,与 operator 面既有层序逐字同序)
12
12
  * ① core 的**本口专属**码(闭集穷举 `switch`,见 {@link sentCoreCode})—— 码与状态两面同;文案按受众(下一节,一条规则);
13
- * ② `memory.target_outside_store`(core 7.31.1 #1094 跨面 store 状态码)—— 409;
13
+ * ② **店形拒**两码 `memory.target_outside_store`(core 7.31.1 #1094)/ `memory.scope_home_not_directory`(core 7.44.0 #1368)—— 跨面 store 状态码,409、码原样
14
+ * (判据 `memory-store-refusal.ts` 一只;运维行 `memory_store_form_refused { code, path?, form? }` 两受众同一只铸点,路径只在那里);
14
15
  * ③ `ControlPlaneCorruptError` —— typed 500 `internal.memory_control_plane_corrupt`(fail-closed 信号,一码两义见下);
15
16
  * 其余 ⇒ `false`(未分类,调用方发它自己那句 500 且**不回显**内部文本)。
16
17
  *
17
18
  * ## 文案按受众分(码与状态不分)—— **一条规则,三层同施,不按码挑**(F9,7.108.0 合并复审 opus 腿 O-9)
18
- * core 原句是写给运维的诊断材料:② ③ 点名**宿主路径**(store 根、链解析到哪 / 控制面账本文件),① 的 `selector_mismatch` 带**首个请求的
19
+ * core 原句是写给运维的诊断材料:③ 点名**宿主路径**(控制面账本文件;② 自 core 7.44.0 起句子不带路径、路径在 `detail.path` ⇒ 运维行),① 的 `selector_mismatch` 带**首个请求的
19
20
  * 选择子哈希**(`pinned set <selectHash>`,无盐 sha256 —— operator 选择子可字典还原,首个请求未必是这位会话所有者发的):
20
21
  * · operator 受众 ⇒ 原句上 wire(过了 operator 门,这些材料对这个受众不是泄露);
21
22
  * · 会话所有者受众 ⇒ **一律**一句固定指路(每码一句,只说「怎么办」),原句整句交 `withheld` 落运维 warn —— 不逐码判断哪句安全
22
23
  * (逐码挑 = core 改一句文案就可能静默漏出一样新东西)。② ③ 的固定句**两形都真**:
23
- * ② 链先于请求在场 ⇒ 提交点之前拒、什么都没做;提交点之后才换上的链 ⇒ 已删的几条算数、journal 留盘(codex 一审 C1);
24
+ * ② 链先于请求在场 ⇒ 提交点之前拒、什么都没做;提交点之后才换上的链 ⇒ 已删的几条算数、journal 留盘(codex 一审 C1);scope home 不是真目录那一句
25
+ * 同样不许诺零删除(home 被换掉的时刻本层判不出),只指路「运维放回真目录后用同一个 requestId 重发」;
24
26
  * ③ 一码两义 —— 操作前账本读不出 ⇒ 什么都没发生;落定后残留枚举失败 ⇒ 抹除已提交、重放收敛(core 原话)。两句都不许诺零删除,
25
27
  * 都指路「运维修好后用同一个 requestId 重发」(两义下这句都对)。本层**不**嗅 core 文案分辨前后(拿上游自由文本当判别式是第二真源)。
26
28
  */
27
29
  import type { ServerResponse } from "node:http";
30
+ import type { Logger } from "../observability/logger.js";
28
31
  /**
29
32
  * 已分类拒 → HTTP 状态的**唯一**码表 = 自铸 wire 值登记处的 `sessionMemoryEraseStatus` 集(F3:wire 变更清单从同一张表 diff,状态改一处、
30
33
  * 清单跟着列)。两面同读(S-930:码与状态只判一次)。键写错 ⇒ tsc 红(索引的是 `as const` 表的键)。
@@ -36,6 +39,9 @@ export type MemoryComplianceAudience = {
36
39
  readonly kind: "session_owner";
37
40
  readonly withheld: (coreMessage: string) => void;
38
41
  };
39
- /** 按上面三层应答;返回 `true` = 已应答,`false` = 未分类(一个字节都没写,调用方发它自己那句 500 并落它自己的运维 warn)。 */
40
- export declare function sentMemoryComplianceFailure(res: ServerResponse, err: unknown, audience: MemoryComplianceAudience): boolean;
42
+ /**
43
+ * 按上面三层应答;返回 `true` = 已应答,`false` = 未分类(一个字节都没写,调用方发它自己那句 500 并落它自己的运维 warn)。
44
+ * `logger` = 店形拒那一条运维行的去处(两受众同一只;缺席 ⇒ 不落)。
45
+ */
46
+ export declare function sentMemoryComplianceFailure(res: ServerResponse, err: unknown, audience: MemoryComplianceAudience, logger: Pick<Logger, "warn"> | undefined): boolean;
41
47
  //# sourceMappingURL=memory-compliance-failure-reply.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { ControlPlaneCorruptError } from "@sema-agent/core";
2
- import { targetOutsideStoreOf } from "../memory-store-refusal.js";
2
+ import { logStoreFormRefusal, storeFormRefusalOf } from "../memory-store-refusal.js";
3
3
  import { sentMemoryStoreRefusal } from "./memory-store-refusal-reply.js";
4
4
  import { sendError } from "./send.js";
5
5
  import { assertNever } from "./route-ctx.js";
@@ -38,19 +38,28 @@ function codeOf(err) {
38
38
  const { code } = err;
39
39
  return typeof code === "string" ? code : undefined;
40
40
  }
41
- export function sentMemoryComplianceFailure(res, err, audience) {
41
+ export function sentMemoryComplianceFailure(res, err, audience, logger) {
42
42
  const coreCode = codeOf(err);
43
43
  if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err), audience))
44
44
  return true;
45
- const outside = targetOutsideStoreOf(err);
46
- if (outside !== undefined) {
45
+ const storeForm = storeFormRefusalOf(err);
46
+ if (storeForm !== undefined) {
47
47
  switch (audience.kind) {
48
48
  case "operator":
49
- return sentMemoryStoreRefusal(res, err);
49
+ return sentMemoryStoreRefusal(res, err, logger);
50
50
  case "session_owner":
51
- audience.withheld(outside.message);
52
- sendError(res, 409, "memory.target_outside_store", "session memory erasure refused — a directory in this deployment's memory store leads outside it (a link), so this erasure did not complete (a refusal before its commit point changed nothing; one after it keeps the deletions already made and a journal that finishes the rest once the link is removed); ask the operator to remove the link, then resend with the same requestId");
53
- return true;
51
+ logStoreFormRefusal(logger, storeForm);
52
+ audience.withheld(storeForm.message);
53
+ switch (storeForm.code) {
54
+ case "memory.target_outside_store":
55
+ sendError(res, 409, "memory.target_outside_store", "session memory erasure refused — a directory in this deployment's memory store leads outside it (a link), so this erasure did not complete (a refusal before its commit point changed nothing; one after it keeps the deletions already made and a journal that finishes the rest once the link is removed); ask the operator to remove the link, then resend with the same requestId");
56
+ return true;
57
+ case "memory.scope_home_not_directory":
58
+ sendError(res, 409, "memory.scope_home_not_directory", "session memory erasure refused — a scope directory in this deployment's memory store is not a real directory (it was replaced by a link or a file), so this erasure did not complete; ask the operator to put the real directory back, then resend with the same requestId");
59
+ return true;
60
+ default:
61
+ return assertNever(storeForm.code);
62
+ }
54
63
  default:
55
64
  return assertNever(audience);
56
65
  }
@@ -1,24 +1,27 @@
1
1
  /**
2
- * core 7.31.1 #1094 `memory.target_outside_store` 的 **operator 面 HTTP 映射** —— 唯一一处字面量(判据在
3
- * `src/memory-store-refusal.ts`)。挂在每一个触 store 树的 operator 记忆口的已分类收尾上:
2
+ * **店形拒**两码(`memory.target_outside_store` / `memory.scope_home_not_directory`,判据在 `src/memory-store-refusal.ts`)的
3
+ * **operator 面 HTTP 映射** + 两受众共用的**运维行** —— 唯一一处码字面量。挂在每一个触 store 树的 operator 记忆口的已分类收尾上:
4
4
  * `routes/memory-compliance.ts`(出处 / 抹除;经 `http/memory-compliance-failure-reply.ts` 的 operator 受众臂)· `routes/memory-origin.ts`(外源列表 / 清标;**不含**只读控制面的清标账列表 ——
5
5
  * core 的 wire 表逐字:只碰控制面的面不判,给它挂一条永远收不到的码路径正是附录 A 活触发门要防的形)·
6
- * `routes/memory-bundle.ts`(导出 / 导入)。会话所有者的自助抹除口**不走这里**(S-930 起经 `http/memory-compliance-failure-reply.ts` 的会话所有者受众臂:同码、固定文案)。
6
+ * `routes/memory-bundle.ts`(导出 / 导入)。会话所有者的自助抹除口**不走这里的 wire 句**(经 `http/memory-compliance-failure-reply.ts` 的会话所有者受众臂:同码、固定文案),
7
+ * 但运维行({@link logStoreFormRefusal},铸点在 `src/memory-store-refusal.ts`)两受众同一只。
7
8
  * `POST /v1/admin/memory/consolidation/run` 也不走这里:那口把 core 的**任何**有码拒一律收成 409 `memory.consolidation_refused`
8
- * (码进文案,附录 A 该行),本码已在那条臂上,不再给它第二个码。
9
+ * (码进文案,附录 A 该行),这一族已在那条臂上,不再给它第二个码。
9
10
  *
10
- * ## 为什么 409
11
- * core 的读法:operator 要处置的拒(去掉 store 里那只通到外面的目录链),不是可重试的 IO 故障,也不是对调用方的裁决。与
11
+ * ## 为什么 409(两码同一条)
12
+ * core 的读法:operator 要处置的盘形(去掉 store 里那只通到外面的目录链 / 把 scope home 放回真目录),不是可重试的 IO 故障,也不是对调用方的裁决。与
12
13
  * `memory.erasure_census_incomplete` 同一类(**当前 store 状态**下这次做不了;状态修好后重发即改判)⇒ 409,不 500(服务器没坏)、
13
14
  * 不 4xx 请求族(请求没写错)。抹除面:链先于请求在场 ⇒ core 在提交点之前拒,零删除、零 journal(真 core 格钉在
14
15
  * `test/memory-compliance-http.test.ts`);链在提交点之后才换上 ⇒ 已删的几条算数、journal 留盘 —— core 原句会带
15
16
  * `after the commit point; the journal is kept`,本层原样转发、不嗅它分类(codex 一审 C1)。
16
17
  *
17
- * ## 文案原样
18
- * core 的诊断句点名动作、路径与解析到哪 —— 那是运维找到那只链的唯一材料,与本族其它 core 码同一条「码与文案都不改写」纪律。
19
- * 只对 operator 面这么做:这几口都过了 operator 门,宿主路径对这个受众不是泄露。
18
+ * ## 文案原样,路径进运维行
19
+ * operator 面上 wire 的是 core 原句(码领头;core 7.44.0 起句子里**没有**宿主路径 —— 路径在 `detail.path`)。路径不上 wire(两受众同,裁定:运维腿的
20
+ * 句子不带服务器路径,路径只进日志):每次应答这一族,{@link logStoreFormRefusal} 落一条运维 warn `memory_store_form_refused { code, path?, form? }` ——
21
+ * 运维找到那只链 / 那只坏 home 的唯一材料。
20
22
  */
21
23
  import type { ServerResponse } from "node:http";
22
- /** `err` 是 `memory.target_outside_store` ⇒ 409 带码 + core 原句,返回 true(已应答);否则 false,一个字节都不写。 */
23
- export declare function sentMemoryStoreRefusal(res: ServerResponse, err: unknown): boolean;
24
+ import type { Logger } from "../observability/logger.js";
25
+ /** `err` 是店形拒 ⇒ 409 带码 + core 原句 + 运维行,返回 true(已应答);否则 false,一个字节都不写。 */
26
+ export declare function sentMemoryStoreRefusal(res: ServerResponse, err: unknown, logger: Pick<Logger, "warn"> | undefined): boolean;
24
27
  //# sourceMappingURL=memory-store-refusal-reply.d.ts.map
@@ -1,10 +1,21 @@
1
- import { targetOutsideStoreOf } from "../memory-store-refusal.js";
1
+ import { logStoreFormRefusal, storeFormRefusalOf } from "../memory-store-refusal.js";
2
2
  import { sendError } from "./send.js";
3
- export function sentMemoryStoreRefusal(res, err) {
4
- const refusal = targetOutsideStoreOf(err);
3
+ export function sentMemoryStoreRefusal(res, err, logger) {
4
+ const refusal = storeFormRefusalOf(err);
5
5
  if (refusal === undefined)
6
6
  return false;
7
- sendError(res, 409, "memory.target_outside_store", refusal.message);
8
- return true;
7
+ logStoreFormRefusal(logger, refusal);
8
+ switch (refusal.code) {
9
+ case "memory.target_outside_store":
10
+ sendError(res, 409, "memory.target_outside_store", refusal.message);
11
+ return true;
12
+ case "memory.scope_home_not_directory":
13
+ sendError(res, 409, "memory.scope_home_not_directory", refusal.message);
14
+ return true;
15
+ default: {
16
+ const unhandled = refusal.code;
17
+ throw new Error(`unhandled store-form refusal word: ${String(unhandled)}`);
18
+ }
19
+ }
9
20
  }
10
21
  //# sourceMappingURL=memory-store-refusal-reply.js.map
@@ -1,7 +1,7 @@
1
1
  import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, taskNotificationDedupKey, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
4
- import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
4
+ import { decideParkedAgent, denyWorkflowParkForked, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
5
5
  const sessionFaceUnavailable = async (sessionId) => {
6
6
  throw new Error(`parked decide cannot verify which occupant session ${sessionId} belongs to: this deployment's session store has no sessionFaceOf read (every built-in store has one) — refusing rather than resuming unverified`);
7
7
  };
@@ -15,7 +15,7 @@ import { recordResumeResourceWindow } from "../resource-window.js";
15
15
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
16
16
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
17
17
  import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
18
- import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerMayRetire } from "../tool-approval.js";
18
+ import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerDurablyRecordable } from "../tool-approval.js";
19
19
  import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
20
20
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
21
21
  import { withPrincipal } from "../observability/principal-context.js";
@@ -274,7 +274,7 @@ export function createResumeLegs(ctx) {
274
274
  const rememberableTool = paForGrant !== undefined &&
275
275
  paForGrant.toolName &&
276
276
  paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
277
- standingAnswerMayRetire(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
277
+ standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
278
278
  ? paForGrant.toolName
279
279
  : null;
280
280
  const boundGrant = onResumeCommitted
@@ -345,6 +345,10 @@ export function createResumeLegs(ctx) {
345
345
  void ctxPromise.catch(() => { });
346
346
  return denyWorkflowParksUnreadable(origin);
347
347
  }
348
+ if (origin?.kind === "workflow_park_forked") {
349
+ void ctxPromise.catch(() => { });
350
+ return denyWorkflowParkForked(origin);
351
+ }
348
352
  if (origin?.kind === "workflow") {
349
353
  void ctxPromise.catch(() => { });
350
354
  const plan = planWorkflowParkedDecide(origin, parkedReq);
@@ -1,8 +1,8 @@
1
1
  import { screenedGate } from "../../trace/project.js";
2
- import { hostDecisionFacts } from "../../host-decision.js";
2
+ import { hostDecisionFacts, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
5
- import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, standingAnswerMayRetire } from "../../tool-approval.js";
5
+ import { APPROVAL_GATE_KINDS, standingAnswerDurablyRecordable } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
8
  import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
@@ -17,7 +17,7 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
19
  import { governanceOriginOf } from "../../runtime-governance.js";
20
- import { answerGrantsBeyondCall } from "../../standing-answer-grant.js";
20
+ import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
21
21
  import { sendResumeOutcome } from "../route-ctx.js";
22
22
  import { assertNever } from "../route-ctx.js";
23
23
  import * as facts from "../../capability-facts.js";
@@ -25,10 +25,10 @@ import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
25
25
  import { recordFailOpen } from "../../observability/fail-open.js";
26
26
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
27
27
  const DECIDE_BODY_DECLARED_KEYS = [
28
- "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
28
+ "answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decision", "reason", "remember", "updatedInput",
29
29
  ];
30
30
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
31
- const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
31
+ const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
32
32
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
33
33
  "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
34
34
  ];
@@ -356,6 +356,9 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
356
356
  ...(s.mandate !== undefined ? { mandate: s.mandate } : {}),
357
357
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
358
358
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
359
+ ...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
360
+ ...(s.boundGrants !== undefined ? { boundGrants: s.boundGrants } : {}),
361
+ ...(s.classifierNoRuling !== undefined ? { classifierNoRuling: s.classifierNoRuling } : {}),
359
362
  objective,
360
363
  input,
361
364
  };
@@ -637,7 +640,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
637
640
  const body = (await readJson(req));
638
641
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
639
642
  if (!decision) {
640
- sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, settledBy? }");
643
+ sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }");
641
644
  return;
642
645
  }
643
646
  {
@@ -708,29 +711,22 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
708
711
  sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
709
712
  return;
710
713
  }
711
- const settledByVerdict = judgeSettledByBody(body.settledBy, decision, deps.config.directDoorActive);
712
- switch (settledByVerdict.kind) {
714
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
715
+ switch (decidedByReading.kind) {
713
716
  case "not_in_proof":
714
- sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
717
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
715
718
  return;
716
- case "deny_only":
717
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
719
+ case "outside_closed_set":
720
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
718
721
  return;
719
- case "not_string":
720
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
721
- return;
722
- case "too_wide":
723
- sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
724
- return;
725
- case "absent":
726
722
  case "accepted":
727
723
  break;
728
724
  default: {
729
- const unreachable = settledByVerdict;
730
- throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
725
+ const unreachable = decidedByReading;
726
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
731
727
  }
732
728
  }
733
- const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
729
+ const decidedBy = decidedByReading.decidedBy;
734
730
  const binding = {
735
731
  checkpointToken: body.checkpointToken,
736
732
  boundCallId: body.boundCallId,
@@ -779,7 +775,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
779
775
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
780
776
  }
781
777
  : undefined;
782
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
778
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(decidedBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
783
779
  const outErrorCode = out.body.errorCode;
784
780
  if (pendingGrant !== undefined && out.status === 200 && (outErrorCode !== "cancelled" || out.decisionCommitted === true) && deps.approvalExemptionStore) {
785
781
  try {
@@ -826,11 +822,12 @@ function redactPendingDisclosures(row) {
826
822
  function projectPendingForWire(row, governance) {
827
823
  const redacted = redactPendingDisclosures(row);
828
824
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
829
- const rememberRefused = !standingAnswerMayRetire({
825
+ const rememberRefused = !standingAnswerDurablyRecordable({
830
826
  governanceForced: governanceForced === true,
831
827
  requiresRealApproval: row.requiresRealApproval === true,
832
828
  mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
833
- grantsBeyondCall: answerGrantsBeyondCall(row),
829
+ grantBinding: grantBindingOf(row),
830
+ classes: row.classes,
834
831
  });
835
832
  return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
836
833
  }
@@ -137,6 +137,7 @@ export async function handleCapabilities(req, res, match, ctx) {
137
137
  taskAgents: deps.config.requirePrincipal !== true,
138
138
  retainBackgroundProcesses: deps.config.requirePrincipal !== true,
139
139
  interactiveTools: true,
140
+ approvalReach: true,
140
141
  disableSkills: true,
141
142
  userInvokedSkills: true,
142
143
  skillToolLists: true,
@@ -158,6 +159,7 @@ export async function handleCapabilities(req, res, match, ctx) {
158
159
  approvalsStreamLive: projectApprovalsStreamLiveCapability(deps),
159
160
  streamApproval: facts.streamApproval(deps).available,
160
161
  approvalDecisionNote: true,
162
+ approvalDecidedBy: true,
161
163
  sideQuery: true,
162
164
  agentRoster: facts.agentRoster(deps).available,
163
165
  retention: retentionAdvertisement(deps.config),