@sema-agent/server 7.145.0 → 7.146.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 (275) hide show
  1. package/MIGRATION.md +492 -0
  2. package/USAGE.md +218 -47
  3. package/deploy/facts/CHANGES.json +25 -0
  4. package/deploy/facts/README.md +57 -0
  5. package/deploy/facts/lanes.json +54 -0
  6. package/deploy/facts/lanes.schema.json +57 -0
  7. package/deploy/facts/sandbox-rbac.json +113 -0
  8. package/deploy/facts/sandbox-rbac.schema.json +74 -0
  9. package/deploy/facts/sealed-key.json +9 -0
  10. package/deploy/facts/sealed-key.schema.json +52 -0
  11. package/deploy/sema-up/chart/templates/_helpers.tpl +19 -0
  12. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +47 -7
  13. package/deploy/sema-up/chart/templates/server-v4pro.yaml +4 -0
  14. package/deploy/sema-up/chart/templates/server.yaml +4 -0
  15. package/deploy/sema-up/chart/values.yaml +7 -0
  16. package/deploy/sema-up/stack/sema-stack-cluster.yaml +30 -14
  17. package/dist/adoption/import-limits.d.ts +6 -0
  18. package/dist/adoption/import-limits.js +7 -0
  19. package/dist/adoption/import.d.ts +62 -0
  20. package/dist/adoption/import.js +107 -0
  21. package/dist/adoption/wire.d.ts +52 -0
  22. package/dist/adoption/wire.js +9 -0
  23. package/dist/approval-ask-machine.d.ts +2 -2
  24. package/dist/approval-card.d.ts +3 -12
  25. package/dist/approval-card.js +0 -4
  26. package/dist/approval-gate-currency.d.ts +1 -1
  27. package/dist/approval-gate-currency.js +1 -1
  28. package/dist/approval-reconciler.d.ts +1 -1
  29. package/dist/boot/deployment-mods.d.ts +11 -1
  30. package/dist/boot/deployment-mods.js +15 -2
  31. package/dist/boot/device-lane.js +2 -0
  32. package/dist/boot/engine-lease.d.ts +193 -21
  33. package/dist/boot/engine-lease.js +146 -20
  34. package/dist/boot/execution-env.d.ts +4 -0
  35. package/dist/boot/execution-env.js +12 -0
  36. package/dist/boot/exit-boundary.d.ts +50 -0
  37. package/dist/boot/exit-boundary.js +32 -0
  38. package/dist/boot/exit-last.d.ts +20 -4
  39. package/dist/boot/exit-last.js +21 -4
  40. package/dist/boot/governance-seams.js +1 -0
  41. package/dist/boot/memory-consolidation.d.ts +29 -17
  42. package/dist/boot/memory-consolidation.js +26 -15
  43. package/dist/boot/reapers.d.ts +12 -4
  44. package/dist/boot/reapers.js +39 -15
  45. package/dist/boot/resolve-spec.d.ts +2 -0
  46. package/dist/boot/resolve-spec.js +38 -15
  47. package/dist/boot/runner-deps.js +1 -1
  48. package/dist/boot/session-faces.js +3 -1
  49. package/dist/boot/shutdown.js +27 -9
  50. package/dist/boot/stage-01-config.js +1 -1
  51. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  52. package/dist/boot/stage-03-store-backend.js +2 -2
  53. package/dist/boot/stage-04-stores.d.ts +1 -0
  54. package/dist/boot/stage-05-execution-env.d.ts +3 -0
  55. package/dist/boot/stage-05-execution-env.js +3 -3
  56. package/dist/boot/stage-06-runners.d.ts +3 -0
  57. package/dist/boot/stage-06-runners.js +11 -10
  58. package/dist/boot/stage-07-capability-layer.d.ts +3 -0
  59. package/dist/boot/stage-07-capability-layer.js +7 -2
  60. package/dist/boot/stage-08-reapers.d.ts +3 -0
  61. package/dist/boot/stage-08-reapers.js +3 -3
  62. package/dist/boot/stage-09-leader.d.ts +3 -0
  63. package/dist/boot/stage-10-http-server.d.ts +3 -0
  64. package/dist/boot/stage-10-http-server.js +3 -2
  65. package/dist/boot/stores.d.ts +1 -0
  66. package/dist/boot/stores.js +35 -19
  67. package/dist/boot/workflow-orchestration.d.ts +1 -1
  68. package/dist/boot/workflow-orchestration.js +2 -1
  69. package/dist/capabilities/composite-members.d.ts +3 -1
  70. package/dist/capabilities/composite-members.js +3 -1
  71. package/dist/capabilities/scenarios.d.ts +2 -2
  72. package/dist/capability-facts.d.ts +18 -1
  73. package/dist/capability-facts.js +25 -7
  74. package/dist/checkpoint-ctx-seal-m28.d.ts +1 -1
  75. package/dist/config-catalog.js +13 -4
  76. package/dist/config-provider.d.ts +2 -2
  77. package/dist/config-types.d.ts +43 -4
  78. package/dist/config.d.ts +6 -1
  79. package/dist/config.js +67 -11
  80. package/dist/deleted-session-ids.d.ts +19 -0
  81. package/dist/deleted-session-ids.js +26 -0
  82. package/dist/deploy-facts.d.ts +155 -0
  83. package/dist/deploy-facts.js +94 -0
  84. package/dist/device-ws-hub.d.ts +48 -3
  85. package/dist/device-ws-hub.js +48 -2
  86. package/dist/error-code-family.d.ts +35 -0
  87. package/dist/error-code-family.js +26 -0
  88. package/dist/fleet/drain-entry.d.ts +39 -3
  89. package/dist/fleet/drain-entry.js +6 -0
  90. package/dist/fleet/fleet-bus.d.ts +2 -1
  91. package/dist/hooks/branch-transcript.d.ts +1 -1
  92. package/dist/hooks/branch-transcript.js +1 -0
  93. package/dist/hooks/classic-stations.d.ts +25 -8
  94. package/dist/hooks/classic-stations.js +29 -3
  95. package/dist/hooks/hook-runner.d.ts +33 -16
  96. package/dist/hooks/hook-runner.js +166 -73
  97. package/dist/hooks/hook-source.d.ts +14 -2
  98. package/dist/hooks/hook-source.js +8 -0
  99. package/dist/http/active-run-conflict.d.ts +1 -1
  100. package/dist/http/active-run-conflict.js +1 -1
  101. package/dist/http/admission.js +11 -1
  102. package/dist/http/checkpoint-ctx.d.ts +59 -71
  103. package/dist/http/checkpoint-ctx.js +34 -40
  104. package/dist/http/dispatch.js +18 -2
  105. package/dist/http/durable-run-leg.d.ts +5 -0
  106. package/dist/http/durable-run-leg.js +1 -0
  107. package/dist/http/execution-context.d.ts +4 -0
  108. package/dist/http/execution-context.js +9 -0
  109. package/dist/http/health-self-report.d.ts +6 -2
  110. package/dist/http/health-self-report.js +3 -2
  111. package/dist/http/memory-compliance-failure-reply.d.ts +1 -1
  112. package/dist/http/memory-compliance-failure-reply.js +2 -15
  113. package/dist/http/memory-control-plane-corrupt-reply.d.ts +25 -0
  114. package/dist/http/memory-control-plane-corrupt-reply.js +19 -0
  115. package/dist/http/principal-gate.d.ts +35 -12
  116. package/dist/http/principal-gate.js +16 -12
  117. package/dist/http/request-guards.d.ts +1 -1
  118. package/dist/http/request-guards.js +5 -5
  119. package/dist/http/resume-legs.d.ts +22 -0
  120. package/dist/http/resume-legs.js +26 -17
  121. package/dist/http/route-ctx.d.ts +11 -10
  122. package/dist/http/route-table.js +2 -0
  123. package/dist/http/routes/adoption.d.ts +11 -0
  124. package/dist/http/routes/adoption.js +80 -6
  125. package/dist/http/routes/agents-roster.js +1 -0
  126. package/dist/http/routes/approvals-assistant.js +2 -1
  127. package/dist/http/routes/background.js +1 -0
  128. package/dist/http/routes/capabilities.js +4 -1
  129. package/dist/http/routes/diagnostics.d.ts +9 -14
  130. package/dist/http/routes/diagnostics.js +10 -20
  131. package/dist/http/routes/engine.d.ts +31 -0
  132. package/dist/http/routes/engine.js +81 -0
  133. package/dist/http/routes/memory-bundle.js +3 -0
  134. package/dist/http/routes/memory-consolidation.js +3 -5
  135. package/dist/http/routes/memory-origin.d.ts +1 -1
  136. package/dist/http/routes/memory-origin.js +2 -4
  137. package/dist/http/routes/memory-policy.js +5 -1
  138. package/dist/http/routes/observability.js +3 -6
  139. package/dist/http/routes/runs.js +7 -3
  140. package/dist/http/routes/session-sync.js +12 -25
  141. package/dist/http/routes/sessions-list.d.ts +1 -1
  142. package/dist/http/routes/sessions.js +4 -0
  143. package/dist/http/routes/tasks.js +23 -5
  144. package/dist/http/routes/trace-usage.js +1 -1
  145. package/dist/http/run-concurrency-cap.d.ts +86 -0
  146. package/dist/http/run-concurrency-cap.js +54 -0
  147. package/dist/http/server.d.ts +15 -12
  148. package/dist/http/server.js +18 -3
  149. package/dist/http/stored-body-replay.d.ts +9 -6
  150. package/dist/http/stored-body-replay.js +19 -5
  151. package/dist/http/wire-types.d.ts +8 -3
  152. package/dist/json-body-limit.d.ts +8 -0
  153. package/dist/json-body-limit.js +2 -0
  154. package/dist/leader/diffout.d.ts +1 -1
  155. package/dist/local-data-root.d.ts +2 -2
  156. package/dist/memory-operator-faces.d.ts +9 -4
  157. package/dist/memory-scope-stores.d.ts +9 -7
  158. package/dist/memory-scope-stores.js +3 -14
  159. package/dist/memory-scope.d.ts +36 -0
  160. package/dist/memory-scope.js +32 -1
  161. package/dist/net-address.d.ts +53 -0
  162. package/dist/net-address.js +111 -0
  163. package/dist/observability/fail-open.d.ts +14 -10
  164. package/dist/observability/fail-open.js +14 -10
  165. package/dist/orchestration/workflow-notify-journal.js +2 -3
  166. package/dist/org-delivery-gate.js +1 -1
  167. package/dist/peer-directory.d.ts +6 -12
  168. package/dist/peer-directory.js +5 -1
  169. package/dist/plugins/caching-session-store.d.ts +3 -1
  170. package/dist/plugins/caching-session-store.js +3 -0
  171. package/dist/plugins/checkpoint-store-face.d.ts +426 -0
  172. package/dist/plugins/checkpoint-store-face.js +152 -0
  173. package/dist/plugins/file-run-store.d.ts +0 -6
  174. package/dist/plugins/file-run-store.js +1 -12
  175. package/dist/plugins/fork-routing-session-store.d.ts +6 -2
  176. package/dist/plugins/fork-routing-session-store.js +2 -2
  177. package/dist/plugins/imported-layer-root-retirement.d.ts +10 -8
  178. package/dist/plugins/local-checkpoint-store.d.ts +2 -2
  179. package/dist/plugins/local-checkpoint-store.js +2 -1
  180. package/dist/plugins/local-session-store.d.ts +28 -5
  181. package/dist/plugins/local-session-store.js +32 -8
  182. package/dist/plugins/memory-engine-pg.d.ts +29 -6
  183. package/dist/plugins/memory-engine-pg.js +96 -214
  184. package/dist/plugins/memory-engine-sql-apply.d.ts +121 -0
  185. package/dist/plugins/memory-engine-sql-apply.js +358 -0
  186. package/dist/plugins/memory-engine-tidb.d.ts +35 -9
  187. package/dist/plugins/memory-engine-tidb.js +97 -227
  188. package/dist/plugins/memory-key-guards.d.ts +1 -1
  189. package/dist/plugins/memory-origin-law.d.ts +3 -3
  190. package/dist/plugins/one-time-migrations.d.ts +32 -56
  191. package/dist/plugins/one-time-migrations.js +25 -117
  192. package/dist/plugins/permission-rule-store-file.d.ts +17 -3
  193. package/dist/plugins/permission-rule-store-file.js +8 -1
  194. package/dist/plugins/pg-pool.js +1 -1
  195. package/dist/plugins/pg-session-storage.d.ts +17 -4
  196. package/dist/plugins/pg-session-storage.js +8 -4
  197. package/dist/plugins/run-store-sql.js +2 -2
  198. package/dist/plugins/schema-ensure-legs.js +2 -2
  199. package/dist/plugins/session-fork.d.ts +31 -9
  200. package/dist/plugins/session-fork.js +3 -2
  201. package/dist/plugins/session-placement.d.ts +4 -2
  202. package/dist/plugins/sql-json-column.d.ts +5 -1
  203. package/dist/plugins/store-backend.d.ts +11 -8
  204. package/dist/plugins/store-backend.js +8 -7
  205. package/dist/plugins/store-contracts.d.ts +1 -11
  206. package/dist/plugins/store-contracts.js +0 -9
  207. package/dist/plugins/store-sweep.d.ts +63 -0
  208. package/dist/plugins/store-sweep.js +48 -0
  209. package/dist/plugins/tidb-pool.js +1 -1
  210. package/dist/plugins/tidb-session-store.d.ts +16 -3
  211. package/dist/plugins/tidb-session-store.js +8 -4
  212. package/dist/prompt-screen.d.ts +6 -9
  213. package/dist/prompt-screen.js +2 -5
  214. package/dist/rules-consent.d.ts +20 -27
  215. package/dist/rules-consent.js +82 -6
  216. package/dist/run-cancel-context.d.ts +4 -1
  217. package/dist/run-cancel-context.js +2 -1
  218. package/dist/runs.js +1 -1
  219. package/dist/sandbox-egress-policy.js +6 -11
  220. package/dist/sandbox-rbac-rules.d.ts +88 -0
  221. package/dist/sandbox-rbac-rules.js +80 -0
  222. package/dist/sealed-key-layout.d.ts +17 -0
  223. package/dist/sealed-key-layout.js +4 -0
  224. package/dist/sealed-key.js +5 -5
  225. package/dist/security.d.ts +38 -6
  226. package/dist/security.js +9 -2
  227. package/dist/session-notify-door.d.ts +5 -3
  228. package/dist/session-resident-background.d.ts +57 -0
  229. package/dist/session-resident-background.js +20 -0
  230. package/dist/session-sync-kernel.d.ts +53 -14
  231. package/dist/session-sync-kernel.js +70 -1
  232. package/dist/session-sync.d.ts +5 -3
  233. package/dist/session-workspace.d.ts +1 -0
  234. package/dist/session-workspace.js +1 -0
  235. package/dist/sighup-idle.d.ts +9 -7
  236. package/dist/sighup-idle.js +4 -5
  237. package/dist/spec-fields.d.ts +16 -0
  238. package/dist/spec-fields.js +7 -0
  239. package/dist/stored-request-env-credentials-m38.d.ts +3 -0
  240. package/dist/stored-request-env-credentials-m38.js +8 -0
  241. package/dist/stored-request-prompt-profile-m30.d.ts +1 -1
  242. package/dist/{plugins → stores-sql}/checkpoint-store-sql.d.ts +13 -427
  243. package/dist/{plugins → stores-sql}/checkpoint-store-sql.js +33 -194
  244. package/dist/{plugins → stores-sql}/mailbox-store-sql.d.ts +2 -2
  245. package/dist/{plugins → stores-sql}/mailbox-store-sql.js +4 -4
  246. package/dist/{plugins → stores-sql}/roster-store-sql.d.ts +2 -2
  247. package/dist/{plugins → stores-sql}/roster-store-sql.js +1 -1
  248. package/dist/{plugins → stores-sql}/tool-result-store-sql.d.ts +2 -2
  249. package/dist/{plugins → stores-sql}/tool-result-store-sql.js +6 -6
  250. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.d.ts +7 -12
  251. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.js +3 -8
  252. package/dist/task-settings.d.ts +59 -4
  253. package/dist/task-settings.js +50 -4
  254. package/dist/tool-approval.d.ts +2 -2
  255. package/dist/trace/core-keyset-guard.d.ts +8 -7
  256. package/dist/trace/engine-notice-wire.d.ts +1 -1
  257. package/dist/trace/engine-notice-wire.js +2 -0
  258. package/dist/trace/exit-seat.d.ts +6 -0
  259. package/dist/trace/exit-seat.js +1 -0
  260. package/dist/trace/project.js +9 -0
  261. package/dist/trace/redact.d.ts +13 -2
  262. package/dist/trace/redact.js +305 -24
  263. package/dist/trace/self-minted-wire.d.ts +326 -8
  264. package/dist/trace/self-minted-wire.js +125 -7
  265. package/dist/trace/wire-projection-faces.d.ts +1 -1
  266. package/dist/trace/wire-projection-faces.js +1 -1
  267. package/dist/usage-analytics.d.ts +4 -0
  268. package/package.json +12 -4
  269. package/scripts/drain-check-local.mjs +8 -29
  270. package/scripts/drain-check.mysql.sql +3 -24
  271. package/scripts/drain-check.pg.sql +3 -24
  272. package/skills/update-config.md +1 -1
  273. package/wire-changes/server@7.146.0.json +3221 -0
  274. package/dist/approval-batch-census.d.ts +0 -27
  275. package/dist/approval-batch-census.js +0 -27
