@sema-agent/server 7.103.2 → 7.105.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 (204) hide show
  1. package/MIGRATION.md +378 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +320 -16
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +6 -1
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +28 -30
  58. package/dist/config.js +95 -77
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/file-history-disclosure.d.ts +7 -31
  66. package/dist/file-history-disclosure.js +12 -0
  67. package/dist/fleet/fleet-bus.d.ts +5 -1
  68. package/dist/http/admission.d.ts +12 -5
  69. package/dist/http/admission.js +5 -1
  70. package/dist/http/dispatch.d.ts +18 -0
  71. package/dist/http/dispatch.js +19 -6
  72. package/dist/http/principal-gate.d.ts +22 -0
  73. package/dist/http/principal-gate.js +20 -0
  74. package/dist/http/resume-legs.d.ts +1 -1
  75. package/dist/http/resume-legs.js +9 -5
  76. package/dist/http/route-table.d.ts +15 -4
  77. package/dist/http/route-table.js +6 -5
  78. package/dist/http/routes/agents-roster.d.ts +7 -0
  79. package/dist/http/routes/agents-roster.js +6 -2
  80. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  81. package/dist/http/routes/approvals-assistant.js +117 -50
  82. package/dist/http/routes/capabilities.js +3 -2
  83. package/dist/http/routes/config-catalog.js +1 -0
  84. package/dist/http/routes/fleet.d.ts +3 -2
  85. package/dist/http/routes/images.js +0 -1
  86. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  87. package/dist/http/routes/memory-policy.js +3 -3
  88. package/dist/http/routes/rules.js +2 -2
  89. package/dist/http/routes/runs.js +29 -10
  90. package/dist/http/routes/session-sync.js +14 -15
  91. package/dist/http/routes/sessions-list.js +3 -2
  92. package/dist/http/routes/sessions.js +49 -25
  93. package/dist/http/routes/shared-memory.js +2 -2
  94. package/dist/http/routes/side-query.js +8 -1
  95. package/dist/http/routes/tasks.js +22 -19
  96. package/dist/http/routes/trace-usage.d.ts +2 -0
  97. package/dist/http/routes/trace-usage.js +8 -13
  98. package/dist/http/send.d.ts +12 -0
  99. package/dist/http/send.js +12 -1
  100. package/dist/http/server.d.ts +30 -8
  101. package/dist/http/server.js +11 -5
  102. package/dist/http/sse-lifecycle.d.ts +1 -1
  103. package/dist/http/sse-log.d.ts +12 -9
  104. package/dist/http/sse-log.js +1 -8
  105. package/dist/http/sync-submit-body.d.ts +43 -0
  106. package/dist/http/sync-submit-body.js +4 -0
  107. package/dist/http/wire-types.d.ts +23 -5
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.js +1 -0
  110. package/dist/leader/merge.d.ts +8 -3
  111. package/dist/memory-incident-sink.js +2 -1
  112. package/dist/model-select.d.ts +9 -0
  113. package/dist/model-select.js +5 -1
  114. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  115. package/dist/observability/corrupt-read-seat.js +158 -15
  116. package/dist/observability/fail-open.d.ts +17 -1
  117. package/dist/observability/fail-open.js +17 -1
  118. package/dist/observability/metrics.js +1 -1
  119. package/dist/observability/run-terminal.d.ts +60 -10
  120. package/dist/observability/run-terminal.js +119 -9
  121. package/dist/parked-decide.d.ts +8 -3
  122. package/dist/parked-decide.js +56 -20
  123. package/dist/parked-revive-surface.d.ts +14 -7
  124. package/dist/parked-revive-surface.js +5 -1
  125. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  126. package/dist/plugins/approval-ask-store-file.js +9 -12
  127. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  128. package/dist/plugins/approval-ask-store-sql.js +4 -1
  129. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  130. package/dist/plugins/approval-exemption-store.js +36 -7
  131. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  132. package/dist/plugins/approval-nonce-store.js +17 -7
  133. package/dist/plugins/caching-session-store.d.ts +5 -0
  134. package/dist/plugins/caching-session-store.js +3 -0
  135. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  136. package/dist/plugins/file-resume-anchor-store.js +19 -5
  137. package/dist/plugins/file-run-store.d.ts +21 -8
  138. package/dist/plugins/file-run-store.js +60 -38
  139. package/dist/plugins/image-bake-store-sql.d.ts +0 -2
  140. package/dist/plugins/image-bake-store-sql.js +0 -5
  141. package/dist/plugins/local-session-store.d.ts +22 -0
  142. package/dist/plugins/local-session-store.js +74 -11
  143. package/dist/plugins/memory-run-store.d.ts +7 -5
  144. package/dist/plugins/memory-run-store.js +11 -4
  145. package/dist/plugins/one-time-migrations.d.ts +38 -1
  146. package/dist/plugins/one-time-migrations.js +143 -15
  147. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  148. package/dist/plugins/permission-rule-store-file.js +37 -51
  149. package/dist/plugins/pg-session-storage.d.ts +3 -0
  150. package/dist/plugins/pg-session-storage.js +19 -10
  151. package/dist/plugins/retention-store-sql.js +1 -0
  152. package/dist/plugins/run-store-sql.d.ts +15 -6
  153. package/dist/plugins/run-store-sql.js +15 -13
  154. package/dist/plugins/schema-indexes.js +1 -0
  155. package/dist/plugins/send-file-ledger.d.ts +5 -1
  156. package/dist/plugins/send-file-ledger.js +11 -6
  157. package/dist/plugins/session-event-payload.d.ts +14 -4
  158. package/dist/plugins/session-event-payload.js +5 -2
  159. package/dist/plugins/session-policy-store-sql.js +18 -5
  160. package/dist/plugins/store-backend.d.ts +13 -7
  161. package/dist/plugins/store-backend.js +10 -6
  162. package/dist/plugins/store-contracts.d.ts +7 -2
  163. package/dist/plugins/tidb-pool.js +12 -5
  164. package/dist/plugins/tidb-session-store.d.ts +6 -1
  165. package/dist/plugins/tidb-session-store.js +17 -10
  166. package/dist/project-identity.d.ts +4 -6
  167. package/dist/read-face-posture.d.ts +21 -0
  168. package/dist/read-face-posture.js +9 -0
  169. package/dist/request-key-closure.js +1 -1
  170. package/dist/request-value-closure.d.ts +72 -3
  171. package/dist/request-value-closure.js +12 -4
  172. package/dist/roster-bound-http-status.d.ts +6 -0
  173. package/dist/roster-bound-http-status.js +6 -0
  174. package/dist/rules-consent.js +0 -11
  175. package/dist/run-local.js +22 -27
  176. package/dist/runs.d.ts +22 -2
  177. package/dist/runs.js +29 -7
  178. package/dist/sealed-key.d.ts +31 -4
  179. package/dist/sealed-key.js +9 -0
  180. package/dist/security.d.ts +5 -1
  181. package/dist/session-children-retire.d.ts +92 -0
  182. package/dist/session-children-retire.js +85 -0
  183. package/dist/session-error-wire.d.ts +58 -0
  184. package/dist/session-error-wire.js +38 -0
  185. package/dist/session-incarnation.d.ts +61 -0
  186. package/dist/session-incarnation.js +43 -0
  187. package/dist/session-policy-wire.d.ts +98 -12
  188. package/dist/session-policy-wire.js +71 -8
  189. package/dist/session-records-skipped-notice.d.ts +20 -0
  190. package/dist/session-records-skipped-notice.js +27 -0
  191. package/dist/tool-approval.d.ts +65 -0
  192. package/dist/tool-approval.js +36 -9
  193. package/dist/trace/core-keyset-guard.d.ts +8 -7
  194. package/dist/trace/engine-notice-wire.d.ts +3 -1
  195. package/dist/trace/engine-notice-wire.js +2 -0
  196. package/dist/trace/project.d.ts +11 -6
  197. package/dist/trace/project.js +12 -3
  198. package/dist/trace/redact.d.ts +7 -0
  199. package/dist/trace/redact.js +153 -61
  200. package/dist/trace/wire-projection-faces.d.ts +10 -3
  201. package/dist/trace/wire-projection-faces.js +8 -2
  202. package/package.json +2 -2
  203. package/dist/plugins/memory-session-policy-store.d.ts +0 -46
  204. package/dist/plugins/memory-session-policy-store.js +0 -52
