@sema-agent/server 7.125.0 → 7.126.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 (248) hide show
  1. package/MIGRATION.md +199 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +263 -140
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +47 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +5 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +1 -1
  10. package/deploy/sema-up/chart/values.yaml +12 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +11 -10
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/parse-machines.py +2 -2
  18. package/deploy/sema-up/lib/shell-access.sh +1 -1
  19. package/deploy/sema-up/preflight.sh +4 -4
  20. package/deploy/sema-up/sema-up.sh +33 -33
  21. package/deploy/sema-up/smoke.sh +3 -3
  22. package/deploy/sema-up/stack/compose.yaml +1 -1
  23. package/deploy/sema-up/stack/sema-stack-cluster.yaml +6 -2
  24. package/dist/approval-card.d.ts +20 -4
  25. package/dist/approval-content-kind.d.ts +38 -22
  26. package/dist/approval-content-kind.js +20 -4
  27. package/dist/audit.js +2 -0
  28. package/dist/bench/s1/arms.d.ts +9 -37
  29. package/dist/bench/s1/arms.js +4 -61
  30. package/dist/bench/s1/live-deps.d.ts +4 -10
  31. package/dist/bench/s1/live-deps.js +1 -222
  32. package/dist/bench/s1/oracle.d.ts +1 -1
  33. package/dist/bench/s1/reviewer.d.ts +1 -1
  34. package/dist/bench/s1/row.d.ts +5 -7
  35. package/dist/bench/s1/row.js +0 -2
  36. package/dist/bench/s1/run-firm.d.ts +2 -2
  37. package/dist/bench/s1/run-firm.js +1 -4
  38. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  39. package/dist/bench/s1/runner-ctx.js +3 -7
  40. package/dist/bench/s1/tasks.d.ts +9 -46
  41. package/dist/bench/s1/tasks.js +4 -63
  42. package/dist/boot/execution-env.d.ts +38 -26
  43. package/dist/boot/execution-env.js +71 -38
  44. package/dist/boot/memory-boundary.d.ts +1 -1
  45. package/dist/boot/parked-revive-gate.d.ts +0 -3
  46. package/dist/boot/parked-revive-gate.js +2 -2
  47. package/dist/boot/resolve-spec.d.ts +0 -2
  48. package/dist/boot/resolve-spec.js +12 -17
  49. package/dist/boot/retention-lane.d.ts +12 -1
  50. package/dist/boot/retention-lane.js +2 -0
  51. package/dist/boot/runner-deps.d.ts +5 -5
  52. package/dist/boot/runner-deps.js +5 -16
  53. package/dist/boot/session-faces.d.ts +5 -1
  54. package/dist/boot/session-faces.js +37 -1
  55. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  56. package/dist/boot/stage-03-store-backend.js +2 -2
  57. package/dist/boot/stage-04-stores.d.ts +1 -0
  58. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  59. package/dist/boot/stage-05-execution-env.js +7 -4
  60. package/dist/boot/stage-06-runners.d.ts +2 -3
  61. package/dist/boot/stage-06-runners.js +8 -7
  62. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  63. package/dist/boot/stage-07-capability-layer.js +3 -4
  64. package/dist/boot/stage-08-reapers.d.ts +2 -3
  65. package/dist/boot/stage-08-reapers.js +2 -2
  66. package/dist/boot/stage-09-leader.d.ts +2 -3
  67. package/dist/boot/stage-09-leader.js +2 -1
  68. package/dist/boot/stage-10-http-server.d.ts +2 -3
  69. package/dist/boot/stage-10-http-server.js +2 -2
  70. package/dist/boot/stores.d.ts +1 -0
  71. package/dist/boot/stores.js +4 -3
  72. package/dist/boot/web-admin.d.ts +5 -0
  73. package/dist/boot/web-admin.js +28 -14
  74. package/dist/brain.d.ts +2 -2
  75. package/dist/brain.js +1 -1
  76. package/dist/capabilities/builtin-tools.d.ts +2 -1
  77. package/dist/capabilities/composite-members.d.ts +1 -1
  78. package/dist/capabilities/composite-members.js +1 -1
  79. package/dist/capabilities/hands-lane.d.ts +9 -6
  80. package/dist/capabilities/hands-lane.js +2 -2
  81. package/dist/capabilities/memory-notice.d.ts +27 -22
  82. package/dist/capabilities/memory-notice.js +15 -17
  83. package/dist/capabilities/scenarios.d.ts +9 -14
  84. package/dist/capabilities/scenarios.js +5 -50
  85. package/dist/capability-facts.d.ts +22 -11
  86. package/dist/capability-facts.js +13 -6
  87. package/dist/config-catalog.js +11 -4
  88. package/dist/config-provider.d.ts +5 -1
  89. package/dist/config-provider.js +23 -4
  90. package/dist/config-types.d.ts +14 -1
  91. package/dist/config.js +39 -6
  92. package/dist/deployment-governance.d.ts +19 -21
  93. package/dist/deployment-governance.js +7 -8
  94. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  95. package/dist/env-name-allowlist-knobs.js +1 -0
  96. package/dist/execution-lane-caps.js +2 -2
  97. package/dist/hooks/hook-runner.d.ts +84 -30
  98. package/dist/hooks/hook-runner.js +74 -41
  99. package/dist/http/admission.js +12 -9
  100. package/dist/http/durable-run-leg.d.ts +5 -4
  101. package/dist/http/durable-run-leg.js +1 -1
  102. package/dist/http/idempotency.d.ts +28 -6
  103. package/dist/http/idempotency.js +10 -2
  104. package/dist/http/principal-gate.d.ts +14 -0
  105. package/dist/http/principal-gate.js +7 -0
  106. package/dist/http/request-guards.d.ts +2 -1
  107. package/dist/http/request-guards.js +14 -14
  108. package/dist/http/resume-legs.d.ts +2 -2
  109. package/dist/http/resume-legs.js +5 -4
  110. package/dist/http/route-ctx.d.ts +8 -6
  111. package/dist/http/routes/a2a-serve.js +11 -7
  112. package/dist/http/routes/approvals-assistant.js +18 -3
  113. package/dist/http/routes/background.d.ts +1 -1
  114. package/dist/http/routes/background.js +1 -1
  115. package/dist/http/routes/capabilities.js +10 -2
  116. package/dist/http/routes/mcp-reconnect.js +1 -1
  117. package/dist/http/routes/runs.d.ts +4 -3
  118. package/dist/http/routes/runs.js +12 -13
  119. package/dist/http/routes/session-sync.js +14 -11
  120. package/dist/http/routes/sessions.js +9 -9
  121. package/dist/http/routes/side-query.js +6 -19
  122. package/dist/http/routes/tasks.js +12 -11
  123. package/dist/http/server.d.ts +4 -0
  124. package/dist/http/server.js +2 -2
  125. package/dist/http/submit-replay.d.ts +15 -8
  126. package/dist/http/submit-replay.js +6 -0
  127. package/dist/http/wire-types.d.ts +7 -9
  128. package/dist/index.d.ts +0 -1
  129. package/dist/index.js +0 -1
  130. package/dist/internal-run-env.d.ts +30 -0
  131. package/dist/internal-run-env.js +11 -0
  132. package/dist/leader/wire.js +3 -2
  133. package/dist/memory-scope.d.ts +19 -7
  134. package/dist/memory-scope.js +25 -8
  135. package/dist/observability/fail-open.d.ts +15 -3
  136. package/dist/observability/fail-open.js +15 -3
  137. package/dist/observability/metrics.d.ts +0 -1
  138. package/dist/observability/metrics.js +0 -3
  139. package/dist/observability/run-terminal.js +69 -4
  140. package/dist/permission-rule-vocab.d.ts +3 -2
  141. package/dist/permission-rule-vocab.js +1 -1
  142. package/dist/plan-cache-probe.d.ts +3 -2
  143. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  144. package/dist/plugins/caching-session-store.d.ts +3 -0
  145. package/dist/plugins/caching-session-store.js +3 -0
  146. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  147. package/dist/plugins/checkpoint-store-sql.js +57 -11
  148. package/dist/plugins/file-run-store.d.ts +25 -21
  149. package/dist/plugins/file-run-store.js +70 -34
  150. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  151. package/dist/plugins/local-checkpoint-store.js +5 -15
  152. package/dist/plugins/local-session-store.d.ts +66 -7
  153. package/dist/plugins/local-session-store.js +168 -101
  154. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  155. package/dist/plugins/memory-run-store.d.ts +19 -3
  156. package/dist/plugins/memory-run-store.js +38 -3
  157. package/dist/plugins/one-time-migrations.d.ts +141 -39
  158. package/dist/plugins/one-time-migrations.js +32 -143
  159. package/dist/plugins/pg-pool.js +11 -1
  160. package/dist/plugins/pg-session-storage.d.ts +2 -0
  161. package/dist/plugins/pg-session-storage.js +9 -5
  162. package/dist/plugins/remote-env-adb.d.ts +3 -0
  163. package/dist/plugins/remote-env-adb.js +1 -0
  164. package/dist/plugins/remote-env-device.d.ts +3 -0
  165. package/dist/plugins/remote-env-device.js +1 -0
  166. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  167. package/dist/plugins/remote-env-e2b.js +2 -1
  168. package/dist/plugins/remote-env-host.d.ts +12 -0
  169. package/dist/plugins/remote-env-host.js +11 -1
  170. package/dist/plugins/remote-env-k8s.d.ts +227 -39
  171. package/dist/plugins/remote-env-k8s.js +450 -75
  172. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  173. package/dist/plugins/remote-env-local-docker.js +2 -1
  174. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  175. package/dist/plugins/remote-env-ssh.js +1 -0
  176. package/dist/plugins/retention-store-sql.d.ts +4 -1
  177. package/dist/plugins/retention-store-sql.js +11 -9
  178. package/dist/plugins/run-store-sql.d.ts +44 -6
  179. package/dist/plugins/run-store-sql.js +92 -27
  180. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  181. package/dist/plugins/sql-driver.d.ts +2 -1
  182. package/dist/plugins/store-backend.js +1 -1
  183. package/dist/plugins/store-contracts.d.ts +17 -3
  184. package/dist/plugins/tidb-pool.js +15 -3
  185. package/dist/plugins/tidb-session-storage.js +3 -2
  186. package/dist/plugins/tidb-session-store.d.ts +2 -0
  187. package/dist/plugins/tidb-session-store.js +6 -2
  188. package/dist/request-key-closure.d.ts +12 -0
  189. package/dist/request-key-closure.js +3 -3
  190. package/dist/router/route-orchestration.d.ts +3 -11
  191. package/dist/router/route-orchestration.js +0 -4
  192. package/dist/rule-entry-list.js +0 -4
  193. package/dist/rules-consent.js +3 -0
  194. package/dist/run-local.d.ts +5 -4
  195. package/dist/run-local.js +23 -17
  196. package/dist/runs.js +1 -0
  197. package/dist/security.d.ts +7 -0
  198. package/dist/security.js +5 -0
  199. package/dist/server-secret-env.d.ts +10 -4
  200. package/dist/server-secret-env.js +4 -1
  201. package/dist/session-incarnation.d.ts +46 -2
  202. package/dist/session-incarnation.js +14 -3
  203. package/dist/session-sync-kernel.d.ts +10 -3
  204. package/dist/spec-fields.d.ts +24 -8
  205. package/dist/spec-fields.js +21 -2
  206. package/dist/task-workflow.d.ts +6 -4
  207. package/dist/task-workflow.js +3 -2
  208. package/dist/tool-approval.d.ts +78 -6
  209. package/dist/tool-approval.js +7 -3
  210. package/dist/trace/core-keyset-guard.d.ts +4 -4
  211. package/dist/trace/engine-notice-wire.d.ts +26 -1
  212. package/dist/trace/engine-notice-wire.js +7 -2
  213. package/dist/trace/project.d.ts +8 -3
  214. package/dist/trace/project.js +15 -1
  215. package/dist/trace/self-minted-wire.d.ts +104 -2
  216. package/dist/trace/self-minted-wire.js +35 -0
  217. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  218. package/dist/web-admin-client/apply-effective.js +11 -26
  219. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  220. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  221. package/dist/web-admin-client/candidate-gate.js +9 -2
  222. package/dist/web-admin-client/facade.d.ts +2 -2
  223. package/dist/web-admin-client/http-client.d.ts +3 -1
  224. package/dist/web-admin-client/http-client.js +4 -3
  225. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  226. package/dist/web-admin-client/read-warnings.js +41 -6
  227. package/dist/web-admin-client/types.d.ts +11 -5
  228. package/dist/workspace-cache.d.ts +45 -0
  229. package/dist/workspace-cache.js +30 -0
  230. package/dist/workspace-space-commit.d.ts +116 -0
  231. package/dist/workspace-space-commit.js +117 -0
  232. package/dist/workspace-space.d.ts +75 -9
  233. package/dist/workspace-space.js +52 -1
  234. package/package.json +4 -4
  235. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  236. package/dist/boot/session-incarnation-seat.js +0 -69
  237. package/dist/capabilities/code-review-council.d.ts +0 -49
  238. package/dist/capabilities/code-review-council.js +0 -111
  239. package/dist/capabilities/collab-wire.d.ts +0 -36
  240. package/dist/capabilities/collab-wire.js +0 -2
  241. package/dist/capabilities/collab-workflows.d.ts +0 -47
  242. package/dist/capabilities/collab-workflows.js +0 -90
  243. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  244. package/dist/capabilities/prompts/code-review.js +0 -39
  245. package/dist/capabilities/prompts/team.d.ts +0 -10
  246. package/dist/capabilities/prompts/team.js +0 -6
  247. package/dist/capabilities/team.d.ts +0 -113
  248. package/dist/capabilities/team.js +0 -70
