@sema-agent/server 7.117.0 → 7.119.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 (159) hide show
  1. package/MIGRATION.md +182 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +58 -28
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +40 -10
  20. package/dist/approval-card.js +11 -1
  21. package/dist/approval.d.ts +55 -17
  22. package/dist/approval.js +74 -33
  23. package/dist/audit.d.ts +12 -1
  24. package/dist/audit.js +62 -2
  25. package/dist/auto-mode-face.d.ts +2 -3
  26. package/dist/bake-runner/main.js +1 -6
  27. package/dist/bake-runner/protocol.d.ts +18 -0
  28. package/dist/bake-runner/protocol.js +22 -2
  29. package/dist/bake-runner/runner.d.ts +12 -8
  30. package/dist/bake-runner/runner.js +5 -9
  31. package/dist/boot/config-center.js +1 -1
  32. package/dist/boot/coordinators.js +4 -1
  33. package/dist/boot/device-lane.d.ts +3 -3
  34. package/dist/boot/device-lane.js +1 -2
  35. package/dist/boot/leader.d.ts +4 -4
  36. package/dist/boot/leader.js +1 -2
  37. package/dist/boot/parked-revive-gate.d.ts +4 -6
  38. package/dist/boot/parked-revive-gate.js +2 -6
  39. package/dist/boot/resolve-spec.js +9 -11
  40. package/dist/boot/runner-deps.js +2 -2
  41. package/dist/boot/stage-07-capability-layer.js +7 -3
  42. package/dist/boot/stage-10-http-server.js +3 -2
  43. package/dist/boot/stores.js +12 -7
  44. package/dist/capabilities/center-plugins.d.ts +2 -0
  45. package/dist/capabilities/center-plugins.js +24 -6
  46. package/dist/capabilities/code-review-council.js +8 -11
  47. package/dist/capabilities/hands-lane.d.ts +25 -0
  48. package/dist/capabilities/hands-lane.js +1 -1
  49. package/dist/capabilities/mcp-probe.d.ts +2 -2
  50. package/dist/capabilities/scenarios.d.ts +34 -7
  51. package/dist/capabilities/scenarios.js +33 -6
  52. package/dist/config-catalog.js +3 -3
  53. package/dist/config.d.ts +14 -18
  54. package/dist/config.js +15 -26
  55. package/dist/deployment-governance.d.ts +5 -7
  56. package/dist/deployment-governance.js +4 -5
  57. package/dist/host-decision.d.ts +114 -31
  58. package/dist/host-decision.js +39 -2
  59. package/dist/http/admission.js +12 -6
  60. package/dist/http/approval-sweeps.js +1 -1
  61. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  62. package/dist/http/mcp-dial-deadline.js +21 -0
  63. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  64. package/dist/http/memory-compliance-failure-reply.js +17 -8
  65. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  66. package/dist/http/memory-store-refusal-reply.js +16 -5
  67. package/dist/http/resume-legs.js +8 -3
  68. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  69. package/dist/http/routes/approvals-assistant.js +27 -26
  70. package/dist/http/routes/capabilities.js +6 -3
  71. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  72. package/dist/http/routes/mcp-reconnect.js +6 -1
  73. package/dist/http/routes/memory-bundle.js +1 -1
  74. package/dist/http/routes/memory-compliance.js +1 -1
  75. package/dist/http/routes/memory-origin.d.ts +1 -1
  76. package/dist/http/routes/memory-origin.js +1 -1
  77. package/dist/http/routes/runs.js +47 -33
  78. package/dist/http/routes/sessions.d.ts +1 -1
  79. package/dist/http/routes/sessions.js +23 -22
  80. package/dist/http/routes/workflows.js +2 -0
  81. package/dist/http/wire-types.d.ts +17 -13
  82. package/dist/images/manifest.d.ts +7 -1
  83. package/dist/images/manifest.js +18 -16
  84. package/dist/memory-store-refusal.d.ts +38 -18
  85. package/dist/memory-store-refusal.js +25 -7
  86. package/dist/model-unreachable-notice.d.ts +1 -1
  87. package/dist/namespaced-rule-name.d.ts +46 -41
  88. package/dist/namespaced-rule-name.js +36 -22
  89. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  90. package/dist/observability/corrupt-read-seat.js +1 -0
  91. package/dist/observability/fail-open.d.ts +19 -3
  92. package/dist/observability/fail-open.js +19 -3
  93. package/dist/operator-ask.d.ts +6 -2
  94. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  95. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  96. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  97. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  98. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  99. package/dist/orchestration/workflow-notify-journal.js +7 -2
  100. package/dist/outbound-dispatcher.d.ts +29 -20
  101. package/dist/outbound-dispatcher.js +105 -16
  102. package/dist/parked-decide.d.ts +43 -48
  103. package/dist/parked-decide.js +54 -16
  104. package/dist/permission-rule-vocab.d.ts +4 -3
  105. package/dist/permission-rule-vocab.js +1 -1
  106. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  107. package/dist/plugins/approval-ask-store-sql.js +0 -3
  108. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  109. package/dist/plugins/approval-exemption-store.js +1 -0
  110. package/dist/plugins/blob-backend.d.ts +13 -0
  111. package/dist/plugins/blob-backend.js +26 -0
  112. package/dist/plugins/local-session-store.d.ts +78 -47
  113. package/dist/plugins/local-session-store.js +152 -122
  114. package/dist/plugins/one-time-migrations.d.ts +11 -0
  115. package/dist/plugins/one-time-migrations.js +128 -4
  116. package/dist/plugins/pg-pool.js +1 -1
  117. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  118. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  119. package/dist/plugins/store-backend.d.ts +17 -6
  120. package/dist/plugins/store-backend.js +46 -16
  121. package/dist/plugins/store-open-wait.d.ts +33 -0
  122. package/dist/plugins/store-open-wait.js +73 -0
  123. package/dist/plugins/tidb-pool.js +1 -1
  124. package/dist/plugins/web-search.d.ts +20 -1
  125. package/dist/plugins/web-search.js +39 -10
  126. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  127. package/dist/plugins/workflow-run-store-sql.js +5 -7
  128. package/dist/request-key-closure.js +1 -1
  129. package/dist/request-value-closure.d.ts +1 -1
  130. package/dist/rule-entry-list.d.ts +35 -46
  131. package/dist/rule-entry-list.js +18 -26
  132. package/dist/rules-consent.js +3 -0
  133. package/dist/run-local.js +5 -3
  134. package/dist/runs.d.ts +1 -1
  135. package/dist/security.d.ts +5 -3
  136. package/dist/security.js +4 -0
  137. package/dist/session-policy-wire.d.ts +13 -12
  138. package/dist/session-policy-wire.js +11 -11
  139. package/dist/spec-fields.d.ts +13 -11
  140. package/dist/spec-fields.js +4 -7
  141. package/dist/standing-answer-grant.d.ts +14 -2
  142. package/dist/standing-answer-grant.js +8 -1
  143. package/dist/store-posture.d.ts +14 -6
  144. package/dist/store-posture.js +7 -4
  145. package/dist/task-settings.d.ts +27 -55
  146. package/dist/task-settings.js +13 -21
  147. package/dist/tool-approval.d.ts +133 -138
  148. package/dist/tool-approval.js +175 -66
  149. package/dist/trace/core-keyset-guard.d.ts +5 -5
  150. package/dist/trace/redact.d.ts +6 -0
  151. package/dist/trace/redact.js +9 -9
  152. package/dist/trace/self-minted-wire.d.ts +16 -2
  153. package/dist/trace/self-minted-wire.js +7 -0
  154. package/dist/user-input-ledger.d.ts +30 -14
  155. package/dist/user-input-ledger.js +25 -14
  156. package/package.json +2 -2
  157. package/scripts/drain-check-local.mjs +74 -3
  158. package/scripts/drain-check.mysql.sql +43 -17
  159. package/scripts/drain-check.pg.sql +46 -20
