@sema-agent/server 7.103.1 → 7.104.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 (177) hide show
  1. package/MIGRATION.md +362 -3
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +279 -18
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +4 -0
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +13 -28
  58. package/dist/config.js +53 -70
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/host-lsp-manager.d.ts +1 -1
  66. package/dist/http/admission.d.ts +11 -4
  67. package/dist/http/admission.js +5 -1
  68. package/dist/http/dispatch.js +3 -3
  69. package/dist/http/principal-gate.d.ts +22 -0
  70. package/dist/http/principal-gate.js +20 -0
  71. package/dist/http/resume-legs.js +9 -5
  72. package/dist/http/routes/agents-roster.d.ts +7 -0
  73. package/dist/http/routes/agents-roster.js +6 -2
  74. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  75. package/dist/http/routes/approvals-assistant.js +117 -50
  76. package/dist/http/routes/capabilities.js +3 -2
  77. package/dist/http/routes/config-catalog.js +1 -0
  78. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  79. package/dist/http/routes/memory-policy.js +3 -3
  80. package/dist/http/routes/rules.js +2 -2
  81. package/dist/http/routes/runs.js +27 -2
  82. package/dist/http/routes/session-sync.js +8 -14
  83. package/dist/http/routes/sessions-list.js +3 -2
  84. package/dist/http/routes/sessions.js +44 -23
  85. package/dist/http/routes/shared-memory.js +2 -2
  86. package/dist/http/routes/tasks.js +22 -19
  87. package/dist/http/routes/trace-usage.js +4 -4
  88. package/dist/http/send.d.ts +12 -0
  89. package/dist/http/send.js +12 -1
  90. package/dist/http/server.d.ts +21 -4
  91. package/dist/http/server.js +7 -4
  92. package/dist/http/sync-submit-body.d.ts +43 -0
  93. package/dist/http/sync-submit-body.js +4 -0
  94. package/dist/http/wire-types.d.ts +23 -5
  95. package/dist/index.d.ts +1 -0
  96. package/dist/index.js +1 -0
  97. package/dist/leader/merge.d.ts +8 -3
  98. package/dist/memory-incident-sink.js +2 -1
  99. package/dist/model-select.d.ts +9 -0
  100. package/dist/model-select.js +5 -1
  101. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  102. package/dist/observability/corrupt-read-seat.js +158 -15
  103. package/dist/observability/fail-open.d.ts +12 -0
  104. package/dist/observability/fail-open.js +12 -0
  105. package/dist/observability/run-terminal.d.ts +15 -1
  106. package/dist/observability/run-terminal.js +4 -0
  107. package/dist/parked-decide.d.ts +8 -3
  108. package/dist/parked-decide.js +56 -20
  109. package/dist/parked-revive-surface.d.ts +14 -7
  110. package/dist/parked-revive-surface.js +5 -1
  111. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  112. package/dist/plugins/approval-ask-store-file.js +9 -12
  113. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  114. package/dist/plugins/approval-ask-store-sql.js +4 -1
  115. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  116. package/dist/plugins/approval-exemption-store.js +36 -7
  117. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  118. package/dist/plugins/approval-nonce-store.js +17 -7
  119. package/dist/plugins/caching-session-store.d.ts +5 -0
  120. package/dist/plugins/caching-session-store.js +3 -0
  121. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  122. package/dist/plugins/file-resume-anchor-store.js +19 -5
  123. package/dist/plugins/file-run-store.d.ts +21 -8
  124. package/dist/plugins/file-run-store.js +60 -38
  125. package/dist/plugins/local-session-store.d.ts +22 -0
  126. package/dist/plugins/local-session-store.js +74 -11
  127. package/dist/plugins/memory-run-store.d.ts +7 -5
  128. package/dist/plugins/memory-run-store.js +11 -4
  129. package/dist/plugins/one-time-migrations.d.ts +38 -1
  130. package/dist/plugins/one-time-migrations.js +143 -15
  131. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  132. package/dist/plugins/permission-rule-store-file.js +37 -51
  133. package/dist/plugins/pg-session-storage.d.ts +3 -0
  134. package/dist/plugins/pg-session-storage.js +19 -10
  135. package/dist/plugins/run-store-sql.d.ts +12 -4
  136. package/dist/plugins/run-store-sql.js +15 -13
  137. package/dist/plugins/send-file-ledger.d.ts +5 -1
  138. package/dist/plugins/send-file-ledger.js +11 -6
  139. package/dist/plugins/session-event-payload.d.ts +14 -4
  140. package/dist/plugins/session-event-payload.js +5 -2
  141. package/dist/plugins/store-backend.d.ts +7 -3
  142. package/dist/plugins/store-backend.js +10 -6
  143. package/dist/plugins/store-contracts.d.ts +7 -2
  144. package/dist/plugins/tidb-pool.js +8 -2
  145. package/dist/plugins/tidb-session-store.d.ts +6 -1
  146. package/dist/plugins/tidb-session-store.js +17 -10
  147. package/dist/project-identity.d.ts +4 -6
  148. package/dist/request-key-closure.js +1 -1
  149. package/dist/request-value-closure.d.ts +72 -3
  150. package/dist/request-value-closure.js +12 -4
  151. package/dist/roster-bound-http-status.d.ts +6 -0
  152. package/dist/roster-bound-http-status.js +6 -0
  153. package/dist/run-local.js +22 -27
  154. package/dist/runs.d.ts +22 -2
  155. package/dist/runs.js +29 -7
  156. package/dist/sealed-key.d.ts +31 -4
  157. package/dist/sealed-key.js +9 -0
  158. package/dist/security.d.ts +5 -1
  159. package/dist/server-secret-env.d.ts +2 -2
  160. package/dist/session-children-retire.d.ts +92 -0
  161. package/dist/session-children-retire.js +85 -0
  162. package/dist/session-error-wire.d.ts +58 -0
  163. package/dist/session-error-wire.js +38 -0
  164. package/dist/session-incarnation.d.ts +61 -0
  165. package/dist/session-incarnation.js +43 -0
  166. package/dist/session-records-skipped-notice.d.ts +20 -0
  167. package/dist/session-records-skipped-notice.js +27 -0
  168. package/dist/tool-approval.d.ts +65 -0
  169. package/dist/tool-approval.js +36 -9
  170. package/dist/trace/core-keyset-guard.d.ts +8 -7
  171. package/dist/trace/engine-notice-wire.d.ts +3 -1
  172. package/dist/trace/engine-notice-wire.js +2 -0
  173. package/dist/trace/project.d.ts +1 -1
  174. package/dist/trace/project.js +9 -0
  175. package/dist/trace/wire-projection-faces.d.ts +10 -3
  176. package/dist/trace/wire-projection-faces.js +8 -2
  177. package/package.json +3 -3
