@sema-agent/server 7.136.0 → 7.137.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 (214) hide show
  1. package/MIGRATION.md +173 -0
  2. package/USAGE.md +84 -16
  3. package/deploy/sema-up/README.md +14 -0
  4. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
  6. package/dist/approval-content-kind.d.ts +6 -0
  7. package/dist/approval-content-kind.js +7 -0
  8. package/dist/approval-gate-currency.d.ts +48 -6
  9. package/dist/approval-gate-currency.js +16 -9
  10. package/dist/approval-hmac.d.ts +13 -1
  11. package/dist/approval-hmac.js +1 -1
  12. package/dist/approval.js +3 -3
  13. package/dist/auth-bridge.js +2 -2
  14. package/dist/boot/execution-env.d.ts +24 -1
  15. package/dist/boot/execution-env.js +69 -6
  16. package/dist/boot/reapers.d.ts +3 -0
  17. package/dist/boot/reapers.js +15 -1
  18. package/dist/boot/resolve-spec.d.ts +4 -0
  19. package/dist/boot/resolve-spec.js +16 -17
  20. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  21. package/dist/boot/stage-04-stores.d.ts +1 -1
  22. package/dist/boot/stage-05-execution-env.d.ts +3 -1
  23. package/dist/boot/stage-05-execution-env.js +11 -2
  24. package/dist/boot/stage-06-runners.d.ts +3 -1
  25. package/dist/boot/stage-06-runners.js +5 -1
  26. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  27. package/dist/boot/stage-07-capability-layer.js +1 -1
  28. package/dist/boot/stage-08-reapers.d.ts +4 -1
  29. package/dist/boot/stage-08-reapers.js +2 -1
  30. package/dist/boot/stage-09-leader.d.ts +4 -1
  31. package/dist/boot/stage-10-http-server.d.ts +4 -1
  32. package/dist/boot/stage-10-http-server.js +3 -2
  33. package/dist/boot/stores.d.ts +1 -1
  34. package/dist/broker/broker.d.ts +89 -0
  35. package/dist/broker/broker.js +287 -0
  36. package/dist/broker/constants.d.ts +39 -0
  37. package/dist/broker/constants.js +15 -0
  38. package/dist/broker/http1.d.ts +111 -0
  39. package/dist/broker/http1.js +256 -0
  40. package/dist/broker/lease-book.d.ts +87 -0
  41. package/dist/broker/lease-book.js +142 -0
  42. package/dist/broker/lease-file.d.ts +60 -0
  43. package/dist/broker/lease-file.js +127 -0
  44. package/dist/broker/main.d.ts +2 -0
  45. package/dist/broker/main.js +20 -0
  46. package/dist/capabilities/composite-members.d.ts +2 -1
  47. package/dist/capabilities/composite-members.js +2 -1
  48. package/dist/capabilities/engine-capabilities.js +1 -0
  49. package/dist/capabilities/scenarios.d.ts +23 -8
  50. package/dist/capabilities/scenarios.js +13 -8
  51. package/dist/capabilities/skills.d.ts +28 -5
  52. package/dist/capabilities/skills.js +8 -0
  53. package/dist/center-credential-renewer.js +5 -1
  54. package/dist/checkpoint-ctx-seal-m28.d.ts +3 -10
  55. package/dist/checkpoint-ctx-seal-m28.js +1 -1
  56. package/dist/checkpoint-identity.d.ts +3 -3
  57. package/dist/config-catalog.js +9 -6
  58. package/dist/config-types.d.ts +9 -0
  59. package/dist/config.d.ts +1 -1
  60. package/dist/config.js +34 -8
  61. package/dist/deployment-governance.d.ts +2 -2
  62. package/dist/deployment-governance.js +7 -9
  63. package/dist/device-ws-protocol.d.ts +1 -0
  64. package/dist/device-ws-protocol.js +2 -1
  65. package/dist/file-history-disclosure.d.ts +4 -2
  66. package/dist/file-history-disclosure.js +2 -0
  67. package/dist/hooks/classic-stations.d.ts +11 -54
  68. package/dist/http/admission.js +10 -7
  69. package/dist/http/approval-sweeps.js +4 -3
  70. package/dist/http/caller-trust.d.ts +2 -1
  71. package/dist/http/caller-trust.js +1 -3
  72. package/dist/http/checkpoint-ctx.d.ts +48 -7
  73. package/dist/http/checkpoint-ctx.js +40 -0
  74. package/dist/http/dispatch.js +15 -10
  75. package/dist/http/durable-run-leg.d.ts +5 -1
  76. package/dist/http/durable-run-leg.js +1 -1
  77. package/dist/http/health-self-report.d.ts +2 -2
  78. package/dist/http/mcp-remote-text.d.ts +5 -0
  79. package/dist/http/mcp-remote-text.js +7 -0
  80. package/dist/http/principal-gate.d.ts +15 -10
  81. package/dist/http/principal-gate.js +48 -9
  82. package/dist/http/resume-legs.d.ts +2 -20
  83. package/dist/http/resume-legs.js +52 -38
  84. package/dist/http/route-ctx.d.ts +8 -2
  85. package/dist/http/routes/approvals-assistant.js +18 -4
  86. package/dist/http/routes/capabilities.js +11 -1
  87. package/dist/http/routes/mcp-reconnect.d.ts +1 -1
  88. package/dist/http/routes/mcp-reconnect.js +5 -7
  89. package/dist/http/routes/runs.js +2 -1
  90. package/dist/http/routes/session-sync.js +42 -6
  91. package/dist/http/routes/sessions.js +2 -1
  92. package/dist/http/routes/tasks.js +8 -6
  93. package/dist/http/server.d.ts +4 -0
  94. package/dist/http/stored-body-replay.d.ts +3 -3
  95. package/dist/http/stored-body-replay.js +6 -1
  96. package/dist/http/wire-types.d.ts +52 -27
  97. package/dist/identity-source.d.ts +65 -0
  98. package/dist/identity-source.js +45 -0
  99. package/dist/memory-store-refusal.d.ts +3 -3
  100. package/dist/model-entry-refusal.d.ts +2 -0
  101. package/dist/model-entry-refusal.js +11 -0
  102. package/dist/model-select.d.ts +6 -17
  103. package/dist/model-select.js +0 -3
  104. package/dist/namespaced-rule-name.d.ts +1 -1
  105. package/dist/observability/fail-open.d.ts +13 -1
  106. package/dist/observability/fail-open.js +13 -1
  107. package/dist/observability/metrics.js +1 -1
  108. package/dist/observability/run-terminal.js +1 -0
  109. package/dist/operator-ask.d.ts +2 -2
  110. package/dist/org-delivery-gate.js +1 -1
  111. package/dist/org-memory-admission.d.ts +2 -2
  112. package/dist/org-memory-admission.js +39 -2
  113. package/dist/parked-decide.js +3 -2
  114. package/dist/peer-directory.js +2 -0
  115. package/dist/permission-rule-list.d.ts +2 -2
  116. package/dist/plugins/checkpoint-store-sql.d.ts +19 -9
  117. package/dist/plugins/checkpoint-store-sql.js +6 -6
  118. package/dist/plugins/file-history-store-sql.js +7 -14
  119. package/dist/plugins/file-run-store.js +8 -2
  120. package/dist/plugins/k8s-exec-protocol.d.ts +69 -13
  121. package/dist/plugins/k8s-exec-protocol.js +116 -28
  122. package/dist/plugins/k8s-pod-delete-failure.d.ts +25 -0
  123. package/dist/plugins/k8s-pod-delete-failure.js +17 -0
  124. package/dist/plugins/k8s-pod-reaper.d.ts +112 -0
  125. package/dist/plugins/k8s-pod-reaper.js +122 -0
  126. package/dist/plugins/local-checkpoint-store.d.ts +3 -6
  127. package/dist/plugins/local-checkpoint-store.js +3 -3
  128. package/dist/plugins/memory-run-store.js +2 -0
  129. package/dist/plugins/one-time-migrations.d.ts +28 -9
  130. package/dist/plugins/one-time-migrations.js +48 -5
  131. package/dist/plugins/permission-rule-store-file.js +2 -2
  132. package/dist/plugins/permission-rule-store-sql.d.ts +17 -17
  133. package/dist/plugins/permission-rule-store-sql.js +16 -17
  134. package/dist/plugins/pg-pool.js +4 -0
  135. package/dist/plugins/pg-session-storage.js +2 -2
  136. package/dist/plugins/remote-env-adb.d.ts +3 -0
  137. package/dist/plugins/remote-env-adb.js +1 -0
  138. package/dist/plugins/remote-env-device.d.ts +3 -0
  139. package/dist/plugins/remote-env-device.js +4 -0
  140. package/dist/plugins/remote-env-e2b.d.ts +3 -0
  141. package/dist/plugins/remote-env-e2b.js +1 -0
  142. package/dist/plugins/remote-env-file-error.d.ts +8 -1
  143. package/dist/plugins/remote-env-file-error.js +7 -1
  144. package/dist/plugins/remote-env-host.d.ts +7 -1
  145. package/dist/plugins/remote-env-host.js +7 -1
  146. package/dist/plugins/remote-env-k8s.d.ts +137 -10
  147. package/dist/plugins/remote-env-k8s.js +276 -14
  148. package/dist/plugins/remote-env-local-docker.d.ts +3 -0
  149. package/dist/plugins/remote-env-local-docker.js +1 -0
  150. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  151. package/dist/plugins/remote-env-ssh.js +1 -0
  152. package/dist/plugins/run-store-sql.js +10 -4
  153. package/dist/plugins/session-ownership.d.ts +17 -3
  154. package/dist/plugins/session-ownership.js +19 -5
  155. package/dist/plugins/sql-errors.d.ts +23 -2
  156. package/dist/plugins/sql-errors.js +15 -0
  157. package/dist/plugins/store-contracts.d.ts +10 -1
  158. package/dist/plugins/store-fault.d.ts +2 -2
  159. package/dist/plugins/store-fault.js +4 -2
  160. package/dist/plugins/tidb-pool.js +7 -0
  161. package/dist/plugins/tidb-session-store.js +2 -2
  162. package/dist/request-key-closure.js +1 -1
  163. package/dist/request-value-closure.d.ts +31 -2
  164. package/dist/request-value-closure.js +7 -1
  165. package/dist/rule-entry-list.d.ts +3 -2
  166. package/dist/rule-entry-list.js +2 -2
  167. package/dist/rules-consent.js +7 -4
  168. package/dist/run-local.d.ts +2 -2
  169. package/dist/runs.js +2 -1
  170. package/dist/runtime-governance.d.ts +5 -5
  171. package/dist/sandbox-git-lease.d.ts +49 -0
  172. package/dist/sandbox-git-lease.js +60 -0
  173. package/dist/sandbox-git-plane.d.ts +84 -0
  174. package/dist/sandbox-git-plane.js +105 -0
  175. package/dist/security.d.ts +16 -4
  176. package/dist/security.js +8 -3
  177. package/dist/server-secret-env.d.ts +6 -0
  178. package/dist/server-secret-env.js +14 -0
  179. package/dist/session-error-wire.d.ts +14 -6
  180. package/dist/session-error-wire.js +2 -1
  181. package/dist/session-policy-wire.d.ts +1 -1
  182. package/dist/session-policy-wire.js +2 -2
  183. package/dist/session-sync.d.ts +1 -1
  184. package/dist/spec-fields.d.ts +43 -13
  185. package/dist/spec-fields.js +33 -12
  186. package/dist/stored-request-prompt-profile-m30.d.ts +49 -0
  187. package/dist/stored-request-prompt-profile-m30.js +25 -0
  188. package/dist/task-mcp.d.ts +1 -1
  189. package/dist/task-settings.d.ts +2 -2
  190. package/dist/tool-approval.d.ts +12 -2
  191. package/dist/tool-approval.js +8 -3
  192. package/dist/tool-rule-names.d.ts +5 -0
  193. package/dist/trace/core-keyset-guard.d.ts +4 -4
  194. package/dist/trace/engine-notice-wire.d.ts +1 -1
  195. package/dist/trace/engine-notice-wire.js +2 -0
  196. package/dist/trace/exit-seat.js +2 -2
  197. package/dist/trace/ledger-sink.d.ts +4 -5
  198. package/dist/trace/ledger-sink.js +3 -7
  199. package/dist/trace/project.d.ts +37 -3
  200. package/dist/trace/project.js +20 -6
  201. package/dist/trace/redact.d.ts +18 -6
  202. package/dist/trace/redact.js +118 -53
  203. package/dist/trace/self-minted-wire.d.ts +377 -3
  204. package/dist/trace/self-minted-wire.js +122 -0
  205. package/dist/trace/wire-projection-faces.d.ts +5 -3
  206. package/dist/trace/wire-projection-faces.js +7 -3
  207. package/dist/web-admin-client/apply-effective.js +0 -1
  208. package/dist/web-admin-client/center-request.d.ts +20 -2
  209. package/dist/web-admin-client/center-request.js +40 -5
  210. package/dist/web-admin-client/http-client.js +6 -6
  211. package/dist/web-admin-client/types.d.ts +5 -5
  212. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
  213. package/package.json +2 -2
  214. package/wire-changes/server@7.137.0.json +825 -0