@@ -5,7 +5,7 @@ import { createLogger } from "./observability/logger.js";
5
5
  import { ABSOLUTE_HOST_PATH_MAX, hasControlOrBidi } from "@sema-agent/settings-schema";
6
6
  import { isDirectWritableRuleBehavior } from "./permission-rule-vocab.js";
7
7
  import { hasBidiControls } from "./text-bidi.js";
8
- import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, addPersistedRule, removePersistedRule, isValidDurableScope, effectiveOrThrow, readRowBehavior, readRuleOwner, } from "@sema-agent/core";
8
+ import { confirmRuleApproval, RULE_BEHAVIORS, RULE_REFUSAL_CODES, parseRuleText, ruleReachesProgramRun, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, addPersistedRule, removePersistedRule, isValidDurableScope, effectiveOrThrow, readRowBehavior, readRuleOwner, } from "@sema-agent/core";
9
9
  import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
10
10
  export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
11
11
  export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
@@ -21,7 +21,7 @@ const ImportOutcomeSchema = z
21
21
  .object({
22
22
  key: z.object({ behavior: z.preprocess((v) => readRowBehavior(v) ?? v, z.enum(RULE_BEHAVIORS)), rule: z.string().min(1), scope: z.string().min(1) }).strict(),
23
23
  outcome: z.enum(["persisted", "no-op", "refused", "indeterminate"]),
24
- reason: z.literal("store_refused").optional(),
24
+ reason: z.enum(RULE_REFUSAL_CODES).optional(),
25
25
  detail: z.string().optional(),
26
26
  })