@@ -2,7 +2,7 @@ import { screenedGate } from "../../trace/project.js";
2
2
  import { hostDecisionFacts } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
5
- import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING } from "../../tool-approval.js";
5
+ import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
8
  import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
@@ -27,30 +27,6 @@ const DECIDE_BODY_DECLARED_KEYS = [
27
27
  ];
28
28
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
29
29
  const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
30
- const ACCEPTED_DECIDE_BODY = new Set(DECIDE_BODY_DECLARED_KEYS);
31
- const UNSUPPORTED_DECIDE_BODY = new Set(["editedPlan", "sessionId", "taskId"]);
32
- export function decideBodyKeyIssue(raw) {
33
- if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
34
- return null;
35
- const buckets = { unknown: [], unsupported: [] };
36
- scanKeyClosure(raw, "", ACCEPTED_DECIDE_BODY, UNSUPPORTED_DECIDE_BODY, buckets);
37
- if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
38
- return null;
39
- buckets.unknown.sort();
40
- buckets.unsupported.sort();
41
- const parts = [
42
- ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a decide-request key — check the spelling)")] : []),
43
- ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (a sibling approval door's field, or a path parameter — never read from this body)")] : []),
44
- ];
45
- return {
46
- message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller ` +
47
- `(a misspelled \`remember\` or \`boundInputHash\` silently turns off an exemption / the decision binding). ` +
48
- `${parts.join("; ")}. ` +
49
- `Accepted: ${[...ACCEPTED_DECIDE_BODY].sort().join(",")}`,
50
- unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
51
- unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
52
- };
53
- }
54
30
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
55
31
  "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
56
32
  ];
@@ -71,29 +47,108 @@ function buildApprovalEnvelope(sessionId, covered, reading) {
71
47
  Object.assign(envelope, { [PROOF_SLOT_OF[key]]: reading[key] });
72
48
  return envelope;
73
49
  }