@@ -13,6 +13,7 @@ import { spaceCommitAlive } from "../workspace-space-commit.js";
13
13
  import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
14
14
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
15
15
  import { recordFailOpen } from "../observability/fail-open.js";
16
+ import { PodDeleteRefused, reportPodDeleteFailed } from "./k8s-pod-delete-failure.js";
16
17
  import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
17
18
  import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
18
19
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
@@ -39,9 +40,12 @@ const unsupported = (op) => ({
39
40
  const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
40
41
  const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
41
42
  const WRITE_CHUNK_BYTES = 64 * 1024;
42
- import { applyPodSpecPatch } from "./k8s-exec-protocol.js";
43
+ import { applyPodSpecPatch, ADAPTER_VOLUME_NAMES, SANDBOX_POD_LABELS, SANDBOX_RUN_END_HOLD_ANNOTATION, SANDBOX_RUN_LABEL, SANDBOX_SECRET_SEAT_VERBS, isLabelValue } from "./k8s-exec-protocol.js";
43
44
  export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
44
- const CACHE_VOLUME = "ws-cache";
45
+ import { sandboxRunEndHoldMs } from "./k8s-pod-reaper.js";
46
+ import { BROKER_COMMAND, BROKER_CONTAINER_NAME, BROKER_LEASE_DIR, BROKER_LEASE_KEY, BROKER_PROBE_ARG, BROKER_VOLUME_NAME } from "../broker/constants.js";
47
+ import { buildSandboxLeaseFile, placeholderLeaseEntries, sandboxGitRouteEnv } from "../sandbox-git-lease.js";
48
+ const CACHE_VOLUME = ADAPTER_VOLUME_NAMES.cache;
45
49
  function occupantKeyOf(occupant) {
46
50
  if (occupant === undefined)
47
51
  return { state: "unavailable", reason: "incarnation_unknown" };
@@ -168,12 +172,39 @@ export function errorMessageOf(e) {
168
172
  }
169
173
  export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
170
174
  export const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
171
- const SANDBOX_POD_LABELS = { app: "sema-sandbox", "managed-by": "sema-server" };
175
+ const SANDBOX_SECRET_NAME_PREFIX = "sbx-env-";
176
+ const SANDBOX_SECRET_LABEL = "sema.io/sandbox-secret";
177
+ const secretEnvKey = (name) => `env.${name}`;
178
+ const SANDBOX_LEASE_NAME_PREFIX = "sbx-lease-";
179
+ const SANDBOX_LEASE_LABEL = "sema.io/sandbox-lease";
180
+ function brokerContainer(image) {
181
+ return {
182
+ name: BROKER_CONTAINER_NAME,
183
+ image,
184
+ command: [...BROKER_COMMAND],
185
+ env: [{ name: "BUN_RUNTIME_TRANSPILER_CACHE_PATH", value: "0" }],
186
+ securityContext: {
187
+ runAsUser: 0,
188
+ runAsGroup: 0,
189
+ runAsNonRoot: false,
190
+ privileged: false,
191
+ allowPrivilegeEscalation: false,
192
+ readOnlyRootFilesystem: true,
193
+ capabilities: { drop: ["ALL"] },
194
+ seccompProfile: { type: "RuntimeDefault" },
195
+ procMount: "Default",
196
+ },
197
+ volumeMounts: [{ name: BROKER_VOLUME_NAME, mountPath: BROKER_LEASE_DIR, readOnly: true }],
198
+ readinessProbe: { exec: { command: [...BROKER_COMMAND, BROKER_PROBE_ARG] }, periodSeconds: 2, timeoutSeconds: 5 },
199
+ resources: { limits: { cpu: "250m", memory: "256Mi" } },
200
+ };
201
+ }
172
202
  export class RemoteK8sExecutionEnv {
173
203
  capabilities;
174
204
  cwd;
175
205
  podName;
176
206
  podIP;
207
+ leaseRef;
177
208
  destroyed = false;
178
209
  handleSnapshotId;
179
210
  inFlight = new Set();
@@ -185,6 +216,7 @@ export class RemoteK8sExecutionEnv {
185
216
  }
186
217
  homeSeat;
187
218
  cdPath = "unknown";
219
+ withheldEnv = [];
188
220
  cfg;
189
221
  deps;
190
222
  occupantIncarnation;
@@ -220,6 +252,9 @@ export class RemoteK8sExecutionEnv {
220
252
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
221
253
  ...(config.podSpecPatch ? { podSpecPatch: config.podSpecPatch } : {}),
222
254
  ...(config.podEnv ? { podEnv: config.podEnv } : {}),
255
+ ...(config.podSecretEnv && Object.keys(config.podSecretEnv).length > 0 ? { podSecretEnv: config.podSecretEnv } : {}),
256
+ ...(config.runId !== undefined && config.runId !== "" && isLabelValue(config.runId) ? { runId: config.runId } : {}),
257
+ ...(config.sandboxBroker !== undefined ? { sandboxBroker: config.sandboxBroker } : {}),
223
258
  ...(config.snapshotExclude && config.snapshotExclude.length > 0 ? { snapshotExclude: config.snapshotExclude } : {}),
224
259
  };
225
260
  this.cwd = this.cfg.mountPath;
@@ -243,6 +278,9 @@ export class RemoteK8sExecutionEnv {
243
278
  supportsDetach: true,
244
279
  };
245
280
  }
281
+ get sandboxLeaseRef() {
282
+ return this.leaseRef !== undefined && this.leaseRef.podName === this.podName ? this.leaseRef : undefined;
283
+ }
246
284
  get lifetimeMs() {
247
285
  return this.cfg.timeoutMs;
248
286
  }
@@ -293,7 +331,7 @@ export class RemoteK8sExecutionEnv {
293
331
  headers: {
294
332
  authorization: `Bearer ${token}`,
295
333
  accept: "application/json",
296
- ...(payload ? { "content-type": "application/json", "content-length": Buffer.byteLength(payload) } : {}),
334
+ ...(payload ? { "content-type": opts?.contentType ?? "application/json", "content-length": Buffer.byteLength(payload) } : {}),
297
335
  },
298
336
  ...(isHttps ? { ca: ca ? [ca] : undefined, rejectUnauthorized: !this.cfg.insecureTls } : {}),
299
337
  timeout: opts?.timeoutMs ?? this.cfg.rpcTimeoutMs,
@@ -509,7 +547,7 @@ export class RemoteK8sExecutionEnv {
509
547
  const claim = {
510
548
  apiVersion: "v1",
511
549
  kind: "PersistentVolumeClaim",
512
- metadata: { name: cache.claim.name, labels: { [WORKSPACE_CACHE_SPACE_LABEL]: cache.claim.label, "managed-by": "sema-server" } },
550
+ metadata: { name: cache.claim.name, labels: { [WORKSPACE_CACHE_SPACE_LABEL]: cache.claim.label, "managed-by": SANDBOX_POD_LABELS["managed-by"] } },
513
551
  spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: cache.size } } },
514
552
  };
515
553
  const made = await this.request("POST", base, claim, { signal });
@@ -562,6 +600,38 @@ export class RemoteK8sExecutionEnv {
562
600
  applyPodSpecPatch(probe, { labels: this.cfg.podSpecPatch.labels });
563
601
  return sandboxEgressGaps({ nodes, policies, podLabels: probe.metadata.labels, ipv6: this.cfg.ipv6 });
564
602
  }
603
+ async secretSeatReading(seat) {
604
+ const answers = await Promise.all(SANDBOX_SECRET_SEAT_VERBS[seat].map(async (verb) => {
605
+ try {
606
+ const r = await this.request("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", {
607
+ apiVersion: "authorization.k8s.io/v1",
608
+ kind: "SelfSubjectAccessReview",
609
+ spec: { resourceAttributes: { namespace: this.cfg.namespace, verb, resource: "secrets" } },
610
+ });
611
+ const allowed = r.json?.status?.allowed;
612
+ const answered = r.status >= 200 && r.status < 300;
613
+ return answered && typeof allowed === "boolean" ? { verb, allowed } : { verb, unasked: answered ? "answer without status.allowed" : `HTTP ${r.status}` };
614
+ }
615
+ catch (e) {
616
+ return { verb, unasked: errorMessageOf(e) };
617
+ }
618
+ }));
619
+ const denied = answers.flatMap((a) => ("allowed" in a && !a.allowed ? [a.verb] : []));
620
+ const unasked = answers.flatMap((a) => ("unasked" in a ? [`${a.verb}: ${a.unasked}`] : []));
621
+ if (denied.length > 0)
622
+ return { state: "denied", verbs: denied };
623
+ if (unasked.length > 0)
624
+ return { state: "unverified", detail: unasked.join("; ") };
625
+ return { state: "granted" };
626
+ }
627
+ async listSandboxPods() {
628
+ const selector = encodeURIComponent(Object.entries(SANDBOX_POD_LABELS).map(([k, v]) => `${k}=${v}`).join(","));
629
+ const r = await this.request("GET", `/api/v1/namespaces/${this.cfg.namespace}/pods?labelSelector=${selector}`);
630
+ if (r.status < 200 || r.status >= 300)
631
+ throw new Error(`sandbox pod listing answered HTTP ${r.status}`);
632
+ const items = r.json?.items;
633
+ return (Array.isArray(items) ? items : []).flatMap((item) => sandboxPodListingOf(item) ?? []);
634
+ }
565
635
  reclaimDeadlinePassed(what) {
566
636
  return `reclaim deadline (${this.cfg.dataTimeoutMs} ms, the snapshot time bound, one per reclaim call) passed — ${what}`;
567
637
  }
@@ -579,19 +649,36 @@ export class RemoteK8sExecutionEnv {
579
649
  }
580
650
  async createPod(signal, cache) {
581
651
  const ns = this.cfg.namespace;
582
- const podEnv = { ...(cache ? workspaceCacheEnv(cache.mount) : {}), ...(this.cfg.podEnv ?? {}) };
652
+ const secretEnv = this.cfg.podSecretEnv ?? {};
653
+ const secretName = Object.keys(secretEnv).length > 0 ? `${SANDBOX_SECRET_NAME_PREFIX}${randomUUID()}` : undefined;
654
+ const broker = this.cfg.sandboxBroker;
655
+ const leaseName = broker !== undefined ? `${SANDBOX_LEASE_NAME_PREFIX}${randomUUID()}` : undefined;
656
+ const placeholder = broker !== undefined ? buildSandboxLeaseFile(broker.origins, placeholderLeaseEntries(broker.origins)) : undefined;
657
+ if (placeholder !== undefined && !placeholder.ok) {
658
+ return this.holdRefusal(new RemoteExecutionError("connect_failed", `the member-credential broker cannot be set up for this pod: ${placeholder.problem}`));
659
+ }
660
+ const layered = { ...(cache ? workspaceCacheEnv(cache.mount) : {}), ...(this.cfg.podEnv ?? {}) };
661
+ const envEntries = [
662
+ ...Object.entries({ ...layered, ...(broker !== undefined ? sandboxGitRouteEnv(broker.origins) : {}) })
663
+ .filter(([name]) => !Object.hasOwn(secretEnv, name))
664
+ .map(([name, value]) => ({ name, value: String(value) })),
665
+ ...(secretName !== undefined ? Object.keys(secretEnv).map((name) => ({ name, valueFrom: { secretKeyRef: { name: secretName, key: secretEnvKey(name) } } })) : []),
666
+ ];
667
+ const pkgSourceMarked = Boolean(this.cfg.podEnv?.SEMA_PKG_SOURCE || secretEnv.SEMA_PKG_SOURCE);
583
668
  const sysctls = sandboxPodSysctls(this.cfg.ipv6);
584
669
  const pod = {
585
670
  apiVersion: "v1",
586
671
  kind: "Pod",
587
672
  metadata: {
588
673
  generateName: "sbx-",
589
- labels: { ...SANDBOX_POD_LABELS },
674
+ labels: { ...SANDBOX_POD_LABELS, ...(this.cfg.runId !== undefined ? { [SANDBOX_RUN_LABEL]: this.cfg.runId } : {}) },
675
+ ...(this.cfg.runId !== undefined ? { annotations: { [SANDBOX_RUN_END_HOLD_ANNOTATION]: String(sandboxRunEndHoldMs(this.cfg.dataTimeoutMs)) } } : {}),
590
676
  },
591
677
  spec: {
592
678
  ...(this.cfg.runtimeClass ? { runtimeClassName: this.cfg.runtimeClass } : {}),
593
679
  ...(sysctls ? { securityContext: { sysctls: sysctls.map((x) => ({ ...x })) } } : {}),
594
680
  restartPolicy: "Never",
681
+ ...(broker !== undefined ? { shareProcessNamespace: false } : {}),
595
682
  automountServiceAccountToken: false,
596
683
  activeDeadlineSeconds: Math.ceil(this.cfg.timeoutMs / 1000),
597
684
  terminationGracePeriodSeconds: 1,
@@ -599,18 +686,30 @@ export class RemoteK8sExecutionEnv {
599
686
  {
600
687
  name: "sandbox",
601
688
  image: this.cfg.image,
602
- command: this.cfg.podEnv?.SEMA_PKG_SOURCE
689
+ command: pkgSourceMarked
603
690
  ? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
604
691
  : ["sh", "-c", "sleep infinity"],
605
- ...(Object.keys(podEnv).length > 0 ? { env: Object.entries(podEnv).map(([name, value]) => ({ name, value: String(value) })) } : {}),
692
+ ...(envEntries.length > 0 ? { env: envEntries } : {}),
606
693
  ...(cache ? { volumeMounts: [{ name: CACHE_VOLUME, mountPath: cache.mount }] } : {}),
607
694
  resources: { limits: { cpu: this.cfg.cpu, memory: this.cfg.memory } },
608
695
  },
696
+ ...(broker !== undefined ? [brokerContainer(broker.image)] : []),
609
697
  ],
610
- ...(cache ? { volumes: [{ name: CACHE_VOLUME, persistentVolumeClaim: { claimName: cache.claim.name } }] } : {}),
698
+ ...((volumes) => (volumes.length > 0 ? { volumes } : {}))([
699
+ ...(cache ? [{ name: CACHE_VOLUME, persistentVolumeClaim: { claimName: cache.claim.name } }] : []),
700
+ ...(leaseName !== undefined ? [{ name: BROKER_VOLUME_NAME, secret: { secretName: leaseName, items: [{ key: BROKER_LEASE_KEY, path: BROKER_LEASE_KEY }], defaultMode: 0o400 } }] : []),
701
+ ]),
611
702
  },
612
703
  };
613
- if (this.cfg.podSpecPatch)
704
+ if (this.cfg.podSpecPatch && broker !== undefined) {
705
+ try {
706
+ applyPodSpecPatch(pod, this.cfg.podSpecPatch, { broker: true });
707
+ }
708
+ catch (e) {
709
+ return this.holdRefusal(new RemoteExecutionError("connect_failed", `${errorMessageOf(e)} — no pod is created (the member-credential broker never shares a pod with a patch that could reach it)`));
710
+ }
711
+ }
712
+ else if (this.cfg.podSpecPatch)
614
713
  applyPodSpecPatch(pod, this.cfg.podSpecPatch);
615
714
  let created;
616
715
  try {
@@ -628,6 +727,20 @@ export class RemoteK8sExecutionEnv {
628
727
  const name = created.json?.metadata?.name;
629
728
  if (!name)
630
729
  return { ok: false, error: new RemoteExecutionError("connect_failed", "pod create returned no name") };
730
+ if (secretName !== undefined) {
731
+ const sealed = await this.createPodSecret(secretName, name, created.json?.metadata?.uid, secretEnv, signal);
732
+ if (!sealed.ok) {
733
+ await this.discardRefusedPod(name, "env_secret_unsealed");
734
+ return sealed;
735
+ }
736
+ }
737
+ if (leaseName !== undefined && placeholder?.ok === true) {
738
+ const leased = await this.createLeaseSecret(leaseName, name, created.json?.metadata?.uid, placeholder.text, signal);
739
+ if (!leased.ok) {
740
+ await this.discardRefusedPod(name, "lease_secret_unsealed");
741
+ return leased;
742
+ }
743
+ }
631
744
  const deadline = Date.now() + this.cfg.readyTimeoutMs;
632
745
  let podIP;
633
746
  for (;;) {
@@ -645,7 +758,8 @@ export class RemoteK8sExecutionEnv {
645
758
  }
646
759
  const status = got.json?.status;
647
760
  const phase = status?.phase;
648
- if (phase === "Running") {
761
+ const brokerStatus = status?.containerStatuses?.find((c) => c.name === BROKER_CONTAINER_NAME);
762
+ if (phase === "Running" && (broker === undefined || brokerStatus?.ready === true)) {
649
763
  podIP = status?.podIP;
650
764
  break;
651
765
  }
@@ -653,11 +767,16 @@ export class RemoteK8sExecutionEnv {
653
767
  await this.deletePod(name).catch(() => { });
654
768
  return this.refusePod(name, phase, status?.reason, status?.message);
655
769
  }
656
- const waiting = status?.containerStatuses?.[0]?.state?.waiting;
770
+ const waiting = status?.containerStatuses?.find((c) => c.name !== BROKER_CONTAINER_NAME)?.state?.waiting;
657
771
  if (waiting?.reason === "ImagePullBackOff" || waiting?.reason === "ErrImagePull") {
658
772
  await this.deletePod(name).catch(() => { });
659
773
  return { ok: false, error: new RemoteExecutionError("connect_failed", `image pull failed for ${this.cfg.image}: ${waiting.message ?? waiting.reason}`) };
660
774
  }
775
+ const brokerWaiting = brokerStatus?.state?.waiting;
776
+ if (broker !== undefined && (brokerWaiting?.reason === "ImagePullBackOff" || brokerWaiting?.reason === "ErrImagePull")) {
777
+ await this.discardRefusedPod(name, "broker_image_pull_failed");
778
+ return { ok: false, error: new RemoteExecutionError("connect_failed", `image pull failed for the member-credential broker image ${broker.image} (K8S_SANDBOX_BROKER_IMAGE): ${brokerWaiting.message ?? brokerWaiting.reason}`) };
779
+ }
661
780
  if (Date.now() > deadline) {
662
781
  if (cache) {
663
782
  const bound = await this.cacheClaimBound(cache, signal);
@@ -671,6 +790,9 @@ export class RemoteK8sExecutionEnv {
671
790
  : { cacheMiss: "claim_unbound", detail: `the claim ${cache.claim.name} was not bound within ${this.cfg.dataTimeoutMs}ms (no default StorageClass, or its provisioner is down)` };
672
791
  }
673
792
  await this.deletePod(name).catch(() => { });
793
+ if (phase === "Running" && broker !== undefined) {
794
+ return { ok: false, error: new RemoteExecutionError("timeout", `the member-credential broker container was not ready within ${this.cfg.readyTimeoutMs}ms (it reads its lease file and answers its own readiness probe; image ${broker.image})`) };
795
+ }
674
796
  return { ok: false, error: new RemoteExecutionError("timeout", `pod not Running within ${this.cfg.readyTimeoutMs}ms (phase=${phase ?? "?"})`) };
675
797
  }
676
798
  await new Promise((r) => setTimeout(r, 1000));
@@ -681,6 +803,7 @@ export class RemoteK8sExecutionEnv {
681
803
  }
682
804
  this.podName = name;
683
805
  this.podIP = podIP;
806
+ this.leaseRef = leaseName !== undefined && broker !== undefined ? { namespace: ns, podName: name, leaseSecretName: leaseName, origins: [...broker.origins] } : undefined;
684
807
  const askHome = this.homeSeat.asks;
685
808
  const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}${askHome ? ` && { ${LANE_HOME_PROBE_COMMAND} || true; }` : ""}`);
686
809
  if (!mk.ok || mk.value.exitCode !== 0) {
@@ -703,6 +826,94 @@ export class RemoteK8sExecutionEnv {
703
826
  this.logger?.warn("workspace_cache_fresh", { space: incarnationLogPrefix(cache.spaceKey), claim: cache.claim.name, size: cache.size });
704
827
  return ok(this.workspaceHandle());
705
828
  }
829
+ async createPodSecret(secretName, podName, podUid, values, signal) {
830
+ const encoded = Object.entries(values).map(([n, v]) => [secretEnvKey(n), Buffer.from(String(v), "utf8").toString("base64")]);
831
+ return this.createOwnedSecret({
832
+ name: secretName,
833
+ kind: "env",
834
+ label: SANDBOX_SECRET_LABEL,
835
+ immutable: true,
836
+ data: Object.fromEntries(encoded),
837
+ hidden: [...Object.values(values).map(String), ...encoded.map(([, b64]) => b64)],
838
+ what: "sandbox secret",
839
+ effect: "the pod is deleted (deployer-named sandbox env reaches a pod only through its own Secret, never as plaintext in the pod spec)",
840
+ }, podName, podUid, signal);
841
+ }
842
+ async createLeaseSecret(leaseName, podName, podUid, leaseText, signal) {
843
+ const b64 = Buffer.from(leaseText, "utf8").toString("base64");
844
+ return this.createOwnedSecret({
845
+ name: leaseName,
846
+ kind: "lease",
847
+ label: SANDBOX_LEASE_LABEL,
848
+ immutable: false,
849
+ data: { [BROKER_LEASE_KEY]: b64 },
850
+ hidden: [b64],
851
+ what: "sandbox lease secret",
852
+ effect: "the pod is deleted (a pod that carries the member-credential broker never runs without the lease its broker reads)",
853
+ }, podName, podUid, signal);
854
+ }
855
+ async createOwnedSecret(seat, podName, podUid, signal) {
856
+ const { effect } = seat;
857
+ if (typeof podUid !== "string" || podUid === "")
858
+ return { ok: false, error: new RemoteExecutionError("connect_failed", `pod create returned no uid — the ${seat.what} cannot be bound to its pod; ${effect}`) };
859
+ const secret = {
860
+ apiVersion: "v1",
861
+ kind: "Secret",
862
+ metadata: {
863
+ name: seat.name,
864
+ labels: { "managed-by": SANDBOX_POD_LABELS["managed-by"], [seat.label]: podName },
865
+ ownerReferences: [{ apiVersion: "v1", kind: "Pod", name: podName, uid: podUid, blockOwnerDeletion: false }],
866
+ },
867
+ type: "Opaque",
868
+ ...(seat.immutable ? { immutable: true } : {}),
869
+ data: seat.data,
870
+ };
871
+ const hidden = seat.hidden.filter((v) => v !== "");
872
+ const scrub = (text) => hidden.reduce((out, v) => out.split(v).join("<redacted>"), text);
873
+ let made;
874
+ try {
875
+ made = await this.request("POST", `/api/v1/namespaces/${this.cfg.namespace}/secrets`, secret, { signal });
876
+ }
877
+ catch (e) {
878
+ if (signal?.aborted)
879
+ return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
880
+ return { ok: false, error: new RemoteExecutionError("connect_failed", `${seat.what} create RPC failed: ${scrub(errorMessageOf(e)).slice(0, 300)}; ${effect}`) };
881
+ }
882
+ if (made.status === 200 || made.status === 201)
883
+ return { ok: true };
884
+ const detail = scrub(JSON.stringify(made.json) ?? "").slice(0, 300);
885
+ if (made.status === 401 || made.status === 403) {
886
+ return { ok: false, error: new RemoteExecutionError("auth_failed", `${seat.what} create denied (${made.status}): ${detail} — the sandbox Role needs secrets ${SANDBOX_SECRET_SEAT_VERBS[seat.kind].join(" / ")}; ${effect}`) };
887
+ }
888
+ return { ok: false, error: new RemoteExecutionError("connect_failed", `${seat.what} create failed (${made.status}): ${detail}; ${effect}`) };
889
+ }
890
+ async writeLease(ref, entries) {
891
+ const fail = (code, message) => {
892
+ this.logger?.warn("sandbox_lease_write_failed", { namespace: ref.namespace, pod: ref.podName, secretName: ref.leaseSecretName, code, detail: message });
893
+ return { ok: false, code, message };
894
+ };
895
+ const built = buildSandboxLeaseFile(ref.origins, entries);
896
+ if (!built.ok)
897
+ return fail("lease_invalid", `the lease was not sent — the broker would refuse it: ${built.problem}`);
898
+ const b64 = Buffer.from(built.text, "utf8").toString("base64");
899
+ const hidden = [b64, ...entries.flatMap((e) => (e.state === "ready" ? [e.secret, Buffer.from(`${e.username}:${e.secret}`, "utf8").toString("base64")] : []))].filter((v) => v !== "");
900
+ const scrub = (text) => hidden.reduce((out, v) => out.split(v).join("<redacted>"), text);
901
+ let r;
902
+ try {
903
+ r = await this.request("PATCH", `/api/v1/namespaces/${ref.namespace}/secrets/${encodeURIComponent(ref.leaseSecretName)}`, { data: { [BROKER_LEASE_KEY]: b64 } }, { contentType: "application/merge-patch+json" });
904
+ }
905
+ catch (e) {
906
+ return fail("write_failed", `the lease write request failed: ${scrub(errorMessageOf(e)).slice(0, 300)}`);
907
+ }
908
+ if (r.status === 200 || r.status === 201)
909
+ return { ok: true };
910
+ const detail = scrub(JSON.stringify(r.json) ?? "").slice(0, 300);
911
+ if (r.status === 401 || r.status === 403)
912
+ return fail("auth_failed", `the lease write was denied (${r.status}): ${detail} — the sandbox Role needs secrets ${SANDBOX_SECRET_SEAT_VERBS.lease.join(" / ")}`);
913
+ if (r.status === 404)
914
+ return fail("gone", `the lease Secret ${ref.leaseSecretName} is gone (its pod ${ref.podName} was deleted)`);
915
+ return fail("write_failed", `the lease write failed (${r.status}): ${detail}`);
916
+ }
706
917
  presignSnapshot(req) {
707
918
  const s3 = this.cfg.s3Snapshot;
708
919
  const [label, partition] = req.lane === "space" ? ["spaceIncarnation", req.space.incarnation] : ["sessionId", this.cfg.sessionId ?? "_"];
@@ -1090,7 +1301,27 @@ export class RemoteK8sExecutionEnv {
1090
1301
  return this.connect();
1091
1302
  }
1092
1303
  async deletePod(name) {
1093
- await this.request("DELETE", `/api/v1/namespaces/${this.cfg.namespace}/pods/${name}?gracePeriodSeconds=0`, undefined, { timeoutMs: this.cfg.rpcTimeoutMs });
1304
+ const r = await this.request("DELETE", `/api/v1/namespaces/${this.cfg.namespace}/pods/${encodeURIComponent(name)}?gracePeriodSeconds=0`, undefined, { timeoutMs: this.cfg.rpcTimeoutMs });
1305
+ if (r.status === 404)
1306
+ return "gone";
1307
+ if (r.status < 200 || r.status >= 300)
1308
+ throw new PodDeleteRefused(r.status);
1309
+ return "deleted";
1310
+ }
1311
+ async discardRefusedPod(name, why) {
1312
+ try {
1313
+ await this.deletePod(name);
1314
+ }
1315
+ catch (e) {
1316
+ reportPodDeleteFailed({
1317
+ namespace: this.cfg.namespace,
1318
+ pod: name,
1319
+ why,
1320
+ error: e,
1321
+ effect: "the refused connect's pod, and the Secret it owns, stay in the sandbox namespace until the pod reaper reaches them (static data; the connect is refused either way)",
1322
+ logger: this.logger,
1323
+ });
1324
+ }
1094
1325
  }
1095
1326
  async destroy() {
1096
1327
  await this.disposeBackgroundShells().catch(() => { });
@@ -1878,6 +2109,37 @@ export function createSpaceReclaimer(config, deps) {
1878
2109
  export async function checkSandboxEgressPolicy(config, deps) {
1879
2110
  return new RemoteK8sExecutionEnv(config, deps).egressPolicyGaps();
1880
2111
  }
2112
+ export async function readSandboxSecretSeat(config, seat, deps) {
2113
+ return new RemoteK8sExecutionEnv(config, deps).secretSeatReading(seat);
2114
+ }
2115
+ export async function writeSandboxLease(config, ref, entries, deps) {
2116
+ return new RemoteK8sExecutionEnv(config, deps).writeLease(ref, entries);
2117
+ }
2118
+ export function createSandboxPodApi(config, deps) {
2119
+ let env;
2120
+ const control = () => (env ??= new RemoteK8sExecutionEnv(config, deps));
2121
+ return {
2122
+ namespace: config.namespace ?? K8S_SANDBOX_NAMESPACE_DEFAULT,
2123
+ list: () => control().listSandboxPods(),
2124
+ delete: (name) => control().deletePod(name),
2125
+ };
2126
+ }
2127
+ function sandboxPodListingOf(item) {
2128
+ const o = item;
2129
+ const name = o?.metadata?.name;
2130
+ if (typeof name !== "string" || name === "")
2131
+ return undefined;
2132
+ const runId = o?.metadata?.labels?.[SANDBOX_RUN_LABEL];
2133
+ const runEndHold = o?.metadata?.annotations?.[SANDBOX_RUN_END_HOLD_ANNOTATION];
2134
+ const phase = o?.status?.phase;
2135
+ return {
2136
+ name,
2137
+ ...(typeof runId === "string" && runId !== "" ? { runId } : {}),
2138
+ ...(typeof runEndHold === "string" ? { runEndHold } : {}),
2139
+ ...(typeof phase === "string" ? { phase } : {}),
2140
+ terminating: typeof o?.metadata?.deletionTimestamp === "string",
2141
+ };
2142
+ }
1881
2143
  export function k8sExecutionEnvFactory(config, deps) {
1882
2144
  return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId }, deps);
1883
2145
  }
@@ -74,6 +74,9 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
74
74
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在容器里(镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
75
75
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
76
76
  readonly cdPath: "unknown";
77
+ /** core 7.56.0 [ref]:shell 子进程继承的环境里收不到的条目 —— 本车道 shell 的环境在容器里**构建**(本进程的 env 从不注入,只有引擎逐次交的 `env`
78
+ * 这条显式授予),没有一条是从本进程继承的 ⇒ `[]`(core 契约:「`[]` when its shell environment is built rather than inherited」)。 */
79
+ readonly withheldEnv: readonly [];
77
80
  private readonly cfg;
78
81
  private readonly spawnFn;
79
82
  /** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
@@ -27,6 +27,7 @@ export class RemoteLocalDockerExecutionEnv {
27
27
  return this.homeSeat.home;
28
28
  }
29
29
  cdPath = "unknown";
30
+ withheldEnv = [];
30
31
  cfg;
31
32
  spawnFn;
32
33
  containerName;
@@ -95,6 +95,9 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
95
95
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在远端登录 shell 里(目标机的 profile / env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
96
96
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
97
97
  readonly cdPath: "unknown";
98
+ /** core 7.56.0 [ref]:shell 子进程继承的环境里收不到的条目 —— 本车道 shell 的环境在目标机上**构建**(本进程的 env 从不注入,只有引擎逐次交的 `env`
99
+ * 这条显式授予),没有一条是从本进程继承的 ⇒ `[]`(core 契约:「`[]` when its shell environment is built rather than inherited」)。 */
100
+ readonly withheldEnv: readonly [];
98
101
  private readonly cfg;
99
102
  private readonly injectedConnectOnce?;
100
103
  constructor(config: SshEnvConfig, deps?: SshEnvDeps);
@@ -32,6 +32,7 @@ export class RemoteSshExecutionEnv {
32
32
  homeChecked;
33
33
  homeRefusal;
34
34
  cdPath = "unknown";
35
+ withheldEnv = [];
35
36
  cfg;
36
37
  injectedConnectOnce;
37
38
  constructor(config, deps) {
@@ -10,10 +10,11 @@ import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR, readLegacyHolderDeathCode }
10
10
  import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal.js";
11
11
  import { mysqlDriver, pgDriver, dialectJsonEncoder, SQL_DB_NOW_MS, sqlDbNowMs } from "./sql-driver.js";
12
12
  import { isDupKeyError } from "./sql-errors.js";
13
+ import { readRunIdentityCells, runIdentityCells } from "../identity-source.js";
13
14
  import { decideSpaceCommitAbandon, decideSpaceCommitClaim, fenceSpaceCommitAtTerminal, spaceCommitStateOf } from "../workspace-space-commit.js";
14
15
  function runRowCols(dialect) {
15
16
  return (`task_id, session_id, owner, status, ${jsonColumnText(dialect, "result")}, error, error_code, job_id, source, ` +
16
- "instance_id, cancel_requested, objective_preview, created_at, updated_at");
17
+ "identity_source, identity_scope, identity_principal_inc, instance_id, cancel_requested, objective_preview, created_at, updated_at");
17
18
  }
18
19
  const readPersistedTaskResultBody = createObjectColumnReader();
19
20
  function decodeResultColumn(v, taskId) {
@@ -84,15 +85,19 @@ export class SqlRunStore {
84
85
  try {
85
86
  await conn.query(this.q("INSERT INTO task_active (session_id, task_id, since) VALUES (?,?,?)", "INSERT INTO task_active (session_id, task_id, since) VALUES ($1,$2,$3)"), [sessionId, taskId, now]);
86
87
  stage = "row";
87
- await conn.query(this.q("INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
88
- "VALUES (?,?,?,'running',?,?,?,?,?,?,?,?)", "INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
89
- "VALUES ($1,$2,$3,'running',$4,$5,$6,$7,$8,$9,$10,$11)"), [
88
+ const cells = runIdentityCells(meta.identity ?? null);
89
+ await conn.query(this.q("INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, identity_source, identity_scope, identity_principal_inc, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
90
+ "VALUES (?,?,?,'running',?,?,?,?,?,?,?,?,?,?,?)", "INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, identity_source, identity_scope, identity_principal_inc, objective_preview, idem_key, idem_body_fingerprint, created_at, updated_at) " +
91
+ "VALUES ($1,$2,$3,'running',$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)"), [
90
92
  taskId,
91
93
  sessionId,
92
94
  owner,
93
95
  instanceId,
94
96
  meta.jobId ?? null,
95
97
  meta.source ?? null,
98
+ cells.identitySource,
99
+ cells.identityScope,
100
+ cells.identityPrincipalInc,
96
101
  this.db.dialect === "tidb"
97
102
  ? meta.objectivePreview ?? null
98
103
  : meta.objectivePreview == null
@@ -299,6 +304,7 @@ export class SqlRunStore {
299
304
  errorCode: readLegacyHolderDeathCode({ status: String(r.status), error: r.error ?? null, errorCode: r.error_code ?? null }),
300
305
  jobId: r.job_id ?? null,
301
306
  source: r.source ?? null,
307
+ identity: readRunIdentityCells(r.identity_source, r.identity_scope, r.identity_principal_inc),
302
308
  instanceId: r.instance_id ?? null,
303
309
  cancelRequested: Boolean(r.cancel_requested),
304
310
  objectivePreview: r.objective_preview ?? null,
@@ -1,11 +1,25 @@
1
+ /**
2
+ * 「谁也不指的串」的描述句 —— 拒句里「给的是什么」那一格的串形(从不回显值:一个谁也不指的串可能就是一段被粘进来的凭据)。
3
+ * 本模块的属主拒与鉴权边界的主体拒(`security.ts` `assertPrincipalShape` 的 `principal_not_a_name`)读这同一句:判官同一只,描述也同一句。
4
+ */
5
+ export declare const NOT_A_PRINCIPAL_NAME = "a string that names nobody (empty, blank, or holding a control, separator or invisible format character)";
6
+ /**
7
+ * 「这个值能不能作一只会话行的属主格」—— core 7.56.0 [ref] 起只有一只判官 `principalNameOf`:主体名(有可见内容、不含控制 / 分隔 / 非连接符格式字符,
8
+ * 原样保存不规整)或 `null`(匿名)。空串、空白串、带不可见字符的串与一切非串都不是属主(它们谁也不指 —— 读成匿名会放行每一位主体)。
9
+ *
10
+ * 边界同判官:HTTP 请求带来的主体在鉴权边界已经过同一只 `principalNameOf`(`security.ts` `principalShapeProblem` 的 `not_a_name`,7.137.0)⇒
11
+ * 经过边界的请求到不了这里的拒臂;这里是纵深(嵌入方直调店、别的写者),规则只有引擎那一只,不在本仓另写。
12
+ */
13
+ export declare function isSessionOwnerValue(owner: unknown): owner is string | null;
1
14
  /**
2
15
  * fork 拷贝的属主 —— core `SessionStore.fork` 契约的唯一读法:
3
- * 显式串 ⇒ 该串;无参 / 显式 `null` ⇒ 源会话记下的属主(`undefined` = 源会话没记,拷贝也不记)。
16
+ * 显式主体名 ⇒ 该名;无参 / 显式 `null` ⇒ 源会话记下的属主(`undefined` = 源会话没记,拷贝也不记)。
17
+ * 显式给了却不是主体名(空串 / 空白 / 带不可见字符,core 7.56.0 [ref])⇒ 响亮拒(`SessionError("storage")`,不留行)—— 与 `register` 首写同一只判官。
4
18
  */
5
19
  export declare function forkOwnerOf(requested: string | null | undefined, sourceOwner: string | null | undefined): string | null | undefined;
6
20
  /**
7
- * `register` 首写的 owner 守卫 —— core `SessionStore.register` 契约的唯一读法(三只店同调):首写的 owner 既不是串也不是 `null` ⇒ 响亮拒(不留行);
8
- * 「首写」由调用方判(已记下属主的会话上的调用照旧静默不改,不经这里)。
21
+ * `register` 首写的 owner 守卫 —— core `SessionStore.register` 契约的唯一读法(三只店同调):首写的 owner 既不是主体名也不是 `null` ⇒ 响亮拒(不留行);
22
+ * 「首写」由调用方判(已记下属主的会话上的调用照旧静默不改,不经这里)。判官 = {@link isSessionOwnerValue}(core 7.56.0 [ref] `principalNameOf`)。
9
23
  */
10
24
  export declare function assertFirstOwnerWritable(sessionId: string, owner: unknown): asserts owner is string | null;
11
25
  //# sourceMappingURL=session-ownership.d.ts.map
@@ -1,11 +1,25 @@
1
- import { SessionError } from "@sema-agent/core";
1
+ import { SessionError, principalNameOf } from "@sema-agent/core";
2
+ export const NOT_A_PRINCIPAL_NAME = "a string that names nobody (empty, blank, or holding a control, separator or invisible format character)";
3
+ function ownerValueFault(owner) {
4
+ if (typeof owner === "string")
5
+ return NOT_A_PRINCIPAL_NAME;
6
+ return owner === undefined ? "undefined" : typeof owner;
7
+ }
8
+ export function isSessionOwnerValue(owner) {
9
+ return owner === null || principalNameOf(owner) !== undefined;
10
+ }
2
11
  export function forkOwnerOf(requested, sourceOwner) {
3
- return typeof requested === "string" ? requested : sourceOwner;
12
+ if (requested === undefined || requested === null)
13
+ return sourceOwner;
14
+ if (principalNameOf(requested) === undefined) {
15
+ throw new SessionError("storage", `fork: session owner must be a principal name or null, got ${ownerValueFault(requested)} — refusing, nothing written`);
16
+ }
17
+ return requested;
4
18
  }
5
19
  export function assertFirstOwnerWritable(sessionId, owner) {
6
- if (typeof owner === "string" || owner === null)
20
+ if (isSessionOwnerValue(owner))
7
21
  return;
8
- throw new SessionError("storage", `register(${sessionId}): a first ownership write must carry a principal (string) or null (anonymous), got ${owner === undefined ? "undefined" : typeof owner} — ` +
9
- "a stored value that the head read reads back as 'no row' would leave the session claimable forever and ownable never; refusing, nothing written");
22
+ throw new SessionError("storage", `register(${sessionId}): a first ownership write must carry a principal name or null (anonymous), got ${ownerValueFault(owner)} — ` +
23
+ "a value that names nobody would leave the session claimable by anyone or ownable by no one; refusing, nothing written");
10
24
  }
11
25
  //# sourceMappingURL=session-ownership.js.map