@@ -3,6 +3,7 @@ import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
4
  import { type ToolApprovalCoordinator } from "../../tool-approval.js";
5
5
  import { type GovernancePosture } from "../../runtime-governance.js";
6
+ import { type GateIntentView } from "../../approval.js";
6
7
  import { type RouteCtx, type RouteDecl, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
7
8
  import { type KeyClosureIssue } from "../../request-key-closure.js";
8
9
  /** 族的成员 = 路由表行 id(改名 / 删行 ⇒ 下表的键当场编译红)。 */
@@ -104,6 +105,27 @@ export declare function checkQuestionAnswer(v: unknown): QuestionAnswerCheck;
104
105
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
105
106
  * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
106
107
  export declare function handleApprovalsAssistant(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ApprovalsAssistantRouteId>, ctx: RouteCtx): Promise<void>;
108
+ /**
109
+ * 🔴 [ref]-2 / [ref]②:两条 durable 读面(`GET /v1/approvals` + `/v1/approvals/stream`)的**唯一**
110
+ * 投影 —— 脱敏([ref] 件4)+ 出身归因([ref] 的另一半)一次做完。
111
+ *
112
+ * 出身格的病灶如实记:`governanceForced` 此前只在 409 体上算(`buildActiveRunConflict`),而运维队列
113
+ * 的行不算 —— 同一道 park 门,客户端从 409 看得见「这是运维治理层下的」,从队列看不见。它**不是**一个
114
+ * 落库缺口:判据的两半(行上 `riskDescriptor.shellGateDoctrine` + 本部署的治理姿态)在这条读面上都够得着,
115
+ * 缺的只是**没有人算**。所以补法 = 让两面共用 `governanceOriginOf` 这一个属主(在读面算的已知失真、
116
+ * 以及「根治要 core 在 mint 写进行里」的登记,逐字在 `runtime-governance.ts` 那个函数的顶注,不复述)。
117
+ *
118
+ * 🔴 `true` 才写键,恒不写 `false` —— 与活卡帧 / 409 体 / `card_json` 同一条纪律:缺席 = 「没有治理来源
119
+ * 的证据」,不是「这不是治理门」。
120
+ *
121
+ * ⚠️ **`delegation` 刻意不在这里**(同批如实登记,禁编造键):park 行上**没有**这条信息 —— core 的
122
+ * `Checkpoint` / `PendingAction` / `CheckpointGate` 三个面都不带 `{parentToolCallId, depth, agentName}`
123
+ * (`Checkpoint.state.delegationProvenance` 是 [ref] 的内容安全聚合 `{version, coverageStartTurn,
124
+ * sawExternal, incomplete}`,与 ask 的出处链是两件事)。同步腿的这一格来自 `AskRequest.delegation`,
125
+ * 那条路 park 时不随行。⇒ 无供给,诚实缺席;要它得先有 core 侧的属主面。
126
+ */
127
+ /** 运维队列行投影要的部署事实:治理姿态(出身归因的部署侧一半)+ 三张审批名单(问的属主:部署名单门问不问这一行的调用)。生产两处都递活配置。 */
128
+ export type PendingProjectionDeployment = GovernancePosture & Pick<GateIntentView, "approvalRequire" | "approvalDeny" | "approvalNeverAuto">;
107
129
  export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">,
108
130
  /**
109
131
  * 🔴 S-454:**第二条 diff 源** —— `GET /v1/approvals` 的 `livePending` 那**同一只**读函数
@@ -118,9 +140,9 @@ export declare function streamApprovals(req: IncomingMessage, res: ServerRespons
118
140
  * 与 `governance` 同一条理由:`pollMs` 只有测试传,而本参数是每个生产调用点都必须表态的那一个。
119
141
  */
120
142
  live: Pick<ToolApprovalCoordinator, "listLivePending"> | undefined, scope: string | undefined,
121
- /** [ref]②:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席,与 best-effort 同方向)。**位置在
122
- * `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
123
- governance?: GovernancePosture, pollMs?: number): Promise<void>;
143
+ /** [ref]②:出身归因的部署侧一半 + 三张审批名单(问的属主)。缺席 ⇒ 归不出治理出身(键缺席,与 best-effort 同方向)、没有一行读作
144
+ * 部署名单的问(记不住,方向是多问)。**位置在 `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
145
+ governance?: PendingProjectionDeployment, pollMs?: number): Promise<void>;
124
146
  export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
125
147
  readonly id: "approvals-list";
126
148
  readonly path: "/v1/approvals";
@@ -1,8 +1,8 @@
1
1
  import { screenedGate } from "../../trace/project.js";
2
- import { hostDecisionFacts } from "../../host-decision.js";
2
+ import { hostDecisionFacts, readDecidedBy, retiredDecisionKeyFacts, 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, carriesDeploymentListQuestion, 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,8 @@ 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 { deploymentListAsks } from "../../approval.js";
21
+ import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
21
22
  import { sendResumeOutcome } from "../route-ctx.js";
22
23
  import { assertNever } from "../route-ctx.js";
23
24
  import * as facts from "../../capability-facts.js";
@@ -25,10 +26,10 @@ import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
25
26
  import { recordFailOpen } from "../../observability/fail-open.js";
26
27
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
27
28
  const DECIDE_BODY_DECLARED_KEYS = [
28
- "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
29
+ "answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decision", "reason", "remember", "updatedInput",
29
30
  ];
30
31
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
31
- const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
32
+ const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
32
33
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
33
34
  "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
34
35
  ];
@@ -356,6 +357,9 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
356
357
  ...(s.mandate !== undefined ? { mandate: s.mandate } : {}),
357
358
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
358
359
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
360
+ ...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
361
+ ...(s.boundGrants !== undefined ? { boundGrants: s.boundGrants } : {}),
362
+ ...(s.classifierNoRuling !== undefined ? { classifierNoRuling: s.classifierNoRuling } : {}),
359
363
  objective,
360
364
  input,
361
365
  };
@@ -637,13 +641,14 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
637
641
  const body = (await readJson(req));
638
642
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
639
643
  if (!decision) {
640
- sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, settledBy? }");
644
+ sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }", retiredDecisionKeyFacts(body)?.extra);
641
645
  return;
642
646
  }
643
647
  {
644
648
  const keyIssue = decisionBodyKeyIssue("approval-decide", body);
645
649
  if (keyIssue) {
646
- sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
650
+ const retired = retiredDecisionKeyFacts(body);
651
+ sendError(res, 400, "request.body_shape", retired?.error ?? keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys, ...retired?.extra });
647
652
  return;
648
653
  }
649
654
  }
@@ -708,29 +713,22 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
708
713
  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
714
  return;
710
715
  }
711
- const settledByVerdict = judgeSettledByBody(body.settledBy, decision, deps.config.directDoorActive);
712
- switch (settledByVerdict.kind) {
716
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
717
+ switch (decidedByReading.kind) {
713
718
  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)");
715
- return;
716
- case "deny_only":
717
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
718
- return;
719
- case "not_string":
720
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
719
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
721
720
  return;
722
- case "too_wide":
723
- sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
721
+ case "outside_closed_set":
722
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
724
723
  return;
725
- case "absent":
726
724
  case "accepted":
727
725
  break;
728
726
  default: {
729
- const unreachable = settledByVerdict;
730
- throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
727
+ const unreachable = decidedByReading;
728
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
731
729
  }
732
730
  }
733
- const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
731
+ const decidedBy = decidedByReading.decidedBy;
734
732
  const binding = {
735
733
  checkpointToken: body.checkpointToken,
736
734
  boundCallId: body.boundCallId,
@@ -779,7 +777,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
779
777
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
780
778
  }
781
779
  : undefined;
782
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
780
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(decidedBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
783
781
  const outErrorCode = out.body.errorCode;
784
782
  if (pendingGrant !== undefined && out.status === 200 && (outErrorCode !== "cancelled" || out.decisionCommitted === true) && deps.approvalExemptionStore) {
785
783
  try {
@@ -823,14 +821,17 @@ function redactPendingDisclosures(row) {
823
821
  const next = redactRiskDescriptor(rd);
824
822
  return next === rd ? row : { ...row, riskDescriptor: next };
825
823
  }
826
- function projectPendingForWire(row, governance) {
824
+ function projectPendingForWire(row, deployment) {
825
+ const governance = deployment;
827
826
  const redacted = redactPendingDisclosures(row);
828
827
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
829
- const rememberRefused = !standingAnswerMayRetire({
828
+ const rememberRefused = !standingAnswerDurablyRecordable({
830
829
  governanceForced: governanceForced === true,
831
830
  requiresRealApproval: row.requiresRealApproval === true,
832
831
  mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
833
- grantsBeyondCall: answerGrantsBeyondCall(row),
832
+ grantBinding: grantBindingOf(row),
833
+ classes: row.classes,
834
+ deploymentListQuestion: carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes),
834
835
  });
835
836
  return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
836
837
  }
@@ -1,4 +1,4 @@
1
- import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
1
+ import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { modelImageInput, isModelAllowlisted, selectableCatalog } from "../../model-select.js";
3
3
  import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { projectSealedKeyCapability } from "../../sealed-key.js";
@@ -23,7 +23,7 @@ import { workflowModelAllowlistFor } from "../../task-workflow.js";
23
23
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
24
24
  import * as facts from "../../capability-facts.js";
25
25
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
26
- import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
26
+ import { coercePermissionModeStrict, PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
27
27
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
28
28
  function routePairingOf(deps, name, m) {
29
29
  if (modelKeyPoisonOf(name, deps.config.modelApiKeyEnv ?? {}, process.env, deps.config.modelApiKeys ?? {}) !== undefined)
@@ -60,7 +60,7 @@ export async function handleCapabilities(req, res, match, ctx) {
60
60
  const requestedModeRaw = new URLSearchParams(match.search).get("permissionMode");
61
61
  const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
62
62
  if (requestedModeRaw !== null && requestedMode === undefined) {
63
- sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
63
+ sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${PERMISSION_MODE_WORDS}`);
64
64
  return;
65
65
  }
66
66
  const entitlementSource = centerEntitlementSourceWired(deps.config);
@@ -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,8 @@ 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,
163
+ permissionModes: [...PERMISSION_MODES],
161
164
  sideQuery: true,
162
165
  agentRoster: facts.agentRoster(deps).available,
163
166
  retention: retentionAdvertisement(deps.config),
@@ -36,6 +36,23 @@
36
36
  * (`isParkedRunStatus` / `isTerminalRunStatus`)与 `runProvenElsewhere`(手抄已收编成一只谓词,S-430 起同住 `plugins/store-contracts.ts`)。
37
37
  * 显式 operator 可重拨任何租户的会话(监督动作,与 notify / steer 同臂;**不是** bare `isOperator`)。
38
38
  *
39
+ * ── 应答期限(S-431)────────────────────────────────────────────────────────────────────────────
40
+ * 修前这一口对 `live.reconnectMcpServer(server)` 是裸 `await`:server 侧没有期限,上界只来自 core 自己的启动期钟
41
+ * (env `MCP_TIMEOUT`,未设落 SDK 缺省 60s;拨号之前还有一次不在那条钟里的旧连接 `close()`),一台拨不通 / 半开的远端能把这一口
42
+ * HTTP 请求挂很久。现在与 `GET /v1/sessions/:id/mcp` 面板读**同一只**数 `MCP_DIAL_DEADLINE_MS`(`../mcp-dial-deadline.ts`),
43
+ * 到点答 **503 `state.mcp_reconnect_unsettled`**。
44
+ * 🔴 **只界应答,不取消重拨**:core 的 `TaskStream.reconnectMcpServer(server)` 不收 `AbortSignal`(d.ts 逐字:单参),
45
+ * 本面没有取消把手可递,也不伪造一个。到点之后重拨**继续跑**,落定时照常把名册写上这条 run(成功)或撤走(拨号失败)—— 所以
46
+ * 503 说的是「这一口没等到结果」,**不是**「失败」,文案与契约 §G.7c 都按这个读法写。重发**不作保证**:core 的在途守卫
47
+ * (`redialsInFlight`,`dist/core/mcp.js`)只罩**拨号本身**(关旧 + 连 + 列),拨号一落定就放开,而 `redialMcpServer`
48
+ * (`dist/core/runner/mcp-redial.js`)随后还要 `await commitMcpRosterChanges`(重建工具面)—— 那一段里守卫已不在,
49
+ * 此刻再发会开第二次「先关后拨」。所以文案只说「拨号还在跑时 core 会拒第二次」,不说「重发一定得 refused」,也不把重发
50
+ * 推荐成探针(对抗复审 [medium],验真后采纳;守卫延到名册提交之后是 core 的活,本仓不自造第二份在途判)。
51
+ * 为什么是 503 而不是 200 + 第四个 `outcome` 词:`outcome` 三词是 core 结果的投影,这里**没有结果**,编一个词就是替
52
+ * core 说话(与 `unsupported` 那一行「没有 core 结果就不铸 `prefix`」同一条纪律);503 是 `state.*` 族的唯一状态
53
+ * (附录 A 判据 3),与 `state.mcp_probe_incomplete` 同族同档,但**不复用**它的码 —— 那一码的语义钉在 probe 面上。
54
+ * 本面**不**带 `Retry-After`:重发不是无代价的重试(见上),一个会被通用重试臂自动执行的头在一次交易动词上是错的方向。
55
+ *
39
56
  * ── 计费/门 ─────────────────────────────────────────────────────────────────────────────────────
40
57
  * 零模型工作(不进 billable;不吃 drain / roster-pending);mutating ⇒ `rateLimited`。能力位 `mcpReconnect`
41
58
  * = 持久 run 账面 ∧ 会话属主面(两道 501 的同一批 deps 推导出来,「说 yes 但 501」结构上不可能)。
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { isParkedRunStatus, isTerminalRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
3
3
  import { redactSecrets } from "../../trace/redact.js";
4
+ import { DeadlineExceeded, MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
4
5
  import { sendJson, sendError } from "../send.js";
5
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
7
  import {} from "../route-ctx.js";
@@ -99,11 +100,15 @@ export async function handleMcpReconnect(req, res, match, ctx) {
99
100
  const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
100
101
  if (live) {
101
102
  try {
102
- const result = await live.reconnectMcpServer(server);
103
+ const result = await raceDeadline(live.reconnectMcpServer(server), MCP_DIAL_DEADLINE_MS);
103
104
  sendJson(res, 200, { taskId: activeTaskId, sessionId, ...projectResult(result) });
104
105
  return;
105
106
  }
106
107
  catch (e) {
108
+ if (e instanceof DeadlineExceeded) {
109
+ sendError(res, 503, "state.mcp_reconnect_unsettled", "the MCP re-dial did not settle within the time this endpoint waits, so there is no outcome to report — the re-dial itself was NOT cancelled (the engine takes no cancellation for it) and may still finish, so do not assume it failed; do not resend blindly either: the engine refuses a second re-dial of the same server only while the dial itself is still running, and a request sent after that point starts another close-then-dial on a server that may now be healthy, so check the server's state before re-sending");
110
+ return;
111
+ }
107
112
  const code = e.code;
108
113
  const msg = e instanceof Error ? e.message : String(e);
109
114
  switch (code) {
@@ -37,7 +37,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
37
37
  const code = codeOf(err);
38
38
  if (code !== undefined && sentCoreCode(res, code, err instanceof Error ? err.message : String(err)))
39
39
  return true;
40
- if (sentMemoryStoreRefusal(res, err))
40
+ if (sentMemoryStoreRefusal(res, err, ctx.deps.logger))
41
41
  return true;
42
42
  ctx.deps.logger?.warn?.(`memory_bundle_${face}_failed`, { err: String(err) });
43
43
  return false;
@@ -12,7 +12,7 @@ const EraseRequestSchema = z.object({
12
12
  allowUnevidenced: z.boolean().optional(),
13
13
  });
14
14
  function sentClassifiedFailure(res, ctx, face, err) {
15
- if (sentMemoryComplianceFailure(res, err, { kind: "operator" }))
15
+ if (sentMemoryComplianceFailure(res, err, { kind: "operator" }, ctx.deps.logger))
16
16
  return true;
17
17
  ctx.deps.logger?.warn?.(`memory_compliance_${face}_failed`, { err: String(err) });
18
18
  return false;
@@ -114,7 +114,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
114
114
  baseRev: string;
115
115
  origin: {
116
116
  at: number;
117
- cause?: "static" | "observed" | "unattributed" | "derived" | undefined;
117
+ cause?: "static" | "unattributed" | "observed" | "derived" | undefined;
118
118
  taint: "external";
119
119
  };
120
120
  };
@@ -54,7 +54,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
54
54
  if (coreCode !== undefined && sentClearCode(res, coreCode, err instanceof Error ? err.message : String(err)))
55
55
  return true;
56
56
  }
57
- if (face !== "clearances" && sentMemoryStoreRefusal(res, err))
57
+ if (face !== "clearances" && sentMemoryStoreRefusal(res, err, ctx.deps.logger))
58
58
  return true;
59
59
  if (err instanceof ControlPlaneCorruptError) {
60
60
  sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
@@ -1,7 +1,7 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
3
3
  import { z } from "zod";
4
- import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS } from "@sema-agent/core";
4
+ import { uuidv7, mintCheckpointToken, CheckpointError, policyCannotAnswer, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
5
5
  import { RUN_HOLDER_DEATH_CODES, isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
6
6
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
7
7
  import { redactSecrets } from "../../trace/redact.js";
@@ -14,13 +14,16 @@ import { createHash } from "node:crypto";
14
14
  import { scopedIdempotencyKey } from "../idempotency.js";
15
15
  import { streamSseLog } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
- import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
18
- import { ASK_DECISION_CONSUMER_ABSENT, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, outcomeOfDecidedRow, resolveStreamApprovalGate, respondSettledBy } from "../../tool-approval.js";
17
+ import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
18
+ import { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
19
+ import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
19
20
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
20
21
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
21
22
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
22
23
  import { readTurnActivityMs } from "../../turn-activity.js";
23
24
  import { mintCancelledResult } from "../../run-cancel-context.js";
25
+ import { acceptedInputKindOf } from "../../user-input-ledger.js";
26
+ import { INJECTION_TIERS } from "../../trace/injection-tier.js";
24
27
  import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
25
28
  import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
26
29
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
@@ -31,6 +34,9 @@ import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
31
34
  import { withPrincipal } from "../../observability/principal-context.js";
32
35
  import { readResumeCtx } from "../resume-ctx-read.js";
33
36
  import { storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "../checkpoint-ctx.js";
37
+ const STEER_PRIORITY_WORDS = INJECTION_TIERS.map((tier) => `'${tier}'`);
38
+ const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
39
+ const STEER_PRIORITY_CHOICES = STEER_PRIORITY_WORDS.length <= 2 ? STEER_PRIORITY_WORDS.join(" or ") : `${STEER_PRIORITY_WORDS.slice(0, -1).join(", ")}, or ${STEER_PRIORITY_WORDS[STEER_PRIORITY_WORDS.length - 1]}`;
34
40
  const ASK_PARKING_RETRY_AFTER_SEC = 2;
35
41
  const ASK_ACTOR_LABEL_MAX = 256;
36
42
  const AskDecisionActorSchema = ActorAssertionWire.strip();
@@ -46,7 +52,7 @@ const AskDecisionBodySchema = z
46
52
  .refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
47
53
  .optional(),
48
54
  actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
49
- settledBy: z.unknown().optional(),
55
+ decidedBy: z.unknown().optional(),
50
56
  })
51
57
  .strict();
52
58
  const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
@@ -136,11 +142,17 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
136
142
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
137
143
  }, taskId, staleMs);
138
144
  }
145
+ function sendSteerReplayRefusal(res, refusal) {
146
+ const detail = refusal?.detail;
147
+ const differs = detail !== null && typeof detail === "object" ? readClosedWordSeat(detail, "differs") : undefined;
148
+ sendError(res, 409, "steering.duplicate_input_id", refusal instanceof Error ? refusal.message : "this steering input id was already accepted with different values", differs !== undefined ? { detail: { differs } } : undefined);
149
+ }
139
150
  async function acceptLiveUserInput(deps, res, at) {
140
151
  if (!deps.userInputLedger)
141
152
  return undefined;
153
+ const input = acceptedInputKindOf(at.tier);
142
154
  try {
143
- return await deps.userInputLedger.accept({ input: "steer", messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
155
+ return await deps.userInputLedger.accept({ input, messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
144
156
  }
145
157
  catch (e) {
146
158
  deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
@@ -429,15 +441,15 @@ export async function handleRuns(req, res, match, ctx) {
429
441
  return;
430
442
  }
431
443
  if (typeof body.text !== "string" || body.text.length === 0) {
432
- sendError(res, 400, "request.body_shape", "body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: 'now' | 'next' | 'later' }");
444
+ sendError(res, 400, "request.body_shape", `body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: ${STEER_PRIORITY_UNION} }`);
433
445
  return;
434
446
  }
435
447
  if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
436
448
  sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
437
449
  return;
438
450
  }
439
- if (body.priority !== undefined && body.priority !== "now" && body.priority !== "next" && body.priority !== "later") {
440
- sendError(res, 400, "request.field_invalid", "priority must be 'now', 'next', or 'later' when present");
451
+ if (body.priority !== undefined && !isSystemInjectionPriority(body.priority)) {
452
+ sendError(res, 400, "request.field_invalid", `priority must be ${STEER_PRIORITY_CHOICES} when present`);
441
453
  return;
442
454
  }
443
455
  const text = body.text;
@@ -495,19 +507,19 @@ export async function handleRuns(req, res, match, ctx) {
495
507
  return "queue-full";
496
508
  }
497
509
  if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
498
- parkDuplicateKey = e.message;
510
+ parkDuplicateKey = e;
499
511
  return "duplicate-key";
500
512
  }
501
513
  throw e;
502
514
  }
503
515
  };
504
516
  const sendQueueFull = () => sendError(res, 409, "steering.queue_full", parkQueueFull ?? "the parked steering queue on this checkpoint is full");
505
- const sendDuplicateKey = () => sendError(res, 409, "steering.duplicate_input_id", parkDuplicateKey ?? "this Idempotency-Key is already parked with different steering content — reissue with a fresh key");
517
+ const sendDuplicateKey = () => sendSteerReplayRefusal(res, parkDuplicateKey);
506
518
  const sendParked = (status) => sendJson(res, 202, { taskId, status, delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
507
519
  const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
508
520
  const live = steerableRuns.get(taskId);
509
521
  if (live) {
510
- const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live });
522
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live, tier: priority });
511
523
  if (acceptance === "refused")
512
524
  return;
513
525
  try {
@@ -526,7 +538,7 @@ export async function handleRuns(req, res, match, ctx) {
526
538
  return;
527
539
  }
528
540
  if (code === "steering.duplicate_input_id") {
529
- parkDuplicateKey = e instanceof Error ? e.message : undefined;
541
+ parkDuplicateKey = e;
530
542
  sendDuplicateKey();
531
543
  return;
532
544
  }
@@ -601,7 +613,7 @@ export async function handleRuns(req, res, match, ctx) {
601
613
  return;
602
614
  }
603
615
  if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
604
- parkDuplicateKey = e.message;
616
+ parkDuplicateKey = e;
605
617
  mintResult.duplicateKey = true;
606
618
  return;
607
619
  }
@@ -831,7 +843,7 @@ export async function handleRuns(req, res, match, ctx) {
831
843
  });
832
844
  return;
833
845
  }
834
- const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream });
846
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream, tier: "now" });
835
847
  if (acceptance === "refused")
836
848
  return;
837
849
  try {
@@ -848,7 +860,7 @@ export async function handleRuns(req, res, match, ctx) {
848
860
  return;
849
861
  }
850
862
  if (code === "steering.duplicate_input_id") {
851
- sendError(res, 409, "steering.duplicate_input_id", e instanceof Error ? e.message : "this Idempotency-Key was already accepted with different content or at a different tier — reissue with a fresh key");
863
+ sendSteerReplayRefusal(res, e);
852
864
  return;
853
865
  }
854
866
  if (code !== "steering.not_running")
@@ -1221,7 +1233,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1221
1233
  return;
1222
1234
  }
1223
1235
  const messageId = uuidv7();
1224
- const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1236
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle, tier: undefined });
1225
1237
  if (acceptance === "refused")
1226
1238
  return;
1227
1239
  const marker = await relayAcceptedInput(deps, acceptance, () => handle.steer(redacted));
@@ -1361,33 +1373,31 @@ export async function handleRunVerbs(req, res, match, ctx) {
1361
1373
  }
1362
1374
  const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
1363
1375
  if (!parsedBody.success) {
1364
- sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / settledBy");
1376
+ const retired = retiredDecisionKeyFacts(rawBody);
1377
+ if (retired !== undefined) {
1378
+ sendError(res, 400, "request.body_shape", retired.error, retired.extra);
1379
+ return;
1380
+ }
1381
+ sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
1365
1382
  return;
1366
1383
  }
1367
1384
  const body = parsedBody.data;
1368
- const settledByVerdict = judgeSettledByBody(body.settledBy, body.decision, deps.config.directDoorActive);
1369
- switch (settledByVerdict.kind) {
1385
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
1386
+ switch (decidedByReading.kind) {
1370
1387
  case "not_in_proof":
1371
- sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
1372
- return;
1373
- case "deny_only":
1374
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
1388
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
1375
1389
  return;
1376
- case "not_string":
1377
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
1390
+ case "outside_closed_set":
1391
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
1378
1392
  return;
1379
- case "too_wide":
1380
- sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
1381
- return;
1382
- case "absent":
1383
1393
  case "accepted":
1384
1394
  break;
1385
1395
  default: {
1386
- const unreachable = settledByVerdict;
1387
- throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
1396
+ const unreachable = decidedByReading;
1397
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
1388
1398
  }
1389
1399
  }
1390
- const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
1400
+ const decidedBy = decidedByReading.decidedBy;
1391
1401
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1392
1402
  const taskId = match.params.taskId;
1393
1403
  const askId = match.params.askId;
@@ -1513,6 +1523,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
1513
1523
  sendTerminal(row0);
1514
1524
  return;
1515
1525
  }
1526
+ if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decidedBy)) {
1527
+ sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK);
1528
+ return;
1529
+ }
1516
1530
  let outcome;
1517
1531
  const releaseEditMark = body.decision === "approve" && hasUpdatedInput ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
1518
1532
  try {
@@ -1521,7 +1535,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1521
1535
  decisionActor: actor,
1522
1536
  decisionNote: body.note ?? null,
1523
1537
  idempotencyKey: body.idempotencyKey ?? null,
1524
- ...(settledBy !== undefined ? { settledBy: respondSettledBy({ settledBy }) } : {}),
1538
+ ...((sb) => (sb !== undefined ? { settledBy: sb } : {}))(liveSettledBy(decidedBy)),
1525
1539
  ...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
1526
1540
  });
1527
1541
  }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 单域独占状态(lens1 §C2 的 12 条里的 5 条)全部收进 `SessionsLocal`,每个 server 实例一份:
8
8
  * SSE 连接坑计数 `sseConnections`、owner 查探许可 `sseOwnerLookups`、`/mcp` 面板的短 TTL single-flight
9
- * 缓存 `mcpStatusCache` 与它的两个时限常量。
9
+ * 缓存 `mcpStatusCache` 与它的时限常量(应答期限 `MCP_DIAL_DEADLINE_MS` 与 `POST …/mcp/reconnect` 共读,见 `../mcp-dial-deadline.ts`)。
10
10
  *
11
11
  * ⚠️ 搬运例外(仅此一处,语义等价):两个原为闭包 `let` 的计数器进了 local 盒子,于是 6 个使用点写成
12
12
  * `sse.sseConnections` / `sse.sseOwnerLookups`——`let` 无法跨模块共享自增。判据/时机/配对释放逐字未动。