74
- const ACCEPTED_PLAN_REVIEW_BODY = new Set(PLAN_REVIEW_BODY_DECLARED_KEYS);
75
- const UNSUPPORTED_PLAN_REVIEW_BODY = new Set([
76
- ...DECIDE_BODY_DECLARED_KEYS.filter((k) => !PLAN_REVIEW_BODY_DECLARED_KEYS.includes(k)),
77
- "permissionMode", "sessionId", "taskId",
78
- ]);
79
- export function planReviewBodyKeyIssue(raw) {
50
+ const DECISION_DOOR_FAMILY = {
51
+ "approval-decide": {
52
+ accepted: DECIDE_BODY_DECLARED_KEYS,
53
+ requestNoun: "decide-request",
54
+ costliestTypo: "a misspelled `remember` or `boundInputHash` silently turns off an exemption / the decision binding",
55
+ },
56
+ "assistant-task-plan-review": {
57
+ accepted: PLAN_REVIEW_BODY_DECLARED_KEYS,
58
+ requestNoun: "plan-review-request",
59
+ costliestTypo: "a misspelled `permissionModeAfter` silently leaves this task read-only after you approved its plan",
60
+ },
61
+ };
62
+ const SUBMIT_BODY_NEAR_MISS_KEYS = ["permissionMode"];
63
+ const SUBMIT_ENDPOINT = "POST /v1/tasks";
64
+ function neighborLabel(n) {
65
+ const where = n.where.join(" / ");
66
+ switch (n.family) {
67
+ case "sibling_field":
68
+ return `unsupported (a body field of ${where} — send it there, this door never reads it)`;
69
+ case "path_segment":
70
+ return `unsupported (a path segment of ${where} — identity travels in the URL, never in a body)`;
71
+ case "submit_body":
72
+ return `unsupported (a submit-body key of ${where} — set when the task is submitted, this door never reads it)`;
73
+ default: {
74
+ const unreachable = n.family;
75
+ throw new Error(`decision-door key closure: unknown neighbor family ${String(unreachable)}`);
76
+ }
77
+ }
78
+ }
79
+ function doorEndpointOf(routes, id) {
80
+ const decl = routes.find((r) => "id" in r && r.id === id);
81
+ if (decl === undefined || !("pattern" in decl) || decl.label === undefined) {
82
+ throw new Error(`decision-door family: route "${id}" is not a labelled pattern row of the route table — the key-closure hints are derived from it`);
83
+ }
84
+ const groups = new RegExp(`${decl.pattern.source}|`, decl.pattern.flags).exec("")?.groups ?? {};
85
+ return { endpoint: `${decl.methods.join("|")} ${decl.label}`, pathSegments: Object.keys(groups) };
86
+ }
87
+ export function deriveDecisionDoorKeyTable(family, routes) {
88
+ const ids = Object.keys(family);
89
+ const endpoint = new Map(ids.map((id) => [id, doorEndpointOf(routes, id)]));
90
+ const collect = (pairs) => {
91
+ const m = new Map();
92
+ for (const [k, where] of pairs)
93
+ m.set(k, [...(m.get(k) ?? []), where]);
94
+ return m;
95
+ };
96
+ const siblingWhere = collect(ids.flatMap((id) => family[id].accepted.map((k) => [k, endpoint.get(id).endpoint])));
97
+ const pathWhere = collect(ids.flatMap((id) => endpoint.get(id).pathSegments.map((k) => [k, endpoint.get(id).endpoint])));
98
+ const submitWhere = collect(SUBMIT_BODY_NEAR_MISS_KEYS.map((k) => [k, SUBMIT_ENDPOINT]));
99
+ const sources = [
100
+ ["sibling_field", siblingWhere],
101
+ ["path_segment", pathWhere],
102
+ ["submit_body", submitWhere],
103
+ ];
104
+ for (let i = 0; i < sources.length; i++) {
105
+ for (let j = i + 1; j < sources.length; j++) {
106
+ const [fa, a] = sources[i];
107
+ const [fb, b] = sources[j];
108
+ const both = [...a.keys()].filter((k) => b.has(k));
109
+ if (both.length > 0)
110
+ throw new Error(`decision-door family: key(s) ${both.join(", ")} belong to both ${fa} and ${fb} — a refused key must have exactly one destination`);
111
+ }
112
+ }
113
+ const derive = (self) => {
114
+ const accepted = new Set(family[self].accepted);
115
+ const neighbors = new Map();
116
+ for (const [fam, source] of sources) {
117
+ for (const [k, where] of source)
118
+ if (!accepted.has(k))
119
+ neighbors.set(k, { family: fam, where });
120
+ }
121
+ return { accepted, neighbors, requestNoun: family[self].requestNoun, costliestTypo: family[self].costliestTypo };
122
+ };
123
+ return { "approval-decide": derive("approval-decide"), "assistant-task-plan-review": derive("assistant-task-plan-review") };
124
+ }
125
+ export function decisionBodyKeyIssue(door, raw, table = DECISION_DOOR_KEY_TABLE) {
80
126
  if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
81
127
  return null;
128
+ const d = table[door];
82
129
  const buckets = { unknown: [], unsupported: [] };
83
- scanKeyClosure(raw, "", ACCEPTED_PLAN_REVIEW_BODY, UNSUPPORTED_PLAN_REVIEW_BODY, buckets);
130
+ scanKeyClosure(raw, "", d.accepted, new Set(d.neighbors.keys()), buckets);
84
131
  if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
85
132
  return null;
86
133
  buckets.unknown.sort();
87
134
  buckets.unsupported.sort();
135
+ const byLabel = new Map();
136
+ for (const k of buckets.unsupported) {
137
+ const n = d.neighbors.get(k);
138
+ if (n === undefined)
139
+ throw new Error(`decision-door key closure: "${k}" was classified unsupported but has no destination`);
140
+ const label = neighborLabel(n);
141
+ byLabel.set(label, [...(byLabel.get(label) ?? []), k]);
142
+ }
88
143
  const parts = [
89
- ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a plan-review-request key — check the spelling)")] : []),
90
- ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (a sibling approval door's field, a submit-body key, or a path parameter — never read from this body)")] : []),
144
+ ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, `unknown (not a ${d.requestNoun} key — check the spelling)`)] : []),
145
+ ...[...byLabel].map(([label, keys]) => keyClosureClause(keys, label)),
91
146
  ];