27
27
  .strict()),
@@ -137,6 +137,52 @@ function confirmRefusalWord(reason) {
137
137
  }
138
138
  }
139
139
  }
140
+ function isRuleStoreFault(code) {
141
+ switch (code) {
142
+ case "store_read":
143
+ case "store_write":
144
+ return true;
145
+ case "unsupported.layer_root":
146
+ case "ticket_invalid":
147
+ case "record_missing":
148
+ case "wrong_principal":
149
+ case "record_schema_stale":
150
+ case "not_confirmed":
151
+ case "record_malformed":
152
+ case "not_chosen":
153
+ case "parse_rejected":
154
+ case "store_unavailable":
155
+ case "record_changed":
156
+ case "withheld":
157
+ case "session_ended":
158
+ case "occ":
159
+ case "dot_removed":
160
+ return false;
161
+ default:
162
+ return code;
163
+ }
164
+ }
165
+ class RuleStoreFaultError extends Error {
166
+ code;
167
+ constructor(refusal) {
168
+ super(refusal.reason);
169
+ this.name = "RuleStoreFaultError";
170
+ this.code = refusal.code;
171
+ }
172
+ }
173
+ function throwIfRuleStoreFault(refusal) {
174
+ const { code } = refusal;
175
+ if (!isRuleStoreFault(code))
176
+ return;
177
+ switch (refusal.committed) {
178
+ case "no":
179
+ throw new RuleStoreFaultError({ code, reason: refusal.reason });
180
+ case "unknown":
181
+ return;
182
+ default:
183
+ return refusal.committed;
184
+ }
185
+ }
140
186
  function refusedMembersOf(members) {
141
187
  const refused = [];
142
188
  for (const m of members) {
@@ -250,7 +296,7 @@ function buildImportOutcomeMembers(members) {
250
296
  case "deduped":
251
297
  return { key, outcome: "no-op" };
252
298
  case "refused":
253
- return { key, outcome: "indeterminate", reason: "store_refused", detail: m.reason };
299
+ return { key, outcome: refusedOutcomeOf(m.committed), reason: m.code, detail: m.reason };
254
300
  default: {
255
301
  const unknown = m;
256
302
  throw new Error(`unknown redeemed-batch member status: ${JSON.stringify(unknown)}`);
@@ -258,6 +304,22 @@ function buildImportOutcomeMembers(members) {
258
304
  }
259
305
  });
260
306
  }
307
+ function unsettledDetailOf(members) {
308
+ const open = members.find((m) => m.outcome === "indeterminate");
309
+ return open === undefined ? undefined : `the outcome of ${open.key.rule} is not known (${open.reason ?? "unknown"}): ${open.detail ?? ""}`;
310
+ }
311
+ function refusedOutcomeOf(committed) {
312
+ switch (committed) {
313
+ case "no":
314
+ return "refused";
315
+ case "unknown":
316
+ return "indeterminate";
317
+ default: {
318
+ const unknown = committed;
319
+ throw new Error(`unknown redeemed-batch member committed word: ${JSON.stringify(unknown)}`);
320
+ }
321
+ }
322
+ }
261
323
  export function createRuleConsentLane(stores, opts) {
262
324
  const deps = { provider: stores.provider, approvals: stores.approvals, cardEdits: true };
263
325
  const ticketTtlMs = opts?.ticketTtlMs ?? RULE_IMPORT_TICKET_TTL_MS;
@@ -300,10 +362,15 @@ export function createRuleConsentLane(stores, opts) {
300
362
  if (!confirmed.ok)
301
363
  return { ok: false, reason: "confirm-refused", detail: confirmed.reason };
302
364
  const result = await redeemRuleBatch({ approvalId: prepared.approvalId, principal: input.principal, deps });
303
- if ("status" in result)
365
+ if ("status" in result) {
366
+ throwIfRuleStoreFault(result);
304
367
  return { ok: false, reason: "redeem-refused", detail: result.reason };
368
+ }
305
369
  const refused = refusedMembersOf(result.members);
306
370
  if (refused.length > 0) {
371
+ if (refused.length === result.members.length && refused.every((m) => m.committed === "no"))
372
+ for (const m of refused)
373
+ throwIfRuleStoreFault(m);
307
374
  return { ok: false, reason: "redeem-refused", detail: `partial batch: ${refused.length} member(s) refused (${refused.map((x) => x.reason).join("; ")})` };
308
375
  }
309
376
  return { ok: true, ...anchorBatchMembers(result.members, redemption.offer), rev: result.rev };
@@ -351,8 +418,10 @@ export function createRuleConsentLane(stores, opts) {
351
418
  ticket = minted.ticket;
352
419
  }
353
420
  const redeemed = await redeemRuleTicket({ ticket, principal: input.principal, deps });
354
- if (redeemed.status !== "redeemed")
421
+ if (redeemed.status !== "redeemed") {
422
+ throwIfRuleStoreFault(redeemed);
355
423
  return { ok: false, reason: "redeem-refused", detail: redeemed.reason };
424
+ }
356
425
  return { ok: true, kind: "single", rule: redeemed.rule, rev: redeemed.rev, alreadyRedeemed: redeemed.alreadyRedeemed };
357
426
  },
358
427
  async prepareImport(principal, layers) {
@@ -449,7 +518,11 @@ export function createRuleConsentLane(stores, opts) {
449
518
  if ("status" in result) {
450
519
  return await indeterminate(result.reason);
451
520
  }
452
- return { ok: true, outcome: { members: buildImportOutcomeMembers(result.members), rev: result.rev } };
521
+ const members = buildImportOutcomeMembers(result.members);
522
+ const open = unsettledDetailOf(members);
523
+ if (open !== undefined)
524
+ return await indeterminate(open);
525
+ return { ok: true, outcome: { members, rev: result.rev } };
453
526
  }
454
527
  catch (err) {
455
528
  return await indeterminate(err instanceof Error ? err.message : String(err));
@@ -607,6 +680,9 @@ export function createRuleConsentLane(stores, opts) {
607
680
  }
608
681
  members = buildImportOutcomeMembers(result.members);
609
682
  rev = result.rev;
683
+ const open = unsettledDetailOf(members);
684
+ if (open !== undefined)
685
+ return await transient(open);
610
686
  }
611
687
  catch (err) {
612
688
  return await transient(err instanceof Error ? err.message : String(err));
@@ -136,6 +136,9 @@ export declare function buildCancelledLedgerRow(c: PersistedTaskResult): {
136
136
  * `signal.reason`:那会改掉引擎与 provider 层收到的拒绝值,是在下游改上游接缝的输入(立宪禁区)。
137
137
  */
138
138
  export interface RunAbort {
139
+ /** 这条腿的**属主**(run 行的 `owner` 列;无主体 = `null`)—— 在飞集(`inflightRuns`)里「谁占着几条」的唯一读处:并发帽按主体数在飞腿时读它
140
+ * (`http/run-concurrency-cap.ts`),不另起一张按 taskId 键控的旁表。句柄与腿同寿,换腿就是换句柄。 */
141
+ readonly owner: string | null;
139
142
  /** 交给引擎的那只信号(`spec.signal` / 审批 / 提问 ALS 的 `abortSignal` / 子代 stoppedBy 归因都用它)。 */
140
143
  readonly signal: AbortSignal;
141
144
  /** 以 `cause` 中止本腿。已中止 ⇒ no-op(成因不改写)。 */
@@ -143,6 +146,6 @@ export interface RunAbort {
143
146
  /** 让本腿停下来的那次中止的成因;未中止 ⇒ `undefined`。 */
144
147
  cause(): AbortCause | undefined;
145
148
  }
146
- export declare function createRunAbort(): RunAbort;
149
+ export declare function createRunAbort(owner: string | null): RunAbort;
147
150
  export {};
148
151
  //# sourceMappingURL=run-cancel-context.d.ts.map
@@ -51,10 +51,11 @@ export function buildCancelledLedgerRow(c) {
51
51
  ...(c.abortCause !== undefined ? { abortCause: c.abortCause } : {}),
52
52
  };
53
53
  }
54
- export function createRunAbort() {
54
+ export function createRunAbort(owner) {
55
55
  const ctrl = new AbortController();
56
56
  let landed;
57
57
  return {
58
+ owner,
58
59
  signal: ctrl.signal,
59
60
  abort(cause) {
60
61
  if (ctrl.signal.aborted)
package/dist/runs.js CHANGED
@@ -285,7 +285,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
285
285
  const startedAt = Date.now();
286
286
  metrics?.addGauge("runs_active", 1);
287
287
  fleetPublisher?.onStart();
288
- const cancelCtrl = createRunAbort();
288
+ const cancelCtrl = createRunAbort(principal ?? null);
289
289
  inflight?.set(taskId, cancelCtrl);
290
290
  markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
291
291
  const preemptEligible = verify === undefined && cascade === undefined;
@@ -1,4 +1,5 @@
1
- import { BlockList, isIP } from "node:net";
1
+ import { isIP } from "node:net";
2
+ import { createCidrSet, parseCidr as readCidrText } from "./net-address.js";
2
3
  import { recordFailOpen } from "./observability/fail-open.js";
3
4
  import { seatText } from "./trace/exit-seat.js";
4
5
  export const SANDBOX_IPV6_WORDS = ["enabled", "disabled"];
@@ -155,22 +156,16 @@ function nodeAddresses(json) {
155
156
  return out;
156
157
  }
157
158
  function parseCidr(cidr) {
158
- const slash = cidr.indexOf("/");
159
- const net = slash < 0 ? cidr : cidr.slice(0, slash);
160
- const fam = isIP(net);
161
- const max = fam === 6 ? 128 : 32;
162
- const prefix = slash < 0 ? max : Number(cidr.slice(slash + 1));
163
- if (fam === 0 || !/^\d+$/.test(slash < 0 ? String(max) : cidr.slice(slash + 1)) || prefix > max)
159
+ const r = readCidrText(cidr);
160
+ if (!r.ok)
164
161
  throw new Unjudgeable(`ipBlock cidr ${JSON.stringify(cidr)} does not parse`);
165
- return { net, prefix, family: fam === 6 ? "ipv6" : "ipv4" };
162
+ return r.cidr;
166
163
  }
167
164
  function inCidr(address, cidr) {
168
165
  const c = parseCidr(cidr);
169
166
  if ((isIP(address) === 6 ? "ipv6" : "ipv4") !== c.family)
170
167
  return false;
171
- const list = new BlockList();
172
- list.addSubnet(c.net, c.prefix, c.family);
173
- return list.check(address, c.family);
168
+ return createCidrSet([c]).has(address);
174
169
  }
175
170
  function ruleAllows(rule, address) {
176
171
  if (rule.to === undefined)
@@ -0,0 +1,88 @@
1
+ /**
2
+ * 沙箱车道(`REMOTE_EXEC=k8s`)的服务账号在沙箱命名空间 / 集群里需要哪些 RBAC 规则,以及每条规则由哪个引擎特性开关决定 ——
3
+ * 「引擎需要哪条规则由哪个开关决定」**只写在这一张表里**。
4
+ *
5
+ * ## 谁读它(都是读者,没有第二份)
6
+ * · `scripts/gen-deploy-lanes.ts`:把本表渲成部署产物 —— chart `templates/sandbox-rbac.yaml` 的 Role / ClusterRole 规则区(开关条件规则包在 helm 条件里)、
7
+ * chart `templates/_helpers.tpl` 里把同一个开关值写进引擎旋钮的助手体、cluster 静态清单的同两个规则区(静态清单不能条件渲,引擎 env 里两个旋钮都没设,
8
+ * 开关条件规则渲成注释掉的规则块)、`docs/DEPLOY-PREREQS.md` 的规则表区,以及给取用方的只读数据 `deploy/facts/sandbox-rbac.json`。
9
+ * 「入库文件 ≡ 生成物」由 `--check` 与 `test/deploy-lane-projections.test.ts` 钉;行为由 `test/sandbox-rbac-table.test.ts` 钉。
10
+ * · secrets 那一行的动词**直接就是** {@link SANDBOX_SECRET_SEAT_VERBS}`.env`(起服门问的、适配器真发的同一张表)—— 不另抄一份。
11
+ * pvc 那一行的动词 ⊇ 适配器对 `persistentvolumeclaims` 真发的每个调用(`test/sandbox-rbac-table.test.ts` 读 `remote-env-k8s.ts` 对拍)。
12
+ *
13
+ * ## 形
14
+ * · `when`:`"always"`(恒授)或一个**特性开关词**({@link SANDBOX_RBAC_SWITCH_WORDS},闭集;加词必须同时给 {@link SANDBOX_RBAC_SWITCHES} 一行 —— 缺行是编译错)。
15
+ * · 开关 = 引擎旋钮的一个状态:`bool` 形 = 旋钮取某个词时开(`WORKSPACE_CACHE=volume`),`names` 形 = 旋钮是 NAME,NAME 名单、非空时开(`K8S_SANDBOX_ENV`)。
16
+ * 随包 chart 里同一个值(`chartValue`)既写引擎旋钮、又决定 Role 带不带那几行规则。
17
+ * · `need`:这一行的动词引擎适配器**真调**(`called`,`why` 里写着调用它的适配器方法〔`remote-env-k8s.ts#<方法名>`〕,源码钉对拍)还是只是**兼容保留**
18
+ * (`retained`:chart 与静态清单一直授着、适配器源码里零调用)。同一个资源的动词按 `need` 拆成两行;chart 与静态清单把同资源的行并回一条规则照授(本版不删 `retained`)。
19
+ * · 给取用方的数据 `deploy/facts/sandbox-rbac.json` = `{ schemaVersion, rules: [{ scope, apiGroups, resources, verbs, when, need, why }] }`(随包;npm 子路径同名),
20
+ * 形由本文件的 {@link SANDBOX_RBAC_FACTS_SCHEMA} 定义(和表放一起;生成器把它写成 `deploy/facts/sandbox-rbac.schema.json`)。
21
+ * 形变的加性 / 破坏性判法、登记与 `schemaVersion` 的派生是随包部署事实的统一规则,见 `src/deploy-facts.ts` 头注 —— 本表不另立一套。
22
+ */
23
+ import type { WorkspaceCacheWord } from "./workspace-cache.js";
24
+ import { type FactsRootSchema } from "./deploy-facts.js";
25
+ /** `deploy/facts/sandbox-rbac.json` 的形版本(从形变登记表派生,见 `src/deploy-facts.ts` 的 `factsSchemaVersion`)。 */
26
+ export declare const SANDBOX_RBAC_SCHEMA_VERSION: number;
27
+ export declare const SANDBOX_RBAC_SCOPES: readonly ["Role", "ClusterRole"];
28
+ export type SandboxRbacScope = (typeof SANDBOX_RBAC_SCOPES)[number];
29
+ /** Kubernetes RBAC 动词闭集(规则只收这些词)。 */
30
+ export declare const SANDBOX_RBAC_VERBS: readonly ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"];
31
+ export declare const SANDBOX_RBAC_ALWAYS: "always";
32
+ /** 特性开关词:引擎旋钮的一个状态。词表是闭集 —— {@link SANDBOX_RBAC_SWITCHES} 对它做穷尽映射(新增一个词而不给开关行 ⇒ 编译错)。 */
33
+ export declare const SANDBOX_RBAC_SWITCH_WORDS: readonly ["WORKSPACE_CACHE=volume", "K8S_SANDBOX_ENV"];
34
+ export type SandboxRbacSwitchWord = (typeof SANDBOX_RBAC_SWITCH_WORDS)[number];
35
+ /** `when` 的全词表:恒授 + 开关词。 */
36
+ export declare const SANDBOX_RBAC_WHEN_WORDS: readonly ["always", "WORKSPACE_CACHE=volume", "K8S_SANDBOX_ENV"];
37
+ export type SandboxRbacWhen = (typeof SANDBOX_RBAC_WHEN_WORDS)[number];
38
+ export type SandboxRbacSwitch = {
39
+ /** 旋钮取 {@link envValue} 时开。 */
40
+ readonly kind: "bool";
41
+ readonly env: string;
42
+ readonly envValue: WorkspaceCacheWord;
43
+ /** 随包 chart 的值路径,恒 `sandbox.<键>`;布尔。 */
44
+ readonly chartValue: string;
45
+ readonly meaning: string;
46
+ } | {
47
+ /** 旋钮是 NAME,NAME 名单,非空时开。 */
48
+ readonly kind: "names";
49
+ readonly env: string;
50
+ /** 随包 chart 的值路径,恒 `sandbox.<键>`;名单字符串。 */
51
+ readonly chartValue: string;
52
+ readonly meaning: string;
53
+ };
54
+ export declare const SANDBOX_RBAC_SWITCHES: {
55
+ readonly [W in SandboxRbacSwitchWord]: SandboxRbacSwitch;
56
+ };
57
+ /** 一行动词的引擎用度:`called` = 适配器源码真调;`retained` = 兼容保留(chart 有、适配器零调用)。 */
58
+ export declare const SANDBOX_RBAC_NEEDS: readonly ["called", "retained"];
59
+ export type SandboxRbacNeed = (typeof SANDBOX_RBAC_NEEDS)[number];
60
+ export interface SandboxRbacRule {
61
+ readonly scope: SandboxRbacScope;
62
+ readonly apiGroups: readonly string[];
63
+ readonly resources: readonly string[];
64
+ readonly verbs: readonly string[];
65
+ readonly when: SandboxRbacWhen;
66
+ readonly need: SandboxRbacNeed;
67
+ /** 这一行为什么在(渲成产物里规则上方的注释;也是 `deploy/facts/sandbox-rbac.json` 的 `why`);`called` 行写着调用方的 `remote-env-k8s.ts#<方法名>` 坐标。 */
68
+ readonly why: string;
69
+ }
70
+ /**
71
+ * 规则表(表序 = 产物里的规则序)。`called` 行的调用坐标 = `remote-env-k8s.ts` 里的方法名(不写行号,免得每改一行适配器就漂);
72
+ * `test/sandbox-rbac-table.test.ts` 读该文件逐方法核「这一行的动词 ⊆ 那些方法真发的调用」,并核 `retained` 行没有任何调用。
73
+ */
74
+ export declare const SANDBOX_RBAC_RULES: readonly SandboxRbacRule[];
75
+ /** 一份完整的表(生成器的输入;测试用变异夹具替换它)。 */
76
+ export interface SandboxRbacModel {
77
+ readonly switches: {
78
+ readonly [W in SandboxRbacSwitchWord]: SandboxRbacSwitch;
79
+ };
80
+ readonly rules: readonly SandboxRbacRule[];
81
+ }
82
+ export declare const SANDBOX_RBAC_MODEL: SandboxRbacModel;
83
+ /**
84
+ * `deploy/facts/sandbox-rbac.json` 的 Schema(随包方言,见 `src/deploy-facts.ts`)。闭集词表直接取本文件的常量:加一个 `when` / `need` / 范围 / 动词词
85
+ * = 闭集加词 = 破坏性形变(读方对闭集做穷尽判别),要登记、要抬版。一条规则的身份 = (范围, apiGroups, resources, need)—— 同一资源按 `need` 拆成两行。
86
+ */
87
+ export declare const SANDBOX_RBAC_FACTS_SCHEMA: FactsRootSchema;
88
+ //# sourceMappingURL=sandbox-rbac-rules.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { SANDBOX_SECRET_SEAT_VERBS } from "./plugins/k8s-exec-protocol.js";
2
+ import { FACTS_SCHEMA_DIALECT, factsSchemaVersion } from "./deploy-facts.js";
3
+ export const SANDBOX_RBAC_SCHEMA_VERSION = factsSchemaVersion("sandbox-rbac");
4
+ export const SANDBOX_RBAC_SCOPES = ["Role", "ClusterRole"];
5
+ export const SANDBOX_RBAC_VERBS = ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"];
6
+ export const SANDBOX_RBAC_ALWAYS = "always";
7
+ export const SANDBOX_RBAC_SWITCH_WORDS = ["WORKSPACE_CACHE=volume", "K8S_SANDBOX_ENV"];
8
+ export const SANDBOX_RBAC_WHEN_WORDS = [SANDBOX_RBAC_ALWAYS, ...SANDBOX_RBAC_SWITCH_WORDS];
9
+ export const SANDBOX_RBAC_SWITCHES = {
10
+ "WORKSPACE_CACHE=volume": { kind: "bool", env: "WORKSPACE_CACHE", envValue: "volume", chartValue: "sandbox.workspaceCacheVolume", meaning: "按工作区空间的构建缓存卷开着" },
11
+ K8S_SANDBOX_ENV: { kind: "names", env: "K8S_SANDBOX_ENV", chartValue: "sandbox.sandboxEnv", meaning: "沙箱带外 env 名单非空(名单点到的值留在服务进程环境里)" },
12
+ };
13
+ export const SANDBOX_RBAC_NEEDS = ["called", "retained"];
14
+ export const SANDBOX_RBAC_RULES = [
15
+ { scope: "Role", apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "create", "delete"], when: "always", need: "called", why: "沙箱 pod 的生命周期:remote-env-k8s.ts#createPod 建(POST pods)并读就绪(GET pods/<名>)· remote-env-k8s.ts#listSandboxPods 按标签列(GET pods?labelSelector)· remote-env-k8s.ts#reconnect 读存活(GET pods/<名>)· remote-env-k8s.ts#deletePod 删(DELETE pods/<名>)" },
16
+ { scope: "Role", apiGroups: [""], resources: ["pods"], verbs: ["watch"], when: "always", need: "retained", why: "兼容保留:chart 一直授着,适配器没有 watch 调用" },
17
+ { scope: "Role", apiGroups: [""], resources: ["pods/exec"], verbs: ["create", "get"], when: "always", need: "called", why: "在沙箱 pod 里执行命令:remote-env-k8s.ts#execUrl 拼 pods/<名>/exec 的连接地址" },
18
+ { scope: "Role", apiGroups: [""], resources: ["pods/attach"], verbs: ["create", "get"], when: "always", need: "retained", why: "兼容保留:chart 一直授着,适配器没有 attach 调用" },
19
+ { scope: "Role", apiGroups: [""], resources: ["pods/log"], verbs: ["get"], when: "always", need: "retained", why: "兼容保留:chart 一直授着,适配器没有读日志的调用" },
20
+ {
21
+ scope: "Role",
22
+ apiGroups: [""],
23
+ resources: ["persistentvolumeclaims"],
24
+ verbs: ["get", "create", "deletecollection"],
25
+ when: "WORKSPACE_CACHE=volume",
26
+ need: "called",
27
+ why: "按工作区空间的构建缓存卷:remote-env-k8s.ts#ensureCacheClaim 按名读、没有就建(GET / POST persistentvolumeclaims)· remote-env-k8s.ts#cacheClaimBound 按名读 · remote-env-k8s.ts#reclaimSpaceCache 空间结束按标签回收(DELETE persistentvolumeclaims?labelSelector = deletecollection);缺它 ⇒ 缓存卷拿不到,适配器按 fail-open 照起、不挂卷(计数 server.workspace-cache.unavailable)",
28
+ },
29
+ { scope: "Role", apiGroups: [""], resources: ["persistentvolumeclaims"], verbs: ["delete"], when: "WORKSPACE_CACHE=volume", need: "retained", why: "兼容保留:chart 一直授着,适配器回收只发按标签的 deletecollection,没有按名 delete 的调用" },
30
+ {
31
+ scope: "Role",
32
+ apiGroups: [""],
33
+ resources: ["secrets"],
34
+ verbs: SANDBOX_SECRET_SEAT_VERBS.env,
35
+ when: "K8S_SANDBOX_ENV",
36
+ need: "called",
37
+ why: "沙箱秘密座:动词取自 SANDBOX_SECRET_SEAT_VERBS.env —— 起服门(boot/execution-env.ts ensureSandboxSecretSeat)问的与 remote-env-k8s.ts#createOwnedSecret 为 env Secret 真发的(POST secrets)是同一行;起服门只在有效转发集合非空(名单点到的键在服务进程环境里有值)时问:答 denied ⇒ server 起服拒,问不到 ⇒ 告警 sandbox_secret_seat_unverified 照起;名单非空而有效集合为空时本行是保守多授(安装器看不到服务进程的运行时环境,引擎此时既不问也不调)",
38
+ },
39
+ { scope: "Role", apiGroups: ["networking.k8s.io"], resources: ["networkpolicies"], verbs: ["list"], when: "always", need: "called", why: "起服的出网自检读本 ns 的 NetworkPolicy 集合:remote-env-k8s.ts#egressPolicyGaps(GET .../networkpolicies)" },
40
+ { scope: "Role", apiGroups: ["networking.k8s.io"], resources: ["networkpolicies"], verbs: ["get"], when: "always", need: "retained", why: "兼容保留:chart 一直授着,适配器只读集合(list),没有按名 get" },
41
+ { scope: "ClusterRole", apiGroups: [""], resources: ["nodes"], verbs: ["list"], when: "always", need: "called", why: "出网自检读节点表(节点是集群级对象 ⇒ ClusterRole + Binding 给同一只服务账号):remote-env-k8s.ts#egressPolicyGaps(GET /api/v1/nodes)" },
42
+ { scope: "ClusterRole", apiGroups: [""], resources: ["nodes"], verbs: ["get"], when: "always", need: "retained", why: "兼容保留:chart 一直授着,适配器只读集合(list),没有按名 get" },
43
+ ];
44
+ export const SANDBOX_RBAC_MODEL = { switches: SANDBOX_RBAC_SWITCHES, rules: SANDBOX_RBAC_RULES };
45
+ const nonEmptyNames = (description) => ({ type: "array", minItems: 1, uniqueItems: true, items: { type: "string" }, description });
46
+ export const SANDBOX_RBAC_FACTS_SCHEMA = {
47
+ $schema: FACTS_SCHEMA_DIALECT,
48
+ description: "k8s 沙箱车道(REMOTE_EXEC=k8s)的服务账号在沙箱命名空间 / 集群里需要的 RBAC 规则;每条规则何时授、引擎适配器真调还是兼容保留。",
49
+ type: "object",
50
+ additionalProperties: false,
51
+ required: ["schemaVersion", "rules"],
52
+ properties: {
53
+ schemaVersion: { type: "integer", const: SANDBOX_RBAC_SCHEMA_VERSION, description: "本份数据的形版本;读方不认识的版本整份拒读。" },
54
+ rules: {
55
+ type: "array",
56
+ minItems: 1,
57
+ "x-uniqueKey": ["scope", "apiGroups", "resources", "need"],
58
+ description: "规则行;同一资源的动词按 need 拆成两行,授权时并回一条规则(Kubernetes 对同一资源的多条规则取并集)。",
59
+ items: {
60
+ type: "object",
61
+ additionalProperties: false,
62
+ required: ["scope", "apiGroups", "resources", "verbs", "when", "need", "why"],
63
+ properties: {
64
+ scope: { type: "string", enum: SANDBOX_RBAC_SCOPES, description: "Role = 沙箱命名空间里的 Role;ClusterRole = 集群级对象(节点)要的 ClusterRole,绑给同一只服务账号。" },
65
+ apiGroups: nonEmptyNames("API 组;核心组写空串。"),
66
+ resources: nonEmptyNames("资源名(子资源写 pods/exec 这种形)。"),
67
+ verbs: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", enum: SANDBOX_RBAC_VERBS }, description: "动词。" },
68
+ when: {
69
+ type: "string",
70
+ enum: SANDBOX_RBAC_WHEN_WORDS,
71
+ description: "always = 恒授;WORKSPACE_CACHE=volume = 引擎开着按工作区空间的缓存卷时授(引擎用到它的条件同一个);K8S_SANDBOX_ENV = 引擎的沙箱 env 名单非空时授 —— 这是保守的授权条件,引擎真正问 / 调用的条件是名单点到的键在服务进程环境里有值,两者之差写在该行 why 里。",
72
+ },
73
+ need: { type: "string", enum: SANDBOX_RBAC_NEEDS, description: "called = 引擎适配器源码真发这些调用;retained = 随包 chart 一直授着、适配器源码里零调用(兼容保留)。" },
74
+ why: { type: "string", description: "这一行为什么在(called 行写着调用它的适配器方法)。" },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ };
80
+ //# sourceMappingURL=sandbox-rbac-rules.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 钥匙保管目录的布局常量 —— 引擎建目录、写钥匙文件、建停驻行封签子目录时用的**唯一**值(`src/sealed-key.ts` 读它们)。
3
+ *
4
+ * 单独成一只零依赖的叶子模块:随包部署事实 `deploy/facts/sealed-key.json`(`src/deploy-facts.ts` 投影、`scripts/gen-deploy-lanes.ts` 生成)
5
+ * 读的是同一份值,生成器不必为了三个常量把整只保管模块(libsodium / 中心上报)装载进来。
6
+ */
7
+ /** 保管目录的权限位:每次建店都 `mkdir` + `chmod` 成它(目录预先存在时 `mkdir` 的 mode 不生效,所以另 `chmod` 一次)。 */
8
+ export declare const SEALED_KEY_DIR_MODE = 448;
9
+ /** 钥匙文件 `<publicKeyId>.json` 的权限位(`writeFile` 的 mode 受 umask 掩,所以另 `chmod` 一次)。 */
10
+ export declare const SEALED_KEY_FILE_MODE = 384;
11
+ /**
12
+ * 停驻行凭据封签那一域的保管子目录名(在模型钥匙保管目录之下:`<保管目录>/checkpoint-ctx-keys/`)。**一个域一对钥匙**:模型钥匙(`ModelEntry.sealedApiKey`,
13
+ * 浏览器对着 caps 公钥裸封)与停驻行封签各用自己目录里的钥匙对 —— 一边的开箱口在另一边的密文上恒「不持有那把钥匙」,不是靠哪一侧多判一句信封。
14
+ * 放在模型钥匙目录之下是为了「多副本共享 / 跨重启持久」只有一个目录要管(整个保管目录);模型钥匙店只读这一层的 `*.json`,子目录不进它的视野。
15
+ */
16
+ export declare const CHECKPOINT_CTX_KEY_SUBDIR = "checkpoint-ctx-keys";
17
+ //# sourceMappingURL=sealed-key-layout.d.ts.map
@@ -0,0 +1,4 @@
1
+ export const SEALED_KEY_DIR_MODE = 0o700;
2
+ export const SEALED_KEY_FILE_MODE = 0o600;
3
+ export const CHECKPOINT_CTX_KEY_SUBDIR = "checkpoint-ctx-keys";
4
+ //# sourceMappingURL=sealed-key-layout.js.map
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import _sodium from "libsodium-wrappers";
5
+ import { CHECKPOINT_CTX_KEY_SUBDIR, SEALED_KEY_DIR_MODE, SEALED_KEY_FILE_MODE } from "./sealed-key-layout.js";
5
6
  import { buildDisplaySafeUrl } from "./trace/redact.js";
6
7
  import { createCenterCredentialSource } from "./center-credential.js";
7
8
  import { centerFetch } from "./web-admin-client/center-request.js";
@@ -61,8 +62,8 @@ export function judgeSealedKeyPublicKey(publicKeyB64) {
61
62
  export async function ensureSealedKeyStore(dir, logger) {
62
63
  await _sodium.ready;
63
64
  const sodium = _sodium;
64
- await mkdir(dir, { recursive: true, mode: 0o700 });
65
- await chmod(dir, 0o700);
65
+ await mkdir(dir, { recursive: true, mode: SEALED_KEY_DIR_MODE });
66
+ await chmod(dir, SEALED_KEY_DIR_MODE);
66
67
  const keys = new Map();
67
68
  for (const name of (await readdir(dir)).filter((n) => n.endsWith(".json")).sort()) {
68
69
  const path = join(dir, name);
@@ -99,8 +100,8 @@ export async function ensureSealedKeyStore(dir, logger) {
99
100
  createdAt: new Date().toISOString(),
100
101
  };
101
102
  const path = join(dir, `${record.publicKeyId}.json`);
102
- await writeFile(path, JSON.stringify(record, null, 2) + "\n", { mode: 0o600 });
103
- await chmod(path, 0o600);
103
+ await writeFile(path, JSON.stringify(record, null, 2) + "\n", { mode: SEALED_KEY_FILE_MODE });
104
+ await chmod(path, SEALED_KEY_FILE_MODE);
104
105
  keys.set(record.publicKeyId, { publicKey: kp.publicKey, privateKey: kp.privateKey, publicKeyB64, createdAt: record.createdAt });
105
106
  logger?.info("sealed_key_generated", { publicKeyId: record.publicKeyId, publicKey: publicKeyB64, dir, note: "new X25519 sealed-box keypair in this directory — the private key exists only here: keep the directory across restarts and identical on every replica that has to open what it sealed, and back it up as a private key" });
106
107
  }
@@ -120,7 +121,6 @@ export async function ensureSealedKeyStore(dir, logger) {
120
121
  },
121
122
  };
122
123
  }
123
- const CHECKPOINT_CTX_KEY_SUBDIR = "checkpoint-ctx-keys";
124
124
  function mintCheckpointCtxKeyStore(store) {
125
125
  return store;
126
126
  }
@@ -4,10 +4,10 @@ import type { ServiceConfig } from "./config-types.js";
4
4
  import { type DeploymentShapeInput } from "./deployment-shape.js";
5
5
  import type { TaskRequestBody } from "./http/wire-types.js";
6
6
  import type { StagingHandle } from "./session-sync-kernel.js";
7
- import type { ClientSessionRowWrite, ReplaySessionRowWrite, ReplaySessionRowWriteOutcome, SessionRowPatch, SessionRowWriteOutcome } from "./session-row.js";
7
+ import type { ClientSessionRowWrite, ReplaySessionRowWrite, ReplaySessionRowWriteOutcome, SessionRowAnswer, SessionRowPatch, SessionRowWriteOutcome } from "./session-row.js";
8
8
  import type { ExecutionPlacementSeat } from "./execution-placement.js";
9
9
  import type { SessionUnreadable } from "./session-error-wire.js";
10
- import type { ForkTarget, ForkToOutcome } from "./plugins/session-fork.js";
10
+ import type { ForkPlacement, ForkTarget, ForkToOutcome } from "./plugins/session-fork.js";
11
11
  /** One row of the session-list projection (CC /resume picker) — the durable session abstraction's enumeration
12
12
  * output (§0.5). Mirrors {@link RunStore}'s shipped `SessionSummary` shape so the wire response is unchanged. */
13
13
  export interface SessionListItem {
@@ -104,7 +104,7 @@ export type SessionOwnerCell = {
104
104
  readonly placed: boolean;
105
105
  };
106
106
  /** 会话行属主门的调用方:验过的主体(`gatedPrincipal`)+ 它是不是 **fleet 调用方**(dev-open 部署上的任何调用方,或出示了这台部署的 service 凭据、
107
- * 不报主体的调用方)。唯一铸点 `http/principal-gate.ts` 的 `sessionCallerOf`。 */
107
+ * 不报主体的调用方 —— 除非部署声明 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false`)。唯一铸点 `http/principal-gate.ts` 的 `sessionCallerOf`。 */
108
108
  export interface SessionCaller {
109
109
  readonly principal: Principal | undefined;
110
110
  readonly fleet: boolean;
@@ -114,7 +114,8 @@ export interface SessionCaller {
114
114
  * 审批豁免面、notify 面的无主臂都经它,不各写一份):
115
115
  * · 无主的**普通**会话行 ⇒ 成立(各臂照它自己的其余条件);
116
116
  * · 无主的 **placed** 行(委派子代:属主 = 引擎判的委派树属主,三只持久会话店建行那一笔原样记 `placement.owner`,`null` = 根匿名;`delegated-session-owner.ts` 头注)⇒ 只对 **fleet 调用方**成立({@link SessionCaller}:dev-open 部署上的任何调用方,
117
- * 或出示了 service 凭据、不报主体的调用方 —— 只配 token 的单用户部署、BFF 的系统调用)。任何具名主体(含经 BFF 代发的)与没有 service 凭据的无身份调用方都不成立:
117
+ * 或出示了 service 凭据、不报主体的调用方 —— 只配 token 的单用户部署、BFF 的系统调用;部署声明 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false` 时后者不是 fleet)。
118
+ * 任何具名主体(含经 BFF 代发的)与无身份调用方(没有 service 凭据的;声明形上不报主体的 service 凭据)都不成立:
118
119
  * 这一行要么属于一棵无身份调用方的树(引擎交的属主是 `null`),要么是属主从没被记下的行 —— 7.144.0 之前写下的存量(7.132.x 及更早建行不记属主;7.133.0–7.143.x 按本服务当年的推断写下,
119
120
  * 推断那一刻来源还没有属主的仍无主;SQL 两店由一次性迁移 M23 在它的窗内回填),以及 local 文件店上 7.144.x 及更早建行两笔之间崩溃留下的行(7.145.0 起建行格先、存档后,不再新生)、
120
121
  * 同一数据根上 run-local 腿经 core 店写下的 placed 存档(local 没有回填臂)
@@ -175,6 +176,20 @@ export declare function legacyUnownedSessionOk(deployment: DeploymentShapeInput<
175
176
  * 单实体读/写点(fork/delete/同步族/workspace)**一律**用三条件齐的 {@link legacyUnownedSessionOk},不要用本函数。
176
177
  */
177
178
  export declare function legacyUnownedAmnestyOpen(deployment: DeploymentShapeInput<"principal">, gateOwner: string | null): boolean;
179
+ /** [ref] 存量哨兵窄互认的行属主字面量 —— cli ≤1.0.70 在本地单用户模式自发的 principal 头,存量会话的属主列就是它(引擎不发明这个字面量)。 */
180
+ export declare const LEGACY_SHELL_SENTINEL_OWNER = "anon:shell-live";
181
+ /**
182
+ * 🔴 **会话属主等价律 —— 「行上记下的属主」与「这一次的调用方 / 住客」是不是同一位,唯一判官**(两只窄互认臂收在这一处,读者不各写一份):
183
+ * · 字面相等(含两边都无主 —— 单用户形无身份的调用方与无主的行);
184
+ * · [ref] 窄互认第二臂的身份半支:单机形 ∧ 行无主 ∧ 对方恰为 `default`({@link legacyUnownedAmnestyOpen});
185
+ * · [ref] 存量哨兵臂:非 `requirePrincipal` ∧ 行恰为 {@link LEGACY_SHELL_SENTINEL_OWNER} ∧ 对方没有主体。
186
+ * **有方向、不传递**:`rowOwner` 是行上记下的(或删前记下的)属主,`principal` 是这一次的调用方 / 住客(没有主体 = `null`);
187
+ * 两条臂各自只认它那一对,不由它们推出 `default` ≡ 哨兵、也不反过来认「`default` 的行 ≡ 无主体的调用方」。
188
+ * 读者:`createAuthorizer` 的会话属主门(有主的行)、`http/principal-gate.ts` 的 `runOwnerReadable`(有主的行)、`deleted-session-ids.ts` 的墓碑读点(删前属主 vs 本次住客)。
189
+ * 不在本判官里的两件(它们问的不是「是不是同一位」):无主的委派子代 placed 行只对 fleet 调用方开放(读面律 {@link ownerlessRowOpen});
190
+ * 删除 / fork / 同步族的放行另有 fleet 臂与 placed 判({@link legacyUnownedSessionOk} = 本判官的第二臂 ∧ {@link ownerlessRowOpen})。
191
+ */
192
+ export declare function sameSessionOwner(deployment: DeploymentShapeInput<"principal">, rowOwner: string | null, principal: string | null): boolean;
178
193
  /**
179
194
  * 本仓会话店的面 = core `SessionStore` + 本仓的行面(登记 / 列表 / 换装 / 同步……)。
180
195
  * 🔴 **按 id 读一行只有一只**:core 的 `headOf(id)`(`{ owner, incarnation, leafId }`,同一行一次读;core 7.55.0 [ref] S2)。
@@ -203,6 +218,19 @@ export type OwnerAwareSessionStore = SessionStore & {
203
218
  limit: number;
204
219
  q?: string;
205
220
  }) => Promise<SessionListItem[]>;
221
+ /** The SAME list face as {@link listSessions} (same SELECT, same mapper, same predicate and keyset) with each row's occupant — the row's owner and
222
+ * incarnation as read in that one read. The incarnation never enters the row (it is not a wire fact: `session-incarnation.ts`); this pair is for the
223
+ * in-process reader that must address a row by occupant — the peer directory's `PeerSessionRecord.occupant`. `listSessions` is this face's rows. */
224
+ listSessionRows?: (opts: {
225
+ owner?: string;
226
+ includeUnowned?: boolean;
227
+ cursor?: {
228
+ lastActivityAt: string;
229
+ sessionId: string;
230
+ };
231
+ limit: number;
232
+ q?: string;
233
+ }) => Promise<Array<SessionRowAnswer<SessionListItem>>>;
206
234
  /** ONE session's list row — the SAME projection {@link listSessions} emits for it (same columns, same mapper). `undefined` = not a
207
235
  * row of the list face (no such session, or a placed / delegation-transcript session, which never lists). Reader: the 2c
208
236
  * manifest (`title` / `pinned`). */
@@ -217,8 +245,12 @@ export type OwnerAwareSessionStore = SessionStore & {
217
245
  writeSessionRowCells?: SessionRowCellsWriter;
218
246
  /** E17 — fork a session's whole history to a NEW session; null if the source is unknown. The fork row records `owner` when it is a
219
247
  * string, else (`null`) the SOURCE row's owner — a copy of an owned history is never anonymous (core `SessionStore.fork`, 7.55.0 S2).
220
- * On the three stores it is the projection `forkTo(…, { id: uuidv7() })` of {@link forkTo} (one private body). */
221
- fork?: (sourceId: string, owner: string | null) => Promise<string | null>;
248
+ * On the three stores it is the projection `forkTo(…, { id: uuidv7(), placement? })` of {@link forkTo} (one private body). `opts.placement`
249
+ * (core `SessionStore.fork`; engine-internal: a background fork child's transcript) forks INTO the delegation partition — the creating write
250
+ * lays the tuple down as a placed acquire does; a store that declares no partition ignores it. */
251
+ fork?: (sourceId: string, owner: string | null, opts?: {
252
+ placement?: ForkPlacement;
253
+ }) => Promise<string | null>;
222
254
  /** fork 续写的店契约(三店同一只私有体;规则与结果闭集在 `plugins/session-fork.ts`):把源会话整份历史拷进调用方给的目标 id;`target.key` 存进新行那一格 fork 键。
223
255
  * `null` = 源不在;`created` / `replayed`(同属主同键)/ `occupied{sameOwner}`(撞上别的行)。目标行原子新建(SQL 主键排他、meta 先;local 两把写锁 + 格先)。 */
224
256
  forkTo?: (sourceId: string, owner: string | null, target: ForkTarget) => Promise<ForkToOutcome | null>;
package/dist/security.js CHANGED
@@ -39,6 +39,14 @@ export function legacyUnownedSessionOk(deployment, caller, cell) {
39
39
  export function legacyUnownedAmnestyOpen(deployment, gateOwner) {
40
40
  return !DEPLOYMENT_SHAPE_PREDICATES.multiTenant.holds(deployment) && gateOwner === LEGACY_UNOWNED_AMNESTY_TENANT;
41
41
  }
42
+ export const LEGACY_SHELL_SENTINEL_OWNER = "anon:shell-live";
43
+ export function sameSessionOwner(deployment, rowOwner, principal) {
44
+ if (rowOwner === principal)
45
+ return true;
46
+ if (rowOwner === null)
47
+ return legacyUnownedAmnestyOpen(deployment, principal);
48
+ return !DEPLOYMENT_SHAPE_PREDICATES.principalRequired.holds(deployment) && rowOwner === LEGACY_SHELL_SENTINEL_OWNER && principal === null;
49
+ }
42
50
  const UUIDV7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
43
51
  export function isUuidV7(s) {
44
52
  return UUIDV7_RE.test(s);
@@ -111,8 +119,7 @@ export function createAuthorizer(config, sessionStore) {
111
119
  await register(requested, principal);
112
120
  owner = await readOwner(requested);
113
121
  }
114
- const legacyShellOwner = !config.requirePrincipal && owner === "anon:shell-live" && principal === undefined;
115
- if (!legacyShellOwner && owner != null && owner !== principal) {
122
+ if (owner != null && !sameSessionOwner(config, owner, principal ?? null)) {
116
123
  throw principal
117
124
  ? new HttpError(403, "session does not belong to this principal")
118
125
  : new HttpError(401, `session is principal-owned; missing principal header '${config.principalHeader}'`);
@@ -24,9 +24,11 @@
24
24
  *
25
25
  * ## 调用方(两只,都经 {@link SessionNotifyDoor.deliver},不各写一份臂)
26
26
  * · `POST /v1/sessions/:id/notify`(`http/routes/notify-wake.ts`):入站鉴权、验型、属主门在路由;属主 = 会话行的属主。
27
- * · **Runner 之外的后台子代完成**({@link createBackgroundCompletionRelay}):停驻赎回腿的复活、操作员 `handle.resume` —— 这些续跑不是哪一次 run 内的调用,
28
- * 没有调用车道(`ToolExecuteContext.onTaskNotification`),引擎把完成交给委派工具挂载期的 `background.notify`;那只 sink 在 `capabilities/scenarios.ts` 接一次,
29
- * 交到委派树根会话的这只门。Runner 之内的调用照旧走调用车道(引擎 `resolveCompletionSink` = 调用 ?? 挂载),这只门不经手。
27
+ * · **Runner 之外的后台子代完成**({@link createBackgroundCompletionRelay}):操作员 `handle.resume`,与**停驻落在别的进程**(另一副本 / 重启之前)的停驻赎回腿复活 ——
28
+ * 这些续跑不是哪一次 run 内的调用,本进程也没有它们欠账的车道,引擎把完成交给委派工具挂载期的 `background.notify`;那只 sink 在 `capabilities/scenarios.ts`
29
+ * 接一次,交到委派树根会话的这只门(停驻赎回那一形引擎另说一次 `delegation.resume_lane_missing`,运维受众)。本进程持有停驻的赎回(core 7.63.0 起)是停驻
30
+ * 周期的延续:完成回到周期欠账的那条车道(父 run 的车道;父 run 已收场 ⇒ 根会话的活 run 或下一轮),这只门不经手。Runner 之内的调用照旧走调用车道
31
+ * (引擎 `resolveCompletionSink` = 调用 ?? 停驻车道 ?? 挂载)。
30
32
  */
31
33
  import type { ExternalNotificationInput, SessionHead, SubagentToolOptions, TaskStream } from "@sema-agent/core";
32
34
  import { type WorkflowCompletionInbox } from "./orchestration/workflow-completion-inbox.js";