@sema-agent/server 7.151.0 → 7.152.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 (286) hide show
  1. package/MIGRATION.md +266 -0
  2. package/README.md +15 -3
  3. package/README.zh-CN.md +10 -2
  4. package/USAGE.md +93 -64
  5. package/deploy/sema-up/README.md +5 -4
  6. package/deploy/sema-up/cluster-up.sh +6 -6
  7. package/deploy/sema-up/kube-up.sh +5 -5
  8. package/deploy/sema-up/lib/flagshape.sh +36 -8
  9. package/deploy/sema-up/machines.example.yaml +3 -1
  10. package/deploy/sema-up/sema-up.sh +22 -16
  11. package/dist/approval-ask-audit-store.d.ts +1 -1
  12. package/dist/approval-ask-machine.d.ts +4 -10
  13. package/dist/approval-ask-retention.d.ts +1 -1
  14. package/dist/approval-card.d.ts +10 -12
  15. package/dist/approval-gate-currency.d.ts +8 -8
  16. package/dist/approval-gate-currency.js +3 -4
  17. package/dist/approval-hmac.d.ts +17 -1
  18. package/dist/approval-hmac.js +7 -1
  19. package/dist/approval-queue-kind.d.ts +1 -1
  20. package/dist/approval.d.ts +2 -2
  21. package/dist/approval.js +2 -2
  22. package/dist/bind-host.d.ts +1 -3
  23. package/dist/bind-host.js +2 -3
  24. package/dist/boot/coordinators.d.ts +1 -1
  25. package/dist/boot/coordinators.js +9 -10
  26. package/dist/boot/device-lane.js +2 -0
  27. package/dist/boot/execution-env.d.ts +1 -0
  28. package/dist/boot/execution-env.js +31 -5
  29. package/dist/boot/leader.d.ts +31 -30
  30. package/dist/boot/leader.js +38 -28
  31. package/dist/boot/leg-path-adjudication.d.ts +40 -0
  32. package/dist/boot/leg-path-adjudication.js +18 -0
  33. package/dist/boot/parked-revive-gate.js +3 -15
  34. package/dist/boot/reapers.d.ts +1 -1
  35. package/dist/boot/reapers.js +1 -1
  36. package/dist/boot/resolve-spec.js +12 -26
  37. package/dist/boot/runner-deps.d.ts +39 -35
  38. package/dist/boot/runner-deps.js +92 -89
  39. package/dist/boot/shutdown.d.ts +6 -0
  40. package/dist/boot/shutdown.js +10 -4
  41. package/dist/boot/stage-02-web-admin-pull.js +4 -5
  42. package/dist/boot/stage-05-execution-env.js +2 -1
  43. package/dist/boot/stage-06-runners.d.ts +2 -0
  44. package/dist/boot/stage-06-runners.js +10 -4
  45. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  46. package/dist/boot/stage-07-capability-layer.js +1 -1
  47. package/dist/boot/stage-08-reapers.d.ts +1 -0
  48. package/dist/boot/stage-09-leader.d.ts +1 -0
  49. package/dist/boot/stage-09-leader.js +2 -2
  50. package/dist/boot/stage-10-http-server.d.ts +8 -0
  51. package/dist/boot/stage-10-http-server.js +11 -15
  52. package/dist/boot/stores.js +7 -4
  53. package/dist/boot/web-admin.js +2 -7
  54. package/dist/capabilities/approvals-push.d.ts +3 -3
  55. package/dist/capabilities/scenarios.d.ts +28 -13
  56. package/dist/capabilities/scenarios.js +2 -2
  57. package/dist/capability-facts.d.ts +22 -20
  58. package/dist/capability-facts.js +18 -34
  59. package/dist/card-rule-offers.d.ts +32 -0
  60. package/dist/card-rule-offers.js +130 -0
  61. package/dist/config-catalog.js +12 -11
  62. package/dist/config-dir-trust.js +4 -2
  63. package/dist/config-invariants.d.ts +18 -7
  64. package/dist/config-invariants.js +51 -8
  65. package/dist/config-lkg.d.ts +5 -19
  66. package/dist/config-lkg.js +8 -62
  67. package/dist/config-provider.d.ts +1 -1
  68. package/dist/config-root-migration.d.ts +2 -2
  69. package/dist/config-types.d.ts +28 -38
  70. package/dist/config.d.ts +7 -5
  71. package/dist/config.js +55 -16
  72. package/dist/control-socket.d.ts +51 -0
  73. package/dist/control-socket.js +115 -0
  74. package/dist/deployment-governance.d.ts +2 -1
  75. package/dist/device-ws-hub.d.ts +7 -0
  76. package/dist/device-ws-hub.js +7 -1
  77. package/dist/elicitation.js +2 -2
  78. package/dist/entry-problems.d.ts +40 -3
  79. package/dist/entry-problems.js +3 -1
  80. package/dist/execution-env-refusal.d.ts +8 -2
  81. package/dist/execution-env-refusal.js +3 -0
  82. package/dist/fleet/engine-signal.d.ts +12 -29
  83. package/dist/fleet/engine-signal.js +1 -7
  84. package/dist/fleet-client.js +2 -5
  85. package/dist/gate-settlement.d.ts +1 -8
  86. package/dist/gate-settlement.js +0 -4
  87. package/dist/history-anchor.js +1 -1
  88. package/dist/hitl-delivery.d.ts +2 -3
  89. package/dist/hitl-delivery.js +0 -2
  90. package/dist/hooks/hook-llm.js +4 -2
  91. package/dist/hooks/hook-runner.d.ts +3 -2
  92. package/dist/hooks/hook-runner.js +16 -1
  93. package/dist/host-decision.d.ts +11 -11
  94. package/dist/http/active-run-conflict.d.ts +35 -2
  95. package/dist/http/active-run-conflict.js +48 -19
  96. package/dist/http/approval-sweeps.d.ts +1 -1
  97. package/dist/http/approval-sweeps.js +5 -2
  98. package/dist/http/checkpoint-ctx.d.ts +2 -0
  99. package/dist/http/checkpoint-ctx.js +1 -0
  100. package/dist/http/dispatch.d.ts +6 -2
  101. package/dist/http/dispatch.js +25 -5
  102. package/dist/http/durable-run-leg.js +0 -1
  103. package/dist/http/health-reader.d.ts +16 -9
  104. package/dist/http/health-reader.js +2 -1
  105. package/dist/http/health-self-report.js +1 -1
  106. package/dist/http/owner-access.d.ts +2 -3
  107. package/dist/http/owner-access.js +3 -4
  108. package/dist/http/principal-gate.d.ts +49 -21
  109. package/dist/http/principal-gate.js +61 -11
  110. package/dist/http/request-channel.d.ts +23 -0
  111. package/dist/http/request-channel.js +9 -0
  112. package/dist/http/request-guards.js +17 -2
  113. package/dist/http/resume-ctx-read.js +20 -3
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +111 -41
  116. package/dist/http/route-ctx.d.ts +73 -7
  117. package/dist/http/route-ctx.js +17 -0
  118. package/dist/http/route-table.d.ts +4 -0
  119. package/dist/http/route-table.js +4 -0
  120. package/dist/http/routes/admin-config-refresh.js +3 -3
  121. package/dist/http/routes/admin-drain.d.ts +3 -0
  122. package/dist/http/routes/admin-drain.js +7 -7
  123. package/dist/http/routes/admin-memory-optout.js +3 -3
  124. package/dist/http/routes/adoption.js +3 -5
  125. package/dist/http/routes/approvals-assistant.d.ts +2 -1
  126. package/dist/http/routes/approvals-assistant.js +95 -48
  127. package/dist/http/routes/capabilities.d.ts +4 -0
  128. package/dist/http/routes/capabilities.js +30 -5
  129. package/dist/http/routes/config-catalog.js +2 -2
  130. package/dist/http/routes/devices.js +1 -1
  131. package/dist/http/routes/diagnostics.d.ts +3 -3
  132. package/dist/http/routes/diagnostics.js +19 -7
  133. package/dist/http/routes/images.js +10 -10
  134. package/dist/http/routes/memory-bundle.js +3 -3
  135. package/dist/http/routes/memory-compliance.js +3 -3
  136. package/dist/http/routes/memory-consolidation.js +2 -2
  137. package/dist/http/routes/memory-origin.js +3 -3
  138. package/dist/http/routes/memory-policy.js +2 -2
  139. package/dist/http/routes/observability.js +3 -3
  140. package/dist/http/routes/retention-ops.js +3 -3
  141. package/dist/http/routes/rules.js +5 -5
  142. package/dist/http/routes/run-permission-mode.d.ts +2 -1
  143. package/dist/http/routes/run-permission-mode.js +2 -2
  144. package/dist/http/routes/runs.d.ts +4 -9
  145. package/dist/http/routes/runs.js +73 -33
  146. package/dist/http/routes/session-sync.d.ts +18 -6
  147. package/dist/http/routes/session-sync.js +38 -36
  148. package/dist/http/routes/sessions.js +20 -9
  149. package/dist/http/routes/tasks.js +28 -37
  150. package/dist/http/routes/workflows.js +1 -1
  151. package/dist/http/server.d.ts +9 -3
  152. package/dist/http/server.js +15 -3
  153. package/dist/http/submit-ctx.d.ts +1 -1
  154. package/dist/http/submit-ctx.js +2 -0
  155. package/dist/human-facing-args.d.ts +33 -0
  156. package/dist/human-facing-args.js +33 -0
  157. package/dist/index.d.ts +1 -1
  158. package/dist/k8s-api-credentials.d.ts +29 -0
  159. package/dist/k8s-api-credentials.js +25 -0
  160. package/dist/lane-home-probe.d.ts +1 -1
  161. package/dist/lane-home-probe.js +2 -1
  162. package/dist/leader/diffup.d.ts +19 -0
  163. package/dist/leader/diffup.js +7 -0
  164. package/dist/leader/leader.d.ts +114 -10
  165. package/dist/leader/leader.js +69 -5
  166. package/dist/leader/repair-wire.js +2 -2
  167. package/dist/leader/run-entry.d.ts +2 -1
  168. package/dist/leader/run-entry.js +1 -1
  169. package/dist/leader/wire.d.ts +42 -132
  170. package/dist/leader/wire.js +81 -67
  171. package/dist/memory-scope.d.ts +20 -9
  172. package/dist/memory-scope.js +15 -3
  173. package/dist/model-entry-headers.d.ts +9 -10
  174. package/dist/model-entry-headers.js +0 -4
  175. package/dist/model-entry-refusal.d.ts +7 -4
  176. package/dist/model-entry-refusal.js +11 -5
  177. package/dist/model-plane-face.d.ts +1 -1
  178. package/dist/model-plane-face.js +3 -1
  179. package/dist/model-route-endpoint.d.ts +13 -7
  180. package/dist/model-route-endpoint.js +14 -11
  181. package/dist/model-select.d.ts +22 -0
  182. package/dist/model-select.js +9 -0
  183. package/dist/model-unreachable-notice.d.ts +11 -0
  184. package/dist/model-unreachable-notice.js +4 -4
  185. package/dist/object-store-buckets.d.ts +640 -6
  186. package/dist/object-store-buckets.js +166 -0
  187. package/dist/observability/fail-open.d.ts +20 -16
  188. package/dist/observability/fail-open.js +20 -16
  189. package/dist/observability/logger.js +3 -1
  190. package/dist/operator-roster.d.ts +68 -0
  191. package/dist/operator-roster.js +32 -0
  192. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  193. package/dist/outbound-dispatcher.d.ts +71 -8
  194. package/dist/outbound-dispatcher.js +48 -7
  195. package/dist/owner-faces.d.ts +1 -1
  196. package/dist/parked-decide.d.ts +2 -2
  197. package/dist/permission-mode-admission.d.ts +59 -26
  198. package/dist/permission-mode-admission.js +37 -10
  199. package/dist/plugins/approval-ask-store-file.d.ts +4 -0
  200. package/dist/plugins/approval-ask-store-file.js +3 -0
  201. package/dist/plugins/approval-ask-store-memory.d.ts +4 -0
  202. package/dist/plugins/approval-ask-store-memory.js +7 -1
  203. package/dist/plugins/approval-ask-store-sql.d.ts +26 -2
  204. package/dist/plugins/approval-ask-store-sql.js +12 -1
  205. package/dist/plugins/blob-backend.d.ts +83 -7
  206. package/dist/plugins/blob-backend.js +57 -6
  207. package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
  208. package/dist/plugins/checkpoint-index-columns.js +3 -6
  209. package/dist/plugins/checkpoint-store-face.d.ts +119 -61
  210. package/dist/plugins/checkpoint-store-face.js +134 -55
  211. package/dist/plugins/landed-attendants.d.ts +2 -2
  212. package/dist/plugins/local-checkpoint-store.d.ts +13 -3
  213. package/dist/plugins/local-checkpoint-store.js +38 -49
  214. package/dist/plugins/one-time-migrations.d.ts +51 -21
  215. package/dist/plugins/one-time-migrations.js +67 -45
  216. package/dist/plugins/pg-pool.js +11 -9
  217. package/dist/plugins/remote-env-host.d.ts +29 -10
  218. package/dist/plugins/remote-env-host.js +99 -19
  219. package/dist/plugins/remote-env-k8s.d.ts +68 -2
  220. package/dist/plugins/remote-env-k8s.js +107 -24
  221. package/dist/plugins/replica-registration.d.ts +1 -1
  222. package/dist/plugins/replica-registration.js +1 -1
  223. package/dist/plugins/s3-presign.d.ts +23 -0
  224. package/dist/plugins/s3-presign.js +37 -3
  225. package/dist/plugins/scheduler-support.d.ts +2 -2
  226. package/dist/plugins/scheduler-support.js +0 -3
  227. package/dist/plugins/schema-indexes.js +2 -0
  228. package/dist/plugins/send-file-ledger.d.ts +2 -2
  229. package/dist/plugins/send-file-ledger.js +2 -2
  230. package/dist/plugins/send-user-file.d.ts +3 -4
  231. package/dist/plugins/send-user-file.js +56 -52
  232. package/dist/plugins/session-blob-claim-token.d.ts +15 -0
  233. package/dist/plugins/session-blob-claim-token.js +20 -0
  234. package/dist/plugins/session-blob-namespace.d.ts +46 -0
  235. package/dist/plugins/session-blob-namespace.js +116 -0
  236. package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
  237. package/dist/plugins/snapshot-bucket-presence.js +8 -2
  238. package/dist/plugins/sql-json-column.d.ts +1 -1
  239. package/dist/plugins/store-backend.d.ts +44 -2
  240. package/dist/plugins/store-backend.js +75 -53
  241. package/dist/plugins/task-attachment-store.d.ts +2 -1
  242. package/dist/plugins/task-attachment-store.js +5 -1
  243. package/dist/plugins/tidb-pool.js +19 -24
  244. package/dist/plugins/workspace-custody.d.ts +4 -3
  245. package/dist/question.d.ts +2 -1
  246. package/dist/question.js +5 -13
  247. package/dist/request-form.d.ts +6 -1
  248. package/dist/request-form.js +4 -1
  249. package/dist/rule-issue-reading.d.ts +1 -1
  250. package/dist/rules-consent.d.ts +1 -1
  251. package/dist/run-local.js +8 -9
  252. package/dist/runs.d.ts +17 -8
  253. package/dist/runs.js +15 -2
  254. package/dist/runtime-governance.d.ts +2 -2
  255. package/dist/sema-config-dir.d.ts +2 -1
  256. package/dist/session-sync.d.ts +27 -11
  257. package/dist/session-sync.js +15 -7
  258. package/dist/stores-sql/blob-index-sql.d.ts +30 -9
  259. package/dist/stores-sql/blob-index-sql.js +40 -21
  260. package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
  261. package/dist/stores-sql/checkpoint-store-sql.js +70 -146
  262. package/dist/stores-sql/file-history-store-sql.d.ts +64 -16
  263. package/dist/stores-sql/file-history-store-sql.js +100 -28
  264. package/dist/stores-sql/retention-store-sql.js +3 -9
  265. package/dist/task-settings.d.ts +6 -5
  266. package/dist/task-settings.js +3 -3
  267. package/dist/tool-approval.d.ts +342 -518
  268. package/dist/tool-approval.js +498 -1207
  269. package/dist/trace/core-keyset-guard.d.ts +1 -1
  270. package/dist/trace/ledger-events.d.ts +3 -3
  271. package/dist/trace/project.d.ts +2 -3
  272. package/dist/trace/redact.js +3 -2
  273. package/dist/trace/self-minted-wire.d.ts +287 -157
  274. package/dist/trace/self-minted-wire.js +93 -48
  275. package/dist/web-admin-client/apply-effective.js +2 -2
  276. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  277. package/dist/web-admin-client/read-warnings.js +1 -1
  278. package/dist/web-admin-client/types.d.ts +1 -1
  279. package/dist/workspace-space.d.ts +10 -4
  280. package/dist/workspace-space.js +4 -1
  281. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +1 -1
  282. package/package.json +6 -2
  283. package/scripts/embed-minimal.mjs +3 -3
  284. package/wire-changes/server@7.152.0.json +900 -0
  285. package/dist/config-d-migration.d.ts +0 -37
  286. package/dist/config-d-migration.js +0 -60