@@ -84,7 +84,8 @@ export declare function createRequestGuards(ctx: {
84
84
  fleetWide: boolean;
85
85
  gateOwner: string | null;
86
86
  };
87
- runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null, notFoundForm?: "run" | "session") => boolean;
87
+ runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
88
+ sessionOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
88
89
  runSessionAcceptOk: (req: IncomingMessage, res: ServerResponse, run: {
89
90
  sessionId: string | null;
90
91
  }, face: string, notFoundError?: string) => boolean;
@@ -2,7 +2,7 @@ import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
2
2
  import { HttpError, principalFrom } from "../security.js";
3
3
  import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js";
4
4
  import { sendError } from "./send.js";
5
- import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
5
+ import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sessionOwnerReadable, devOpenDeployment, sendPrincipalRefusal } from "./principal-gate.js";
6
6
  import { quotaWindowResetsAt } from "../observability/cost-quota.js";
7
7
  const MAX_BODY = 8 * 1024 * 1024;
8
8
  export function leaseRefusalOf(adm) {
@@ -150,9 +150,8 @@ export function createRequestGuards(ctx) {
150
150
  }
151
151
  }
152
152
  function isFleetWide(req) {
153
- const { authToken: at, traceToken: tt } = deps.config;
154
- const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
155
- return (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined) || (!!tt && authorized(req, tt));
153
+ const tt = deps.config.traceToken;
154
+ return devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined || (!!tt && authorized(req, tt));
156
155
  }