92
147
  return {
93
148
  message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller ` +
94
- `(a misspelled \`permissionModeAfter\` silently leaves this task read-only after you approved its plan). ` +
149
+ `(${d.costliestTypo}). ` +
95
150
  `${parts.join("; ")}. ` +
96
- `Accepted: ${[...ACCEPTED_PLAN_REVIEW_BODY].sort().join(",")}`,
151
+ `Accepted: ${[...d.accepted].sort().join(",")}`,
97
152
  unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
98
153
  unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
99
154
  };
@@ -437,7 +492,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
437
492
  return;
438
493
  }
439
494
  {
440
- const keyIssue = planReviewBodyKeyIssue(body);
495
+ const keyIssue = decisionBodyKeyIssue("assistant-task-plan-review", body);
441
496
  if (keyIssue) {
442
497
  sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
443
498
  return;
@@ -570,7 +625,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
570
625
  return;
571
626
  }
572
627
  {
573
- const keyIssue = decideBodyKeyIssue(body);
628
+ const keyIssue = decisionBodyKeyIssue("approval-decide", body);
574
629
  if (keyIssue) {
575
630
  sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
576
631
  return;
@@ -637,18 +692,29 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
637
692
  sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
638
693
  return;
639
694
  }
640
- if (deps.config.directDoorActive && body.settledBy !== undefined) {
641
- sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
642
- return;
643
- }
644
- if (body.settledBy !== undefined && decision !== "deny") {
645
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
646
- return;
647
- }
648
- if (body.settledBy !== undefined && typeof body.settledBy !== "string") {
649
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
650
- return;
695
+ const settledByVerdict = judgeSettledByBody(body.settledBy, decision, deps.config.directDoorActive);
696
+ switch (settledByVerdict.kind) {
697
+ case "not_in_proof":
698
+ sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
699
+ return;
700
+ case "deny_only":
701
+ sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
702
+ return;
703
+ case "not_string":
704
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
705
+ return;
706
+ case "too_wide":
707
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
708
+ return;
709
+ case "absent":
710
+ case "accepted":
711
+ break;
712
+ default: {
713
+ const unreachable = settledByVerdict;
714
+ throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
715
+ }
651
716
  }
717
+ const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
652
718
  const binding = {
653
719
  checkpointToken: body.checkpointToken,
654
720
  boundCallId: body.boundCallId,
@@ -697,7 +763,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
697
763
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
698
764
  }
699
765
  : undefined;
700
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(body.settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
766
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
701
767
  const outErrorCode = out.body.errorCode;
702
768
  if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
703
769
  try {
@@ -832,4 +898,5 @@ export const APPROVALS_ASSISTANT_ROUTES = [
832
898
  { id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: true, methods: ["POST"] },
833
899
  { prefix: "/v1/approvals", billable: true },
834
900
  ];
901
+ const DECISION_DOOR_KEY_TABLE = deriveDecisionDoorKeyTable(DECISION_DOOR_FAMILY, APPROVALS_ASSISTANT_ROUTES);
835
902
  //# sourceMappingURL=approvals-assistant.js.map
@@ -1,7 +1,7 @@
1
1
  import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { isModelAllowlisted, selectableCatalog } from "../../model-select.js";
3
3
  import { resolveModelApiKey } from "../../key-resolver.js";
4
- import { isSealedKeyPoison } from "../../sealed-key.js";
4
+ import { isSealedKeyPoison, projectSealedKeyCapability } from "../../sealed-key.js";
5
5
  import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
6
6
  import { taskApproverPostureHonored, taskWriteFaceRefusal } from "../../hosted-posture.js";
7
7
  import { autonomyOverrides } from "../../runtime-governance.js";
@@ -176,7 +176,7 @@ export async function handleCapabilities(req, res, match, ctx) {
176
176
  rewindFiles: Boolean(deps.fileHistoryStore),
177
177
  restoreFiles: Boolean(deps.fileHistoryStore),
178
178
  rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
179
- fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore),
179
+ fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore, deps.config.fileHistoryCapture),
180
180
  scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
181
181
  sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.sessionStorage?.beginImportStaging && deps.fileHistoryStore),
182
182
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
@@ -199,6 +199,7 @@ export async function handleCapabilities(req, res, match, ctx) {
199
199
  })(),
200
200
  manualCompact: Boolean(deps.runStore),
201
201
  configCatalog: deps.config.configCatalogEnabled === true && deps.config.operatorPrincipals.length > 0,
202
+ sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
202
203
  });