@@ -122,6 +122,7 @@ const SHAPED_LABEL_DECLS = ROUTE_DECLARATIONS.filter((d) => !("path" in d) && ty
122
122
  const BILLABLE_DECLS = ROUTE_DECLARATIONS.filter((d) => d.billable !== undefined);
123
123
  const METHOD_DISPATCHED_DECLS = ROUTE_DECLARATIONS.filter((d) => d.methodDispatched === true);
124
124
  const SUBSCRIPTION_DECLS = ROUTE_DECLARATIONS.filter(isDispatchDecl).filter((d) => d.subscription === true);
125
+ const POWER_DECLS = ROUTE_DECLARATIONS.filter(isDispatchDecl).filter((d) => d.power !== undefined);
125
126
  const CREDENTIAL_GATED_BY_METHOD = (() => {
126
127
  const m = new Map();
127
128
  for (const d of ROUTE_DECLARATIONS)
@@ -156,6 +157,9 @@ export function isCredentialGatedRewrite(method, url) {
156
157
  export function isSubscriptionRequest(method, url) {
157
158
  return SUBSCRIPTION_DECLS.some((d) => d.methods.includes(method) && matchDecl(d, url) !== null);
158
159
  }
160
+ export function declaredPowerOf(method, url) {
161
+ return POWER_DECLS.find((d) => d.methods.includes(method) && matchDecl(d, url) !== null)?.power;
162
+ }
159
163
  export function isMethodDispatchedSubmitPath(url) {
160
164
  return METHOD_DISPATCHED_DECLS.some((d) => declMatches(d, url));
161
165
  }
@@ -13,11 +13,11 @@ export async function handleAdminConfigRefresh(req, res, match, ctx) {
13
13
  sendPrincipalRefusal(res, req, deps.config);
14
14
  return;
15
15
  }
16
- if (!facts.operatorLane(deps).available) {
17
- sendOperatorPrincipalsRequired(res);
16
+ if (!facts.operatorLane(deps, "operate").available) {
17
+ sendOperatorPrincipalsRequired(res, "operate");
18
18
  return;
19
19
  }
20
- if (!explicitOperatorOk(principal, deps.config)) {
20
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
21
21
  sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
22
22
  return;
23
23
  }
@@ -6,6 +6,8 @@
6
6
  * 进入排空经 `drainState.enter`(收尾链装的唯一入口)。
7
7
  * · 门序 = operator 面的既有口径(`routes/retention-ops.ts` 逐字):身份 → 授权 → 验型;billable=false,**刻意不吃排空 503**
8
8
  * (排空中的重放必须能拿到 `already_draining` 回执,吃了就自锁)。
9
+ * · 授权 = 持 `drain` 权力(声明行的 `power` 列,本口的门与分派层的圈禁判读同一格):全运维档(`OPERATOR_PRINCIPALS`,持每一只权力)∪ 排空档
10
+ * (`OPERATOR_DRAIN_PRINCIPALS`,服务主体只持这一只 —— 它的身份只在本口算数,别的口 403 `auth.principal_confined`,见 `operator-roster.ts`)。
9
11
  * · 判序:体坏(含动作号不是标识符形 —— settings-schema 11 `DRAIN_ACTION_ID`)⇒ 400 `request.field_invalid`(零状态变化)→ 没有收尾链(嵌入宿主 / 测试夹具)⇒ 501 `capability.drain_state_required`
10
12
  * → 身份不符 ⇒ 409 `conflict.fleet_identity`(零状态变化;拒句点名不符的字段,不回显值)→ 入口:已在排空 / 硬停已在进行 ⇒ 200 回执
11
13
  * (这一发的期限不会被采用);这一发会进入排空而 `deadlineAt` 离进入那一拍超过 `FLEET_LEASE_OUTAGE_MAX_MS`(`config.fleetLeaseOutageMaxMs`,
@@ -24,6 +26,7 @@ export declare const ADMIN_DRAIN_ROUTES: readonly [{
24
26
  readonly id: "admin-drain";
25
27
  readonly path: "/v1/admin/drain";
26
28
  readonly methods: readonly ["POST"];
29
+ readonly power: "drain";
27
30
  }];
28
31
  type AdminDrainRouteId = RouteIdsOf<typeof ADMIN_DRAIN_ROUTES>;
29
32
  export {};
@@ -7,7 +7,7 @@ import { drainIdentityMismatches } from "../../fleet/drain-entry.js";
7
7
  import { assertNever } from "../route-ctx.js";
8
8
  import { WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
9
9
  export const ADMIN_DRAIN_PATH = "/v1/admin/drain";
10
- const OPERATOR_ONLY_MESSAGE = "drain declaration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS";
10
+ const OPERATOR_ONLY_MESSAGE = "drain declaration is for this deployment's operators and drain principals only: the caller principal is in neither OPERATOR_PRINCIPALS nor OPERATOR_DRAIN_PRINCIPALS";
11
11
  export async function handleAdminDrain(req, res, match, ctx) {
12
12
  const { deps } = ctx;
13
13
  switch (match.decl.id) {
@@ -17,11 +17,12 @@ export async function handleAdminDrain(req, res, match, ctx) {
17
17
  sendPrincipalRefusal(res, req, deps.config);
18
18
  return;
19
19
  }
20
- if (!facts.operatorLane(deps).available) {
21
- sendOperatorPrincipalsRequired(res);
20
+ const power = ADMIN_DRAIN_DECL.power;
21
+ if (!facts.operatorLane(deps, power).available) {
22
+ sendOperatorPrincipalsRequired(res, power);
22
23
  return;
23
24
  }
24
- if (!explicitOperatorOk(principal, deps.config)) {
25
+ if (!explicitOperatorOk(principal, deps.config, power)) {
25
26
  sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
26
27
  return;
27
28
  }
@@ -59,7 +60,6 @@ export async function handleAdminDrain(req, res, match, ctx) {
59
60
  return assertNever(match.decl.id);
60
61
  }
61
62
  }
62
- export const ADMIN_DRAIN_ROUTES = [
63
- { id: "admin-drain", path: ADMIN_DRAIN_PATH, methods: ["POST"] },
64
- ];
63
+ const ADMIN_DRAIN_DECL = { id: "admin-drain", path: ADMIN_DRAIN_PATH, methods: ["POST"], power: "drain" };
64
+ export const ADMIN_DRAIN_ROUTES = [ADMIN_DRAIN_DECL];
65
65
  //# sourceMappingURL=admin-drain.js.map
@@ -19,10 +19,10 @@ export async function handleMemoryOptOut(req, res, match, ctx) {
19
19
  }
20
20
  const fact = facts.memoryOptOutGrant(deps);
21
21
  if (!fact.available && fact.why === "operator_principals_empty") {
22
- sendOperatorPrincipalsRequired(res);
22
+ sendOperatorPrincipalsRequired(res, "operate");
23
23
  return;
24
24
  }
25
- if (!explicitOperatorOk(principal, deps.config)) {
25
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
26
26
  sendError(res, 403, "auth.operator_only", "memory opt-out grant administration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS");
27
27
  return;
28
28
  }
@@ -33,7 +33,7 @@ export async function handleMemoryOptOut(req, res, match, ctx) {
33
33
  sendError(res, 501, "capability.memory_optout_grant_required", "this deployment has no memory opt-out grant table: the per-principal memory-capture opt-out authorization store lives on a SQL store backend (DB_BACKEND=mysql|pg) and the local backend deliberately keeps no companion store for forgetting — wire a SQL backend to administer opt-out grants");
34
34
  return;
35
35
  case "operator_principals_empty":
36
- sendOperatorPrincipalsRequired(res);
36
+ sendOperatorPrincipalsRequired(res, "operate");
37
37
  return;
38
38
  default:
39
39
  return assertNever(fact.why);
@@ -69,10 +69,10 @@ async function operatorLane(req, res, id, params, ctx) {
69
69
  }
70
70
  const fact = facts.adoption(deps);
71
71
  if (!fact.available && fact.why === "operator_principals_empty") {
72
- sendOperatorPrincipalsRequired(res);
72
+ sendOperatorPrincipalsRequired(res, "operate");
73
73
  return;
74
74
  }
75
- if (!explicitOperatorOk(principal, deps.config)) {
75
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
76
76
  sendError(res, 403, "auth.operator_only", "adoption is an operator-only deployment action");
77
77
  return;
78
78
  }
@@ -82,7 +82,7 @@ async function operatorLane(req, res, id, params, ctx) {
82
82
  sendAdoptionStoreRequired(res);
83
83
  return;
84
84
  case "operator_principals_empty":
85
- sendOperatorPrincipalsRequired(res);
85
+ sendOperatorPrincipalsRequired(res, "operate");
86
86
  return;
87
87
  default:
88
88
  return assertNever(fact.why);
@@ -129,8 +129,6 @@ async function operatorLane(req, res, id, params, ctx) {
129
129
  return;
130
130
  }
131
131
  const out = await runner.start(fromPrincipal, toPrincipal);
132
- if (out.kind === "receipt")
133
- await deps.toolApproval?.followRowOwners();
134
132
  sendOutcome(res, out);
135
133
  return;
136
134
  }
@@ -118,6 +118,7 @@ export type PendingRowForWire = PendingRow & {
118
118
  governanceForced?: true;
119
119
  rememberRefused?: true;
120
120
  agent?: SessionAgent;
121
+ askId?: string;
121
122
  };
122
123
  /**
123
124
  * 🔴 [ref]-2 / [ref]②:两条 durable 读面(`GET /v1/approvals` + `/v1/approvals/stream`)的**唯一**
@@ -153,7 +154,7 @@ export declare function streamApprovals(req: IncomingMessage, res: ServerRespons
153
154
  * 缺席,流的字节与 [ref] 之前逐字相同(能力缺席 = 零变化,不是空数组)。**位置在 `pollMs` 之前**
154
155
  * 与 `governance` 同一条理由:`pollMs` 只有测试传,而本参数是每个生产调用点都必须表态的那一个。
155
156
  */
156
- live: Pick<ToolApprovalCoordinator, "listLivePending"> | undefined,
157
+ live: Pick<ToolApprovalCoordinator, "listLivePending" | "readParkedAskOfGate"> | undefined,
157
158
  /**
158
159
  * 🔴 [ref]:`pending` 车道的**第二个半场** —— 被扣的跨会话来信(`peer_inbound` 行),与 `GET /v1/approvals` 的 `pending[]`
159
160
  * **同一只**闭包({@link peerInboundRowsFor},已绑调用方自己的信箱 scope)。与停驻行同一条 delta 车道、同一对事件名
@@ -3,9 +3,9 @@ import { skillCardFacts } from "../../approval-card.js";
3
3
  import { hostDecisionFacts, readDecider, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF } from "../../host-decision.js";
4
4
  import { sameExecutionRecord } from "@sema-agent/core";
5
5
  import { findParkedAgentForCheckpoint } from "../../parked-decide.js";
6
- import { DECIDED_REPLAY_SCAN_CAP, pendingTriageOrder } from "../../plugins/checkpoint-store-face.js";
6
+ import { DECIDED_REPLAY_SCAN_CAP, displayRuleOffers, pendingTriageOrder } from "../../plugins/checkpoint-store-face.js";
7
7
  import { approvalPayloadFingerprint } from "../../plugins/approval-payload-fingerprint.js";
8
- import { carriesDeploymentListQuestion, standingAnswerDurablyRecordable } from "../../tool-approval.js";
8
+ import { cardRuleOffersOfAskRow, carriesDeploymentListQuestion, readPersistRuleSlot, standingAnswerDurablyRecordable } from "../../tool-approval.js";
9
9
  import { APPROVAL_GATE_KINDS } from "../../approval-content-kind.js";
10
10
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
11
11
  import { principalFrom, scopeCellOf, CHECKPOINT_PUBLIC_SCOPE, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
@@ -13,7 +13,7 @@ import { keyAdmits } from "../../owner-faces.js";
13
13
  import { personOf } from "../../owner-brands.js";
14
14
  import { editedPlanBinding, verifyDirectDoorProof } from "../../principal-jwt.js";
15
15
  import { isWellFormedUnicode } from "../../principal-shape.js";
16
- import { MAX_APPROVAL_REASON_CHARS, projectQuestionAnswer, questionAnswerFromProjection } from "../../approval-hmac.js";
16
+ import { MAX_APPROVAL_REASON_CHARS, persistRuleFromProjection, projectPersistRule, projectQuestionAnswer, questionAnswerFromProjection } from "../../approval-hmac.js";
17
17
  import { PLAN_APPROVE_PERMISSION_MODES_AFTER, isPlanApprovePermissionModeAfter, planReviewDecisionExitsPlan, storedBodyPermissionMode } from "../../task-settings.js";
18
18
  import { MAX_APPROVAL_NONCE_CHARS } from "../../plugins/approval-nonce-store.js";
19
19
  import { redactedPreview, redactSecrets } from "../../trace/redact.js";
@@ -22,12 +22,13 @@ import { buildRunName } from "../../fleet/fleet-bus.js";
22
22
  import { sleep } from "../sse-log.js";
23
23
  import { bindSseLifecycle } from "../sse-lifecycle.js";
24
24
  import { sendJson, sendError, sseHeaders, logRequestError, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
25
- import { gatedSubject, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
25
+ import { createLogger } from "../../observability/logger.js";
26
+ import { gatedSubject, explicitOperatorOk, confinedTierOf, sendPrincipalConfined, sendPrincipalRefusal } from "../principal-gate.js";
26
27
  import { admitRunById, admitSessionById, callerEntry, callerOf, callerOfDecision, deciderSubjectOf, enter, latestRunIdOf, listRunsOfScope, protocolEntry, readSessionAgent, readVisibleRun, refuse, runFactsOf, withCaller } from "../owner-access.js";
27
28
  import { governanceOriginOf } from "../../runtime-governance.js";
28
29
  import { deploymentListAsks } from "../../approval.js";
29
30
  import { grantBindingOf } from "../../standing-answer-grant.js";
30
- import { sendResumeOutcome } from "../route-ctx.js";
31
+ import { readRememberSlot, ruleReceiptOf, sendResumeOutcome } from "../route-ctx.js";
31
32
  import { assertNever } from "../route-ctx.js";
32
33
  import * as facts from "../../capability-facts.js";
33
34
  import { storeFaultClassOf } from "../../plugins/store-fault.js";
@@ -42,10 +43,10 @@ import { defaultPeerInboundFace, parsePeerInboundId } from "../../peer-inbound-f
42
43
  import { engineScopeOfOwner } from "../../engine-identity.js";
43
44
  import { RESUME_RESTATABLE_KEYS, readResumeRestatement } from "../resume-restatement.js";
44
45
  const DECIDE_BODY_DECLARED_KEYS = [
45
- "answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decidedVia", "decision", "feedback", "reason", "remember", "updatedInput",
46
+ "answer", "boundCallId", "boundInputHash", "checkpointToken", "decidedBy", "decidedVia", "decision", "feedback", "persistRule", "reason", "remember", "updatedInput",
46
47
  ...RESUME_RESTATABLE_KEYS,
47
48
  ];
48
- const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
49
+ const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "persistRule", "reason"];
49
50
  const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "decidedVia", "feedback", "remember", "updatedInput", ...RESUME_RESTATABLE_KEYS];
50
51
  function readDecisionDoorRestatement(res, body, directDoorActive) {
51
52
  if (directDoorActive && RESUME_RESTATABLE_KEYS.some((key) => body[key] !== undefined)) {
@@ -68,6 +69,7 @@ const PROOF_SLOT_OF = {
68
69
  decision: "decision",
69
70
  editedPlan: "boundInputHash",
70
71
  permissionModeAfter: "permissionModeAfter",
72
+ persistRule: "persistRule",
71
73
  reason: "reason",
72
74
  };
73
75
  function buildApprovalEnvelope(sessionId, covered, reading) {
@@ -312,7 +314,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
312
314
  }
313
315
  const cs = park.checkpointStore;
314
316
  const principal = gatedSubject(req, deps.config);
315
- const operator = explicitOperatorOk(principal === undefined ? undefined : personOf(principal), deps.config);
317
+ const operator = explicitOperatorOk(principal === undefined ? undefined : personOf(principal), deps.config, "operate");
316
318
  const ownScope = principal ?? CHECKPOINT_PUBLIC_SCOPE;
317
319
  if (match.decl.id === "approvals-stream") {
318
320
  const ownerParam = operator ? (new URLSearchParams(match.search).get("owner") ?? undefined) : undefined;
@@ -333,7 +335,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
333
335
  sendApprovalsReadFailure(req, res, deps.logger, err);
334
336
  return;
335
337
  }
336
- const pending = [...(await projectDurableQueue(durableRows, deps.config, deps.sessionStorage)), ...peerInboundRowsFor(defaultPeerInboundFace, principal, ownerParam)()];
338
+ const pending = [...(await projectDurableQueue(durableRows, deps.config, deps.sessionStorage, deps.toolApproval)), ...peerInboundRowsFor(defaultPeerInboundFace, principal, ownerParam)()];
337
339
  const livePending = deps.toolApproval ? await deps.toolApproval.listLivePending(scope) : undefined;
338
340
  sendJson(res, 200, {
339
341
  pending,
@@ -677,19 +679,16 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
677
679
  return;
678
680
  }
679
681
  const storeNowMs = await store.nowMs();
680
- const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
681
- const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, buildApprovalEnvelope(run.sessionId, PLAN_REVIEW_PROOF_COVERED_KEYS, {
682
+ const proof = admitDirectDoorDecider(req, res, buildApprovalEnvelope(run.sessionId, PLAN_REVIEW_PROOF_COVERED_KEYS, {
682
683
  boundCallId: planNonce ?? null,
683
684
  checkpointId: sighting?.value,
684
685
  decision,
685
686
  editedPlan: decision === "edit" ? editedPlanBinding(body.editedPlan) : null,
686
687
  permissionModeAfter: isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined,
687
688
  reason: typeof body.reason === "string" ? body.reason : null,
688
- }), deps.config, { actionBinding: false, nowSec: Math.floor(storeNowMs / 1000) });
689
- if (!proof.ok) {
690
- sendError(res, proof.status, proof.errorCode, proof.error);
689
+ }), deps.config, match.decl.power, { actionBinding: false, nowSec: Math.floor(storeNowMs / 1000) });
690
+ if (proof === undefined)
691
691
  return;
692
- }
693
692
  deciderPrincipal = proof.principal;
694
693
  singleUse = proof.singleUse === undefined ? undefined : { proof: proof.singleUse, store };
695
694
  }
@@ -740,7 +739,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
740
739
  const doorKind = approvalQueueKindOfDoorId(sessionId);
741
740
  switch (doorKind) {
742
741
  case "peer_inbound":
743
- await decidePeerInbound(req, res, sessionId, { readJson, config: deps.config, logger: deps.logger, principal, face: defaultPeerInboundFace });
742
+ await decidePeerInbound(req, res, sessionId, { readJson, config: deps.config, logger: deps.logger, principal, face: defaultPeerInboundFace, power: match.decl.power });
744
743
  return;
745
744
  case "checkpoint":
746
745
  break;
@@ -772,15 +771,12 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
772
771
  return;
773
772
  }
774
773
  let remember = false;
775
- if (body.remember !== undefined) {
776
- if (body.remember !== "session") {
777
- sendError(res, 400, "request.field_invalid", `remember must be "session"`);
778
- return;
779
- }
780
- if (decision !== "approve") {
781
- sendError(res, 400, "request.field_conflict", "remember requires decision 'approve'", fieldConflictFacts("remember_decision", ["remember", "decision"]));
782
- return;
783
- }
774
+ const rememberSlot = readRememberSlot(body.remember, decision);
775
+ if (rememberSlot.kind === "refused") {
776
+ sendError(res, 400, rememberSlot.refusal.errorCode, rememberSlot.refusal.message, rememberSlot.refusal.facts);
777
+ return;
778
+ }
779
+ if (rememberSlot.kind === "session") {
784
780
  if (deps.config.directDoorActive) {
785
781
  sendError(res, 400, "remember_not_in_proof", "remember is not supported on a direct-door worker (not covered by the decision proof)");
786
782
  return;
@@ -791,6 +787,12 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
791
787
  }
792
788
  remember = true;
793
789
  }
790
+ const ruleSlot = readPersistRuleSlot(body.persistRule, decision === "approve" ? "allow" : "deny");
791
+ if (!ruleSlot.ok) {
792
+ sendError(res, 400, "request.body_shape", ruleSlot.error);
793
+ return;
794
+ }
795
+ const ruleProjection = ruleSlot.slot !== undefined ? projectPersistRule(ruleSlot.slot) : undefined;
794
796
  let answer;
795
797
  let answerProjection;
796
798
  if (body.answer !== undefined) {
@@ -864,24 +866,19 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
864
866
  };
865
867
  let deciderPrincipal = principal === undefined ? undefined : personOf(principal);
866
868
  if (deps.config.directDoorActive) {
867
- const hdr = (n) => {
868
- const h = req.headers[n];
869
- return Array.isArray(h) ? h[0] : h;
870
- };
871
- const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, buildApprovalEnvelope(sessionId, DECIDE_PROOF_COVERED_KEYS, {
869
+ const proof = admitDirectDoorDecider(req, res, buildApprovalEnvelope(sessionId, DECIDE_PROOF_COVERED_KEYS, {
872
870
  answer: answerProjection,
873
871
  boundCallId: binding.boundCallId,
874
872
  boundInputHash: binding.boundInputHash,
875
873
  decision,
874
+ persistRule: ruleProjection,
876
875
  reason: typeof body.reason === "string" ? body.reason : null,
877
- }), deps.config);
878
- if (!proof.ok) {
879
- sendError(res, proof.status, proof.errorCode, proof.error);
876
+ }), deps.config, match.decl.power);
877
+ if (proof === undefined)
880
878
  return;
881
- }
882
879
  deciderPrincipal = proof.principal;
883
880
  }
884
- const deciderCaller = { subject: deciderSubjectOf(principal, deciderPrincipal), operator: explicitOperatorOk(deciderPrincipal, deps.config) };
881
+ const deciderCaller = { subject: deciderSubjectOf(principal, deciderPrincipal), operator: explicitOperatorOk(deciderPrincipal, deps.config, "operate") };
885
882
  if (!deciderCaller.operator) {
886
883
  const cpScope = await cs
887
884
  .peekPendingScope(sessionId, { gateKinds: APPROVAL_GATE_KINDS })
@@ -891,7 +888,8 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
891
888
  return;
892
889
  }
893
890
  }
894
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(decider, deciderPrincipal), req, answer, binding, remember ? { grantedBy: deciderPrincipal ?? null, laneCannotRecord: "refuse" } : undefined, true, deciderCaller, restated.restatement);
891
+ const persistRule = ruleProjection !== undefined ? { slot: persistRuleFromProjection(ruleProjection), inputWasEdited: binding.updatedInput !== undefined } : undefined;
892
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(decider, deciderPrincipal), req, answer, binding, remember ? { grantedBy: deciderPrincipal ?? null, laneCannotRecord: "refuse" } : undefined, true, deciderCaller, restated.restatement, persistRule);
895
893
  const replay = await decideIdempotentReplay(cs, {
896
894
  sessionId,
897
895
  out,
@@ -910,12 +908,31 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
910
908
  sendResumeOutcome(res, replay, deps.logger);
911
909
  return;
912
910
  }
913
- sendResumeOutcome(res, out, deps.logger, remember ? { rememberApplied: out.rememberApplied === true } : undefined);
911
+ const rule = out.status >= 200 && out.status < 300 ? ruleReceiptOf(out, persistRule) : undefined;
912
+ const extra = { ...(remember ? { rememberApplied: out.rememberApplied === true } : {}), ...(rule ?? {}) };
913
+ sendResumeOutcome(res, out, deps.logger, Object.keys(extra).length > 0 ? extra : undefined);
914
914
  return;
915
915
  }
916
916
  }
917
917
  return assertNever(match.decl.id);
918
918
  }
919
+ function admitDirectDoorDecider(req, res, envelope, config, power, opts) {
920
+ const hdr = (n) => {
921
+ const h = req.headers[n];
922
+ return Array.isArray(h) ? h[0] : h;
923
+ };
924
+ const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, envelope, config, opts);
925
+ if (!proof.ok) {
926
+ sendError(res, proof.status, proof.errorCode, proof.error);
927
+ return undefined;
928
+ }
929
+ const confined = confinedTierOf(proof.principal, config, power);
930
+ if (confined !== undefined) {
931
+ sendPrincipalConfined(res, confined);
932
+ return undefined;
933
+ }
934
+ return proof;
935
+ }
919
936
  async function decidePeerInbound(req, res, id, env) {
920
937
  const body = (await env.readJson(req));
921
938
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
@@ -938,15 +955,9 @@ async function decidePeerInbound(req, res, id, env) {
938
955
  }
939
956
  let decider = env.principal;
940
957
  if (env.config.directDoorActive) {
941
- const hdr = (n) => {
942
- const h = req.headers[n];
943
- return Array.isArray(h) ? h[0] : h;
944
- };
945
- const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, buildApprovalEnvelope(id, DECIDE_PROOF_COVERED_KEYS, { answer: undefined, boundCallId: undefined, boundInputHash: undefined, decision, reason: null }), env.config);
946
- if (!proof.ok) {
947
- sendError(res, proof.status, proof.errorCode, proof.error);
958
+ const proof = admitDirectDoorDecider(req, res, buildApprovalEnvelope(id, DECIDE_PROOF_COVERED_KEYS, { answer: undefined, boundCallId: undefined, boundInputHash: undefined, decision, persistRule: undefined, reason: null }), env.config, env.power);
959
+ if (proof === undefined)
948
960
  return;
949
- }
950
961
  decider = proof.principal;
951
962
  }
952
963
  const coord = parsePeerInboundId(id);
@@ -1035,9 +1046,45 @@ function projectPendingForWire(row, deployment, agent) {
1035
1046
  });
1036
1047
  return { kind: "checkpoint", ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}), ...(agent !== undefined ? { agent } : {}) };
1037
1048
  }
1038
- async function projectDurableQueue(rows, deployment, sessions) {
1049
+ async function projectDurableQueue(rows, deployment, sessions, cards) {
1039
1050
  const agents = await sessionAgentsOf(rows.map((r) => r.sessionId), sessions);
1040
- return rows.map((r) => projectPendingForWire(r, deployment, agents.get(r.sessionId)));
1051
+ const carded = await Promise.all(rows.map((r) => withCardRecord(r, cards)));
1052
+ return carded.map((r) => projectPendingForWire(r, deployment, agents.get(r.sessionId)));
1053
+ }
1054
+ const queueLogger = createLogger();
1055
+ async function withCardRecord(row, cards) {
1056
+ if (cards === undefined || row.toolCallId === null || row.boundInputHash === undefined)
1057
+ return row;
1058
+ const asked = await cards.readParkedAskOfGate(row.sessionId, { boundCallId: row.toolCallId, boundInputHash: row.boundInputHash });
1059
+ const withOffers = (ruleOffers) => {
1060
+ const next = { ...row };
1061
+ if (ruleOffers === undefined)
1062
+ delete next.ruleOffers;
1063
+ else
1064
+ next.ruleOffers = ruleOffers;
1065
+ return next;
1066
+ };
1067
+ const withAskIdentity = (next, ask) => {
1068
+ if (next.taskId !== null && next.taskId !== ask.taskId) {
1069
+ queueLogger.warn("approval queue row: the parked leg's task differs from the ask's task — the row keeps its checkpoint identity", { sessionId: row.sessionId, askId: ask.askId });
1070
+ return next;
1071
+ }
1072
+ return { ...next, taskId: ask.taskId, askId: ask.askId };
1073
+ };
1074
+ switch (asked.kind) {
1075
+ case "none":
1076
+ return row;
1077
+ case "unreadable":
1078
+ return withOffers(undefined);
1079
+ case "row": {
1080
+ const offers = cardRuleOffersOfAskRow(asked.row);
1081
+ return withAskIdentity(withOffers(offers === undefined ? undefined : displayRuleOffers(offers, { countRedactions: false })), asked.row);
1082
+ }
1083
+ default: {
1084
+ const unhandled = asked;
1085
+ throw new Error(`queue projection: unhandled parked-ask reading ${JSON.stringify(unhandled)}`);
1086
+ }
1087
+ }
1041
1088
  }
1042
1089
  async function sessionAgentsOf(sessionIds, sessions) {
1043
1090
  const out = new Map();
@@ -1122,7 +1169,7 @@ export async function streamApprovals(req, res, cs, live, peerInbound, sessions,
1122
1169
  while (!closed) {
1123
1170
  let snapshot;
1124
1171
  try {
1125
- const pending = [...(await projectDurableQueue(await cs.listPending(scope), governance, sessions)), ...(peerInbound?.() ?? [])];
1172
+ const pending = [...(await projectDurableQueue(await cs.listPending(scope), governance, sessions, live)), ...(peerInbound?.() ?? [])];
1126
1173
  snapshot = { pending, livePending: live ? await live.listLivePending(scope) : [] };
1127
1174
  }
1128
1175
  catch (err) {
@@ -38,6 +38,10 @@ export declare const CAPABILITIES_ROUTES: readonly [{
38
38
  readonly id: "capability-mcp";
39
39
  readonly path: "/v1/capabilities/mcp";
40
40
  readonly methods: readonly ["GET"];
41
+ }, {
42
+ readonly id: "capability-scenario-effective";
43
+ readonly path: "/v1/capabilities/scenario";
44
+ readonly methods: readonly ["GET"];
41
45
  }, {
42
46
  readonly id: "capability-mcp-probe";
43
47
  readonly path: "/v1/capabilities/mcp/probe";
@@ -36,7 +36,8 @@ import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
36
36
  import * as facts from "../../capability-facts.js";
37
37
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
38
38
  import { coercePermissionModeStrict, PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
39
- import { gatedPerson, sendPrincipalRefusal } from "../principal-gate.js";
39
+ import { gatedPerson, gatedSubject, sendPrincipalRefusal } from "../principal-gate.js";
40
+ import { scenarioRequestedBySubject } from "../../request-form.js";
40
41
  import { readAdoptionImportCapability } from "../../adoption/import.js";
41
42
  import { headReadPresent } from "../../session-incarnation.js";
42
43
  import { callerTrust } from "../caller-trust.js";
@@ -57,6 +58,12 @@ function retentionAdvertisement(config) {
57
58
  export function createCapabilitiesLocal() {
58
59
  return { mcpProbe: createMcpProbeLocal() };
59
60
  }
61
+ const QUERY_SCENARIO_REPEATED = Symbol("scenario query given more than once");
62
+ const SCENARIO_QUERY_REPEATED_REFUSAL = "scenario must be given at most once (omit it to read this deployment's default scenario)";
63
+ function scenarioQueryOf(search) {
64
+ const given = new URLSearchParams(search).getAll("scenario");
65
+ return given.length > 1 ? QUERY_SCENARIO_REPEATED : given[0];
66
+ }
60
67
  export async function handleCapabilities(req, res, match, ctx) {
61
68
  const { deps } = ctx;
62
69
  switch (match.decl.id) {
@@ -140,6 +147,7 @@ export async function handleCapabilities(req, res, match, ctx) {
140
147
  permissionRulesRevoke: facts.ruleLane(deps).available,
141
148
  respondFreeFormRules: true,
142
149
  respondBatchRuleOffers: true,
150
+ approvalDecideRuleSlot: true,
143
151
  oneShot: true,
144
152
  policy: true,
145
153
  usage: Boolean(deps.costQuota),
@@ -213,6 +221,7 @@ export async function handleCapabilities(req, res, match, ctx) {
213
221
  retention: retentionAdvertisement(deps.config),
214
222
  peerLane: Boolean(deps.peerDirectory),
215
223
  scenarioSubject: true,
224
+ effectiveScenario: true,
216
225
  sessionList: facts.sessionList(deps).available,
217
226
  sessionSearch: facts.sessionList(deps).available,
218
227
  sessionKinds: facts.sessionKinds(deps).available ? [...SESSION_KINDS] : [],
@@ -263,15 +272,30 @@ export async function handleCapabilities(req, res, match, ctx) {
263
272
  sendPrincipalRefusal(res, req, deps.config);
264
273
  return;
265
274
  }
266
- const scenarioParams = new URLSearchParams(match.search).getAll("scenario");
267
- if (scenarioParams.length > 1) {
268
- sendError(res, 400, "request.field_invalid", "scenario must be given at most once (omit it to read this deployment's default scenario)");
275
+ const named = scenarioQueryOf(match.search);
276
+ if (named === QUERY_SCENARIO_REPEATED) {
277
+ sendError(res, 400, "request.field_invalid", SCENARIO_QUERY_REPEATED_REFUSAL);
269
278
  return;
270
279
  }
271
- const scenarioName = await deps.scenarioGate(principal, scenarioParams[0]);
280
+ const { scenario: scenarioName } = await deps.scenarioGate(principal, scenarioRequestedBySubject(gatedSubject(req, deps.config), named));
272
281
  await serveMcpProbeFace(res, ctx, principal, { declaredBy: "deployment", specs: mcpForScenario(deps.config.mcpServers, scenarioName) ?? [] });
273
282
  return;
274
283
  }
284
+ case "capability-scenario-effective": {
285
+ const principal = gatedPerson(req, deps.config);
286
+ if (deps.config.requirePrincipal && !principal) {
287
+ sendPrincipalRefusal(res, req, deps.config);
288
+ return;
289
+ }
290
+ const named = scenarioQueryOf(match.search);
291
+ if (named === QUERY_SCENARIO_REPEATED) {
292
+ sendError(res, 400, "request.field_invalid", SCENARIO_QUERY_REPEATED_REFUSAL);
293
+ return;
294
+ }
295
+ const choice = await deps.scenarioGate(principal, scenarioRequestedBySubject(gatedSubject(req, deps.config), named));
296
+ sendJson(res, 200, { scenario: choice.scenario, source: choice.source });
297
+ return;
298
+ }
275
299
  case "capability-mcp-probe": {
276
300
  const principal = gatedPerson(req, deps.config);
277
301
  if (deps.config.requirePrincipal && !principal) {
@@ -401,6 +425,7 @@ export const CAPABILITIES_ROUTES = [
401
425
  { id: "capabilities", path: "/v1/capabilities", methods: ["GET"] },
402
426
  { id: "capability-scenario", pattern: /^\/v1\/capabilities\/scenarios\/(?<name>.*)$/, label: "/v1/capabilities/scenarios/:name", methods: ["GET"] },
403
427
  { id: "capability-mcp", path: "/v1/capabilities/mcp", methods: ["GET"] },
428
+ { id: "capability-scenario-effective", path: "/v1/capabilities/scenario", methods: ["GET"] },
404
429
  { id: "capability-mcp-probe", path: "/v1/capabilities/mcp/probe", methods: ["POST"] },
405
430
  ];
406
431
  //# sourceMappingURL=capabilities.js.map
@@ -23,13 +23,13 @@ export async function handleConfigCatalog(req, res, match, ctx) {
23
23
  sendError(res, 501, "feature.config_catalog_disabled", "the config catalog is switched off on this deployment (CONFIG_CATALOG_ENABLED=false)");
24
24
  return;
25
25
  case "operator_principals_empty":
26
- sendOperatorPrincipalsRequired(res);
26
+ sendOperatorPrincipalsRequired(res, "operate");
27
27
  return;
28
28
  default:
29
29
  return assertNever(fact.why);
30
30
  }
31
31
  }
32
- if (!explicitOperatorOk(principal, deps.config)) {
32
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
33
33
  sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
34
34
  return;
35
35
  }
@@ -144,7 +144,7 @@ export async function handleDevices(req, res, match, ctx) {
144
144
  }
145
145
  }
146
146
  const { store, hub, enrollment, runStore } = lane;
147
- const operator = explicitOperatorOk(principal, deps.config);
147
+ const operator = explicitOperatorOk(principal, deps.config, "operate");
148
148
  const domain = deviceOwnerDomainOf(req, deps.config);
149
149
  if (match.decl.id === "devices-list") {
150
150
  if (!operator && domain === undefined) {
@@ -42,7 +42,6 @@ export interface ServerWiringGates {
42
42
  * 不再是装配现场的事实,而是别处算完可能已漂的复述)。 */
43
43
  export interface ServerWiringGateInput {
44
44
  toolApprovalEnabled: boolean;
45
- streamApprovalEnabled: boolean;
46
45
  /** 谓词自己的入参类型(结构形,不 import store 依赖树)—— 取型而非重述,两处不可能漂。 */
47
46
  backend: StreamApprovalGateInput["backend"];
48
47
  /** 谓词自己的入参类型(只收店的形,工厂方法过不了 tsc)—— 取型而非重述,与 `backend` 同理;在场性即事实,本面不读实例的内容。 */
@@ -116,8 +115,9 @@ export declare function assertStaticWiringConsistent(facts: StaticWiringFacts, l
116
115
  * 字符串,端口误暴露到公网就是侦察面(装配拓扑 / 治理姿态 / 写保护名表)。配了 service 凭据的部署上,到得了这里的请求都过了全局凭据门、
117
116
  * 必然带着验过的身份,本判不咬;不要求静态凭据在场(鉴权桥的登录令牌就是身份)。
118
117
  *
119
- * 判源 = TCP 对端地址(`req.socket.remoteAddress`),**非任何头**(XFF 类头在无反代形上是自造面;有反代的部署必然配凭据)。
120
- * `remoteAddress` 缺席(socket 已断等边缘)⇒ 判拒(fail-closed)。
118
+ * 「本机」按请求通道穷举(`request-channel.ts`;加一种通道不接 ⇒ 编译红):经本机控制 socket 来的请求 = 本机(能连上它的只有引擎的 OS 用户 —— 与 `/health` 把它认作
119
+ * 本机用户同读,同一只请求不被一面认作本机、另一面认作远程);经 TCP 来的请求判源 = TCP 对端地址(`req.socket.remoteAddress`),**非任何头**(XFF 类头在无反代形上是自造面;
120
+ * 有反代的部署必然配凭据)。`remoteAddress` 缺席(socket 已断等边缘)⇒ 判拒(fail-closed)。
121
121
  */
122
122
  export declare function remoteWithoutVerifiedIdentity(req: IncomingMessage, config: Parameters<typeof verifiedIdentitySource>[1]): boolean;
123
123
  export declare function handleDiagnostics(req: IncomingMessage, res: ServerResponse, match: RouteMatch<DiagnosticsRouteId>, ctx: RouteCtx): Promise<void>;