157
156
  function sessionOwnerScope(req) {
158
157
  const fleetWide = isFleetWide(req);
@@ -160,27 +159,28 @@ export function createRequestGuards(ctx) {
160
159
  return { fleetWide, gateOwner };
161
160
  }
162
161
  function sessionOwnerScopeForWrite(req) {
163
- const { authToken: at, traceToken: tt } = deps.config;
164
- const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
165
- const fleetWide = (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined);
162
+ const fleetWide = devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined;
166
163
  const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
167
164
  return { fleetWide, gateOwner };
168
165
  }
169
- function runOwnerOk(req, res, owner, notFoundForm = "run") {
166
+ function ownerGate(req, res, readable, notFound) {
170
167
  const principal = gatedPrincipal(req, deps.config);
171
168
  if (deps.config.requirePrincipal && !principal) {
172
169
  sendPrincipalRefusal(res, req, deps.config);
173
170
  return false;
174
171
  }
175
- if (!runOwnerReadable(principal, owner, deps.config)) {
176
- if (notFoundForm === "session")
177
- sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
178
- else
179
- sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
172
+ if (!readable(principal)) {
173
+ notFound();
180
174
  return false;
181
175
  }
182
176
  return true;
183
177
  }
178
+ function runOwnerOk(req, res, owner) {
179
+ return ownerGate(req, res, (principal) => runOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE));
180
+ }
181
+ function sessionOwnerOk(req, res, owner) {
182
+ return ownerGate(req, res, (principal) => sessionOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE));
183
+ }
184
184
  const sessionAssertAbsentWarned = new Set();
185
185
  function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
186
186
  if (!run.sessionId)
@@ -229,6 +229,6 @@ export function createRequestGuards(ctx) {
229
229
  }
230
230
  return Buffer.concat(chunks);
231
231
  }
232
- return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody };
232
+ return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody };
233
233
  }
234
234
  //# sourceMappingURL=request-guards.js.map