203
204
  return;
204
205
  }
@@ -23,6 +23,7 @@ export async function handleConfigCatalog(req, res, match, ctx) {
23
23
  sendJson(res, 200, buildConfigCatalog({
24
24
  config: deps.config,
25
25
  env: process.env,
26
+ boot: { ...(deps.memoryConsolidationValve !== undefined ? { memoryConsolidationValve: deps.memoryConsolidationValve } : {}) },
26
27
  knobs: configKnobTable(),
27
28
  centerDomains: DOMAINS,
28
29
  centerManagedKeys: deps.configCenterManagedKeys?.(),
@@ -11,8 +11,9 @@
11
11
  *
12
12
  * ── 为什么缺席形是 **404 族**而不是 501(D4)───────────────────────────────────────────────────────
13
13
  * 本仓的 501 `capability.*` 说的是「**这家店**在本部署上不存在,换部署形态才行」(记忆引擎没接线、
14
- * 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off`,
15
- * 而且是出厂缺省),不是部署能力的缺失。而阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面
14
+ * 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off`),或
15
+ * 出厂缺省派生成关(S-824 / [ref]:未设而整理座落主模型 —— 起服时有一行提示点名 `MODEL_CHEAP_ID`),不是部署能力的
16
+ * 缺失。而阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面
16
17
  * 归属 / provenance=off)在**启动期**就被拒了(`boot/memory-consolidation.ts` 的
17
18
  * `assertConsolidationDriverWirable`)—— 那些机器根本起不来,不会有一个"挂着的 501"去代表它们。
18
19
  * ⇒ 剩下的唯一缺席成因就是「阀门没开」,而它的诚实形是**这条路径不存在**:挂载条件写在 server.ts 的
@@ -2,7 +2,7 @@ import { formatUserScope } from "@sema-agent/core";
2
2
  import { parseMemorySyncRequest } from "../../memory-sync.js";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
- import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
7
  const ORG_PREFIX = "org:";
8
8
  export const MEMORY_EXPORT_MAX_LIMIT = 500;
@@ -68,7 +68,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
68
68
  case "memory-export": {
69
69
  const principal = gatedPrincipal(req, deps.config);
70
70
  if (!principal) {
71
- sendError(res, 401, "auth.unauthorized", "unauthorized");
71
+ sendUnauthorized(res, "identity");
72
72
  return;
73
73
  }
74
74
  if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
@@ -128,7 +128,7 @@ export async function handleMemoryPolicy(req, res, match, ctx) {
128
128
  case "memory-sync-scope": {
129
129
  const principal = gatedPrincipal(req, deps.config);
130
130
  if (!principal) {
131
- sendError(res, 401, "auth.unauthorized", "unauthorized");
131
+ sendUnauthorized(res, "identity");
132
132
  return;
133
133
  }
134
134
  if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { sendJson, sendError, httpErrorCode } from "../send.js";
3
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
3
+ import { gatedPrincipal, explicitOperatorOk, sendUnauthorized } from "../principal-gate.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import { createLogger } from "../../observability/logger.js";
6
6
  import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
@@ -90,7 +90,7 @@ export async function handleRules(req, res, match, ctx) {
90
90
  return;
91
91
  const principal = gatedPrincipal(req, deps.config);
92
92
  if (principal === undefined) {
93
- sendError(res, 401, "auth.unauthorized", "unauthorized");
93
+ sendUnauthorized(res, "identity");
94
94
  return;
95
95
  }
96
96
  const lane = deps.ruleConsent;
@@ -16,7 +16,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
16
16
  import { streamSseLog } from "../sse-log.js";
17
17
  import { bindSseLifecycle } from "../sse-lifecycle.js";
18
18
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
19
- import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
19
+ import { ASK_DECISION_CONSUMER_ABSENT, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, outcomeOfDecidedRow, resolveStreamApprovalGate, respondSettledBy } from "../../tool-approval.js";
20
20
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
21
21
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
22
22
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
@@ -45,6 +45,7 @@ const AskDecisionBodySchema = z
45
45
  .refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
46
46
  .optional(),
47
47
  actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
48
+ settledBy: z.unknown().optional(),
48
49
  })
49
50
  .strict();
50
51
  const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
@@ -1287,10 +1288,33 @@ export async function handleRunVerbs(req, res, match, ctx) {
1287
1288
  }
1288
1289
  const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
1289
1290
  if (!parsedBody.success) {
1290
- sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label");
1291
+ sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / settledBy");
1291
1292
  return;
1292
1293
  }
1293
1294
  const body = parsedBody.data;
1295
+ const settledByVerdict = judgeSettledByBody(body.settledBy, body.decision, deps.config.directDoorActive);
1296
+ switch (settledByVerdict.kind) {
1297
+ case "not_in_proof":
1298
+ sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
1299
+ return;
1300
+ case "deny_only":
1301
+ sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
1302
+ return;
1303
+ case "not_string":
1304
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
1305
+ return;
1306
+ case "too_wide":
1307
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
1308
+ return;
1309
+ case "absent":
1310
+ case "accepted":
1311
+ break;
1312
+ default: {
1313
+ const unreachable = settledByVerdict;
1314
+ throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
1315
+ }
1316
+ }
1317
+ const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
1294
1318
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1295
1319
  const taskId = match.params.taskId;
1296
1320
  const askId = match.params.askId;
@@ -1424,6 +1448,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1424
1448
  decisionActor: actor,
1425
1449
  decisionNote: body.note ?? null,
1426
1450
  idempotencyKey: body.idempotencyKey ?? null,
1451
+ ...(settledBy !== undefined ? { settledBy: respondSettledBy({ settledBy }) } : {}),
1427
1452
  ...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
1428
1453
  });
1429
1454
  }
@@ -1,13 +1,13 @@
1
1
  import { once } from "node:events";
2
2
  import { randomBytes, createHash } from "node:crypto";
3
- import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, validateFileHistoryExport } from "@sema-agent/core";
3
+ import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, validateFileHistoryExport } from "@sema-agent/core";
4
4
  import { StringDecoder } from "node:string_decoder";