@@ -2,6 +2,9 @@ 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
4
  import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
5
+ const sessionFaceUnavailable = async (sessionId) => {
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
+ };
5
8
  import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
6
9
  import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
7
10
  import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
@@ -36,6 +39,7 @@ import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflic
36
39
  import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js";
37
40
  import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
38
41
  import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
42
+ import { httpErrorCode } from "./send.js";
39
43
  function isCheckpointReopenedFailure(r) {
40
44
  const code = failureCodeOf(r.terminal);
41
45
  return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
@@ -258,7 +262,7 @@ export function createResumeLegs(ctx) {
258
262
  checkpointStore: cs,
259
263
  registry: defaultTaskRegistry,
260
264
  reviveSurface: parkedSurface,
261
- ...(deps.sessionStorage?.ownerOf ? { sessionOwnerOf: deps.sessionStorage.ownerOf.bind(deps.sessionStorage) } : {}),
265
+ sessionFaceOf: deps.sessionStorage?.sessionFaceOf ? deps.sessionStorage.sessionFaceOf.bind(deps.sessionStorage) : sessionFaceUnavailable,
262
266
  ...(deps.parkedKnownAgentTypes ? { knownAgentTypes: deps.parkedKnownAgentTypes } : {}),
263
267
  ...(deps.parkedReviveInheritedGate ? { rebuildInheritedGate: deps.parkedReviveInheritedGate } : {}),
264
268
  ...(deps.logger ? { warn: parkWarn } : {}),
@@ -341,7 +345,7 @@ export function createResumeLegs(ctx) {
341
345
  ok: false,
342
346
  answer: {
343
347
  status: 409,
344
- body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
348
+ body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", blockedBy: httpErrorCode(e.status, e.code), ...(e.extra ?? {}) },
345
349
  },
346
350
  };
347
351
  }
@@ -908,7 +912,7 @@ export function createResumeLegs(ctx) {
908
912
  settleFleet("failed");
909
913
  }
910
914
  else if (retriable) {
911
- await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId) : deps.runStore.setSuspended(taskId)).catch(() => undefined);
915
+ await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId, null) : deps.runStore.setSuspended(taskId, null)).catch(() => undefined);
912
916
  settleFleet(outcome.gate === "plan_review" ? "needs_review" : "suspended");
913
917
  }