@@ -15,7 +15,7 @@ import type { IncomingMessage } from "node:http";
15
15
  import { CheckpointError, type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
16
16
  import type { DecideBinding } from "./wire-types.js";
17
17
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
18
- import { type HostDecision } from "../host-decision.js";
18
+ import type { HostDecision } from "../host-decision.js";
19
19
  import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
20
20
  import type { RouteCtxBase, DriveResumeArgs, HostLegOutcome, PreparedPlanReview, ResumeLegOutcome } from "./route-ctx.js";
21
21
  import { type TrustedRunInternals } from "./durable-run-leg.js";
@@ -74,7 +74,7 @@ export declare function createResumeLegs(ctx: {
74
74
  readonly ok: true;
75
75
  readonly prepared: PreparedPlanReview;
76
76
  }>;
77
- drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
77
+ drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, hostDecision: HostDecision, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
78
78
  status: number;
79
79
  body: object;
80
80
  }>;
@@ -9,7 +9,6 @@ import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../se
9
9
  import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
10
10
  import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
11
11
  import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../run-cancel-context.js";
12
- import {} from "../host-decision.js";
13
12
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
14
13
  import { recordResumeResourceWindow } from "../resource-window.js";
15
14
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
@@ -68,6 +67,7 @@ const WORKER_SWAP_REDEEMABLE = {
68
67
  ledger_event_unknown: true,
69
68
  real_approval_damaged: false,
70
69
  real_approval_forged: false,
70
+ requirements_damaged: false,
71
71
  read_face_damaged: false,
72
72
  read_face_forged: false,
73
73
  write_face_damaged: false,
@@ -162,7 +162,7 @@ function notResumableAnswer() {
162
162
  return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
163
163
  }
164
164
  export function createResumeLegs(ctx) {
165
- const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
165
+ const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
166
166
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
167
167
  async function resumeCheckpoint(sessionId, decision, reason, hostDecision, req, answer, binding, onResumeCommitted, acceptEarly, decider) {
168
168
  const cs = deps.checkpointStore;
@@ -1377,7 +1377,7 @@ export function createResumeLegs(ctx) {
1377
1377
  return refuse(refuseStoredBodyReplay(sessionId, replay));
1378
1378
  return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
1379
1379
  }
1380
- async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
1380
+ async function drivePlanReview(prepared, decision, editedPlan, reason, hostDecision, req, acceptEarly, permissionModeAfter) {
1381
1381
  const { sessionId, token, cp, ctx } = prepared;
1382
1382
  if ((await deps.checkpointStore.findPendingTokenBySession(sessionId, undefined, { gateKinds: ["plan_review"] })) !== token)
1383
1383
  return notResumableAnswer();
@@ -1399,6 +1399,7 @@ export function createResumeLegs(ctx) {
1399
1399
  decision,
1400
1400
  ...(editedPlan !== undefined ? { editedPlan } : {}),
1401
1401
  ...(reason ? { reason } : {}),
1402
+ hostDecision,
1402
1403
  };
1403
1404
  const out = await driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx: { sessionId, ctx: rebuildCtx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1404
1405
  return out;
@@ -1406,7 +1407,7 @@ export function createResumeLegs(ctx) {
1406
1407
  const routeCtxBase = {
1407
1408
  deps,
1408
1409
  registry: { idemCache, inflightRuns, preemptableRuns, steerableRuns, wakeParkMints, counters },
1409
- helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1410
+ helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
1410
1411
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
1411
1412
  legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
1412
1413
  };
@@ -72,11 +72,11 @@ export interface RouteHelpers {
72
72
  usageWindowDenied(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
73
73
  safeDecode(seg: string): string | null;
74
74
  isFleetWide(req: IncomingMessage): boolean;
75
- /** 属主门。`notFoundForm` 选 not-yours 臂的 404 拼法:缺省 `"run"`(not_found.run 长文,[ref]);**每一个 session
76
- * 资源面都必须传 `"session"`**(not_found.session + {@link SESSION_NOT_FOUND_MESSAGE})与其 unknown 臂同码同串
77
- * (G52 [ref];S-989 补上了 detail / head / events 三个读口 —— 它们此前吃缺省 "run",单端点内 unknown / not-yours
78
- * 两臂码与串都不同)。缺省 `"run"` 的语义不变:run 面不传。 */
79
- runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null, notFoundForm?: "run" | "session"): boolean;
75
+ /** run 面的属主门(谓词 `runOwnerReadable`;非属主 ⇒ 404 `not_found.run` + 长文,与 run 读口的 unknown 臂同码同串,[ref])。 */
76
+ runOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
77
+ /** 会话面的属主门(谓词 `sessionOwnerReadable`,S-1452:没有身份边界的部署不钉人,与会话列表同读;非属主 ⇒ 404 `not_found.session` +
78
+ * {@link SESSION_NOT_FOUND_MESSAGE},与会话读口的 unknown 臂同码同串 —— G52 [ref] / S-989)。每一个按会话寻址的属主判都走它。 */
79
+ sessionOwnerOk(req: IncomingMessage, res: ServerResponse, owner: string | null): boolean;
80
80
  sessionOwnerScope(req: IncomingMessage): {
81
81
  fleetWide: boolean;
82
82
  gateOwner: string | null;
@@ -461,7 +461,9 @@ export interface RouteLegs {
461
461
  readonly prepared: PreparedPlanReview;
462
462
  }>;
463
463
  /** plan_review 三态腿的**驱动段**(S-609):只吃 prepare 的结果,用它选定的 token,不再选卡、不再读 ctx。 */
464
- drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
464
+ drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined,
465
+ /** core 7.49.0 #1396:决策事实(谁决定的 + 归属),语义属主 = `resume-legs.ts` 的同名形参顶注。 */
466
+ hostDecision: HostDecision, req: IncomingMessage | undefined,
465
467
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
466
468
  acceptEarly?: boolean,
467
469
  /** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在把计划带出 plan 的决定
@@ -123,7 +123,11 @@ function asNonEmptyString(v, what, max) {
123
123
  throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be at most ${max} characters`);
124
124
  return v;
125
125
  }
126
- const MAX_CONTEXT_ID_CHARS = 64;
126
+ function asString(v, what) {
127
+ if (typeof v !== "string")
128
+ throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be a string`);
129
+ return v;
130
+ }
127
131
  const MAX_MESSAGE_ID_CHARS = 200;
128
132
  const MAX_TASK_ID_CHARS = 128;
129
133
  const MAX_SKILL_ID_CHARS = 128;
@@ -224,7 +228,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
224
228
  if (message.role !== "user")
225
229
  throw new RpcError(JSONRPC_INVALID_PARAMS, `params.message.role must be "user" (this is the inbound leg — an agent-role message has no meaning here)`);
226
230
  const objective = objectiveFromParts(message.parts);
227
- const contextId = message.contextId === undefined ? undefined : asNonEmptyString(message.contextId, "params.message.contextId", MAX_CONTEXT_ID_CHARS);
231
+ const contextId = message.contextId === undefined ? undefined : asString(message.contextId, "params.message.contextId");
228
232
  const messageId = asNonEmptyString(message.messageId, "params.message.messageId", MAX_MESSAGE_ID_CHARS);
229
233
  const skill = skillFromMetadata(message.metadata, serve);
230
234
  const skillId = skill?.id;
@@ -238,19 +242,19 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
238
242
  .update(skillId ?? "")
239
243
  .digest("hex");
240
244
  const submitter = caller.principal ?? undefined;
241
- const idemKey = idemKeyOf(fingerprint, caller, "run");
245
+ const idem = { key: idemKeyOf(fingerprint, caller, "run"), bodyFingerprint: fingerprint };
242
246
  const body = {
243
247
  objective,
244
248
  taskId: durableIdempotencyTaskId(idemIdentityOf(caller), fingerprint),
245
249
  ...(contextId !== undefined ? { sessionId: contextId } : {}),
246
250
  ...(scenario !== undefined ? { scenario } : {}),
247
251
  };
248
- const keyed = (await runStore.getRunByIdem(idemKey).catch(() => undefined)) ?? (await runStore.getRun(body.taskId).catch(() => undefined));
249
- const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId }, "durable", deps, runStore) : undefined;
252
+ const keyed = (await runStore.getRunByIdem(idem.key).catch(() => undefined)) ?? (await runStore.getRun(body.taskId).catch(() => undefined));
253
+ const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId, idem }, "durable", deps, runStore) : undefined;
250
254
  if (keyed && replay?.status === 202) {
251
255
  return taskFromRun(keyed, isStaleRun(keyed, deps.config.runStaleSec));
252
256
  }
253
- const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(idemKey, "durable", body.taskId)) : undefined;
257
+ const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(idem, "durable", body.taskId)) : undefined;
254
258
  let receipt;
255
259
  if (replay !== undefined) {
256
260
  receipt = replay;
@@ -271,7 +275,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
271
275
  throw rpcErrorFromTypedFailure(typed, skillId);
272
276
  if (!prepared)
273
277
  return undefined;
274
- receipt = await createDurableRun(req, res, ctx, prepared, idemKey);
278
+ receipt = await createDurableRun(req, res, ctx, prepared, idem);
275
279
  }
276
280
  if (!receipt)
277
281
  return undefined;
@@ -31,10 +31,10 @@ const DECIDE_BODY_DECLARED_KEYS = [
31
31
  const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
32
32
  const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "decidedBy", "remember", "updatedInput"];
33
33
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
34
- "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
34
+ "boundCallId", "decidedBy", "decision", "editedPlan", "permissionModeAfter", "reason",
35
35
  ];
36
36
  const PLAN_REVIEW_PROOF_COVERED_KEYS = ["boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason"];
37
- const PLAN_REVIEW_NOT_IN_PROOF_KEYS = [];
37
+ const PLAN_REVIEW_NOT_IN_PROOF_KEYS = ["decidedBy"];
38
38
  const PROOF_SLOT_OF = {
39
39
  answer: "answer",
40
40
  boundCallId: "boundCallId",
@@ -522,6 +522,21 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
522
522
  sendError(res, 400, "request.field_conflict", "decision 'edit' requires a non-empty editedPlan (the operator's revised plan)");
523
523
  return;
524
524
  }
525
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
526
+ switch (decidedByReading.kind) {
527
+ case "not_in_proof":
528
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
529
+ return;
530
+ case "outside_closed_set":
531
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
532
+ return;
533
+ case "accepted":
534
+ break;
535
+ default: {
536
+ const unreachable = decidedByReading;
537
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
538
+ }
539
+ }
525
540
  if (decision === "edit" && !isWellFormedUnicode(body.editedPlan)) {
526
541
  sendError(res, 400, "request.field_invalid", "editedPlan must be well-formed Unicode text (a lone surrogate cannot be signed unambiguously)");
527
542
  return;
@@ -626,7 +641,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
626
641
  return;
627
642
  }
628
643
  }
629
- const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
644
+ const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, hostDecisionFacts(decidedByReading.decidedBy, deciderPrincipal), req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
630
645
  sendResumeOutcome(res, out, deps.logger);
631
646
  return;
632
647
  }
@@ -29,7 +29,7 @@
29
29
  * `rateLimited`。能力位 `background: { listFace, exitFaces }` 与两口的 501 从同一个谓词推导(会话属主面在场)。
30
30
  *
31
31
  * 🔴 **停止口在无凭据写门内**(`credentialGated: ["POST"]`;车OL F1 = 7.102.0 合并复审 gpt-6 腿 [high]):门的两个合取项它都中
32
- * —— ① 授权的唯一输入是 principal 头(属主判 `runOwnerOk` 与 operator 臂 `explicitOperatorOk` 读的都是 `gatedPrincipal`,
32
+ * —— ① 授权的唯一输入是 principal 头(属主判 `sessionOwnerOk` 与 operator 臂 `explicitOperatorOk` 读的都是 `gatedPrincipal`,
33
33
  * 无 service 凭证的部署上它收调用方自报的头);② 不可撤销、爆炸半径跨租户(知道会话 id + 自报它的属主 ⇒ 停掉那个会话的后台
34
34
  * 进程,`includeRetained:true` 连显式 retain 的活服务一起停)。于是无 service 凭证且未开 `ALLOW_UNAUTHED_WRITES` 的部署上它与
35
35
  * 其余改写门同码同句 503 `auth.service_token_required`(在路由之前,`dispatch.ts` 那一道),同一只会话级收敛函数的另一个调用点
@@ -68,7 +68,7 @@ export async function handleBackground(req, res, match, ctx) {
68
68
  return;
69
69
  }
70
70
  const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
71
- if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
71
+ if (!isOperator && !ctx.helpers.sessionOwnerOk(req, res, owner))
72
72
  return;
73
73
  const scope = owner ?? "default";
74
74
  switch (id) {
@@ -1,6 +1,7 @@
1
1
  import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { modelImageInput, isAtMentionable, selectableCatalog } from "../../model-select.js";
3
3
  import { buildDroppedEntriesWire } from "../../web-admin-client/read-warnings.js";
4
+ import { buildCandidateRejectedWire } from "../../web-admin-client/candidate-gate.js";
4
5
  import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
5
6
  import { projectSealedKeyCapability } from "../../sealed-key.js";
6
7
  import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
@@ -205,7 +206,7 @@ export async function handleCapabilities(req, res, match, ctx) {
205
206
  manualCompact: facts.durableRuns(deps).available,
206
207
  configCatalog: facts.configCatalog(deps).available,
207
208
  sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
208
- workspaceSync: ((f) => (f.available ? f.policy : undefined))(facts.workspaceSpace(deps.config)),
209
+ workspaceSync: ((space, cache) => space.available || cache.available ? { ...(space.available ? space.policy : {}), ...(cache.available ? { cache: cache.cache } : {}) } : undefined)(facts.workspaceSpace(deps.config), facts.workspaceCache(deps.config)),
209
210
  }));
210
211
  return;
211
212
  }
@@ -256,7 +257,14 @@ export async function handleCapabilities(req, res, match, ctx) {
256
257
  }));
257
258
  const nonAlias = Object.entries(deps.config.models).filter(([n]) => n !== "default");
258
259
  const defaultName = (nonAlias.find(([, m]) => m === deps.config.model) ?? nonAlias.find(([, m]) => m.id === deps.config.model.id))?.[0];
259
- sendJson(res, 200, { models, default: deps.config.model.id, defaultModel: { id: deps.config.model.id, ...(defaultName ? { name: defaultName } : {}) }, droppedEntries: buildDroppedEntriesWire(deps.config.droppedCatalogEntries) });
260
+ const candidateRejection = deps.candidateRejectionState?.();
261
+ sendJson(res, 200, {
262
+ models,
263
+ default: deps.config.model.id,
264
+ defaultModel: { id: deps.config.model.id, ...(defaultName ? { name: defaultName } : {}) },
265
+ droppedEntries: buildDroppedEntriesWire(deps.config.droppedCatalogEntries),
266
+ ...(candidateRejection !== undefined ? { candidateRejected: buildCandidateRejectedWire(candidateRejection) } : {}),
267
+ });
260
268
  return;
261
269
  }
262
270
  default:
@@ -94,7 +94,7 @@ export async function handleMcpReconnect(req, res, match, ctx) {
94
94
  return;
95
95
  }
96
96
  const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
97
- if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
97
+ if (!isOperator && !ctx.helpers.sessionOwnerOk(req, res, owner))
98
98
  return;
99
99
  const activeTaskId = await runStore.getActiveTaskId(sessionId);
100
100
  const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type RunIdemBinding } from "../../plugins/store-contracts.js";
2
3
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
3
4
  import type { RouteCtx, PreparedTaskSubmission, RouteMatch, RouteIdsOf } from "../route-ctx.js";
4
5
  /** 行上的 `decision_actor`(裸 JSON)投成回显形;形不认 ⇒ undefined(调用点省略该键,不 500)。
@@ -31,9 +32,9 @@ export declare function handleRunVerbs(req: IncomingMessage, res: ServerResponse
31
32
  * 防御性复检(缺席 ⇒ 501),但两个调用方都在自己的入口先判过,那才是各自面上的正确错误形。
32
33
  */
33
34
  export declare function createDurableRun(req: IncomingMessage, res: ServerResponse, ctx: RouteCtx, prepared: PreparedTaskSubmission,
34
- /** 这次提交的幂等键(`idemKeyOf(…, "run")`:`POST /v1/runs` = `Idempotency-Key` 头派生;A2A = `message.messageId`
35
- * + 内容指纹派生)—— 落 `task_run.idem_key`,同时是同进程在飞合流的键。undefined = 调用方没带键。 */
36
- idemKey: string | undefined): Promise<{
35
+ /** 这次提交的账本绑定(键 `idemKeyOf(…, "run")` + 请求体指纹:`POST /v1/runs` = `Idempotency-Key` 头 + 线上收到的体;A2A =
36
+ * `message.messageId` + 内容指纹)—— 落 `task_run.idem_key` / `idem_body_fingerprint`,键同时是同进程在飞合流的键。undefined = 调用方没带键。 */
37
+ idem: RunIdemBinding | undefined): Promise<{
37
38
  status: number;
38
39
  body: unknown;
39
40
  } | null>;
@@ -7,7 +7,7 @@ import { redactSecrets } from "../../trace/redact.js";
7
7
  import { HttpError, isUuidV7, encodeCheckpointScope } from "../../security.js";
8
8
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
9
9
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
10
- import { idemCallerOf, idemKeyOf, inflightKeyOf } from "../idempotency.js";
10
+ import { idemCallerOf, idemKeyOf, inflightKeyOf, submissionIdemOf } from "../idempotency.js";
11
11
  import { streamSseLog } from "../sse-log.js";
12
12
  import { bindSseLifecycle } from "../sse-lifecycle.js";
13
13
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
@@ -188,19 +188,18 @@ export async function handleRuns(req, res, match, ctx) {
188
188
  const runStore = runs.runStore;
189
189
  const rawIdem = headerStr(req.headers["idempotency-key"]);
190
190
  const submitter = gatedPrincipal(req, deps.config);
191
- const idemKey = rawIdem ? idemKeyOf(rawIdem, idemCallerOf(req, ctx), "run") : undefined;
192
- let keyedBody;
193
- if (idemKey) {
194
- keyedBody = await readJson(req);
191
+ const keyedBody = rawIdem ? await readJson(req) : undefined;
192
+ const idem = rawIdem ? submissionIdemOf(rawIdem, idemCallerOf(req, ctx), keyedBody) : undefined;
193
+ if (idem) {
195
194
  const clientTaskId = clientTaskIdOf(keyedBody);
196
195
  if (clientTaskId === false) {
197
196
  sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
198
197
  return;
199
198
  }
200
- const inflight = idemCache.peek(inflightKeyOf(idemKey, "durable", clientTaskId));
201
- const keyedRow = inflight ? undefined : await runStore.getRunByIdem(idemKey);
199
+ const inflight = idemCache.peek(inflightKeyOf(idem, "durable", clientTaskId));
200
+ const keyedRow = inflight ? undefined : await runStore.getRunByIdem(idem.key);
202
201
  if (inflight || keyedRow) {
203
- const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
202
+ const resp = inflight ? await inflight : await replayOfExistingRun(keyedRow, { principal: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) }, "durable", deps, runStore);
204
203
  sendJson(res, resp.status, resp.body);
205
204
  return;
206
205
  }
@@ -210,7 +209,7 @@ export async function handleRuns(req, res, match, ctx) {
210
209
  const prepared = await prepareSpec(req, res, keyedBody);
211
210
  if (!prepared)
212
211
  return;
213
- const resp = await createDurableRun(req, res, ctx, prepared, idemKey);
212
+ const resp = await createDurableRun(req, res, ctx, prepared, idem);
214
213
  if (!resp)
215
214
  return;
216
215
  sendJson(res, resp.status, resp.body);
@@ -1586,7 +1585,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1586
1585
  }
1587
1586
  return assertNever(match.decl.id);
1588
1587
  }
1589
- export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1588
+ export async function createDurableRun(req, res, ctx, prepared, idem) {
1590
1589
  const { deps } = ctx;
1591
1590
  const { idemCache } = ctx.registry;
1592
1591
  const { leaseDenied, usageWindowDenied } = ctx.helpers;
@@ -1603,7 +1602,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1603
1602
  return null;
1604
1603
  }
1605
1604
  const submitter = gatedPrincipal(req, deps.config);
1606
- const caller = { principal: submitter, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
1605
+ const caller = { principal: submitter, idem, ...(clientTaskId !== undefined ? { clientTaskId } : {}) };
1607
1606
  if (clientTaskId) {
1608
1607
  const existing = await runStore.getRun(clientTaskId);
1609
1608
  if (existing)
@@ -1611,11 +1610,11 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1611
1610
  }
1612
1611
  if ((await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
1613
1612
  return null;
1614
- return await idemCache.run(idemKey !== undefined ? inflightKeyOf(idemKey, "durable", clientTaskId) : undefined, async () => {
1613
+ return await idemCache.run(idem !== undefined ? inflightKeyOf(idem, "durable", clientTaskId) : undefined, async () => {
1615
1614
  const out = await mintAndDriveDurableRun({ deps, registry: ctx.registry, helpers: ctx.helpers, local: ctx.local, legs: ctx.legs }, runStore, {
1616
1615
  prepared,
1617
1616
  ...(clientTaskId !== undefined ? { clientTaskId } : {}),
1618
- ...(idemKey !== undefined ? { idemKey } : {}),
1617
+ ...(idem !== undefined ? { idem } : {}),
1619
1618
  fleetScope: submitter ?? "default",
1620
1619
  elicitOwner: submitter ?? null,
1621
1620
  source,
@@ -44,7 +44,6 @@ export async function handleSessionSync(req, res, match, ctx) {
44
44
  return;
45
45
  }
46
46
  const { backend, sessionStorage, historyStore } = sync;
47
- const ownerOf = sessionStorage.ownerOf.bind(sessionStorage);
48
47
  const scope = sessionOwnerScopeForWrite(req);
49
48
  if (!scope.fleetWide && scope.gateOwner === null) {
50
49
  sendUnauthorized(res, "identity");
@@ -55,7 +54,8 @@ export async function handleSessionSync(req, res, match, ctx) {
55
54
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
56
55
  return;
57
56
  }
58
- const owner = await ownerOf(sessionId);
57
+ const gateFace = await sessionStorage.sessionFaceOf(sessionId);
58
+ const owner = gateFace === undefined ? undefined : gateFace.owner;
59
59
  const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
60
60
  const pullGateOk = () => {
61
61
  if (owner === undefined) {
@@ -75,13 +75,10 @@ export async function handleSessionSync(req, res, match, ctx) {
75
75
  }
76
76
  return true;
77
77
  };
78
- const replaySessionRow = async (row, importedOwner) => {
78
+ const replaySessionRow = async (row, face) => {
79
79
  if (Object.keys(row).length === 0)
80
- return;
81
- const face = await sessionStorage.sessionFaceOf(sessionId);
82
- if (face === undefined || face.owner !== importedOwner)
83
- return;
84
- await sessionStorage.patchSessionMeta(sessionId, row, face);
80
+ return true;
81
+ return face !== undefined && (await sessionStorage.patchSessionMeta(sessionId, row, face)) !== undefined;
85
82
  };
86
83
  if (match.decl.id === "session-sync-manifest") {
87
84
  if (!pullGateOk())
@@ -331,7 +328,10 @@ export async function handleSessionSync(req, res, match, ctx) {
331
328
  if (deps.resumeAnchorStore)
332
329
  for (const a of anchors)
333
330
  await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
334
- await replaySessionRow(row, owner);
331
+ if (!(await replaySessionRow(row, gateFace))) {
332
+ sendError(res, 409, "conflict", "sync conflict");
333
+ return;
334
+ }
335
335
  sendJson(res, 200, {
336
336
  relation: "identical",
337
337
  basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
@@ -521,9 +521,13 @@ export async function handleSessionSync(req, res, match, ctx) {
521
521
  if (anchorStore)
522
522
  for (const a of pending.anchors)
523
523
  await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
524
- await replaySessionRow(pending.row, pending.owner);
524
+ const rowLanded = await replaySessionRow(pending.row, committed.face);
525
525
  sessionStorage.evict?.(sessionId);
526
526
  cleanupStaging(stagingId);
527
+ if (!rowLanded) {
528
+ sendError(res, 409, "conflict", "sync conflict");
529
+ return;
530
+ }
527
531
  sendJson(res, 200, { relation: committed.relation });
528
532
  }
529
533
  catch (e) {
@@ -567,7 +571,6 @@ export async function handleSessionSync(req, res, match, ctx) {
567
571
  function sendSessionSyncUnavailable(res, why) {
568
572
  switch (why) {
569
573
  case "store_backend_absent":
570
- case "session_owner_face_absent":
571
574
  case "session_row_faces_absent":
572
575
  sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
573
576
  return;
@@ -9,7 +9,7 @@ import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.
9
9
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
10
10
  import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
11
11
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
12
- import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
12
+ import { gatedPrincipal, explicitOperatorOk, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
13
13
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
14
14
  import * as facts from "../../capability-facts.js";
15
15
  import { rosterAgentId } from "./agents-roster.js";
@@ -54,7 +54,7 @@ async function readLastLegMcp(deps, sessionId, principal) {
54
54
  const latest = await runStore.latestRunForSession(sessionId);
55
55
  if (latest === undefined)
56
56
  return undefined;
57
- if (!runOwnerReadable(principal, latest.owner, deps.config) && !explicitOperatorOk(principal, deps.config.operatorPrincipals))
57
+ if (!sessionOwnerReadable(principal, latest.owner, deps.config) && !explicitOperatorOk(principal, deps.config.operatorPrincipals))
58
58
  return undefined;
59
59
  const events = await runStore.getEvents(latest.taskId, 0);
60
60
  for (let i = events.length - 1; i >= 0; i--) {
@@ -70,7 +70,7 @@ async function readLastLegMcp(deps, sessionId, principal) {
70
70
  }
71
71
  export async function handleSessions(req, res, match, ctx) {
72
72
  const { deps } = ctx;
73
- const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
73
+ const { readJson, safeDecode, sessionOwnerOk, sessionOwnerScopeForWrite } = ctx.helpers;
74
74
  const readOwnedSessionView = async (sessionAudit, sessionId, readOpts) => {
75
75
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
76
76
  if (ownerOf !== undefined) {
@@ -79,7 +79,7 @@ export async function handleSessions(req, res, match, ctx) {
79
79
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
80
80
  return undefined;
81
81
  }
82
- if (!runOwnerOk(req, res, owner, "session"))
82
+ if (!sessionOwnerOk(req, res, owner))
83
83
  return undefined;
84
84
  }
85
85
  let audit;
@@ -95,7 +95,7 @@ export async function handleSessions(req, res, match, ctx) {
95
95
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
96
96
  return undefined;
97
97
  }
98
- if (!runOwnerOk(req, res, audit.owner, "session"))
98
+ if (!sessionOwnerOk(req, res, audit.owner))
99
99
  return undefined;
100
100
  return audit;
101
101
  };
@@ -265,7 +265,7 @@ export async function handleSessions(req, res, match, ctx) {
265
265
  return;
266
266
  }
267
267
  owner = head.owner;
268
- if (!runOwnerOk(req, res, owner ?? null, "session"))
268
+ if (!sessionOwnerOk(req, res, owner ?? null))
269
269
  return;
270
270
  leafId = head.leafId;
271
271
  }
@@ -275,7 +275,7 @@ export async function handleSessions(req, res, match, ctx) {
275
275
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
276
276
  return;
277
277
  }
278
- if (!runOwnerOk(req, res, owner ?? null, "session"))
278
+ if (!sessionOwnerOk(req, res, owner ?? null))
279
279
  return;
280
280
  leafId = await getLeafId(headSession);
281
281
  }
@@ -359,7 +359,7 @@ export async function handleSessions(req, res, match, ctx) {
359
359
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
360
360
  return;
361
361
  }
362
- if (!runOwnerOk(req, res, owner ?? null, "session")) {
362
+ if (!sessionOwnerOk(req, res, owner ?? null)) {
363
363
  cleanup();
364
364
  return;
365
365
  }
@@ -666,7 +666,7 @@ export async function handleSessions(req, res, match, ctx) {
666
666
  sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
667
667
  return;
668
668
  }
669
- if (!runOwnerOk(req, res, face.owner, "session"))
669
+ if (!sessionOwnerOk(req, res, face.owner))
670
670
  return;
671
671
  const read = readSessionRowPatch(await readJson(req));
672
672
  if ("problem" in read) {