5
5
  import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, overwriteWipeIncapable, wipeSessionAttendants } from "../../session-sync.js";
6
6
  import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
7
7
  import { isUuidShape } from "../../security.js";
8
- import { sendJson, sendError, sendSessionPolicyError, msg } from "../send.js";
8
+ import { sendJson, sendError, sendSessionError, sendSessionPolicyError, msg } from "../send.js";
9
9
  import { buildDisplaySafeUrl } from "../../trace/redact.js";
10
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
10
+ import { gatedPrincipal, explicitOperatorOk, sendUnauthorized } from "../principal-gate.js";
11
11
  const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
12
12
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
13
13
  const SYNC_IMPORT_BATCH = 500;
@@ -43,12 +43,12 @@ export async function handleSessionSync(req, res, match, ctx) {
43
43
  }
44
44
  const historyStore = deps.fileHistoryStore;
45
45
  if (historyStore === undefined) {
46
- sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: file-history capture is turned off (FILE_HISTORY_CAPTURE=false), so there is no file history to export or import");
46
+ sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: no file-history store is wired, so there is no file history to export or import");
47
47
  return;
48
48
  }
49
49
  const scope = sessionOwnerScopeForWrite(req);
50
50
  if (!scope.fleetWide && scope.gateOwner === null) {
51
- sendError(res, 401, "auth.unauthorized", "unauthorized");
51
+ sendUnauthorized(res, "identity");
52
52
  return;
53
53
  }
54
54
  const sessionId = safeDecode(match.params.sessionId);
@@ -459,20 +459,14 @@ export async function handleSessionSync(req, res, match, ctx) {
459
459
  }
460
460
  }
