@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
@@ -5,6 +5,7 @@ import { readOutboundWiring } from "../../outbound-dispatcher.js";
5
5
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
6
6
  import { sendJson, sendError } from "../send.js";
7
7
  import { gatedPerson, explicitOperatorOk, isLoopbackAddress, sendOperatorPrincipalsRequired, verifiedIdentitySource } from "../principal-gate.js";
8
+ import { requestChannelOf } from "../request-channel.js";
8
9
  import * as facts from "../../capability-facts.js";
9
10
  import { assertNever } from "../route-ctx.js";
10
11
  import { STORE_NOT_DURABLE_CODE, StoreNotDurableError, describeStoreRoot, projectStoreDurabilityWiring, unwiredStorePosture } from "../../store-posture.js";
@@ -12,7 +13,6 @@ import { describeLocalStoreDeclaration } from "../../local-store-declaration.js"
12
13
  export function buildServerWiringGates(input) {
13
14
  const gate = resolveStreamApprovalGate({
14
15
  toolApprovalEnabled: input.toolApprovalEnabled,
15
- streamApprovalEnabled: input.streamApprovalEnabled,
16
16
  backend: input.backend,
17
17
  checkpointStore: input.checkpointStore,
18
18
  });
@@ -126,17 +126,30 @@ export function assertStaticWiringConsistent(facts, logger) {
126
126
  return audit;
127
127
  }
128
128
  export function remoteWithoutVerifiedIdentity(req, config) {
129
- return !isLoopbackAddress(req.socket.remoteAddress) && verifiedIdentitySource(req, config) === undefined;
129
+ return !requestFromThisHost(req) && verifiedIdentitySource(req, config) === undefined;
130
+ }
131
+ function requestFromThisHost(req) {
132
+ const channel = requestChannelOf(req);
133
+ switch (channel) {
134
+ case "tcp":
135
+ return isLoopbackAddress(req.socket.remoteAddress);
136
+ case "control-socket":
137
+ return true;
138
+ default: {
139
+ const unknownChannel = channel;
140
+ throw new Error(`diagnostics: request channel ${String(unknownChannel)} has no locality reading`);
141
+ }
142
+ }
130
143
  }
131
144
  export async function handleDiagnostics(req, res, match, ctx) {
132
145
  const { deps } = ctx;
133
146
  switch (match.decl.id) {
134
147
  case "diagnostics-wiring": {
135
- if (!facts.operatorLane(deps).available) {
136
- sendOperatorPrincipalsRequired(res);
148
+ if (!facts.operatorLane(deps, "operate").available) {
149
+ sendOperatorPrincipalsRequired(res, "operate");
137
150
  return;
138
151
  }
139
- if (!explicitOperatorOk(gatedPerson(req, deps.config), deps.config)) {
152
+ if (!explicitOperatorOk(gatedPerson(req, deps.config), deps.config, "operate")) {
140
153
  sendError(res, 403, "auth.operator_only", "wiring diagnostics are operator-only");
141
154
  return;
142
155
  }
@@ -156,8 +169,7 @@ export async function handleDiagnostics(req, res, match, ctx) {
156
169
  configApply: deps.configApplyState?.() ?? null,
157
170
  mcpRevocations: deps.mcpRevocationState?.() ?? null,
158
171
  serverGates: buildServerWiringGates({
159
- toolApprovalEnabled: deps.toolApproval !== undefined,
160
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
172
+ toolApprovalEnabled: deps.config.toolApprovalEnabled === true,
161
173
  backend: deps.backend,
162
174
  checkpointStore: deps.checkpointStore,
163
175
  durableApproval: deps.config.durableApproval === true,
@@ -49,13 +49,13 @@ export async function handleImages(req, res, match, ctx) {
49
49
  const bakes = deps.imageBakes;
50
50
  const idx = deps.imageIndex;
51
51
  const principal = gatedPerson(req, deps.config);
52
- const isExplicitOperator = explicitOperatorOk(principal, deps.config);
53
- const operatorLane = facts.operatorLane(deps);
52
+ const isExplicitOperator = explicitOperatorOk(principal, deps.config, "operate");
53
+ const operatorLane = facts.operatorLane(deps, "operate");
54
54
  const cfg = deps.config.imageBakes;
55
55
  const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
56
56
  if (match.decl.id === "image-bake-submit") {
57
57
  if (!operatorLane.available) {
58
- sendOperatorPrincipalsRequired(res);
58
+ sendOperatorPrincipalsRequired(res, "operate");
59
59
  return;
60
60
  }
61
61
  if (!isExplicitOperator) {
@@ -130,7 +130,7 @@ export async function handleImages(req, res, match, ctx) {
130
130
  }
131
131
  if (match.decl.id === "image-bake-events") {
132
132
  if (!operatorLane.available) {
133
- sendOperatorPrincipalsRequired(res);
133
+ sendOperatorPrincipalsRequired(res, "operate");
134
134
  return;
135
135
  }
136
136
  if (!isExplicitOperator) {
@@ -152,7 +152,7 @@ export async function handleImages(req, res, match, ctx) {
152
152
  }
153
153
  if (match.decl.id === "image-bake-cancel") {
154
154
  if (!operatorLane.available) {
155
- sendOperatorPrincipalsRequired(res);
155
+ sendOperatorPrincipalsRequired(res, "operate");
156
156
  return;
157
157
  }
158
158
  if (!isExplicitOperator) {
@@ -264,7 +264,7 @@ export async function handleImages(req, res, match, ctx) {
264
264
  }
265
265
  if (match.decl.id === "image-bake-detail") {
266
266
  if (!operatorLane.available) {
267
- sendOperatorPrincipalsRequired(res);
267
+ sendOperatorPrincipalsRequired(res, "operate");
268
268
  return;
269
269
  }
270
270
  if (!isExplicitOperator) {
@@ -290,7 +290,7 @@ export async function handleImages(req, res, match, ctx) {
290
290
  if (pool.available && isIndexRouteId(match.decl.id)) {
291
291
  const idx = pool.imageIndex;
292
292
  const principal = gatedPerson(req, deps.config);
293
- const operator = explicitOperatorOk(principal, deps.config);
293
+ const operator = explicitOperatorOk(principal, deps.config, "operate");
294
294
  const viewer = { operator, tenantId: principal ?? null };
295
295
  const q = new URLSearchParams(match.search);
296
296
  if (match.decl.id === "images-index") {
@@ -340,11 +340,11 @@ export async function handleImages(req, res, match, ctx) {
340
340
  return;
341
341
  }
342
342
  if (match.decl.id === "image-register") {
343
- if (!facts.operatorLane(deps).available) {
344
- sendOperatorPrincipalsRequired(res);
343
+ if (!facts.operatorLane(deps, "operate").available) {
344
+ sendOperatorPrincipalsRequired(res, "operate");
345
345
  return;
346
346
  }
347
- if (!explicitOperatorOk(principal, deps.config)) {
347
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
348
348
  sendError(res, 403, "auth.operator_only", "operator only");
349
349
  return;
350
350
  }
@@ -73,10 +73,10 @@ export async function handleMemoryBundle(req, res, match, ctx) {
73
73
  }
74
74
  const fact = facts.memoryBundle(deps);
75
75
  if (!fact.available && fact.why === "operator_principals_empty") {
76
- sendOperatorPrincipalsRequired(res);
76
+ sendOperatorPrincipalsRequired(res, "operate");
77
77
  return;
78
78
  }
79
- if (!explicitOperatorOk(principal, deps.config)) {
79
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
80
80
  sendError(res, 403, "auth.operator_only", "memory governance bundles are an operator-only deployment action");
81
81
  return;
82
82
  }
@@ -86,7 +86,7 @@ export async function handleMemoryBundle(req, res, match, ctx) {
86
86
  sendError(res, 501, "capability.memory_engine_required", "memory governance bundles are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend implements the v2-c bundle composite (exportSnapshotOf / importBundleCommit); the file memory backend does, the pg/mysql memory backends do not");
87
87
  return;
88
88
  case "operator_principals_empty":
89
- sendOperatorPrincipalsRequired(res);
89
+ sendOperatorPrincipalsRequired(res, "operate");
90
90
  return;
91
91
  default:
92
92
  return assertNever(fact.why);
@@ -29,10 +29,10 @@ export async function handleMemoryCompliance(req, res, match, ctx) {
29
29
  }
30
30
  const fact = facts.memoryCompliance(deps);
31
31
  if (!fact.available && fact.why === "operator_principals_empty") {
32
- sendOperatorPrincipalsRequired(res);
32
+ sendOperatorPrincipalsRequired(res, "operate");
33
33
  return;
34
34
  }
35
- if (!explicitOperatorOk(principal, deps.config)) {
35
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
36
36
  sendError(res, 403, "auth.operator_only", "memory provenance and erasure are operator-only deployment actions");
37
37
  return;
38
38
  }
@@ -42,7 +42,7 @@ export async function handleMemoryCompliance(req, res, match, ctx) {
42
42
  sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/mysql memory backends do not (their control plane would land on a replica's local disk)");
43
43
  return;
44
44
  case "operator_principals_empty":
45
- sendOperatorPrincipalsRequired(res);
45
+ sendOperatorPrincipalsRequired(res, "operate");
46
46
  return;
47
47
  default:
48
48
  return assertNever(fact.why);
@@ -85,14 +85,14 @@ export async function handleMemoryConsolidation(req, res, match, ctx) {
85
85
  sendError(res, 501, "feature.memory_consolidation_disabled", "memory consolidation is switched off on this deployment (MEMORY_CONSOLIDATION_DRIVER is off, or its default derived off because no cheap model is configured); turn it on to use these endpoints");
86
86
  return;
87
87
  case "operator_principals_empty":
88
- sendOperatorPrincipalsRequired(res);
88
+ sendOperatorPrincipalsRequired(res, "operate");
89
89
  return;
90
90
  default:
91
91
  return assertNever(fact.why);
92
92
  }
93
93
  }
94
94
  const faces = fact.valve;
95
- if (!explicitOperatorOk(principal, deps.config)) {
95
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
96
96
  sendError(res, 403, "auth.operator_only", "memory consolidation is operator-only: the caller principal is not in OPERATOR_PRINCIPALS");
97
97
  return;
98
98
  }
@@ -118,10 +118,10 @@ export async function handleMemoryOrigin(req, res, match, ctx) {
118
118
  }
119
119
  const fact = facts.memoryOrigin(deps);
120
120
  if (!fact.available && fact.why === "operator_principals_empty") {
121
- sendOperatorPrincipalsRequired(res);
121
+ sendOperatorPrincipalsRequired(res, "operate");
122
122
  return;
123
123
  }
124
- if (!explicitOperatorOk(principal, deps.config)) {
124
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
125
125
  sendError(res, 403, "auth.operator_only", "memory origin marking and clearance are operator-only deployment actions");
126
126
  return;
127
127
  }
@@ -131,7 +131,7 @@ export async function handleMemoryOrigin(req, res, match, ctx) {
131
131
  sendError(res, 501, "capability.memory_engine_required", "memory origin marking and clearance are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the clearance ledger and its write-ahead custody rows live in that plane, and on a backend without it they would land on one replica's local disk");
132
132
  return;
133
133
  case "operator_principals_empty":
134
- sendOperatorPrincipalsRequired(res);
134
+ sendOperatorPrincipalsRequired(res, "operate");
135
135
  return;
136
136
  default:
137
137
  return assertNever(fact.why);
@@ -9,7 +9,7 @@ import { gatedPerson, explicitOperatorOk, sendPrincipalRefusal, sendUnauthorized
9
9
  import { assertNever } from "../route-ctx.js";
10
10
  import { sentMemoryFaceFault } from "../memory-face-fault-reply.js";
11
11
  function layerFaceAudience(deps, principal, face, scope) {
12
- if (explicitOperatorOk(principal, deps.config))
12
+ if (explicitOperatorOk(principal, deps.config, "operate"))
13
13
  return { kind: "operator" };
14
14
  return { kind: "layer_owner", face, withheld: (coreMessage) => deps.logger?.warn?.(face === "export" ? "memory_export_refused" : "memory_sync_refused", { scope, err: coreMessage }) };
15
15
  }
@@ -44,7 +44,7 @@ function directoryDecidesThisFace(mode) {
44
44
  async function gateMemoryScope(deps, person, scope, need) {
45
45
  if (scope === formatUserScope(person) || parseProjectLayerKey(scope)?.owner === person)
46
46
  return { kind: "allow" };
47
- if (explicitOperatorOk(person, deps.config))
47
+ if (explicitOperatorOk(person, deps.config, "operate"))
48
48
  return { kind: "allow" };
49
49
  const directory = deps.orgMemoryDirectory;
50
50
  if (!scope.startsWith(ORG_PREFIX) || directory === undefined || !directoryDecidesThisFace(deps.config.memoryOrgAdmissionMode)) {
@@ -9,10 +9,10 @@ export async function handleObservability(req, res, match, ctx) {
9
9
  case "outcomes": {
10
10
  const fact = facts.outcomeLedger(deps);
11
11
  if (!fact.available && fact.why === "operator_principals_empty") {
12
- sendOperatorPrincipalsRequired(res);
12
+ sendOperatorPrincipalsRequired(res, "operate");
13
13
  return;
14
14
  }
15
- if (!explicitOperatorOk(gatedPerson(req, deps.config), deps.config)) {
15
+ if (!explicitOperatorOk(gatedPerson(req, deps.config), deps.config, "operate")) {
16
16
  sendError(res, 403, "auth.operator_only", "the outcomes view is operator-only");
17
17
  return;
18
18
  }
@@ -22,7 +22,7 @@ export async function handleObservability(req, res, match, ctx) {
22
22
  sendError(res, 501, "capability.outcome_ledger_required", "no queryable outcome ledger on this worker (File sink — read the JSONL dataset directly)");
23
23
  return;
24
24
  case "operator_principals_empty":
25
- sendOperatorPrincipalsRequired(res);
25
+ sendOperatorPrincipalsRequired(res, "operate");
26
26
  return;
27
27
  default:
28
28
  return assertNever(fact.why);
@@ -33,11 +33,11 @@ export async function handleRetention(req, res, match, ctx) {
33
33
  sendPrincipalRefusal(res, req, deps.config);
34
34
  return;
35
35
  }
36
- if (!facts.operatorLane(deps).available) {
37
- sendOperatorPrincipalsRequired(res);
36
+ if (!facts.operatorLane(deps, "operate").available) {
37
+ sendOperatorPrincipalsRequired(res, "operate");
38
38
  return;
39
39
  }
40
- if (!explicitOperatorOk(principal, deps.config)) {
40
+ if (!explicitOperatorOk(principal, deps.config, "operate")) {
41
41
  sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
42
42
  return;
43
43
  }
@@ -242,9 +242,9 @@ function sendImportReceipt(res, outcome) {
242
242
  function resolveTargetPrincipal(requested, caller, config) {
243
243
  if (requested === undefined || requested === caller)
244
244
  return { ok: true, principal: caller };
245
- if (!facts.operatorLane({ config }).available)
245
+ if (!facts.operatorLane({ config }, "operate").available)
246
246
  return { ok: false, why: "operator_principals_empty" };
247
- if (!explicitOperatorOk(caller, config))
247
+ if (!explicitOperatorOk(caller, config, "operate"))
248
248
  return { ok: false, why: "not_operator" };
249
249
  if (principalShapeProblem(requested) !== undefined)
250
250
  return { ok: false, why: "not_a_principal" };
@@ -259,7 +259,7 @@ async function handleRuleList(res, search, caller, lane, config) {
259
259
  if (!target.ok) {
260
260
  switch (target.why) {
261
261
  case "operator_principals_empty":
262
- sendOperatorPrincipalsRequired(res);
262
+ sendOperatorPrincipalsRequired(res, "operate");
263
263
  return;
264
264
  case "not_operator":
265
265
  sendError(res, 403, "auth.operator_only", "reading another principal's rules is an operator-only action");
@@ -382,7 +382,7 @@ async function handleRuleWrite(res, rawBody, caller, lane, config) {
382
382
  if (!target.ok) {
383
383
  switch (target.why) {
384
384
  case "operator_principals_empty":
385
- sendOperatorPrincipalsRequired(res);
385
+ sendOperatorPrincipalsRequired(res, "operate");
386
386
  return;
387
387
  case "not_operator":
388
388
  sendError(res, 403, "auth.operator_only", "writing another principal's rules is an operator-only action");
@@ -434,7 +434,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, config) {
434
434
  if (!target.ok) {
435
435
  switch (target.why) {
436
436
  case "operator_principals_empty":
437
- sendOperatorPrincipalsRequired(res);
437
+ sendOperatorPrincipalsRequired(res, "operate");
438
438
  return;
439
439
  case "not_operator":
440
440
  sendError(res, 403, "auth.operator_only", "revoking another principal's rules is an operator-only action");
@@ -15,7 +15,8 @@
15
15
  * 拒 ⇒ 422 `request.host_posture_refused`(与提交口同码同句)。
16
16
  *
17
17
  * ── 回写:准入的词写回存储体(run 模式词的唯一来源)────────────────────────────────────────────────────────
18
- * 续跑腿(停驻后续跑 / 重启后续跑)按存储体顶层 `permissionMode` 重建 spec;不写回 = 下一腿回到切档前的词。写回走**认领栅栏**
18
+ * 续跑腿(停驻后续跑 / 重启后续跑)按存储体顶层 `permissionMode` 重建 spec;不写回 = 下一腿回到切档前的词。词与写据经改写口一笔换(写据 `switch`:
19
+ * 切档写下的词不是任何地板的出口,运维事后改严时续跑口照钳 —— `permission-mode-admission.ts` 顶注「写据」)。写回走**认领栅栏**
19
20
  * (checkpoint 店的 `rewriteCtxWhileClaimed`:只在**接下切档的那条腿**此刻仍持有这条 run 时写,只改既有行)—— 这条 run 终局放掉认领、同会话新 run
20
21
  * 已写下它自己的存储体之后,或这条 run 停驻、续跑的新腿(同一个 taskId 回到 running、认领仍在)已写下它续跑用的体之后,迟到的回写零写,
21
22
  * 不会拿旧腿的体盖掉新腿的。腿身份 = {@link switchLegHolds}(栅栏里问)。同一条腿上的切档在本进程按活流对象串行(引擎受理序 = 落盘序;活流只在本副本)。
@@ -5,7 +5,7 @@ import { admitRunById, enter } from "../owner-access.js";
5
5
  import { assertNever } from "../route-ctx.js";
6
6
  import * as facts from "../../capability-facts.js";
7
7
  import { PERMISSION_MODE_WORDS, coercePermissionModeStrict, parseTaskSettings } from "../../task-settings.js";
8
- import { admitPermissionModeWord } from "../../permission-mode-admission.js";
8
+ import { admitPermissionModeWord, withStoredPermissionMode } from "../../permission-mode-admission.js";
9
9
  import { readResumeCtx, resumeCtxCorruptAnswer, resumeCtxUnreadableAnswer, resumeCtxUnsealableAnswer } from "../resume-ctx-read.js";
10
10
  import { sealCheckpointCtx } from "../checkpoint-ctx.js";
11
11
  import { serviceVersion } from "../../service-version.js";
@@ -114,7 +114,7 @@ export async function handlePermissionMode(req, res, match, ctx) {
114
114
  }
115
115
  let persistMiss;
116
116
  try {
117
- const next = sealCheckpointCtx({ ...stored, body: { ...stored.body, permissionMode: admitted }, writerVersion: serviceVersion() }, deps.checkpointCtxSealing, sessionId);
117
+ const next = sealCheckpointCtx({ ...withStoredPermissionMode(stored, admitted, "switch"), writerVersion: serviceVersion() }, deps.checkpointCtxSealing, sessionId);
118
118
  const rewrite = await checkpointStore.rewriteCtxWhileClaimed(sessionId, next, { fence: runStore, taskId, legHolds: (fenced) => switchLegHolds(fenced, deps.instanceId, ctx.registry.steerableRuns.get(taskId), live) });
119
119
  persistMiss = rewrite === "written" ? undefined : rewrite;
120
120
  }
@@ -129,20 +129,15 @@ export declare const RUN_VERBS_ROUTES: readonly [{
129
129
  readonly label: "/v1/questions/:id/respond";
130
130
  readonly methods: readonly ["POST"];
131
131
  }, {
132
- readonly id: "tool-approval-respond";
132
+ readonly id: "task-ask-decision";
133
133
  readonly pattern: RegExp;
134
- readonly label: "/v1/tool-approvals/:id/respond";
134
+ readonly label: "/v1/tasks/:id/asks/:askId/decision";
135
135
  readonly methods: readonly ["POST"];
136
136
  }, {
137
- readonly id: "tool-approval-read";
137
+ readonly id: "task-ask-read";
138
138
  readonly pattern: RegExp;
139
- readonly label: "/v1/tool-approvals/:id";
139
+ readonly label: "/v1/tasks/:id/asks/:askId";
140
140
  readonly methods: readonly ["GET"];
141
- }, {
142
- readonly id: "task-ask-decision";
143
- readonly pattern: RegExp;
144
- readonly label: "/v1/tasks/:id/asks/:askId/decision";
145
- readonly methods: readonly ["POST"];
146
141
  }];
147
142
  export type RunVerbsRouteId = RouteIdsOf<typeof RUN_VERBS_ROUTES>;
148
143
  //# sourceMappingURL=runs.d.ts.map
@@ -15,9 +15,9 @@ import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
15
15
  import { idemCallerOf, idemKeyOf, inflightKeyOf, submissionIdemOf } from "../idempotency.js";
16
16
  import { streamSseLog } from "../sse-log.js";
17
17
  import { bindSseLifecycle } from "../sse-lifecycle.js";
18
- import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
18
+ import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat } from "../../approval-card.js";
19
19
  import { heldAskOf, readDecider, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
20
- import { ASK_DECISION_CONSUMER_ABSENT, approvalPreambleReplays, decidedRowFacts, outcomeOfDecidedRow, parkSessionOf, presentedApprovalFrameFamily, resolveStreamApprovalGate } from "../../tool-approval.js";
20
+ import { ASK_DECISION_CONSUMER_ABSENT, ASK_NOT_FOUND_UNDER_TASK, approvalPreambleReplays, decidedRowFacts, outcomeOfDecidedRow, parkSessionOf, presentedApprovalFrameFamily, readPersistRuleSlot, readReadRootGrantSlot, resolveStreamApprovalGate, READ_ROOT_GRANT_LIVE_ONLY, READ_ROOT_GRANT_NOT_ACCEPTED } from "../../tool-approval.js";
21
21
  import { MAX_DECISION_NOTE_CHARS, noteLandedOnRow, noteRecordedReceipt } from "../../approval-ask-machine.js";
22
22
  import { askRowEndingCause } from "../../approval-deny-reasons.js";
23
23
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
@@ -28,7 +28,7 @@ import { INJECTION_TIERS } from "../../trace/injection-tier.js";
28
28
  import { headerStr, gatedSubject, explicitOperatorOk, sendPrincipalRefusal, runIdentityOf } from "../principal-gate.js";
29
29
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
30
30
  import * as facts from "../../capability-facts.js";
31
- import { assertNever } from "../route-ctx.js";
31
+ import { assertNever, readRememberSlot } from "../route-ctx.js";
32
32
  import { readRunFace } from "../run-read-face.js";
33
33
  import { replayOfExistingRun, replayOfKey } from "../submit-replay.js";
34
34
  import { mintAndDriveDurableRun } from "../durable-run-leg.js";
@@ -49,6 +49,7 @@ const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
49
49
  const STEER_PRIORITY_CHOICES = STEER_PRIORITY_WORDS.length <= 2 ? STEER_PRIORITY_WORDS.join(" or ") : `${STEER_PRIORITY_WORDS.slice(0, -1).join(", ")}, or ${STEER_PRIORITY_WORDS[STEER_PRIORITY_WORDS.length - 1]}`;
50
50
  const ASK_PARKING_RETRY_AFTER_SEC = 2;
51
51
  const ASK_ACTOR_LABEL_MAX = 256;
52
+ const ASK_LEDGER_DISABLED = "the durable stream-approval ask ledger is not enabled on this worker (needs a store backend (DB_BACKEND) and DURABLE_APPROVAL)";
52
53
  const AskDecisionActorSchema = ActorAssertionWire.strip();
53
54
  function mirrorRetryAfter(res, body) {
54
55
  const retryAfterSec = body !== null && typeof body === "object" ? body.retryAfterSec : undefined;
@@ -69,6 +70,9 @@ const AskDecisionBodySchema = z
69
70
  actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
70
71
  decidedBy: z.unknown().optional(),
71
72
  decidedVia: z.unknown().optional(),
73
+ remember: z.unknown().optional(),
74
+ persistRule: z.unknown().optional(),
75
+ readRootGrant: z.unknown().optional(),
72
76
  })
73
77
  .strict();
74
78
  const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
@@ -116,18 +120,19 @@ async function streamRunEvents(req, res, deps, runStore, entry, taskId) {
116
120
  preamble: async (signal) => {
117
121
  const gate = resolveStreamApprovalGate({
118
122
  toolApprovalEnabled: Boolean(deps.toolApproval),
119
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
120
123
  backend: deps.backend,
121
124
  checkpointStore: deps.checkpointStore,
122
125
  });
123
126
  if (!gate.active)
124
127
  return [];
125
- if (!approvalPreambleReplays({
126
- streamApprovalOn: gate.active,
127
- windowMs: deps.config.streamApproval?.windowMs,
128
- windowMarginMs: deps.config.streamAskWindowMarginMs,
129
- declaredFamilies: new Set([presentedApprovalFrameFamily(gate.active)]),
130
- })) {
128
+ const presented = presentedApprovalFrameFamily(gate.active);
129
+ if (presented === undefined ||
130
+ !approvalPreambleReplays({
131
+ streamApprovalOn: gate.active,
132
+ windowMs: deps.config.streamApproval?.windowMs,
133
+ windowMarginMs: deps.config.streamAskWindowMarginMs,
134
+ declaredFamilies: new Set([presented]),
135
+ })) {
131
136
  return [];
132
137
  }
133
138
  const store = gate.askStore;
@@ -1174,12 +1179,12 @@ export async function handleRunVerbs(req, res, match, ctx) {
1174
1179
  sendJson(res, status, respBody);
1175
1180
  return;
1176
1181
  }
1177
- if (match.decl.id === "tool-approval-respond" || match.decl.id === "tool-approval-read") {
1182
+ if (match.decl.id === "task-ask-read") {
1178
1183
  if (rateLimited(req, res))
1179
1184
  return;
1180
- const approvalFact = facts.toolApproval(deps);
1181
- if (!approvalFact.available) {
1182
- sendError(res, 501, "feature.tool_approval_disabled", "live tool-approval HITL is not enabled on this worker (TOOL_APPROVAL_ENABLED)");
1185
+ const streamFact = facts.streamApproval(deps);
1186
+ if (!streamFact.available) {
1187
+ sendError(res, 501, "feature.approval_ask_disabled", ASK_LEDGER_DISABLED);
1183
1188
  return;
1184
1189
  }
1185
1190
  const principal = gatedSubject(req, deps.config);
@@ -1187,15 +1192,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1187
1192
  sendPrincipalRefusal(res, req, deps.config);
1188
1193
  return;
1189
1194
  }
1190
- const id = match.params.id;
1191
- if (match.decl.id === "tool-approval-read") {
1192
- const { status, body: respBody } = await approvalFact.coordinator.readApproval(id, principal);
1193
- sendJson(res, status, respBody);
1194
- return;
1195
- }
1196
- const body = await readJson(req);
1197
- const { status, body: respBody } = await approvalFact.coordinator.respond(id, principal, body, req, { readRootGrantAccepted: facts.readRootGrant(deps).available });
1198
- mirrorRetryAfter(res, respBody);
1195
+ const { status, body: respBody } = await streamFact.coordinator.readAsk(match.params.taskId, match.params.askId, principal);
1199
1196
  sendJson(res, status, respBody);
1200
1197
  return;
1201
1198
  }
@@ -1204,7 +1201,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1204
1201
  return;
1205
1202
  const streamFact = facts.streamApproval(deps);
1206
1203
  if (!streamFact.available) {
1207
- sendError(res, 501, "feature.approval_ask_disabled", "the durable stream-approval ask ledger is not enabled on this worker (needs STREAM_APPROVAL_ENABLED plus a DURABLE store backend — the local backend keeps asks in memory and would lose accepted decisions on restart)");
1204
+ sendError(res, 501, "feature.approval_ask_disabled", ASK_LEDGER_DISABLED);
1208
1205
  return;
1209
1206
  }
1210
1207
  const askStore = streamFact.askStore;
@@ -1222,7 +1219,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1222
1219
  sendError(res, 400, "request.body_shape", retired.error, retired.extra);
1223
1220
  return;
1224
1221
  }
1225
- sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy / decidedVia");
1222
+ sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy / decidedVia / remember / persistRule / readRootGrant");
1226
1223
  return;
1227
1224
  }
1228
1225
  const body = parsedBody.data;
@@ -1242,16 +1239,38 @@ export async function handleRunVerbs(req, res, match, ctx) {
1242
1239
  }
1243
1240
  }
1244
1241
  const decider = decidedByReading.decider;
1242
+ const rememberSlot = readRememberSlot(body.remember, body.decision);
1243
+ if (rememberSlot.kind === "refused") {
1244
+ sendError(res, 400, rememberSlot.refusal.errorCode, rememberSlot.refusal.message, rememberSlot.refusal.facts);
1245
+ return;
1246
+ }
1247
+ const remember = rememberSlot.kind === "session" ? "session" : undefined;
1248
+ const ruleSlot = readPersistRuleSlot(body.persistRule, body.decision === "approve" ? "allow" : "deny");
1249
+ if (!ruleSlot.ok) {
1250
+ sendError(res, 400, "request.body_shape", ruleSlot.error);
1251
+ return;
1252
+ }
1253
+ const persistRule = ruleSlot.slot;
1254
+ const grantSlot = readReadRootGrantSlot(body.readRootGrant, body.decision === "approve" && remember === undefined);
1255
+ if (!grantSlot.ok) {
1256
+ sendError(res, 400, "request.body_shape", grantSlot.error);
1257
+ return;
1258
+ }
1259
+ if (grantSlot.grant !== undefined && !facts.readRootGrant(deps).available) {
1260
+ sendError(res, 400, "request.body_shape", READ_ROOT_GRANT_NOT_ACCEPTED);
1261
+ return;
1262
+ }
1263
+ const readRootGrant = grantSlot.grant;
1245
1264
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1246
1265
  const taskId = match.params.taskId;
1247
1266
  const askId = match.params.askId;
1248
- const askNotFound = () => sendError(res, 404, "not_found.ask", "no such approval ask under this task (never existed, belongs to another task, or is not yours)");
1267
+ const askNotFound = () => sendError(res, 404, "not_found.ask", ASK_NOT_FOUND_UNDER_TASK);
1249
1268
  const row0 = await askStore.getAsk(askId);
1250
1269
  if (!row0 || row0.taskId !== taskId) {
1251
1270
  askNotFound();
1252
1271
  return;
1253
1272
  }
1254
- const callerIsOperator = explicitOperatorOk(principal === undefined ? undefined : personOf(principal), deps.config);
1273
+ const callerIsOperator = explicitOperatorOk(principal === undefined ? undefined : personOf(principal), deps.config, "operate");
1255
1274
  const askAdmission = keyAdmission("ask.decide", { subject: principal, operator: callerIsOperator }, ownerCellOf(row0.owner));
1256
1275
  if (askAdmission === undefined) {
1257
1276
  askNotFound();
@@ -1304,7 +1323,8 @@ export async function handleRunVerbs(req, res, match, ctx) {
1304
1323
  ...noteReceipt(row),
1305
1324
  });
1306
1325
  };
1307
- const policyMayNotApprove = (row) => body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row.cardJson)), decider.decidedBy);
1326
+ const heldLeg = coordinator.holdLiveLeg(askId);
1327
+ const policyMayNotApprove = (row) => body.decision === "approve" && policyCannotAnswer(heldAskOf(coordinator.askDemandsAPerson(row, heldLeg)), decider.decidedBy);
1308
1328
  const refusePolicyApprove = () => sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK, fieldConflictFacts("policy_person_ask", ["decidedBy", "decision"]));
1309
1329
  const conflictWithFirst = (row) => {
1310
1330
  const first = projectDecisionActor(row.decisionActor);
@@ -1319,6 +1339,8 @@ export async function handleRunVerbs(req, res, match, ctx) {
1319
1339
  const answer = await coordinator.decideParkedAsk(askStore, row, {
1320
1340
  write: { decision: body.decision, decider, ...(body.note !== undefined ? { note: body.note } : {}), actor, ...(body.idempotencyKey !== undefined ? { idempotencyKey: body.idempotencyKey } : {}) },
1321
1341
  ...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
1342
+ ...(remember !== undefined ? { remember } : {}),
1343
+ ...(persistRule !== undefined ? { persistRule } : {}),
1322
1344
  ...(principal !== undefined ? { principal } : {}),
1323
1345
  operator: callerIsOperator,
1324
1346
  policyMayNotApprove: policyMayNotApprove(row),
@@ -1366,6 +1388,8 @@ export async function handleRunVerbs(req, res, match, ctx) {
1366
1388
  accepted: "parked_redeemed",
1367
1389
  ...decisionNoteEcho(recorded),
1368
1390
  ...noteReceipt(recorded),
1391
+ ...(answer.rememberApplied !== undefined ? { rememberApplied: answer.rememberApplied } : {}),
1392
+ ...(answer.rule ?? {}),
1369
1393
  });
1370
1394
  return;
1371
1395
  }
@@ -1388,6 +1412,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
1388
1412
  return;
1389
1413
  }
1390
1414
  case "PARKED":
1415
+ if (readRootGrant !== undefined) {
1416
+ sendError(res, 400, "request.body_shape", READ_ROOT_GRANT_LIVE_ONLY);
1417
+ return;
1418
+ }
1391
1419
  await answerParked(row);
1392
1420
  return;
1393
1421
  case "DENIED":
@@ -1440,8 +1468,13 @@ export async function handleRunVerbs(req, res, match, ctx) {
1440
1468
  refusePolicyApprove();
1441
1469
  return;
1442
1470
  }
1471
+ const ruleTextRefusal = persistRule !== undefined ? coordinator.precheckEditedRuleOfAskRow(row0, row0.owner, persistRule, hasUpdatedInput) : undefined;
1472
+ if (ruleTextRefusal !== undefined) {
1473
+ sendError(res, 400, "request.body_shape", ruleTextRefusal.message, ruleTextRefusal.ruleRejectCode !== undefined ? { ruleRejectCode: ruleTextRefusal.ruleRejectCode } : undefined);
1474
+ return;
1475
+ }
1443
1476
  let outcome;
1444
- const releaseEditMark = body.decision === "approve" && hasUpdatedInput ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
1477
+ const releaseEditMark = body.decision === "approve" && (hasUpdatedInput || readRootGrant !== undefined) ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
1445
1478
  try {
1446
1479
  outcome = await coordinator.writeAskDecision(askStore, askId, { kind: "live", batchId: row0.batchId }, row0.owner, {
1447
1480
  decision: body.decision,
@@ -1473,7 +1506,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1473
1506
  confirmed.decision === body.decision &&
1474
1507
  confirmed.idempotencyKey === body.idempotencyKey &&
1475
1508
  !confirmed.provisional) {
1476
- const settledNow = deps.toolApproval.notifyExternalDecision(askId, outcomeOfDecidedRow(confirmed)).settled;
1509
+ const settledNow = deps.toolApproval.notifyExternalDecision(askId, outcomeOfDecidedRow(confirmed), readRootGrant).settled;
1477
1510
  deps.logger?.warn?.("ask_decision_edit_settled_after_ambiguous_commit", { taskId, askId, settled: settledNow });
1478
1511
  }
1479
1512
  }
@@ -1520,7 +1553,13 @@ export async function handleRunVerbs(req, res, match, ctx) {
1520
1553
  sendError(res, 500, "internal.error", "the ask row was rewritten between commit and read-back — the decision that stands is not the one this request submitted; re-read the ask before acting on it");
1521
1554
  return;
1522
1555
  }
1523
- const settled = deps.toolApproval?.notifyExternalDecision(outcome.row.askId, outcomeOfDecidedRow(outcome.row)).settled ?? 0;
1556
+ const live = await coordinator.settleLiveDecision(outcome.row, heldLeg, row0.owner, principal, {
1557
+ ...(remember !== undefined ? { remember } : {}),
1558
+ ...(readRootGrant !== undefined ? { readRootGrant } : {}),
1559
+ ...(persistRule !== undefined ? { persistRule } : {}),
1560
+ inputWasEdited: hasUpdatedInput,
1561
+ });
1562
+ const settled = live.settled;
1524
1563
  const forwarded = outcome.row.decision === "approve" && outcome.row.updatedInput !== undefined && settled > 0;
1525
1564
  sendJson(res, 200, {
1526
1565
  askId: outcome.row.askId,
@@ -1532,6 +1571,8 @@ export async function handleRunVerbs(req, res, match, ctx) {
1532
1571
  ...(settled === 0 ? { consumerState: ASK_DECISION_CONSUMER_ABSENT } : {}),
1533
1572
  ...decisionNoteEcho(outcome.row),
1534
1573
  ...noteReceipt(outcome.row),
1574
+ ...(live.rememberApplied !== undefined ? { rememberApplied: live.rememberApplied } : {}),
1575
+ ...(live.rule ?? {}),
1535
1576
  });
1536
1577
  return;
1537
1578
  }
@@ -1620,8 +1661,7 @@ export const RUN_VERBS_ROUTES = [
1620
1661
  { id: "run-task-stop", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/tasks\/(?<tid>[^/]+)\/stop$/, label: "/v1/runs/:id/tasks/:tid/*", methods: ["POST"] },
1621
1662
  { id: "elicitation-respond", pattern: /^\/v1\/elicitations\/(?<id>[^/]+)\/respond$/, label: "/v1/elicitations/:id/respond", methods: ["POST"] },
1622
1663
  { id: "question-respond", pattern: /^\/v1\/questions\/(?<id>[^/]+)\/respond$/, label: "/v1/questions/:id/respond", methods: ["POST"] },
1623
- { id: "tool-approval-respond", pattern: /^\/v1\/tool-approvals\/(?<id>[^/]+)\/respond$/, label: "/v1/tool-approvals/:id/respond", methods: ["POST"] },
1624
- { id: "tool-approval-read", pattern: /^\/v1\/tool-approvals\/(?<id>[^/]+)$/, label: "/v1/tool-approvals/:id", methods: ["GET"] },
1625
1664
  { id: "task-ask-decision", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/asks\/(?<askId>[^/]+)\/decision$/, label: "/v1/tasks/:id/asks/:askId/decision", methods: ["POST"] },
1665
+ { id: "task-ask-read", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/asks\/(?<askId>[^/]+)$/, label: "/v1/tasks/:id/asks/:askId", methods: ["GET"] },
1626
1666
  ];
1627
1667
  //# sourceMappingURL=runs.js.map