@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
@@ -0,0 +1,9 @@
1
+ export const REQUEST_CHANNELS = ["tcp", "control-socket"];
2
+ const controlSocketRequests = new WeakSet();
3
+ export function markControlSocketRequest(req) {
4
+ controlSocketRequests.add(req);
5
+ }
6
+ export function requestChannelOf(req) {
7
+ return controlSocketRequests.has(req) ? "control-socket" : "tcp";
8
+ }
9
+ //# sourceMappingURL=request-channel.js.map
@@ -9,6 +9,7 @@ import { escapedStatementBytes, packetWallStatementBytes, rowWallPayloadBytes }
9
9
  import { MAX_JSON_BODY_BYTES } from "../json-body-limit.js";
10
10
  import { deliverVia } from "../submission-exit.js";
11
11
  import { canonicalAddress } from "../net-address.js";
12
+ import { requestChannelOf } from "./request-channel.js";
12
13
  export function refuseSubmitBody(walls, body) {
13
14
  const rowWall = walls.maxRowBytes;
14
15
  const packetWall = walls.maxPacketBytes;
@@ -171,8 +172,7 @@ export function createRequestGuards(ctx) {
171
172
  function rateLimited(req, res) {
172
173
  if (!deps.rateLimiter)
173
174
  return false;
174
- const peer = req.socket.remoteAddress;
175
- const key = (deps.config.directDoorActive ? undefined : principalFrom(req, deps.config)) ?? (peer === undefined ? "anon" : (canonicalAddress(peer) ?? peer));
175
+ const key = (deps.config.directDoorActive ? undefined : principalFrom(req, deps.config)) ?? rateLimitAddressKeyOf(req);
176
176
  const d = deps.rateLimiter.check(key);
177
177
  if (d.allowed)
178
178
  return false;
@@ -220,4 +220,19 @@ export function createRequestGuards(ctx) {
220
220
  }
221
221
  return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, readJson, readRawBody };
222
222
  }
223
+ function rateLimitAddressKeyOf(req) {
224
+ const channel = requestChannelOf(req);
225
+ switch (channel) {
226
+ case "tcp": {
227
+ const peer = req.socket.remoteAddress;
228
+ return peer === undefined ? "anon" : (canonicalAddress(peer) ?? peer);
229
+ }
230
+ case "control-socket":
231
+ return "control-socket";
232
+ default: {
233
+ const unknownChannel = channel;
234
+ throw new Error(`rate limit: request channel ${String(unknownChannel)} has no address key`);
235
+ }
236
+ }
237
+ }
223
238
  //# sourceMappingURL=request-guards.js.map
@@ -3,6 +3,7 @@ import { storeFaultClassOf } from "../plugins/store-fault.js";
3
3
  import { refusalBody } from "./refusal-body.js";
4
4
  import { frozenProjectContextOf } from "../project-context.js";
5
5
  import { frozenMemorySeedOf } from "../project-registry.js";
6
+ import { permissionModeWriteOf } from "../permission-mode-admission.js";
6
7
  export async function readResumeCtx(load, opening) {
7
8
  const read = await readStoredCtx(load);
8
9
  if (read.kind !== "ok")
@@ -11,9 +12,10 @@ export async function readResumeCtx(load, opening) {
11
12
  if (opened.kind !== "ok")
12
13
  return { kind: "unsealable", field: opened.field, reason: opened.reason };
13
14
  const where = (key) => ({ kind: "corrupt", where: `checkpoint ctx for session ${JSON.stringify(opening.sessionId)}: ${key}` });
14
- const { memorySeed: _storedSeed, projectContext: _storedContext, ...rest } = opened.ctx;
15
+ const { memorySeed: _storedSeed, projectContext: _storedContext, permissionModeWrite: _storedWrite, ...rest } = opened.ctx;
15
16
  const seed = frozenMemorySeedOf(opened.ctx.memorySeed);
16
17
  const frozen = frozenProjectContextOf(opened.ctx.projectContext);
18
+ const written = permissionModeWriteOf(opened.ctx.permissionModeWrite);
17
19
  let seedPart;
18
20
  switch (seed.kind) {
19
21
  case "absent":
@@ -32,18 +34,33 @@ export async function readResumeCtx(load, opening) {
32
34
  throw new Error(`resume-ctx-read: unplaced frozen memory seed reading ${JSON.stringify(unhandled)}`);
33
35
  }
34
36
  }
37
+ let contextPart;
35
38
  switch (frozen.kind) {
36
39
  case "absent":
37
- return { kind: "ok", ctx: { ...rest, ...seedPart } };
40
+ contextPart = {};
41
+ break;
38
42
  case "malformed":
39
43
  return where("projectContext");
40
44
  case "ok":
41
- return { kind: "ok", ctx: { ...rest, ...seedPart, projectContext: frozen.context } };
45
+ contextPart = { projectContext: frozen.context };
46
+ break;
42
47
  default: {
43
48
  const unhandled = frozen;
44
49
  throw new Error(`resume-ctx-read: unplaced frozen project context reading ${JSON.stringify(unhandled)}`);
45
50
  }
46
51
  }
52
+ switch (written.kind) {
53
+ case "absent":
54
+ return { kind: "ok", ctx: { ...rest, ...seedPart, ...contextPart } };
55
+ case "malformed":
56
+ return where("permissionModeWrite");
57
+ case "ok":
58
+ return { kind: "ok", ctx: { ...rest, ...seedPart, ...contextPart, permissionModeWrite: written.write } };
59
+ default: {
60
+ const unhandled = written;
61
+ throw new Error(`resume-ctx-read: unplaced permission-mode write reading ${JSON.stringify(unhandled)}`);
62
+ }
63
+ }
47
64
  }
48
65
  async function readStoredCtx(load) {
49
66
  let stored;
@@ -21,7 +21,7 @@ import type { HostDecision } from "../host-decision.js";
21
21
  import { type GateSighting } from "../approval-gate-currency.js";
22
22
  import { type CancelRunReader } from "../run-cancel-card.js";
23
23
  import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
24
- import type { RouteCtxBase, DriveResumeArgs, PreparedPlanReview, RememberSessionRequest, ResumeCheckpointOutcome, ResumeLegOutcome, WorkflowDecisionDelivery } from "./route-ctx.js";
24
+ import type { RouteCtxBase, DriveResumeArgs, PersistRuleRequest, PreparedPlanReview, RememberSessionRequest, ResumeCheckpointOutcome, ResumeLegOutcome, WorkflowDecisionDelivery } from "./route-ctx.js";
25
25
  import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
26
26
  import type { createRequestGuards } from "./request-guards.js";
27
27
  import type { FlatServiceDeps } from "./server.js";
@@ -121,7 +121,7 @@ export declare function createResumeLegs(ctx: {
121
121
  runnerFor: FlatServiceDeps["runnerFor"];
122
122
  } & RunRegistry & RouteHelpers & Pick<RouteLegs, "prepareSpec" | "finalizeTaskResult"> & Pick<ReturnType<typeof createRequestGuards>, "ownerCostQuotaDenied" | "ownerFleetLeaseDenied" | "usageWindowDenialFor">): {
123
123
  routeCtxBase: RouteCtxBase;
124
- resumeCheckpoint: (sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req: IncomingMessage | undefined, answer: QuestionAnswer | undefined, binding: DecideBinding, remember?: RememberSessionRequest, acceptEarly?: boolean, decider?: KeyCaller, restatement?: ResumeRestatement) => Promise<ResumeCheckpointOutcome>;
124
+ resumeCheckpoint: (sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req: IncomingMessage | undefined, answer: QuestionAnswer | undefined, binding: DecideBinding, remember?: RememberSessionRequest, acceptEarly?: boolean, decider?: KeyCaller, restatement?: ResumeRestatement, persistRule?: PersistRuleRequest) => Promise<ResumeCheckpointOutcome>;
125
125
  driveResumeIntoRunLog: (args: DriveResumeArgs) => Promise<ResumeLegOutcome>;
126
126
  driveResumeLeg: (args: DriveResumeArgs, onAccepted?: (receipt: {
127
127
  status: number;
@@ -8,7 +8,7 @@ import { isAdoptionQuiescing, trackMaintenanceLeg } from "../adoption/quiesce.js
8
8
  import { runFactsOf, sealRouteDeps, sessionFactsReadOf, sessionOccupantReadOf } from "./owner-access.js";
9
9
  import { createSessionWorkspaceRecorder, sessionWorkspaceStoreOf } from "../session-workspace.js";
10
10
  const DELIVERY_NOT_RECORDED = "workflow park decide: this delivery no longer holds the host session (its claim was released) — the decision was not recorded and this delivery was not started";
11
- import { HttpError, checkpointPartsOf, decodeCheckpointScope, scopeCellOf, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
11
+ import { HttpError, checkpointPartsOf, decodeCheckpointScope, ownerOfCheckpointScope, scopeCellOf, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
12
12
  import { keyAdmits } from "../owner-faces.js";
13
13
  import { personOf } from "../owner-brands.js";
14
14
  import { recoveredSubject } from "../owner-faces.js";
@@ -24,7 +24,7 @@ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotific
24
24
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals } from "../fleet/fleet-bus.js";
25
25
  import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
26
26
  import { currentApprovalGateToken, currentGateFacesOf, sightsCurrentGate } from "../approval-gate-currency.js";
27
- import { approverFacingArgs, declaredApprovalFrameFamilies, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerDurablyRecordable } from "../tool-approval.js";
27
+ import { approverFacingArgs, declaredApprovalFrameFamilies, parkRowStandingAnswerFacts, readCardRuleRecordOfAskRow, readCardRuleRecordOfParkRow, redeemCardRule, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerDurablyRecordable } from "../tool-approval.js";
28
28
  import { createHitlTransport } from "../hitl-delivery.js";
29
29
  import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
30
30
  import { APPROVAL_ENDING_CAUSES } from "../approval-deny-reasons.js";
@@ -72,7 +72,7 @@ import { buildCheckpointVersionKeys, resumeAssemblyRefusalAnswer, resumeRefusalW
72
72
  import { refusalBody, resumeRefusalFacts } from "./refusal-body.js";
73
73
  import { storedBodyReplayRefusal } from "./stored-body-replay.js";
74
74
  import { restateResumeCtx } from "./resume-restatement.js";
75
- import { stampReplayEntryMode } from "../permission-mode-admission.js";
75
+ import { stampReplayEntryMode, withStoredPermissionMode } from "../permission-mode-admission.js";
76
76
  import { httpErrorRefusal } from "./send.js";
77
77
  import { createRunAcceptance } from "../run-acceptance.js";
78
78
  import { createSubmissionSeat } from "../submission-exit.js";
@@ -219,6 +219,35 @@ function decisionRefusalCauseOf(e, gate) {
219
219
  function notResumableAnswer() {
220
220
  return { status: 409, body: refusalBody({ errorCode: "conflict.not_resumable", message: "run is not in a resumable (suspended) state (already resumed, decided, or expired)" }) };
221
221
  }
222
+ function planeOfEndedRow(row) {
223
+ return { status: row?.status ?? "failed", errorCode: row?.errorCode ?? undefined, errorMessage: row?.error ?? undefined };
224
+ }
225
+ async function landedTerminalRow(runStore, taskId, arm) {
226
+ let row;
227
+ try {
228
+ row = await runStore.getRun(taskId);
229
+ }
230
+ catch (e) {
231
+ recordFailOpen("server.resume.cancel-answer-row-unreadable", `task=${taskId} arm=${arm} err=${errorText(e)}`);
232
+ return undefined;
233
+ }
234
+ return row !== undefined && isTerminalRunStatus(row.status) ? row : undefined;
235
+ }
236
+ function settledLegAnswer(o) {
237
+ return {
238
+ status: 200,
239
+ body: {
240
+ ...(o.taskId !== undefined ? { taskId: o.taskId } : {}),
241
+ sessionId: o.sessionId,
242
+ status: o.plane.status,
243
+ bindingEnforced: true,
244
+ ...(o.plane.errorCode ? { errorCode: o.plane.errorCode } : {}),
245
+ ...(o.plane.errorMessage ? { errorMessage: redactErrorMessage(o.plane.errorMessage) ?? "" } : {}),
246
+ ...(o.reparked ? { retriable: true } : {}),
247
+ ...o.evidence,
248
+ },
249
+ };
250
+ }
222
251
  function resumeClaimFaultAnswerOf(e) {
223
252
  return faultedAnswerOf(e, CONTINUATION_CLAIM_FACE.faulted);
224
253
  }
@@ -279,7 +308,7 @@ function acceptableFormFacts(detail) {
279
308
  const rewritten = detail !== null && typeof detail === "object" ? detail.editRewritten : undefined;
280
309
  if (rewritten === null || typeof rewritten !== "object" || !Object.hasOwn(rewritten, "acceptableForm"))
281
310
  return {};
282
- const shown = approverFacingArgs(rewritten.acceptableForm);
311
+ const shown = approverFacingArgs(rewritten.acceptableForm, undefined);
283
312
  return shown === undefined ? {} : { acceptableForm: shown };
284
313
  }
285
314
  export function checkpointRefusalAnswer(e, at) {
@@ -315,8 +344,7 @@ export function createResumeLegs(ctx) {
315
344
  const restated = restateResumeCtx(stored, restatement);
316
345
  if (restated.unattendedLeg === true)
317
346
  return restated;
318
- const body = stampReplayEntryMode(restated.body, deps.config);
319
- return body === restated.body ? restated : { ...restated, body };
347
+ return stampReplayEntryMode(restated, deps.config);
320
348
  };
321
349
  const resumeFleetScope = (req, auth) => req ? (gatedSubject(req, deps.config) ?? "default") : (auth.subject ?? "default");
322
350
  function createRememberIntent(sessionId, remember) {
@@ -343,9 +371,23 @@ export function createResumeLegs(ctx) {
343
371
  },
344
372
  };
345
373
  }
346
- async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, remember, acceptEarly, decider, restatement) {
374
+ function createRuleIntent(persistRule) {
375
+ let reading;
376
+ return {
377
+ record: (r) => {
378
+ reading = r;
379
+ },
380
+ settle: async (committed) => {
381
+ if (!committed || reading === undefined)
382
+ return { rulePersisted: false, ruleRefusal: "rule_lane_unavailable" };
383
+ return redeemCardRule(deps.ruleConsent, reading, persistRule.slot, { inputWasEdited: persistRule.inputWasEdited, eligible: true });
384
+ },
385
+ };
386
+ }
387
+ async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, remember, acceptEarly, decider, restatement, persistRule) {
347
388
  const cs = deps.checkpointStore;
348
389
  const intent = remember !== undefined ? createRememberIntent(sessionId, remember) : undefined;
390
+ const ruleIntent = persistRule !== undefined ? createRuleIntent(persistRule) : undefined;
349
391
  const token = await currentApprovalGateToken(cs, sessionId);
350
392
  if (binding.checkpointToken && !sightsCurrentGate({ face: "checkpointToken", value: binding.checkpointToken }, token ? { checkpointToken: token } : null)) {
351
393
  let seenUnreadable = false;
@@ -476,12 +518,36 @@ export function createResumeLegs(ctx) {
476
518
  return approvalStaleAnswer(s57CurrentPending ? { currentPending: s57CurrentPending } : {});
477
519
  }
478
520
  const paForGrant = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction : undefined;
521
+ const parkFacts = paForGrant !== undefined && paForGrant.toolName ? parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config, deploymentListAsks(deps.config, { toolName: paForGrant.toolName })) : undefined;
479
522
  const rememberableTool = paForGrant !== undefined &&
480
- paForGrant.toolName &&
523
+ parkFacts !== undefined &&
481
524
  paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
482
- standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config, deploymentListAsks(deps.config, { toolName: paForGrant.toolName })))
525
+ standingAnswerDurablyRecordable(parkFacts)
483
526
  ? paForGrant.toolName
484
527
  : null;
528
+ if (ruleIntent !== undefined) {
529
+ const owner = ownerOfCheckpointScope(cp.scope);
530
+ if (paForGrant === undefined || parkFacts === undefined || !token || !paForGrant.toolCallId || !paForGrant.boundInputHash)
531
+ ruleIntent.record({ kind: "absent", refusal: "rule_material_absent" });
532
+ else {
533
+ const asked = deps.toolApproval !== undefined ? await deps.toolApproval.readParkedAskOfGate(cp.sessionId, { boundCallId: paForGrant.toolCallId, boundInputHash: paForGrant.boundInputHash }) : { kind: "none" };
534
+ switch (asked.kind) {
535
+ case "row":
536
+ ruleIntent.record(readCardRuleRecordOfAskRow(asked.row, owner));
537
+ break;
538
+ case "unreadable":
539
+ ruleIntent.record({ kind: "absent", refusal: "rule_store_error" });
540
+ break;
541
+ case "none":
542
+ ruleIntent.record(readCardRuleRecordOfParkRow(paForGrant, { owner, governanceForced: parkFacts.governanceForced, scopeRoot: await cs.cardScopeRootOf(token) }));
543
+ break;
544
+ default: {
545
+ const unhandled = asked;
546
+ throw new Error(`resumeCheckpoint: unhandled parked-ask reading ${JSON.stringify(unhandled)}`);
547
+ }
548
+ }
549
+ }
550
+ }
485
551
  const boundGrant = intent
486
552
  ? (overrideSessionId) => intent.record({ ...(overrideSessionId !== undefined ? { overrideSessionId } : {}), rememberableToolName: rememberableTool })
487
553
  : undefined;
@@ -506,7 +572,11 @@ export function createResumeLegs(ctx) {
506
572
  const landAfterCommit = async (out) => {
507
573
  const committed = decisionCommittedOf(out);
508
574
  noteLateSettleIfAccepted(committed);
509
- return intent === undefined ? out : { ...out, rememberApplied: await intent.settle(committed) };
575
+ return {
576
+ ...out,
577
+ ...(intent !== undefined ? { rememberApplied: await intent.settle(committed) } : {}),
578
+ ...(ruleIntent !== undefined ? { ruleReceipt: await ruleIntent.settle(committed) } : {}),
579
+ };
510
580
  };
511
581
  const parkedForkQuotaDenial = req !== undefined ? ownerCostQuotaDenied(checkpointPartsOf(cp.scope)?.person) : undefined;
512
582
  if (parkedForkQuotaDenial)
@@ -771,7 +841,6 @@ export function createResumeLegs(ctx) {
771
841
  const legKey = createHash("sha256").update(String(token)).digest("hex");
772
842
  const streamApprovalOn = resolveStreamApprovalGate({
773
843
  toolApprovalEnabled: Boolean(deps.toolApproval),
774
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
775
844
  backend: deps.backend,
776
845
  checkpointStore: deps.checkpointStore,
777
846
  }).active;
@@ -797,7 +866,7 @@ export function createResumeLegs(ctx) {
797
866
  let legClaim;
798
867
  let engineHandedOff = false;
799
868
  let restored;
800
- const cancelledInClaimWindow = () => restored?.kind === "ended" && restored.row?.errorCode === CANCELLED_CODE;
869
+ const claimWindowCancelRow = () => (restored?.kind === "ended" && restored.row?.errorCode === CANCELLED_CODE ? restored.row : undefined);
801
870
  if (args.mintFreshRun && taskId && deps.runStore) {
802
871
  return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, turnActivity: readTurnActivityMs }, sessionId, taskId) };
803
872
  }
@@ -948,7 +1017,8 @@ export function createResumeLegs(ctx) {
948
1017
  if (placementReject) {
949
1018
  if (inflightRuns.get(claimedTask) === cancelCtrl)
950
1019
  inflightRuns.delete(claimedTask);
951
- return cancelledInClaimWindow() ? { status: 200, body: { taskId: claimedTask, sessionId, status: "failed", errorCode: "cancelled" } } : placementReject;
1020
+ const cancelledRow = claimWindowCancelRow();
1021
+ return cancelledRow !== undefined ? settledLegAnswer({ taskId: claimedTask, sessionId, plane: planeOfEndedRow(cancelledRow), reparked: false, evidence: NO_SETTLEMENT_EVIDENCE }) : placementReject;
952
1022
  }
953
1023
  }
954
1024
  markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, subject);
@@ -1433,16 +1503,19 @@ export function createResumeLegs(ctx) {
1433
1503
  const disposition = resumeRefusalDisposition(e);
1434
1504
  if (disposition === "cancelled") {
1435
1505
  const abortedText = (await settleCardForCancel()) ? CARD_SETTLED_BY_CANCEL_MESSAGE : e.message;
1506
+ const handleCause = cancelCtrl.cause();
1507
+ if (handleCause === undefined)
1508
+ recordFailOpen("server.runs.abort-cause-missing", `task=${taskId ?? ""} arm=resume-aborted`);
1509
+ const c = mintCancelledResult({ taskId: taskId ?? "", sessionId, startedAtMs: resumeLegStartedAt, message: abortedText, cause: handleCause ?? "cancel_verb" });
1510
+ const cancelled = outwardPlane(c.terminal);
1511
+ let landed;
1436
1512
  if (claimedRow && taskId && deps.runStore) {
1437
- const handleCause = cancelCtrl.cause();
1438
- if (handleCause === undefined)
1439
- recordFailOpen("server.runs.abort-cause-missing", `task=${taskId} arm=resume-aborted`);
1440
- const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: abortedText, cause: handleCause ?? "cancel_verb" });
1441
1513
  await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-resume-aborted ${errorText(err)}`));
1442
- await deps.runStore.setTerminal(taskId, "failed", c, abortedText).catch(() => undefined);
1514
+ await deps.runStore.setTerminal(taskId, "failed", c, cancelled.errorMessage ?? null).catch(() => undefined);
1515
+ landed = await landedTerminalRow(deps.runStore, taskId, "resume-aborted");
1443
1516
  }
1444
1517
  settleFleet("failed");
1445
- return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
1518
+ return settledLegAnswer({ taskId, sessionId, plane: landed !== undefined ? planeOfEndedRow(landed) : cancelled, reparked: false, evidence: NO_SETTLEMENT_EVIDENCE });
1446
1519
  }
1447
1520
  const retriable = disposition === "retriable";
1448
1521
  let reparkEnded = false;
@@ -1469,22 +1542,26 @@ export function createResumeLegs(ctx) {
1469
1542
  }
1470
1543
  const thrownCause = cancelCtrl.cause();
1471
1544
  if (thrownCause !== undefined) {
1545
+ const c = mintCancelledResult({ taskId: taskId ?? "", sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", cause: thrownCause });
1546
+ const cancelled = outwardPlane(c.terminal);
1547
+ let landed;
1472
1548
  if (taskId && deps.runStore) {
1473
1549
  await voidHeldParkCard(heldPark, sessionId, deps.checkpointStore, "resume");
1474
- const c = mintCancelledResult({ taskId, sessionId, startedAtMs: resumeLegStartedAt, message: "cancelled by user", cause: thrownCause });
1475
1550
  await cancelLedgerAppend?.("failed", buildCancelledLedgerRow(c)).catch((err) => recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=resume-append-thrown-cancel ${errorText(err)}`));
1476
- await deps.runStore.setTerminal(taskId, "failed", c, "cancelled by user").catch(() => undefined);
1551
+ await deps.runStore.setTerminal(taskId, "failed", c, cancelled.errorMessage ?? null).catch(() => undefined);
1552
+ landed = await landedTerminalRow(deps.runStore, taskId, "thrown-cancel");
1477
1553
  }
1478
1554
  settleFleet("failed");
1479
- return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
1555
+ return settledLegAnswer({ taskId, sessionId, plane: landed !== undefined ? planeOfEndedRow(landed) : cancelled, reparked: false, evidence: NO_SETTLEMENT_EVIDENCE });
1480
1556
  }
1481
1557
  if (!engineHandedOff && legClaim !== undefined) {
1482
1558
  const claim = legClaim;
1483
1559
  await withdrawClaim(deps, claim, `the resume could not be handed to the engine: ${outwardThrownMessage(e, errorText(e))}`, "faulted");
1484
1560
  const parkedAgain = claim.kind === "resumed" && restored?.kind !== "ended";
1485
1561
  settleFleet(parkedAgain ? reparkStatus : "failed");
1486
- if (cancelledInClaimWindow())
1487
- return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
1562
+ const cancelledRow = claimWindowCancelRow();
1563
+ if (cancelledRow !== undefined)
1564
+ return settledLegAnswer({ taskId, sessionId, plane: planeOfEndedRow(cancelledRow), reparked: false, evidence: NO_SETTLEMENT_EVIDENCE });
1488
1565
  return resumeClaimFaultAnswerOf(e);
1489
1566
  }
1490
1567
  const heldParkWord = heldPark === undefined ? undefined : heldParkOf(heldPark);
@@ -1539,20 +1616,13 @@ export function createResumeLegs(ctx) {
1539
1616
  await deps.runStore.setTerminal(taskId, resumePlane.status, sealed, resumePlane.errorMessage ?? null);
1540
1617
  }
1541
1618
  settleFleet(parkEnded !== undefined ? "failed" : checkpointReopened ? reparkStatus : resumePlane.status, fleetRunResiduals(sealed, sealed ? failureCodeOf(sealed.terminal) : undefined));
1542
- const answered = parkEnded !== undefined
1543
- ? { status: parkEnded.row?.status ?? "failed", errorCode: parkEnded.row?.errorCode ?? undefined, errorMessage: parkEnded.row?.error ?? undefined }
1544
- : { status: resumePlane.status, errorCode: resumePlane.errorCode, errorMessage: resumePlane.errorMessage };
1545
- return {
1546
- status: 200,
1547
- body: {
1548
- taskId, sessionId, status: answered.status,
1549
- bindingEnforced: true,
1550
- ...(answered.errorCode ? { errorCode: answered.errorCode } : {}),
1551
- ...(answered.errorMessage ? { errorMessage: redactErrorMessage(answered.errorMessage) ?? "" } : {}),
1552
- ...(checkpointReopened && parkEnded === undefined ? { retriable: true } : {}),
1553
- ...buildSettlementEvidence({ at: "engine_terminal", commitment: args.commitment }),
1554
- },
1555
- };
1619
+ return settledLegAnswer({
1620
+ taskId,
1621
+ sessionId,
1622
+ plane: parkEnded !== undefined ? planeOfEndedRow(parkEnded.row) : resumePlane,
1623
+ reparked: checkpointReopened && parkEnded === undefined,
1624
+ evidence: buildSettlementEvidence({ at: "engine_terminal", commitment: args.commitment }),
1625
+ });
1556
1626
  }
1557
1627
  async function mayDiscloseRowError(cs, token, decider) {
1558
1628
  if (decider === undefined || decider.operator)
@@ -1683,7 +1753,7 @@ export function createResumeLegs(ctx) {
1683
1753
  const { spec, engineNotices } = resolved;
1684
1754
  const hostSpec = { ...spec, objective };
1685
1755
  const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
1686
- prepared: { spec: hostSpec, runner: resolved.runner, legFacts: resolved.legFacts, auth, body: ctx.body, ...(objectiveWithheldIn(ctx) ? { bodyFromScreenedCtx: true } : {}), seat: createSubmissionSeat({ deployment: deps.config }), acceptance: createRunAcceptance({}, { objective, screened: false }), peerLease: resolved.peerLease, execution: resolved.execution, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
1756
+ prepared: { spec: hostSpec, runner: resolved.runner, legFacts: resolved.legFacts, auth, body: ctx.body, ...(objectiveWithheldIn(ctx) ? { bodyFromScreenedCtx: true } : {}), bodyPermissionModeWrite: { stored: ctx.permissionModeWrite }, seat: createSubmissionSeat({ deployment: deps.config }), acceptance: createRunAcceptance({}, { objective, screened: false }), peerLease: resolved.peerLease, execution: resolved.execution, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
1687
1757
  fleetScope: resumeFleetScope(req, auth),
1688
1758
  elicitOwner: subject ?? null,
1689
1759
  source: "workflow_parked_decide",
@@ -1709,7 +1779,7 @@ export function createResumeLegs(ctx) {
1709
1779
  const token = (await cs.findPendingTokenBySession(sessionId, undefined, wakeBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
1710
1780
  if (!token)
1711
1781
  return { status: 404, body: refusalBody({ errorCode: "not_found.parked_checkpoint", message: "no parked checkpoint for this session (nothing to wake)" }) };
1712
- const who = { subject: caller, operator: explicitOperatorOk(caller === undefined ? undefined : personOf(caller), deps.config) };
1782
+ const who = { subject: caller, operator: explicitOperatorOk(caller === undefined ? undefined : personOf(caller), deps.config, "operate") };
1713
1783
  if (!who.operator) {
1714
1784
  const scope = await cs.peekPendingScope(sessionId, wakeBinding).then((s) => (s === undefined ? cs.peekPendingScope(sessionId) : s)).catch(() => undefined);
1715
1785
  if (!keyAdmits("scope.decide", who, scopeCellOf(scope ?? CHECKPOINT_PUBLIC_SCOPE))) {
@@ -2061,7 +2131,7 @@ export function createResumeLegs(ctx) {
2061
2131
  throw new Error("drivePlanReview: permissionModeAfter was passed for a task whose stored permission mode is not \"plan\" — the caller must refuse this before any state changes (see the plan_review route's pre-nonce gate)");
2062
2132
  }
2063
2133
  const approvedMode = planReviewDecisionExitsPlan(decision) && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
2064
- const rebuildCtx = approvedMode === undefined ? ctx : { ...ctx, body: { ...ctx.body, permissionMode: approvedMode } };
2134
+ const rebuildCtx = approvedMode === undefined ? ctx : withStoredPermissionMode(ctx, approvedMode, "plan_review");
2065
2135
  const resolved = await resolveResumeSpecOrBlocked(rebuildCtx, req, auth);
2066
2136
  if (!resolved.ok)
2067
2137
  return resolved.answer;
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import type { HostDecision } from "../host-decision.js";
19
+ import type { Refusal } from "./refusal-body.js";
19
20
  import type { EntryProblem } from "../entry-problems.js";
20
21
  import type { SessionNotifyDoor } from "../session-notify-door.js";
21
22
  import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, Checkpoint, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice, PendingSteerInput } from "@sema-agent/core";
@@ -26,7 +27,9 @@ import type { ImagesLocal } from "./routes/images.js";
26
27
  import type { CapabilitiesLocal } from "./routes/capabilities.js";
27
28
  import type { SessionsLocal } from "./routes/sessions.js";
28
29
  import type { SessionSyncLocal } from "./routes/session-sync.js";
30
+ import type { OperatorPower } from "../operator-roster.js";
29
31
  import type { PlanApprovePermissionModeAfter } from "../task-settings.js";
32
+ import type { PermissionModeWrite } from "../permission-mode-admission.js";
30
33
  import type { GateSighting } from "../approval-gate-currency.js";
31
34
  import type { VerifyRoundsSpec } from "./verify-rounds.js";
32
35
  import type { RunAbort } from "../run-cancel-context.js";
@@ -36,6 +39,7 @@ import type { RootedTaskSpec } from "./execution-context.js";
36
39
  import type { GateCommitment, GateSettlementEvidence } from "../gate-settlement.js";
37
40
  import type { ResumeRestatement } from "./resume-restatement.js";
38
41
  import type { RememberLaneCannotRecord } from "../parked-decide.js";
42
+ import type { ParsedPersistRule, RuleSlotReceipt } from "../tool-approval.js";
39
43
  import type { RowWayOut } from "./resume-legs.js";
40
44
  import type { KeyCaller } from "../owner-faces.js";
41
45
  import type { Subject } from "../owner-brands.js";
@@ -156,6 +160,14 @@ export interface PreparedTaskSubmission {
156
160
  * 它能不能展示在它第一次落盘时就定了,这一跑的受理座答不了。缺席 = 体是这次请求自己的,标不标由受理座定。
157
161
  */
158
162
  bodyFromScreenedCtx?: true;
163
+ /**
164
+ * `body` 是从一份存量续跑输入里取回的(与 {@link bodyFromScreenedCtx} 同一种提交:`/decide` 的宿主新 turn)⇒ 这一跑新写的续跑输入**照抄**那一份的写据
165
+ * (`CheckpointCtx.permissionModeWrite`):这一跑没有写体顶层 `permissionMode` 那一格,只是抄了体。`stored` 缺席 = 那一份没有写据(7.152.0 之前写下的)⇒ 照缺席。
166
+ * 本位缺席 = 体是这次请求自己的 ⇒ 写据是提交写者的那一张(`submit-ctx.ts` `persistSubmissionCtx`)。
167
+ */
168
+ bodyPermissionModeWrite?: {
169
+ readonly stored: PermissionModeWrite | undefined;
170
+ };
159
171
  /**
160
172
  * 这次执行的**执行上下文**(唯一解析器 `http/execution-context.ts` `resolveExecutionContext` 的产物:根 · 住客 · 放置态 · 车道 · device 车道的设备意图)。
161
173
  * 提交门在装配之前铸(fresh);`/decide` 的宿主新 turn 交续跑折叠器铸的那一只(continuation)。每个 `runStore.createRun(` 成功点**必须**把它交给
@@ -237,6 +249,15 @@ export interface CheckpointCtx {
237
249
  * 形只有一种读法(`frozenMemorySeedOf`,读口 `resume-ctx-read.ts`)。
238
250
  */
239
251
  memorySeed?: import("../project-registry.js").MemorySeed | null;
252
+ /**
253
+ * **写据**:谁写了 `body` 顶层 `permissionMode` 那一格、写下的是什么(形与规则的属主 = `permission-mode-admission.ts` 顶注「写据」)。**服务端写、不来自请求体**
254
+ * (与 {@link unattendedLeg} 同为 `body` 的兄弟键,客户端够不着)。写点:`buildCheckpointCtx`(必填键:提交写口写 `submission`;`/decide` 宿主新 turn 照抄宿主那一行的,
255
+ * 见 {@link PreparedTaskSubmission.bodyPermissionModeWrite};leader worker 写 `submission`)与改写口 `withStoredPermissionMode`(切档 `switch` · 续跑口的钳 `replay` ·
256
+ * `plan_review` 出口 `plan_review`)。续跑腿的写回传原 ctx / 其展开 ⇒ 随行。读者只有一只:续跑口 `stampReplayEntryMode`(地板在场时问「这一格是不是这块地板的出口写下的、
257
+ * 且还是它写下的那一格」)。形只有一种读法(`permissionModeWriteOf`,读口 `resume-ctx-read.ts`:读不出 ⇒ 坏体)。
258
+ * 缺席 = 7.152.0 之前写下的行(不回填:没有证据可补),或从这样的行照抄的体 ⇒ 不是出口写的,地板在场时照钳。
259
+ */
260
+ permissionModeWrite?: PermissionModeWrite;
240
261
  }
241
262
  /**
242
263
  * `/decide` 第三条车道交给宿主投递腿(wake:`DriveResumeArgs` / fresh:`DurableRunLegArgs`)的**一件**:这一跑在内存里带着的决定,
@@ -489,7 +510,9 @@ export interface RouteLegs {
489
510
  * 同一属主判(缺席 ⇒ 恒不造键;判据属主=server.ts 铸键点注)。 */
490
511
  decider?: KeyCaller,
491
512
  /** 续跑请求的重述键(`mcpServers` / `a2aPeers`),门读出并判过的那一份;缺席 = 冻结读法。语义属主 = `resume-restatement.ts` 顶注。 */
492
- restatement?: ResumeRestatement): Promise<ResumeCheckpointOutcome>;
513
+ restatement?: ResumeRestatement,
514
+ /** 这次决定带着「不再询问」请求({@link PersistRuleRequest})⇒ 本函数是它的**唯一铸点**,结局的 `ruleReceipt` 是它的回执({@link ResumeCheckpointOutcome})。 */
515
+ persistRule?: PersistRuleRequest): Promise<ResumeCheckpointOutcome>;
493
516
  /** 全 resume 家族的共同下半场(lease admission + 写 run log + fleet 发布)。 */
494
517
  driveResumeIntoRunLog(args: DriveResumeArgs): Promise<ResumeLegOutcome>;
495
518
  /** assistant 抢占腿的复位(gate-kind 守卫 + 行级属主复核 + 驱动)。 */
@@ -719,6 +742,12 @@ interface RouteDeclIdentity {
719
742
  * 不带它 —— 那条流就是 sync 腿本身(关流 = 断连语义),照旧算在飞。漏标一条订阅的方向是**多算在飞**(排空多等),不是少等。
720
743
  */
721
744
  readonly subscription?: true;
745
+ /**
746
+ * 「本路由对持这只运维权力的**非人档**开放;本口的运维门按它判」(档表 `operator-roster.ts`)。缺席 = 没声明 —— 非人档(服务主体)的身份在本路由上不算数,
747
+ * 分派层整次请求 403 `auth.principal_confined`(`principal-gate.ts` 的 `requestConfinedTier`,只扫带本列的行)。只住在可分派行上(纯门行是前缀、比任何路由都宽,
748
+ * 型上就开不出一段前缀的权力)。今天只有排空口声明 `power: "drain"`。
749
+ */
750
+ readonly power?: OperatorPower;
722
751
  }
723
752
  /**
724
753
  * **可分派行**:一条真路由。匹配式二选一 —— `path`(精确;标签**就是** path 自身,所以没有 label 列,
@@ -760,23 +789,60 @@ export interface ResumeLegOutcome {
760
789
  body: object;
761
790
  }
762
791
  /**
763
- * 「本会话不再询问」的请求 —— **两扇门同一只**:`/decide` 体上的 `remember:"session"`、应答口(`POST /v1/tool-approvals/:id/respond`)命中停驻行的
764
- * `allow_session`。铸点只有一只:{@link RouteLegs.resumeCheckpoint} —— 可记与否从**已属主校验的载入停驻行**判(`standingAnswerDurablyRecordable`,
792
+ * 「本会话不再询问」的请求 —— **两扇门同一只**:`/decide` 体上的 `remember:"session"`、决断口(`POST /v1/tasks/:taskId/asks/:askId/decision`)命中停驻行的
793
+ * `remember:"session"`。铸点只有一只:{@link RouteLegs.resumeCheckpoint} —— 可记与否从**已属主校验的载入停驻行**判(`standingAnswerDurablyRecordable`,
765
794
  * 与运维队列行的 `rememberRefused` 同一只判据)、锚由车道定(任务级 = 这个会话;bg 停驻子代 = 行的 root/host 会话)、落豁免店**只在**这次决定
766
795
  * 被证明已结清之后(`decisionCommittedOf`)、`rememberApplied` = 落没落(店抛 ⇒ `false` + warn,这次的放行不回滚)。门自己不写店、不判可记。
767
796
  * 两口唯一的差 = `laneCannotRecord`(记不住的车道上怎么答;闭集与理由的属主 = `parked-decide.ts` 的 {@link RememberLaneCannotRecord})。
768
797
  */
769
798
  export interface RememberSessionRequest {
770
- /** 豁免行的签发人(`grantedBy`)= 门已验的身份(`/decide` 的决断方 / 应答口的调用方);匿名开放部署 ⇒ `null`。 */
799
+ /** 豁免行的签发人(`grantedBy`)= 门已验的身份(`/decide` 的决断方 / 决断口的调用方);匿名开放部署 ⇒ `null`。 */
771
800
  readonly grantedBy: string | null;
772
801
  readonly laneCannotRecord: RememberLaneCannotRecord;
773
802
  }
774
- /** {@link RouteLegs.resumeCheckpoint} 的结局 = 腿的应答 + 带了「本会话放行」请求时铸点的答(`rememberApplied`:落没落)。键只在证得出提交的两条车道
775
- * (bg 停驻子代赎回 / 任务级续跑)的结清点上铸;其余出口(拒 / workflow 投递 / 抛)键缺席 ⇒ 门读作「没记住」(`=== true`)。
776
- * 这一位是进程内的结局位,不进 `body` —— 由门决定挂不挂到回执上(两扇门都只挂在这次真做了授予判定的回执上;幂等回放不挂)。 */
803
+ /**
804
+ * 决定体上「本会话放行」那一键(`remember`)的**唯一读法** —— 两扇人决口同调:有问行的卡的决断口(`POST /v1/tasks/:taskId/asks/:askId/decision`)与
805
+ * 运维队列的耐久门(`POST /v1/approvals/:sessionId/decide`)。闭集一词 `"session"`;只配 `approve`(「记住的拒」是另一种语义,不从这一键带进来)。
806
+ * 拒句与码两口逐字同一(由本读法铸);各口自己的门(直连门的签名覆盖、豁免店在场)在本读法之后各判各的。
807
+ * 拒 = 一只 {@link Refusal}(码 + 句 + 机读位在本读法一处铸,冲突形的机读位随拒同铸),两扇口原样发 `400`、不再按码二次分支。
808
+ */
809
+ export type RememberSlotReading = {
810
+ readonly kind: "absent";
811
+ } | {
812
+ readonly kind: "session";
813
+ } | {
814
+ readonly kind: "refused";
815
+ readonly refusal: Refusal;
816
+ };
817
+ /** `remember` 闭集外的拒句(`400 request.field_invalid`;两扇人决口同一句,由 {@link readRememberSlot} 铸)。 */
818
+ export declare const REMEMBER_MUST_BE_SESSION = "remember must be \"session\"";
819
+ /** `remember` 配 deny 的拒句(`400 request.field_conflict` + 机读位 `remember_decision`;同上)。 */
820
+ export declare const REMEMBER_REQUIRES_APPROVE = "remember requires decision 'approve'";
821
+ export declare function readRememberSlot(raw: unknown, decision: "approve" | "deny"): RememberSlotReading;
822
+ /**
823
+ * 「不再询问」的请求 —— **两扇门同一只**(「本会话放行」{@link RememberSessionRequest} 的孪生):`/decide` 体上的 `persistRule`、赎回席交来的那一问(活卡回决口
824
+ * 命中停驻行时卡上的 `persistRule`)。体形判官只有一只(`tool-approval.ts` 的 `readPersistRuleSlot`)。铸点只有一只:{@link RouteLegs.resumeCheckpoint} 在已属主
825
+ * 校验的载入停驻行上读这张卡的记录(有问行读问行,没有读停驻行),在结清点(`landAfterCommit`,与「本会话放行」同一读 `decisionCommittedOf`)交唯一兑付函数
826
+ * `redeemCardRule`。证不出提交 ⇒ 不兑,回执 `rulePersisted:false` + `rule_lane_unavailable`;**不 400**:规则位的成文律是「裁决本身绝不因规则没存上而改判」
827
+ * (与 `remember` 在 `/decide` 上「硬意图键,记不住就 400」刻意不同 —— 两个键各守各的成文律)。
828
+ */
829
+ export interface PersistRuleRequest {
830
+ readonly slot: ParsedPersistRule;
831
+ /** 这次决定带了 `updatedInput`(编辑放行)⇒ 兑付答 `rule_input_edited`:生效的是编辑后的调用,候选是从原始命令铸的。 */
832
+ readonly inputWasEdited: boolean;
833
+ }
834
+ /** {@link RouteLegs.resumeCheckpoint} 的结局 = 腿的应答 + 带了「本会话放行」请求时铸点的答(`rememberApplied`:落没落)+ 带了「不再询问」请求时铸点的回执(`ruleReceipt`)。
835
+ * 两位只在证得出提交的两条车道(bg 停驻子代赎回 / 任务级续跑)的结清点上铸;其余出口(拒 / workflow 投递 / 抛)缺席 ⇒ 门读作「没记住」(`=== true`)/ 规则位
836
+ * 由 {@link ruleReceiptOf} 补成「没存上」。两位都是进程内的结局位,不进 `body` —— 由门挂到这次真做了授予判定的回执上(幂等回放不挂)。 */
777
837
  export type ResumeCheckpointOutcome = ResumeLegOutcome & {
778
838
  readonly rememberApplied?: boolean;
839
+ readonly ruleReceipt?: RuleSlotReceipt;
779
840
  };
841
+ /**
842
+ * 一次带了 `persistRule` 的续跑在回执上的规则位(两扇门同调这一只:`/decide` 与赎回席)—— 结清点铸了就是它;没铸(不经结清点的出口:workflow 投递 / 证不出提交)
843
+ * ⇒ `rulePersisted:false` + `rule_lane_unavailable`(「请求带了、结局没答」只在这里补一处)。没带 ⇒ `undefined`(一个键都不加)。只在 2xx 上挂(调用方判)。
844
+ */
845
+ export declare function ruleReceiptOf(out: ResumeCheckpointOutcome, persistRule: PersistRuleRequest | undefined): RuleSlotReceipt | undefined;
780
846
  /**
781
847
  * **「决定/续跑已受理」那一枚 200 收据的单一铸点**([ref])。
782
848
  *
@@ -1,4 +1,5 @@
1
1
  import { sendJson } from "./send.js";
2
+ import { fieldConflictFacts } from "../field-conflict.js";
2
3
  export function noteResumeClientGone(res, out, logger) {
3
4
  if (out.status !== 200 || out.body.status !== "resuming")
4
5
  return;
@@ -21,6 +22,22 @@ export function assertNever(x) {
21
22
  throw new Error(`unreachable route id: ${String(x)}`);
22
23
  }
23
24
  export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE"];
25
+ export const REMEMBER_MUST_BE_SESSION = `remember must be "session"`;
26
+ export const REMEMBER_REQUIRES_APPROVE = "remember requires decision 'approve'";
27
+ export function readRememberSlot(raw, decision) {
28
+ if (raw === undefined)
29
+ return { kind: "absent" };
30
+ if (raw !== "session")
31
+ return { kind: "refused", refusal: { errorCode: "request.field_invalid", message: REMEMBER_MUST_BE_SESSION } };
32
+ if (decision !== "approve")
33
+ return { kind: "refused", refusal: { errorCode: "request.field_conflict", message: REMEMBER_REQUIRES_APPROVE, facts: fieldConflictFacts("remember_decision", ["remember", "decision"]) } };
34
+ return { kind: "session" };
35
+ }
36
+ export function ruleReceiptOf(out, persistRule) {
37
+ if (persistRule === undefined)
38
+ return undefined;
39
+ return out.ruleReceipt ?? { rulePersisted: false, ruleRefusal: "rule_lane_unavailable" };
40
+ }
24
41
  export function acceptedResumingReceipt(taskId, sessionId, evidence) {
25
42
  return { status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true, ...evidence } };
26
43
  }
@@ -55,6 +55,7 @@ import type { FlatServiceDeps } from "./server.js";
55
55
  import type { IncomingMessage, ServerResponse } from "node:http";
56
56
  import type { RouteDecl, RouteDispatchDecl, RouteMatch, RouteCtx, RouteMethod } from "./route-ctx.js";
57
57
  import type { BillableKind } from "./readiness.js";
58
+ import type { OperatorPower } from "../operator-roster.js";
58
59
  /** 一只域 handler 相对**全局服务凭据门**的位置。门前 = 未鉴权可达(每一只都要单独论证),门后 = 先过凭据。 */
59
60
  /**
60
61
  * 一行路由在全局服务凭据门的哪一侧、吃哪道门(闭集,三员;[ref] 起由两员拆成三员):
@@ -144,6 +145,9 @@ export declare function isCredentialGatedRewrite(method: string, url: string): b
144
145
  /** [ref]:这次请求是不是一条**只读订阅**(`RouteDeclFacts.subscription` 那一列;方法也要对上 —— 订阅行恒只认 GET)。
145
146
  * 分派器在 admission 那一拍(第一个 await 之前)同步判它:订阅不进在飞计数,只进附着集与订阅集。 */
146
147
  export declare function isSubscriptionRequest(method: string, url: string): boolean;
148
+ /** 这次请求(方法 + 路径)命中的那一行声明的运维权力(声明行的 `power` 列;`undefined` = 没命中任何带 `power` 的行)—— 分派层的圈禁判只读这一只
149
+ * (`principal-gate.ts` 的 `requestConfinedTier`)。方法也要对上:路径命中而方法不在列的请求对非人档同答圈禁(不再区分 404 / 405)。 */
150
+ export declare function declaredPowerOf(method: string, url: string): OperatorPower | undefined;
147
151
  /** 见 `RouteDeclFacts.methodDispatched` 的头注(单 URL 多方法端点)。 */
148
152
  export declare function isMethodDispatchedSubmitPath(url: string): boolean;
149
153
  /** 一次分派查找的结果:命中的行与它的参数,外加**这条路径上所有行**的方法并集(405 的 `Allow`)。 */