461
461
  catch (e) {
462
+ await pending.handle.abort().catch(() => undefined);
463
+ cleanupStaging(stagingId);
462
464
  if (e instanceof PrecheckError) {
463
- await pending.handle.abort().catch(() => undefined);
464
- cleanupStaging(stagingId);
465
465
  sendError(res, e.status, "invalid_entries", e.message);
466
466
  return;
467
467
  }
468
- if (e instanceof SessionError && e.code === "invalid_session") {
469
- await pending.handle.abort().catch(() => undefined);
470
- cleanupStaging(stagingId);
471
- sendError(res, 422, "invalid_entries", "staged session log failed validation");
468
+ if (sendSessionError(res, e, "import"))
472
469
  return;
473
- }
474
- await pending.handle.abort().catch(() => undefined);
475
- cleanupStaging(stagingId);
476
470
  throw e;
477
471
  }
478
472
  const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
@@ -1,5 +1,6 @@
1
1
  import { isUuidV7, legacyUnownedAmnestyOpen } from "../../security.js";
2
2
  import { sendJson, sendError } from "../send.js";
3
+ import { sendUnauthorized } from "../principal-gate.js";
3
4
  import { filterFingerprint } from "../cursor-fingerprint.js";
4
5
  import { sessionTempoOfRunStatus } from "../../plugins/store-contracts.js";
5
6
  import { assertNever } from "../route-ctx.js";