914
918
  else {
@@ -973,9 +977,9 @@ export function createResumeLegs(ctx) {
973
977
  const resumePlane = outwardPlane(sealed.terminal);
974
978
  if (taskId && deps.runStore) {
975
979
  if (resumePlane.status === "suspended" || checkpointReopened)
976
- await deps.runStore.setSuspended(taskId);
980
+ await deps.runStore.setSuspended(taskId, sealed.terminal.kind === "paused" ? sealed : null);
977
981
  else if (resumePlane.status === "needs_review")
978
- await deps.runStore.setNeedsReview(taskId);
982
+ await deps.runStore.setNeedsReview(taskId, sealed);
979
983
  else
980
984
  await deps.runStore.setTerminal(taskId, resumePlane.status, sealed, resumePlane.errorMessage ?? null);
981
985
  }
@@ -55,7 +55,17 @@ import type { FlatServiceDeps } from "./server.js";
55
55
  import type { IncomingMessage, ServerResponse } from "node:http";
56
56
  import type { RouteDecl, RouteDispatchDecl, RouteMatch, RouteCtx, RouteMethod } from "./route-ctx.js";
57
57
  /** 一只域 handler 相对**全局服务凭据门**的位置。门前 = 未鉴权可达(每一只都要单独论证),门后 = 先过凭据。 */
58
- export type RoutePosition = "pre-credential" | "post-credential";
58
+ /**
59
+ * 一行路由在全局服务凭据门的哪一侧、吃哪道门(闭集,三员;S-672 起由两员拆成三员):
60
+ * · `"read-credential"` —— 门**之前**,但过**一只**读路谓词(`dispatch.ts` 的 `readCredentialOk`:部署配了 service
61
+ * 凭据时要「服务凭据(含只读 TRACE_TOKEN)∨ 已验 principal」,没配时与全局门同样放行)。只读路才能坐这一段:
62
+ * 它们在门前只为收只读 TRACE_TOKEN 单凭据的调用方,不是为了对端口全开;
63
+ * · `"public"` —— 门之前、**不吃**任何凭据门:按裁定匿名可达的面(今天只有 A2A agent card,R-11「公网匿名可发现」,
64
+ * 零存量披露);
65
+ * · `"post-credential"` —— 全局服务凭据门(+ 无凭据写门 / 排空 503 / roster-pending 503 / 破坏性会话写门)之后。
66
+ * 修前是 `"pre-credential" | "post-credential"` 两员:门前四族一律不吃任何门,三族只读路因此对端口全开(T-72)。
67
+ */
68
+ export type RoutePosition = "read-credential" | "public" | "post-credential";
59
69
  export interface RouteRow {
60
70
  /** 域入口(`routes/*.ts` 的 `handleX`)。签名一致性是编译期钉:入口漂了形 = 编译红,不是运行时静默 404。
61
71
  *
@@ -79,9 +89,10 @@ export interface RouteRow {
79
89
  readonly enabled?: (deps: FlatServiceDeps) => boolean;
80
90
  }
81
91
  export declare const ROUTES: readonly RouteRow[];
82
- /** 两段(`dispatch.ts` 的分派循环各跑一段)。段内序 = 本表行序;
83
- * 「门前行不许出现在门后行之后」由 `test/route-order-freeze.test.ts` 钉住。 */
84
- export declare const PRE_CREDENTIAL_ROUTES: readonly RouteRow[];
92
+ /** 三段(`dispatch.ts` 的分派循环按 read-credential → public → 全局门 → post-credential 各跑一段)。段内序 = 本表行序;
93
+ * 「三段各自连续、按这个次序排」由 `test/route-order-freeze.test.ts` 钉住。 */
94
+ export declare const READ_CREDENTIAL_ROUTES: readonly RouteRow[];
95
+ export declare const PUBLIC_ROUTES: readonly RouteRow[];
85
96
  export declare const POST_CREDENTIAL_ROUTES: readonly RouteRow[];
86
97
  /**
87
98
  * **探针路径**(k8s / 编排器 / 抓取器的常态高频采样):不计 admission inflight、不推活性时钟(`http/server.ts`
@@ -38,10 +38,10 @@ function row(spec) {
38
38
  return spec;
39
39
  }
40
40
  export const ROUTES = [
41
- row({ handler: handleTraceUsage, decls: TRACE_USAGE_ROUTES, position: "pre-credential", label: "trace-usage" }),
42
- row({ handler: handleWorkflows, decls: WORKFLOWS_ROUTES, position: "pre-credential", label: "workflows" }),
43
- row({ handler: handleFleet, decls: FLEET_ROUTES, position: "pre-credential", label: "fleet" }),
44
- row({ handler: handleA2aServe, decls: A2A_SERVE_ROUTES, position: "pre-credential", label: "a2a-serve" }),
41
+ row({ handler: handleTraceUsage, decls: TRACE_USAGE_ROUTES, position: "read-credential", label: "trace-usage" }),
42
+ row({ handler: handleWorkflows, decls: WORKFLOWS_ROUTES, position: "read-credential", label: "workflows" }),
43
+ row({ handler: handleFleet, decls: FLEET_ROUTES, position: "read-credential", label: "fleet" }),
44
+ row({ handler: handleA2aServe, decls: A2A_SERVE_ROUTES, position: "public", label: "a2a-serve" }),
45
45
  row({ handler: handleCapabilities, decls: CAPABILITIES_ROUTES, position: "post-credential", label: "capabilities" }),
46
46
  row({ handler: handleSideQuery, decls: SIDE_QUERY_ROUTES, position: "post-credential", label: "side-query" }),
47
47
  row({ handler: handleTasks, decls: TASKS_ROUTES, position: "post-credential", label: "tasks" }),
@@ -78,7 +78,8 @@ export const ROUTES = [
78
78
  row({ handler: handleAttachments, decls: ATTACHMENTS_ROUTES, position: "post-credential", label: "attachments" }),
79
79
  row({ handler: handleNotifyWake, decls: NOTIFY_WAKE_ROUTES, position: "post-credential", label: "notify-wake" }),
80
80
  ];
81
- export const PRE_CREDENTIAL_ROUTES = ROUTES.filter((r) => r.position === "pre-credential");
81
+ export const READ_CREDENTIAL_ROUTES = ROUTES.filter((r) => r.position === "read-credential");
82
+ export const PUBLIC_ROUTES = ROUTES.filter((r) => r.position === "public");
82
83
  export const POST_CREDENTIAL_ROUTES = ROUTES.filter((r) => r.position === "post-credential");
83
84
  const PIPELINE_ROUTES = [
84
85
  { id: "health", path: "/health", methods: ["GET"] },
@@ -25,10 +25,17 @@
25
25
  * · **能力位同源**:`capabilities.agentRoster` 与这里的 501 谓词是同一个 `deps.backgroundAgentStore`。
26
26
  */
27
27
  import type { IncomingMessage, ServerResponse } from "node:http";
28
+ import type { BackgroundAgentRowSummary } from "@sema-agent/core";
28
29
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
29
30
  /** 名册窗的扫描上限(与 fleet 的终态窗同姿势:读面要有界)。窗满时回体带 `truncated:true`,
30
31
  * `total` 于是诚实地是「窗内行数」而不是租户全量 —— 一个悄悄封顶的 total 比没有 total 更坏。 */
31
32
  export declare const ROSTER_SCAN_LIMIT = 500;
33
+ /**
34
+ * 后台子代在 wire 上的 **id 投影**(唯一一只):名册行的 `handle` 与 `DELETE /v1/sessions/:id` 的 409
35
+ * `conflict.session_children_alive` 体里 `remaining[]` 的每一项都经它出门(S-790,裁定 2026-09-28)—— 两个面上
36
+ * 「同一只子代」必须是同一个串,消费方拿删除答复里的 id 才对得上名册里的行、才能去它运行的地方停它。
37
+ */
38
+ export declare function rosterAgentId(row: Pick<BackgroundAgentRowSummary, "handle">): string;
32
39
  export declare function handleAgentRoster(req: IncomingMessage, res: ServerResponse, match: RouteMatch<AgentRosterRouteId>, ctx: RouteCtx): Promise<void>;
33
40
  export declare const AGENT_ROSTER_ROUTES: readonly [{
34
41
  readonly id: "agents-roster";
@@ -1,4 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
+ import { sendUnauthorized } from "../principal-gate.js";
2
3
  import { assertNever } from "../route-ctx.js";
3
4
  export const ROSTER_SCAN_LIMIT = 500;
4
5
  const ROSTER_STATUSES = ["running", "parked", "completed", "failed", "killed"];
@@ -18,9 +19,12 @@ const ROSTER_KEY_DISPOSITION = {
18
19
  usage: "strip",
19
20
  };
20
21
  void ROSTER_KEY_DISPOSITION;
22
+ export function rosterAgentId(row) {
23
+ return row.handle;
24
+ }
21
25
  function projectRow(r) {
22
26
  return {
23
- handle: r.handle,
27
+ handle: rosterAgentId(r),
24
28
  ...(r.name !== undefined ? { name: r.name } : {}),
25
29
  ...(r.agentType !== undefined ? { agentType: r.agentType } : {}),
26
30
  status: r.status,
@@ -56,7 +60,7 @@ export async function handleAgentRoster(req, res, match, ctx) {
56
60
  case "agents-roster": {
57
61
  const { fleetWide, gateOwner } = sessionOwnerScope(req);
58
62
  if (!fleetWide && gateOwner === null) {
59
- sendError(res, 401, "auth.unauthorized", "unauthorized");
63
+ sendUnauthorized(res, "identity");
60
64
  return;
61
65
  }
62
66
  const store = deps.backgroundAgentStore;
@@ -3,31 +3,59 @@ 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 RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
6
+ import { type RouteCtx, type RouteDecl, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
7
7
  import { type KeyClosureIssue } from "../../request-key-closure.js";
8
+ /** 族的成员 = 路由表行 id(改名 / 删行 ⇒ 下表的键当场编译红)。 */
9
+ export type DecisionDoorId = Extract<ApprovalsAssistantRouteId, "approval-decide" | "assistant-task-plan-review">;
10
+ /** 一道门的静态描述 —— 只写**这道门自己**的东西;路径、路径段、别门的键一概不在这里写(从来源读)。 */
11
+ export interface DecisionDoor {
12
+ /** 体的受理集 = 本门的 `*_BODY_DECLARED_KEYS`(那张表的双向编译期门照旧咬着它的类型)。 */
13
+ readonly accepted: readonly string[];
14
+ /** `unknown` 桶标签里的门名(「not a <这里> key — check the spelling」)。 */
15
+ readonly requestNoun: string;
16
+ /** 拒体引子里「这道门上拼错一个键最贵的后果」那半句(人读;每门一句)。 */
17
+ readonly costliestTypo: string;
18
+ }
19
+ export type DecisionDoorFamily = {
20
+ readonly [K in DecisionDoorId]: DecisionDoor;
21
+ };
22
+ /** 「名字是真的、只是不在这个体上」的三族(闭集;加第四族 = 下面 `switch` 编译红)。 */
23
+ type NeighborFamily = "sibling_field" | "path_segment" | "submit_body";
24
+ /** 一个邻键:它属哪一族 + 它在哪儿是真的(wire 路径,可多条)。 */
25
+ interface NeighborKey {
26
+ readonly family: NeighborFamily;
27
+ readonly where: readonly string[];
28
+ }
29
+ /** 派生结果里一道门的全部判据(受理集 + 邻键表 + 两句人读)。 */
30
+ export interface DerivedDecisionDoor {
31
+ readonly accepted: ReadonlySet<string>;
32
+ readonly neighbors: ReadonlyMap<string, NeighborKey>;
33
+ readonly requestNoun: string;
34
+ readonly costliestTypo: string;
35
+ }
36
+ export type DecisionDoorKeyTable = {
37
+ readonly [K in DecisionDoorId]: DerivedDecisionDoor;
38
+ };
8
39
  /**
9
- * FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
10
- * 全部受理 ⇒ `null`。判据边界与孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue`)逐字同一条:
11
- * **只判键名**(值形校验归各自既有的门,本门零抢话);体本身非对象 ⇒ 本门不判(留给「body must be
12
- * { decision … }」那道根形门,免得同一个体被两句话抢答)。
13
- *
14
- * 🔴 **重放不额外过门**,也不需要:本腿的幂等回放(`decideIdempotentReplay`)重放的是**同一次请求的同一份
15
- * 体**,它与首决走同一条路由、同一道门;而 checkpoint 的 resume 重建腿根本不读这个体(它读盘上的 body,
16
- * 走 `resolveSpec` 的宽容层)⇒ 4xx 砖不到任何存量 parked 任务。这与 tasks 面「RESUME 重放不过门」是同一条律
17
- * 的两种兑现方式:**门只站在人**(或客户端)**现写的那份字节**上。
40
+ * 回决门族的键闭集判据表 —— **唯一**的派生口(纯函数:同一份族描述 + 同一张路由表 ⇒ 同一张表)。
41
+ * 生产只在本模块尾调一次(路由表声明在文件尾,见 {@link DECISION_DOOR_KEY_TABLE});测试可以喂一份构造的族
42
+ * (「给兄弟门受理集加一个假键 ⇒ 本门的 unsupported 集自动多这一键」就是这么钉的)。
43
+ * 🔴 族两两不交是**前提**不是愿望:同一个键若既是某门的体字段又是某条 URL 的路径段,它的去处就有两个答案
44
+ * ⇒ 抛(模块加载期 = 启动即炸,不是请求期 500)。
18
45
  */
19
- export declare function decideBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
46
+ export declare function deriveDecisionDoorKeyTable(family: DecisionDoorFamily, routes: readonly RouteDecl[]): DecisionDoorKeyTable;
20
47
  /**
21
- * FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
22
- * 全部受理 ⇒ `null`。判据边界与三个孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue` /
23
- * {@link decideBodyKeyIssue})逐字同一条:**只判键名**(值形校验归各自既有的门,本门零抢话);体本身
24
- * 非对象 ⇒ 本门不判(留给「body must be { decision … }」那道根形门,免得同一个体被两句话抢答)。
48
+ * 回决门族**一道门**的顶层键闭集裁决:受理集之外的键 ⇒ 拒体(调用方 400 `request.body_shape`);全部受理 ⇒ `null`。
49
+ * 判据边界与两个孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue`)逐字同一条:**只判键名**(值形校验归各自
50
+ * 既有的门,本门零抢话);体本身非对象 ⇒ 本门不判(留给「body must be { decision … }」那道根形门,免得同一个体
51
+ * 被两句话抢答)。
25
52
  *
26
- * 🔴 **重放不额外过门**,也不需要 —— 逐字同 {@link decideBodyKeyIssue} 的同名段:本腿的反重放是
27
- * 一次性 nonce(S-280),它守的是**同一份现写的字节**;而续跑重建读的是盘上的体、走 `resolveSpec` 的
28
- * 宽容层 ⇒ 4xx 砖不到任何存量挂起的任务。**门只站在人(或客户端)现写的那份字节上**。
53
+ * 🔴 **重放不额外过门**,也不需要:`/decide` 的幂等回放(`decideIdempotentReplay`)重放的是**同一次请求的同一份
54
+ * 体**、走同一条路由同一道门;plan_review 的反重放是一次性 nonce(S-280),守的也是同一份现写的字节;而续跑重建
55
+ * 读的是盘上的体、走 `resolveSpec` 的宽容层 ⇒ 4xx 砖不到任何存量挂起的任务。**门只站在人(或客户端)现写的
56
+ * 那份字节上**(与 tasks 面「RESUME 重放不过门」同一条律)。
29
57
  */
30
- export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
58
+ export declare function decisionBodyKeyIssue(door: DecisionDoorId, raw: unknown, table?: DecisionDoorKeyTable): KeyClosureIssue | null;
31
59
  /**
32
60
  * S-342 —— 「空串的理由 = 没有理由 = 这是一次**裸拒**」。这条语义只适用于**本仓自铸**的那一个域。
33
61
  *
@@ -147,4 +175,5 @@ export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
147
175
  }];
148
176
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
149
177
  export type ApprovalsAssistantRouteId = RouteIdsOf<typeof APPROVALS_ASSISTANT_ROUTES>;
178
+ export {};
150
179
  //# sourceMappingURL=approvals-assistant.d.ts.map
@@ -2,7 +2,7 @@ import { screenedGate } from "../../trace/project.js";
2
2
  import { hostDecisionFacts } 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 } from "../../tool-approval.js";
5
+ import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody } 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";
@@ -27,30 +27,6 @@ const DECIDE_BODY_DECLARED_KEYS = [
27
27
  ];
28
28
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
29
29
  const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
30
- const ACCEPTED_DECIDE_BODY = new Set(DECIDE_BODY_DECLARED_KEYS);
31
- const UNSUPPORTED_DECIDE_BODY = new Set(["editedPlan", "sessionId", "taskId"]);
32
- export function decideBodyKeyIssue(raw) {
33
- if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
34
- return null;
35
- const buckets = { unknown: [], unsupported: [] };
36
- scanKeyClosure(raw, "", ACCEPTED_DECIDE_BODY, UNSUPPORTED_DECIDE_BODY, buckets);
37
- if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
38
- return null;
39
- buckets.unknown.sort();
40
- buckets.unsupported.sort();
41
- const parts = [
42
- ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a decide-request key — check the spelling)")] : []),
43
- ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (a sibling approval door's field, or a path parameter — never read from this body)")] : []),
44
- ];
45
- return {
46
- message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller ` +
47
- `(a misspelled \`remember\` or \`boundInputHash\` silently turns off an exemption / the decision binding). ` +
48
- `${parts.join("; ")}. ` +
49
- `Accepted: ${[...ACCEPTED_DECIDE_BODY].sort().join(",")}`,
50
- unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
51
- unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
52
- };
53
- }
54
30
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
55
31
  "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
56
32
  ];
@@ -71,29 +47,108 @@ function buildApprovalEnvelope(sessionId, covered, reading) {
71
47
  Object.assign(envelope, { [PROOF_SLOT_OF[key]]: reading[key] });
72
48
  return envelope;
73
49
  }
74
- const ACCEPTED_PLAN_REVIEW_BODY = new Set(PLAN_REVIEW_BODY_DECLARED_KEYS);
75
- const UNSUPPORTED_PLAN_REVIEW_BODY = new Set([
76
- ...DECIDE_BODY_DECLARED_KEYS.filter((k) => !PLAN_REVIEW_BODY_DECLARED_KEYS.includes(k)),
77
- "permissionMode", "sessionId", "taskId",
78
- ]);
79
- export function planReviewBodyKeyIssue(raw) {
50
+ const DECISION_DOOR_FAMILY = {
51
+ "approval-decide": {
52
+ accepted: DECIDE_BODY_DECLARED_KEYS,
53
+ requestNoun: "decide-request",
54
+ costliestTypo: "a misspelled `remember` or `boundInputHash` silently turns off an exemption / the decision binding",
55
+ },
56
+ "assistant-task-plan-review": {
57
+ accepted: PLAN_REVIEW_BODY_DECLARED_KEYS,
58
+ requestNoun: "plan-review-request",
59
+ costliestTypo: "a misspelled `permissionModeAfter` silently leaves this task read-only after you approved its plan",
60
+ },
61
+ };
62
+ const SUBMIT_BODY_NEAR_MISS_KEYS = ["permissionMode"];
63
+ const SUBMIT_ENDPOINT = "POST /v1/tasks";
64
+ function neighborLabel(n) {
65
+ const where = n.where.join(" / ");
66
+ switch (n.family) {
67
+ case "sibling_field":
68
+ return `unsupported (a body field of ${where} — send it there, this door never reads it)`;
69
+ case "path_segment":
70
+ return `unsupported (a path segment of ${where} — identity travels in the URL, never in a body)`;
71
+ case "submit_body":
72
+ return `unsupported (a submit-body key of ${where} — set when the task is submitted, this door never reads it)`;
73
+ default: {
74
+ const unreachable = n.family;
75
+ throw new Error(`decision-door key closure: unknown neighbor family ${String(unreachable)}`);
76
+ }
77
+ }
78
+ }
79
+ function doorEndpointOf(routes, id) {
80
+ const decl = routes.find((r) => "id" in r && r.id === id);
81
+ if (decl === undefined || !("pattern" in decl) || decl.label === undefined) {
82
+ throw new Error(`decision-door family: route "${id}" is not a labelled pattern row of the route table — the key-closure hints are derived from it`);
83
+ }
84
+ const groups = new RegExp(`${decl.pattern.source}|`, decl.pattern.flags).exec("")?.groups ?? {};
85
+ return { endpoint: `${decl.methods.join("|")} ${decl.label}`, pathSegments: Object.keys(groups) };
86
+ }
87
+ export function deriveDecisionDoorKeyTable(family, routes) {
88
+ const ids = Object.keys(family);
89
+ const endpoint = new Map(ids.map((id) => [id, doorEndpointOf(routes, id)]));
90
+ const collect = (pairs) => {
91
+ const m = new Map();
92
+ for (const [k, where] of pairs)
93
+ m.set(k, [...(m.get(k) ?? []), where]);
94
+ return m;
95
+ };
96
+ const siblingWhere = collect(ids.flatMap((id) => family[id].accepted.map((k) => [k, endpoint.get(id).endpoint])));
97
+ const pathWhere = collect(ids.flatMap((id) => endpoint.get(id).pathSegments.map((k) => [k, endpoint.get(id).endpoint])));
98
+ const submitWhere = collect(SUBMIT_BODY_NEAR_MISS_KEYS.map((k) => [k, SUBMIT_ENDPOINT]));
99
+ const sources = [
100
+ ["sibling_field", siblingWhere],
101
+ ["path_segment", pathWhere],
102
+ ["submit_body", submitWhere],
103
+ ];
104
+ for (let i = 0; i < sources.length; i++) {
105
+ for (let j = i + 1; j < sources.length; j++) {
106
+ const [fa, a] = sources[i];
107
+ const [fb, b] = sources[j];
108
+ const both = [...a.keys()].filter((k) => b.has(k));
109
+ if (both.length > 0)
110
+ throw new Error(`decision-door family: key(s) ${both.join(", ")} belong to both ${fa} and ${fb} — a refused key must have exactly one destination`);
111
+ }
112
+ }
113
+ const derive = (self) => {
114
+ const accepted = new Set(family[self].accepted);
115
+ const neighbors = new Map();
116
+ for (const [fam, source] of sources) {
117
+ for (const [k, where] of source)
118
+ if (!accepted.has(k))
119
+ neighbors.set(k, { family: fam, where });
120
+ }
121
+ return { accepted, neighbors, requestNoun: family[self].requestNoun, costliestTypo: family[self].costliestTypo };
122
+ };
123
+ return { "approval-decide": derive("approval-decide"), "assistant-task-plan-review": derive("assistant-task-plan-review") };
124
+ }
125
+ export function decisionBodyKeyIssue(door, raw, table = DECISION_DOOR_KEY_TABLE) {
80
126
  if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
81
127
  return null;
128
+ const d = table[door];
82
129
  const buckets = { unknown: [], unsupported: [] };
83
- scanKeyClosure(raw, "", ACCEPTED_PLAN_REVIEW_BODY, UNSUPPORTED_PLAN_REVIEW_BODY, buckets);
130
+ scanKeyClosure(raw, "", d.accepted, new Set(d.neighbors.keys()), buckets);
84
131
  if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
85
132
  return null;
86
133
  buckets.unknown.sort();
87
134
  buckets.unsupported.sort();
135
+ const byLabel = new Map();
136
+ for (const k of buckets.unsupported) {
137
+ const n = d.neighbors.get(k);
138
+ if (n === undefined)
139
+ throw new Error(`decision-door key closure: "${k}" was classified unsupported but has no destination`);
140
+ const label = neighborLabel(n);
141
+ byLabel.set(label, [...(byLabel.get(label) ?? []), k]);
142
+ }
88
143
  const parts = [
89
- ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a plan-review-request key — check the spelling)")] : []),
90
- ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (a sibling approval door's field, a submit-body key, or a path parameter — never read from this body)")] : []),
144
+ ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, `unknown (not a ${d.requestNoun} key — check the spelling)`)] : []),
145
+ ...[...byLabel].map(([label, keys]) => keyClosureClause(keys, label)),
91
146
  ];
92
147
  return {
93
148
  message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller ` +
94
- `(a misspelled \`permissionModeAfter\` silently leaves this task read-only after you approved its plan). ` +
149
+ `(${d.costliestTypo}). ` +
95
150
  `${parts.join("; ")}. ` +
96
- `Accepted: ${[...ACCEPTED_PLAN_REVIEW_BODY].sort().join(",")}`,
151
+ `Accepted: ${[...d.accepted].sort().join(",")}`,
97
152
  unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
98
153
  unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
99
154
  };
@@ -437,7 +492,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
437
492
  return;
438
493
  }
439
494
  {
440
- const keyIssue = planReviewBodyKeyIssue(body);
495
+ const keyIssue = decisionBodyKeyIssue("assistant-task-plan-review", body);
441
496
  if (keyIssue) {
442
497
  sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
443
498
  return;
@@ -570,7 +625,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
570
625
  return;
571
626
  }
572
627
  {
573
- const keyIssue = decideBodyKeyIssue(body);
628
+ const keyIssue = decisionBodyKeyIssue("approval-decide", body);
574
629
  if (keyIssue) {
575
630
  sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
576
631
  return;
@@ -637,18 +692,29 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
637
692
  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");
638
693
  return;
639
694
  }
640
- if (deps.config.directDoorActive && body.settledBy !== undefined) {
641
- sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
642
- return;
643
- }
644
- if (body.settledBy !== undefined && decision !== "deny") {
645
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
646
- return;
647
- }
648
- if (body.settledBy !== undefined && typeof body.settledBy !== "string") {
649
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
650
- return;
695
+ const settledByVerdict = judgeSettledByBody(body.settledBy, decision, deps.config.directDoorActive);
696
+ switch (settledByVerdict.kind) {
697
+ case "not_in_proof":
698
+ sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
699
+ return;
700
+ case "deny_only":
701
+ sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
702
+ return;
703
+ case "not_string":
704
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
705
+ return;
706
+ case "too_wide":
707
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
708
+ return;
709
+ case "absent":
710
+ case "accepted":
711
+ break;
712
+ default: {
713
+ const unreachable = settledByVerdict;
714
+ throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
715
+ }
651
716
  }
717
+ const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
652
718
  const binding = {
653
719
  checkpointToken: body.checkpointToken,
654
720
  boundCallId: body.boundCallId,
@@ -697,7 +763,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
697
763
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
698
764
  }
699
765
  : undefined;
700
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(body.settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
766
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
701
767
  const outErrorCode = out.body.errorCode;
702
768
  if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
703
769
  try {
@@ -832,4 +898,5 @@ export const APPROVALS_ASSISTANT_ROUTES = [
832
898
  { id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: true, methods: ["POST"] },
833
899
  { prefix: "/v1/approvals", billable: true },
834
900
  ];
901
+ const DECISION_DOOR_KEY_TABLE = deriveDecisionDoorKeyTable(DECISION_DOOR_FAMILY, APPROVALS_ASSISTANT_ROUTES);
835
902
  //# sourceMappingURL=approvals-assistant.js.map
@@ -1,7 +1,7 @@
1
1
  import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { isModelAllowlisted, selectableCatalog } from "../../model-select.js";
3
3
  import { resolveModelApiKey } from "../../key-resolver.js";
4
- import { isSealedKeyPoison } from "../../sealed-key.js";
4
+ import { isSealedKeyPoison, projectSealedKeyCapability } from "../../sealed-key.js";
5
5
  import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
6
6
  import { taskApproverPostureHonored, taskWriteFaceRefusal } from "../../hosted-posture.js";
7
7
  import { autonomyOverrides } from "../../runtime-governance.js";
@@ -176,7 +176,7 @@ export async function handleCapabilities(req, res, match, ctx) {
176
176
  rewindFiles: Boolean(deps.fileHistoryStore),
177
177
  restoreFiles: Boolean(deps.fileHistoryStore),
178
178
  rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
179
- fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore),
179
+ fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore, deps.config.fileHistoryCapture),
180
180
  scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
181
181
  sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.sessionStorage?.beginImportStaging && deps.fileHistoryStore),
182
182
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
@@ -199,6 +199,7 @@ export async function handleCapabilities(req, res, match, ctx) {
199
199
  })(),
200
200
  manualCompact: Boolean(deps.runStore),
201
201
  configCatalog: deps.config.configCatalogEnabled === true && deps.config.operatorPrincipals.length > 0,
202
+ sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
202
203
  });
203
204
  return;
204
205
  }
@@ -23,6 +23,7 @@ export async function handleConfigCatalog(req, res, match, ctx) {
23
23
  sendJson(res, 200, buildConfigCatalog({
24
24
  config: deps.config,
25
25
  env: process.env,
26
+ boot: { ...(deps.memoryConsolidationValve !== undefined ? { memoryConsolidationValve: deps.memoryConsolidationValve } : {}) },
26
27
  knobs: configKnobTable(),
27
28
  centerDomains: DOMAINS,
28
29
  centerManagedKeys: deps.configCenterManagedKeys?.(),
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * [ref] A9 域模块 —— MF-Fleet 实时舰队流。
3
3
  *
4
- * 路由:`GET /v1/fleet/stream`(SSE PUSH:连上先快照,之后每条 per-row delta)。跑在服务凭据门**之前**
5
- * (按 principal 自门,与 workflow 只读路由同列)。本域零可变状态。
4
+ * 路由:`GET /v1/fleet/stream`(SSE PUSH:连上先快照,之后每条 per-row delta)。跑在服务凭据门**之前**的
5
+ * 读路谓词段(S-672:先过 `dispatch.ts` 的 `readCredentialOk`「服务凭据 ∨ 已验 principal」,再按 principal 自门,
6
+ * 与 workflow 只读路由同段)。本域零可变状态。
6
7
  */
7
8
  import type { IncomingMessage, ServerResponse } from "node:http";
8
9
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
@@ -420,7 +420,6 @@ async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
420
420
  return bake ? { status: bake.status, updatedAt: bake.updatedAt } : undefined;
421
421
  },
422
422
  getEvents: (id, after) => bakes.getEvents(id, after),
423
- retainedFrom: (id) => bakes.retainedFrom(id),
424
423
  formatEvent: (ev) => ({
425
424
  id: ev.seq,
426
425
  event: ev.kind,
@@ -11,8 +11,9 @@
11
11
  *
12
12
  * ── 为什么缺席形是 **404 族**而不是 501(D4)───────────────────────────────────────────────────────
13
13
  * 本仓的 501 `capability.*` 说的是「**这家店**在本部署上不存在,换部署形态才行」(记忆引擎没接线、
14
- * 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off`,
15
- * 而且是出厂缺省),不是部署能力的缺失。而阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面
14
+ * 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off`),或
15
+ * 出厂缺省派生成关(S-824 / [ref]:未设而整理座落主模型 —— 起服时有一行提示点名 `MODEL_CHEAP_ID`),不是部署能力的
16
+ * 缺失。而阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面
16
17
  * 归属 / provenance=off)在**启动期**就被拒了(`boot/memory-consolidation.ts` 的
17
18
  * `assertConsolidationDriverWirable`)—— 那些机器根本起不来,不会有一个"挂着的 501"去代表它们。
18
19
  * ⇒ 剩下的唯一缺席成因就是「阀门没开」,而它的诚实形是**这条路径不存在**:挂载条件写在 server.ts 的
@@ -2,7 +2,7 @@ import { formatUserScope } from "@sema-agent/core";
2
2
  import { parseMemorySyncRequest } from "../../memory-sync.js";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
7
  const ORG_PREFIX = "org:";
8
8
  export const MEMORY_EXPORT_MAX_LIMIT = 500;
@@ -68,7 +68,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
68
68
  case "memory-export": {
69
69
  const principal = gatedPrincipal(req, deps.config);
70
70
  if (!principal) {
71
- sendError(res, 401, "auth.unauthorized", "unauthorized");
71
+ sendUnauthorized(res, "identity");
72
72
  return;
73
73
  }
74
74
  if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
@@ -128,7 +128,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
128
128
  case "memory-sync-scope": {
129
129
  const principal = gatedPrincipal(req, deps.config);
130
130
  if (!principal) {
131
- sendError(res, 401, "auth.unauthorized", "unauthorized");
131
+ sendUnauthorized(res, "identity");
132
132
  return;
133
133
  }
134
134
  if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { sendJson, sendError, httpErrorCode } from "../send.js";
3
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
3
+ import { gatedPrincipal, explicitOperatorOk, sendUnauthorized } from "../principal-gate.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import { createLogger } from "../../observability/logger.js";
6
6
  import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
@@ -90,7 +90,7 @@ export async function handleRules(req, res, match, ctx) {
90
90
  return;
91
91
  const principal = gatedPrincipal(req, deps.config);
92
92
  if (principal === undefined) {
93
- sendError(res, 401, "auth.unauthorized", "unauthorized");
93
+ sendUnauthorized(res, "identity");
94
94
  return;
95
95
  }
96
96
  const lane = deps.ruleConsent;