@@ -10,7 +11,7 @@ export async function handleSessionsList(req, res, match, ctx) {
10
11
  case "sessions-list": {
11
12
  const { fleetWide, gateOwner } = sessionOwnerScope(req);
12
13
  if (!fleetWide && gateOwner === null) {
13
- sendError(res, 401, "auth.unauthorized", "unauthorized");
14
+ sendUnauthorized(res, "identity");
14
15
  return;
15
16
  }
16
17
  const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
@@ -25,7 +26,7 @@ export async function handleSessionsList(req, res, match, ctx) {
25
26
  case "sendfile-links": {
26
27
  const { fleetWide, gateOwner } = sessionOwnerScope(req);
27
28
  if (!fleetWide && gateOwner === null) {
28
- sendError(res, 401, "auth.unauthorized", "unauthorized");
29
+ sendUnauthorized(res, "identity");
29
30
  return;
30
31
  }
31
32
  if (!deps.sendFileLedger) {
@@ -7,9 +7,10 @@ import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { targetOutsideStoreOf } from "../../memory-store-refusal.js";
8
8
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
9
9
  import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
- import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
- import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
10
+ import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
+ import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
12
12
  import { assertNever } from "../route-ctx.js";
13
+ import { rosterAgentId } from "./agents-roster.js";
13
14
  const MCP_STATUS_TTL_MS = 5_000;
14
15
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
15
16
  const A2A_STATUS_TTL_MS = 5_000;
@@ -67,6 +68,34 @@ async function readLastLegMcp(deps, sessionId, principal) {
67
68
  export async function handleSessions(req, res, match, ctx) {
68
69
  const { deps } = ctx;
69
70
  const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
71
+ const readOwnedSessionView = async (sessionAudit, sessionId, notFoundForm) => {
72
+ const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
73
+ if (ownerOf !== undefined) {
74
+ const owner = await ownerOf(sessionId);
75
+ if (owner === undefined) {
76
+ sendError(res, 404, "not_found.session", "session not found");
77
+ return undefined;
78
+ }
79
+ if (!runOwnerOk(req, res, owner, notFoundForm))
80
+ return undefined;
81
+ }
82
+ let audit;
83
+ try {
84
+ audit = await sessionAudit(sessionId);
85
+ }
86
+ catch (e) {
87
+ if (ownerOf !== undefined)
88
+ throw e;
89
+ throw new Error(`session view read failed before ownership could be established: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
90
+ }
91
+ if (!audit) {
92
+ sendError(res, 404, "not_found.session", "session not found");
93
+ return undefined;
94
+ }
95
+ if (!runOwnerOk(req, res, audit.owner, notFoundForm))
96
+ return undefined;
97
+ return audit;
98
+ };
70
99
  const sse = ctx.local.sessions;
71
100
  const { mcpStatusCache, a2aStatusCache } = ctx.local.sessions;
72
101
  {
@@ -441,6 +470,8 @@ export async function handleSessions(req, res, match, ctx) {
441
470
  }
442
471
  catch (e) {
443
472
  cleanup();
473
+ if (!res.headersSent && sendSessionError(res, e, "open"))
474
+ return;
444
475
  if (!res.headersSent)
445
476
  sendError(res, 500, "internal.error", `session events failed: ${e instanceof Error ? e.message : "unknown"}`);
446
477
  else
@@ -467,12 +498,7 @@ export async function handleSessions(req, res, match, ctx) {
467
498
  sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
468
499
  return;
469
500
  }
470
- const audit = await deps.sessionAudit(sessionId);
471
- if (!audit) {
472
- sendError(res, 404, "not_found.session", "session not found");
473
- return;
474
- }
475
- if (!runOwnerOk(req, res, audit.owner, "session"))
501
+ if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
476
502
  return;
477
503
  const faces = deps.memoryCompliance;
478
504
  if (faces === undefined) {
@@ -532,12 +558,7 @@ export async function handleSessions(req, res, match, ctx) {
532
558
  sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
533
559
  return;
534
560
  }
535
- const audit = await deps.sessionAudit(sessionId);
536
- if (!audit) {
537
- sendError(res, 404, "not_found.session", "session not found");
538
- return;
539
- }
540
- if (!runOwnerOk(req, res, audit.owner, "session"))
561
+ if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
541
562
  return;
542
563
  const face = deps.sessionMemoryStatus;
543
564
  if (face === undefined) {
@@ -581,12 +602,8 @@ export async function handleSessions(req, res, match, ctx) {
581
602
  sendError(res, 404, "not_found.route", "not found");
582
603
  return;
583
604
  }
584
- const audit = await deps.sessionAudit(match.params.sessionId);
585
- if (!audit) {
586
- sendError(res, 404, "not_found.session", "session not found");
587
- return;
588
- }
589
- if (!runOwnerOk(req, res, audit.owner))
605
+ const audit = await readOwnedSessionView(deps.sessionAudit, match.params.sessionId, "run");
606
+ if (audit === undefined)
590
607
  return;
591
608
  const q = new URLSearchParams(match.search);
592
609
  const qi = (k) => {
@@ -636,7 +653,7 @@ export async function handleSessions(req, res, match, ctx) {
636
653
  }
637
654
  const scope = sessionOwnerScopeForWrite(req);
638
655
  if (!scope.fleetWide && scope.gateOwner === null) {
639
- sendError(res, 401, "auth.unauthorized", "unauthorized");
656
+ sendUnauthorized(res, "identity");
640
657
  return;
641
658
  }
642
659
  const sourceId = safeDecode(match.params.sessionId);
@@ -673,7 +690,7 @@ export async function handleSessions(req, res, match, ctx) {
673
690
  }
674
691
  const scope = sessionOwnerScopeForWrite(req);
675
692
  if (!scope.fleetWide && scope.gateOwner === null) {
676
- sendError(res, 401, "auth.unauthorized", "unauthorized");
693
+ sendUnauthorized(res, "identity");
677
694
  return;
678
695
  }
679
696
  const sessionId = safeDecode(match.params.sessionId);
@@ -700,6 +717,10 @@ export async function handleSessions(req, res, match, ctx) {
700
717
  sendError(res, 409, "conflict.session_active_run", "session has an active run; cancel it before deleting", { activeTaskId: result.active });
701
718
  return;
702
719
  }
720
+ if ("childrenAlive" in result) {
721
+ sendError(res, 409, "conflict.session_children_alive", "this session still has background agents running where this replica cannot stop them (on another replica, for example), so the delete was refused before anything was deleted — `remaining` lists their ids (the same ids `GET /v1/agents/roster` shows); retry the delete once they finish, or stop them where they run", { remaining: result.childrenAlive.map((handle) => rosterAgentId({ handle })) });
722
+ return;
723
+ }
703
724
  sendJson(res, 200, result);
704
725
  return;
705
726
  }
@@ -714,7 +735,7 @@ export async function handleSessions(req, res, match, ctx) {
714
735
  }
715
736
  const principal = gatedPrincipal(req, deps.config);
716
737
  if (deps.config.requirePrincipal && principal === undefined) {
717
- sendError(res, 401, "auth.unauthorized", "unauthorized");
738
+ sendUnauthorized(res, "identity");
718
739
  return;
719
740
  }
720
741
  const sessionId = safeDecode(match.params.sessionId);
@@ -1,6 +1,6 @@
1
1
  import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError } from "@sema-agent/core";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { gatedPrincipal } from "../principal-gate.js";
3
+ import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  const RETRY_AFTER_SEC = 10;
6
6
  const HTTP_LIST_LIMIT = 100;
@@ -29,7 +29,7 @@ async function bindSnapshot(req, res, ctx) {
29
29
  const { deps } = ctx;
30
30
  const principal = gatedPrincipal(req, deps.config);
31
31
  if (!principal && deps.config.requirePrincipal === true) {
32
- sendError(res, 401, "auth.unauthorized", "unauthorized");
32
+ sendUnauthorized(res, "identity");
33
33
  return undefined;
34
34
  }
35
35
  const provider = deps.sharedMemoryStore;