@sema-agent/server 7.135.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 (342) hide show
  1. package/MIGRATION.md +237 -2
  2. package/USAGE.md +146 -41
  3. package/deploy/sema-up/README.md +15 -1
  4. package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
  5. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
  6. package/deploy/sema-up/chart/values.yaml +1 -1
  7. package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
  8. package/dist/approval-card.d.ts +60 -4
  9. package/dist/approval-card.js +18 -1
  10. package/dist/approval-content-kind.d.ts +7 -0
  11. package/dist/approval-content-kind.js +8 -1
  12. package/dist/approval-gate-currency.d.ts +48 -6
  13. package/dist/approval-gate-currency.js +16 -9
  14. package/dist/approval-hmac.d.ts +13 -1
  15. package/dist/approval-hmac.js +1 -1
  16. package/dist/approval.js +3 -3
  17. package/dist/auth-bridge.d.ts +34 -5
  18. package/dist/auth-bridge.js +22 -5
  19. package/dist/boot/budget-tracing.d.ts +1 -2
  20. package/dist/boot/budget-tracing.js +2 -2
  21. package/dist/boot/execution-env.d.ts +24 -1
  22. package/dist/boot/execution-env.js +69 -6
  23. package/dist/boot/org-memory.js +1 -1
  24. package/dist/boot/reapers.d.ts +3 -0
  25. package/dist/boot/reapers.js +15 -1
  26. package/dist/boot/resolve-spec.d.ts +4 -0
  27. package/dist/boot/resolve-spec.js +24 -22
  28. package/dist/boot/runner-deps.js +4 -2
  29. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  30. package/dist/boot/stage-04-stores.d.ts +2 -2
  31. package/dist/boot/stage-05-execution-env.d.ts +4 -2
  32. package/dist/boot/stage-05-execution-env.js +11 -2
  33. package/dist/boot/stage-06-runners.d.ts +4 -2
  34. package/dist/boot/stage-06-runners.js +5 -1
  35. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  36. package/dist/boot/stage-07-capability-layer.js +1 -1
  37. package/dist/boot/stage-08-reapers.d.ts +5 -2
  38. package/dist/boot/stage-08-reapers.js +2 -1
  39. package/dist/boot/stage-09-leader.d.ts +5 -2
  40. package/dist/boot/stage-10-http-server.d.ts +5 -2
  41. package/dist/boot/stage-10-http-server.js +3 -2
  42. package/dist/boot/stores.d.ts +1 -1
  43. package/dist/boot/stores.js +2 -3
  44. package/dist/boot/web-admin.js +10 -17
  45. package/dist/brain.js +12 -3
  46. package/dist/broker/broker.d.ts +89 -0
  47. package/dist/broker/broker.js +287 -0
  48. package/dist/broker/constants.d.ts +39 -0
  49. package/dist/broker/constants.js +15 -0
  50. package/dist/broker/http1.d.ts +111 -0
  51. package/dist/broker/http1.js +256 -0
  52. package/dist/broker/lease-book.d.ts +87 -0
  53. package/dist/broker/lease-book.js +142 -0
  54. package/dist/broker/lease-file.d.ts +60 -0
  55. package/dist/broker/lease-file.js +127 -0
  56. package/dist/broker/main.d.ts +2 -0
  57. package/dist/broker/main.js +20 -0
  58. package/dist/budget.d.ts +3 -3
  59. package/dist/budget.js +4 -0
  60. package/dist/capabilities/composite-members.d.ts +2 -1
  61. package/dist/capabilities/composite-members.js +2 -1
  62. package/dist/capabilities/engine-capabilities.js +1 -0
  63. package/dist/capabilities/mcp-probe.d.ts +5 -4
  64. package/dist/capabilities/scenarios.d.ts +23 -8
  65. package/dist/capabilities/scenarios.js +13 -8
  66. package/dist/capabilities/skills.d.ts +28 -5
  67. package/dist/capabilities/skills.js +8 -0
  68. package/dist/capability-facts.d.ts +3 -7
  69. package/dist/capability-facts.js +3 -5
  70. package/dist/center-credential-renewer.js +5 -1
  71. package/dist/center-credential.d.ts +18 -4
  72. package/dist/center-credential.js +18 -3
  73. package/dist/checkpoint-ctx-seal-m28.d.ts +3 -10
  74. package/dist/checkpoint-ctx-seal-m28.js +1 -1
  75. package/dist/checkpoint-identity.d.ts +3 -3
  76. package/dist/config-catalog.js +21 -16
  77. package/dist/config-d-migration.js +1 -1
  78. package/dist/config-invariants.d.ts +1 -1
  79. package/dist/config-invariants.js +8 -0
  80. package/dist/config-lkg.d.ts +0 -38
  81. package/dist/config-lkg.js +1 -83
  82. package/dist/config-types.d.ts +21 -4
  83. package/dist/config.d.ts +1 -1
  84. package/dist/config.js +60 -16
  85. package/dist/deployment-governance.d.ts +2 -2
  86. package/dist/deployment-governance.js +7 -9
  87. package/dist/deployment-shape.d.ts +1 -1
  88. package/dist/device-ws-protocol.d.ts +1 -0
  89. package/dist/device-ws-protocol.js +2 -1
  90. package/dist/elicitation.d.ts +8 -7
  91. package/dist/elicitation.js +9 -8
  92. package/dist/file-history-disclosure.d.ts +8 -4
  93. package/dist/file-history-disclosure.js +24 -6
  94. package/dist/fleet-client.d.ts +32 -1
  95. package/dist/fleet-client.js +24 -4
  96. package/dist/fleet-lease.d.ts +7 -3
  97. package/dist/fleet-lease.js +31 -23
  98. package/dist/hooks/classic-stations.d.ts +11 -54
  99. package/dist/hooks/hook-llm.js +14 -8
  100. package/dist/hooks/hook-runner.d.ts +4 -4
  101. package/dist/hooks/hook-runner.js +2 -2
  102. package/dist/http/admission.js +10 -7
  103. package/dist/http/approval-sweeps.js +4 -3
  104. package/dist/http/caller-trust.d.ts +2 -1
  105. package/dist/http/caller-trust.js +1 -3
  106. package/dist/http/checkpoint-ctx.d.ts +76 -16
  107. package/dist/http/checkpoint-ctx.js +45 -1
  108. package/dist/http/dispatch.js +24 -19
  109. package/dist/http/durable-run-leg.d.ts +5 -1
  110. package/dist/http/durable-run-leg.js +1 -1
  111. package/dist/http/health-self-report.d.ts +2 -2
  112. package/dist/http/mcp-remote-text.d.ts +5 -0
  113. package/dist/http/mcp-remote-text.js +7 -0
  114. package/dist/http/principal-gate.d.ts +15 -10
  115. package/dist/http/principal-gate.js +48 -9
  116. package/dist/http/resume-ctx-read.d.ts +2 -16
  117. package/dist/http/resume-ctx-read.js +2 -3
  118. package/dist/http/resume-legs.d.ts +2 -20
  119. package/dist/http/resume-legs.js +52 -38
  120. package/dist/http/route-ctx.d.ts +8 -2
  121. package/dist/http/routes/approvals-assistant.d.ts +0 -2
  122. package/dist/http/routes/approvals-assistant.js +65 -16
  123. package/dist/http/routes/capabilities.js +11 -1
  124. package/dist/http/routes/mcp-reconnect.d.ts +1 -1
  125. package/dist/http/routes/mcp-reconnect.js +5 -7
  126. package/dist/http/routes/runs.js +4 -3
  127. package/dist/http/routes/session-sync.js +70 -12
  128. package/dist/http/routes/sessions.js +20 -15
  129. package/dist/http/routes/tasks.js +8 -6
  130. package/dist/http/server.d.ts +4 -0
  131. package/dist/http/stored-body-replay.d.ts +3 -3
  132. package/dist/http/stored-body-replay.js +6 -1
  133. package/dist/http/submit-ctx.js +1 -1
  134. package/dist/http/wire-types.d.ts +57 -30
  135. package/dist/identity-source.d.ts +65 -0
  136. package/dist/identity-source.js +45 -0
  137. package/dist/index.d.ts +1 -0
  138. package/dist/index.js +1 -0
  139. package/dist/memory-store-refusal.d.ts +3 -3
  140. package/dist/model-entry-refusal.d.ts +2 -0
  141. package/dist/model-entry-refusal.js +11 -0
  142. package/dist/model-route-gap.d.ts +49 -4
  143. package/dist/model-route-gap.js +34 -1
  144. package/dist/model-select.d.ts +6 -17
  145. package/dist/model-select.js +0 -3
  146. package/dist/namespaced-rule-name.d.ts +1 -1
  147. package/dist/object-store-buckets.d.ts +13 -0
  148. package/dist/object-store-buckets.js +3 -0
  149. package/dist/observability/fail-open.d.ts +19 -7
  150. package/dist/observability/fail-open.js +19 -7
  151. package/dist/observability/metrics.js +2 -2
  152. package/dist/observability/principal-context.d.ts +4 -1
  153. package/dist/observability/principal-context.js +6 -1
  154. package/dist/observability/run-terminal.js +1 -0
  155. package/dist/operator-ask.d.ts +2 -2
  156. package/dist/org-delivery-gate.js +1 -1
  157. package/dist/org-memory-admission.d.ts +2 -2
  158. package/dist/org-memory-admission.js +39 -2
  159. package/dist/parked-decide.d.ts +4 -3
  160. package/dist/parked-decide.js +29 -9
  161. package/dist/peer-directory.d.ts +2 -1
  162. package/dist/peer-directory.js +7 -5
  163. package/dist/permission-rule-list.d.ts +2 -2
  164. package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
  165. package/dist/plugins/approval-ask-store-sql.js +3 -0
  166. package/dist/plugins/blob-backend.js +2 -1
  167. package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
  168. package/dist/plugins/checkpoint-index-columns.js +28 -0
  169. package/dist/plugins/checkpoint-store-sql.d.ts +46 -12
  170. package/dist/plugins/checkpoint-store-sql.js +64 -35
  171. package/dist/plugins/file-history-store-sql.d.ts +6 -0
  172. package/dist/plugins/file-history-store-sql.js +37 -37
  173. package/dist/plugins/file-run-store.js +8 -2
  174. package/dist/plugins/k8s-exec-protocol.d.ts +69 -13
  175. package/dist/plugins/k8s-exec-protocol.js +116 -28
  176. package/dist/plugins/k8s-pod-delete-failure.d.ts +25 -0
  177. package/dist/plugins/k8s-pod-delete-failure.js +17 -0
  178. package/dist/plugins/k8s-pod-reaper.d.ts +112 -0
  179. package/dist/plugins/k8s-pod-reaper.js +122 -0
  180. package/dist/plugins/local-checkpoint-store.d.ts +3 -6
  181. package/dist/plugins/local-checkpoint-store.js +7 -6
  182. package/dist/plugins/memory-run-store.js +2 -0
  183. package/dist/plugins/one-time-migrations.d.ts +49 -37
  184. package/dist/plugins/one-time-migrations.js +87 -128
  185. package/dist/plugins/permission-rule-store-file.js +2 -2
  186. package/dist/plugins/permission-rule-store-sql.d.ts +17 -17
  187. package/dist/plugins/permission-rule-store-sql.js +16 -17
  188. package/dist/plugins/pg-pool.js +4 -0
  189. package/dist/plugins/pg-session-storage.js +2 -2
  190. package/dist/plugins/remote-env-adb.d.ts +3 -0
  191. package/dist/plugins/remote-env-adb.js +1 -0
  192. package/dist/plugins/remote-env-device.d.ts +3 -0
  193. package/dist/plugins/remote-env-device.js +4 -0
  194. package/dist/plugins/remote-env-e2b.d.ts +3 -0
  195. package/dist/plugins/remote-env-e2b.js +1 -0
  196. package/dist/plugins/remote-env-file-error.d.ts +8 -1
  197. package/dist/plugins/remote-env-file-error.js +7 -1
  198. package/dist/plugins/remote-env-host.d.ts +7 -1
  199. package/dist/plugins/remote-env-host.js +7 -1
  200. package/dist/plugins/remote-env-k8s.d.ts +154 -14
  201. package/dist/plugins/remote-env-k8s.js +300 -21
  202. package/dist/plugins/remote-env-local-docker.d.ts +3 -0
  203. package/dist/plugins/remote-env-local-docker.js +1 -0
  204. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  205. package/dist/plugins/remote-env-ssh.js +1 -0
  206. package/dist/plugins/run-store-sql.js +10 -4
  207. package/dist/plugins/s3-answer.d.ts +15 -0
  208. package/dist/plugins/s3-answer.js +5 -0
  209. package/dist/plugins/session-ownership.d.ts +17 -3
  210. package/dist/plugins/session-ownership.js +19 -5
  211. package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
  212. package/dist/plugins/snapshot-bucket-presence.js +35 -7
  213. package/dist/plugins/sql-errors.d.ts +60 -0
  214. package/dist/plugins/sql-errors.js +64 -1
  215. package/dist/plugins/store-contracts.d.ts +10 -1
  216. package/dist/plugins/store-fault.d.ts +4 -0
  217. package/dist/plugins/store-fault.js +10 -0
  218. package/dist/plugins/tidb-pool.js +8 -1
  219. package/dist/plugins/tidb-session-store.js +2 -2
  220. package/dist/plugins/web-search.d.ts +1 -1
  221. package/dist/plugins/web-search.js +2 -2
  222. package/dist/request-declaration-refusal.d.ts +94 -0
  223. package/dist/request-declaration-refusal.js +78 -0
  224. package/dist/request-image-urls.d.ts +6 -0
  225. package/dist/request-image-urls.js +27 -0
  226. package/dist/request-key-closure.d.ts +5 -0
  227. package/dist/request-key-closure.js +5 -2
  228. package/dist/request-value-closure.d.ts +31 -2
  229. package/dist/request-value-closure.js +7 -1
  230. package/dist/rule-entry-list.d.ts +3 -2
  231. package/dist/rule-entry-list.js +2 -2
  232. package/dist/rules-consent.js +7 -4
  233. package/dist/run-local.d.ts +2 -2
  234. package/dist/runs.js +2 -1
  235. package/dist/runtime-caps-resolver.js +1 -1
  236. package/dist/runtime-governance.d.ts +5 -5
  237. package/dist/sandbox-git-lease.d.ts +49 -0
  238. package/dist/sandbox-git-lease.js +60 -0
  239. package/dist/sandbox-git-plane.d.ts +84 -0
  240. package/dist/sandbox-git-plane.js +105 -0
  241. package/dist/sandbox-pkg-source.d.ts +28 -1
  242. package/dist/sandbox-pkg-source.js +1 -2
  243. package/dist/sealed-key.d.ts +19 -0
  244. package/dist/sealed-key.js +47 -4
  245. package/dist/security.d.ts +16 -4
  246. package/dist/security.js +8 -3
  247. package/dist/server-secret-env.d.ts +6 -0
  248. package/dist/server-secret-env.js +14 -0
  249. package/dist/session-error-wire.d.ts +14 -6
  250. package/dist/session-error-wire.js +2 -1
  251. package/dist/session-incarnation.d.ts +16 -5
  252. package/dist/session-incarnation.js +7 -4
  253. package/dist/session-policy-wire.d.ts +1 -1
  254. package/dist/session-policy-wire.js +2 -2
  255. package/dist/session-row.d.ts +3 -17
  256. package/dist/session-row.js +4 -0
  257. package/dist/session-sync.d.ts +15 -3
  258. package/dist/session-sync.js +5 -2
  259. package/dist/spec-fields.d.ts +43 -13
  260. package/dist/spec-fields.js +33 -12
  261. package/dist/stored-request-prompt-profile-m30.d.ts +49 -0
  262. package/dist/stored-request-prompt-profile-m30.js +25 -0
  263. package/dist/task-a2a.d.ts +52 -81
  264. package/dist/task-a2a.js +219 -89
  265. package/dist/task-mcp.d.ts +7 -15
  266. package/dist/task-mcp.js +16 -51
  267. package/dist/task-settings.d.ts +2 -2
  268. package/dist/tool-approval.d.ts +35 -12
  269. package/dist/tool-approval.js +59 -39
  270. package/dist/tool-rule-names.d.ts +5 -0
  271. package/dist/trace/core-keyset-guard.d.ts +5 -5
  272. package/dist/trace/engine-notice-wire.d.ts +1 -1
  273. package/dist/trace/engine-notice-wire.js +2 -0
  274. package/dist/trace/exit-seat.js +37 -13
  275. package/dist/trace/ledger-sink.d.ts +4 -5
  276. package/dist/trace/ledger-sink.js +3 -7
  277. package/dist/trace/project.d.ts +37 -3
  278. package/dist/trace/project.js +20 -6
  279. package/dist/trace/redact.d.ts +40 -12
  280. package/dist/trace/redact.js +332 -128
  281. package/dist/trace/self-minted-wire.d.ts +641 -0
  282. package/dist/trace/self-minted-wire.js +201 -0
  283. package/dist/trace/wire-projection-faces.d.ts +5 -3
  284. package/dist/trace/wire-projection-faces.js +7 -3
  285. package/dist/url-credential-refusal.d.ts +52 -0
  286. package/dist/url-credential-refusal.js +14 -0
  287. package/dist/url-refusal.d.ts +13 -4
  288. package/dist/url-refusal.js +8 -2
  289. package/dist/web-admin-client/apply-effective.js +0 -1
  290. package/dist/web-admin-client/center-request.d.ts +20 -2
  291. package/dist/web-admin-client/center-request.js +40 -5
  292. package/dist/web-admin-client/http-client.js +6 -6
  293. package/dist/web-admin-client/types.d.ts +5 -5
  294. package/dist/workspace-space.d.ts +27 -0
  295. package/dist/workspace-space.js +26 -0
  296. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
  297. package/package.json +4 -3
  298. package/scripts/drain-check-local.mjs +2 -1
  299. package/scripts/drain-check.mysql.sql +3 -1
  300. package/scripts/drain-check.pg.sql +3 -1
  301. package/skills/schedule.md +1 -1
  302. package/wire-changes/server@7.104.0.json +134 -0
  303. package/wire-changes/server@7.105.0.json +53 -0
  304. package/wire-changes/server@7.106.0.json +475 -0
  305. package/wire-changes/server@7.107.0.json +64 -0
  306. package/wire-changes/server@7.108.0.json +153 -0
  307. package/wire-changes/server@7.109.0.json +46 -0
  308. package/wire-changes/server@7.110.0.json +90 -0
  309. package/wire-changes/server@7.111.0.json +86 -0
  310. package/wire-changes/server@7.112.0.json +60 -0
  311. package/wire-changes/server@7.113.0.json +26 -0
  312. package/wire-changes/server@7.114.0.json +26 -0
  313. package/wire-changes/server@7.115.0.json +97 -0
  314. package/wire-changes/server@7.116.0.json +218 -0
  315. package/wire-changes/server@7.116.1.json +26 -0
  316. package/wire-changes/server@7.117.0.json +65 -0
  317. package/wire-changes/server@7.118.0.json +129 -0
  318. package/wire-changes/server@7.119.0.json +65 -0
  319. package/wire-changes/server@7.120.0.json +89 -0
  320. package/wire-changes/server@7.121.0.json +219 -0
  321. package/wire-changes/server@7.122.0.json +54 -0
  322. package/wire-changes/server@7.123.0.json +26 -0
  323. package/wire-changes/server@7.124.0.json +336 -0
  324. package/wire-changes/server@7.124.1.json +26 -0
  325. package/wire-changes/server@7.125.0.json +111 -0
  326. package/wire-changes/server@7.126.0.json +285 -0
  327. package/wire-changes/server@7.127.0.json +77 -0
  328. package/wire-changes/server@7.127.1.json +26 -0
  329. package/wire-changes/server@7.127.2.json +53 -0
  330. package/wire-changes/server@7.128.0.json +26 -0
  331. package/wire-changes/server@7.129.0.json +106 -0
  332. package/wire-changes/server@7.129.1.json +26 -0
  333. package/wire-changes/server@7.130.0.json +26 -0
  334. package/wire-changes/server@7.131.0.json +118 -0
  335. package/wire-changes/server@7.132.0.json +226 -0
  336. package/wire-changes/server@7.133.0.json +1155 -0
  337. package/wire-changes/server@7.134.0.json +107 -0
  338. package/wire-changes/server@7.135.0.json +174 -0
  339. package/wire-changes/server@7.136.0.json +448 -0
  340. package/wire-changes/server@7.137.0.json +825 -0
  341. package/dist/url-credential.d.ts +0 -52
  342. package/dist/url-credential.js +0 -49
@@ -6,11 +6,14 @@ import { StringDecoder } from "node:string_decoder";
6
6
  import WebSocket from "ws";
7
7
  import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode, numEnvOr } from "./remote-shell.js";
8
8
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
9
- import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotNameOwnedBy } from "../workspace-space.js";
9
+ import { WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "../object-store-buckets.js";
10
+ import { SNAPSHOT_BUCKET_MISSING, isNoSuchBucketAnswer } from "./s3-answer.js";
11
+ import { K8S_WORKSPACE_MOUNT_DEFAULT, SNAPSHOT_KEY_SEGMENT, WORKSPACE_SNAPSHOT_PREFIX_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, mintSnapshotId, snapshotKeyPrefixProblem, snapshotNameOwnedBy } from "../workspace-space.js";
10
12
  import { spaceCommitAlive } from "../workspace-space-commit.js";
11
13
  import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
12
14
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
13
15
  import { recordFailOpen } from "../observability/fail-open.js";
16
+ import { PodDeleteRefused, reportPodDeleteFailed } from "./k8s-pod-delete-failure.js";
14
17
  import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
15
18
  import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
16
19
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
@@ -37,9 +40,12 @@ const unsupported = (op) => ({
37
40
  const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
38
41
  const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
39
42
  const WRITE_CHUNK_BYTES = 64 * 1024;
40
- 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";
41
44
  export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
42
- 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;
43
49
  function occupantKeyOf(occupant) {
44
50
  if (occupant === undefined)
45
51
  return { state: "unavailable", reason: "incarnation_unknown" };
@@ -166,12 +172,39 @@ export function errorMessageOf(e) {
166
172
  }
167
173
  export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
168
174
  export const K8S_SANDBOX_NAMESPACE_DEFAULT = "sandboxes";
169
- 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
+ }
170
202
  export class RemoteK8sExecutionEnv {
171
203
  capabilities;
172
204
  cwd;
173
205
  podName;
174
206
  podIP;
207
+ leaseRef;
175
208
  destroyed = false;
176
209
  handleSnapshotId;
177
210
  inFlight = new Set();
@@ -183,6 +216,7 @@ export class RemoteK8sExecutionEnv {
183
216
  }
184
217
  homeSeat;
185
218
  cdPath = "unknown";
219
+ withheldEnv = [];
186
220
  cfg;
187
221
  deps;
188
222
  occupantIncarnation;
@@ -218,6 +252,9 @@ export class RemoteK8sExecutionEnv {
218
252
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
219
253
  ...(config.podSpecPatch ? { podSpecPatch: config.podSpecPatch } : {}),
220
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 } : {}),
221
258
  ...(config.snapshotExclude && config.snapshotExclude.length > 0 ? { snapshotExclude: config.snapshotExclude } : {}),
222
259
  };
223
260
  this.cwd = this.cfg.mountPath;
@@ -241,6 +278,9 @@ export class RemoteK8sExecutionEnv {
241
278
  supportsDetach: true,
242
279
  };
243
280
  }
281
+ get sandboxLeaseRef() {
282
+ return this.leaseRef !== undefined && this.leaseRef.podName === this.podName ? this.leaseRef : undefined;
283
+ }
244
284
  get lifetimeMs() {
245
285
  return this.cfg.timeoutMs;
246
286
  }
@@ -291,7 +331,7 @@ export class RemoteK8sExecutionEnv {
291
331
  headers: {
292
332
  authorization: `Bearer ${token}`,
293
333
  accept: "application/json",
294
- ...(payload ? { "content-type": "application/json", "content-length": Buffer.byteLength(payload) } : {}),
334
+ ...(payload ? { "content-type": opts?.contentType ?? "application/json", "content-length": Buffer.byteLength(payload) } : {}),
295
335
  },
296
336
  ...(isHttps ? { ca: ca ? [ca] : undefined, rejectUnauthorized: !this.cfg.insecureTls } : {}),
297
337
  timeout: opts?.timeoutMs ?? this.cfg.rpcTimeoutMs,
@@ -507,7 +547,7 @@ export class RemoteK8sExecutionEnv {
507
547
  const claim = {
508
548
  apiVersion: "v1",
509
549
  kind: "PersistentVolumeClaim",
510
- 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"] } },
511
551
  spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: cache.size } } },
512
552
  };
513
553
  const made = await this.request("POST", base, claim, { signal });
@@ -560,6 +600,38 @@ export class RemoteK8sExecutionEnv {
560
600
  applyPodSpecPatch(probe, { labels: this.cfg.podSpecPatch.labels });
561
601
  return sandboxEgressGaps({ nodes, policies, podLabels: probe.metadata.labels, ipv6: this.cfg.ipv6 });
562
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
+ }
563
635
  reclaimDeadlinePassed(what) {
564
636
  return `reclaim deadline (${this.cfg.dataTimeoutMs} ms, the snapshot time bound, one per reclaim call) passed — ${what}`;
565
637
  }
@@ -577,19 +649,36 @@ export class RemoteK8sExecutionEnv {
577
649
  }
578
650
  async createPod(signal, cache) {
579
651
  const ns = this.cfg.namespace;
580
- 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);
581
668
  const sysctls = sandboxPodSysctls(this.cfg.ipv6);
582
669
  const pod = {
583
670
  apiVersion: "v1",
584
671
  kind: "Pod",
585
672
  metadata: {
586
673
  generateName: "sbx-",
587
- 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)) } } : {}),
588
676
  },
589
677
  spec: {
590
678
  ...(this.cfg.runtimeClass ? { runtimeClassName: this.cfg.runtimeClass } : {}),
591
679
  ...(sysctls ? { securityContext: { sysctls: sysctls.map((x) => ({ ...x })) } } : {}),
592
680
  restartPolicy: "Never",
681
+ ...(broker !== undefined ? { shareProcessNamespace: false } : {}),
593
682
  automountServiceAccountToken: false,
594
683
  activeDeadlineSeconds: Math.ceil(this.cfg.timeoutMs / 1000),
595
684
  terminationGracePeriodSeconds: 1,
@@ -597,18 +686,30 @@ export class RemoteK8sExecutionEnv {
597
686
  {
598
687
  name: "sandbox",
599
688
  image: this.cfg.image,
600
- command: this.cfg.podEnv?.SEMA_PKG_SOURCE
689
+ command: pkgSourceMarked
601
690
  ? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
602
691
  : ["sh", "-c", "sleep infinity"],
603
- ...(Object.keys(podEnv).length > 0 ? { env: Object.entries(podEnv).map(([name, value]) => ({ name, value: String(value) })) } : {}),
692
+ ...(envEntries.length > 0 ? { env: envEntries } : {}),
604
693
  ...(cache ? { volumeMounts: [{ name: CACHE_VOLUME, mountPath: cache.mount }] } : {}),
605
694
  resources: { limits: { cpu: this.cfg.cpu, memory: this.cfg.memory } },
606
695
  },
696
+ ...(broker !== undefined ? [brokerContainer(broker.image)] : []),
607
697
  ],
608
- ...(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
+ ]),
609
702
  },
610
703
  };
611
- 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)
612
713
  applyPodSpecPatch(pod, this.cfg.podSpecPatch);
613
714
  let created;
614
715
  try {
@@ -626,6 +727,20 @@ export class RemoteK8sExecutionEnv {
626
727
  const name = created.json?.metadata?.name;
627
728
  if (!name)
628
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
+ }
629
744
  const deadline = Date.now() + this.cfg.readyTimeoutMs;
630
745
  let podIP;
631
746
  for (;;) {
@@ -643,7 +758,8 @@ export class RemoteK8sExecutionEnv {
643
758
  }
644
759
  const status = got.json?.status;
645
760
  const phase = status?.phase;
646
- if (phase === "Running") {
761
+ const brokerStatus = status?.containerStatuses?.find((c) => c.name === BROKER_CONTAINER_NAME);
762
+ if (phase === "Running" && (broker === undefined || brokerStatus?.ready === true)) {
647
763
  podIP = status?.podIP;
648
764
  break;
649
765
  }
@@ -651,11 +767,16 @@ export class RemoteK8sExecutionEnv {
651
767
  await this.deletePod(name).catch(() => { });
652
768
  return this.refusePod(name, phase, status?.reason, status?.message);
653
769
  }
654
- const waiting = status?.containerStatuses?.[0]?.state?.waiting;
770
+ const waiting = status?.containerStatuses?.find((c) => c.name !== BROKER_CONTAINER_NAME)?.state?.waiting;
655
771
  if (waiting?.reason === "ImagePullBackOff" || waiting?.reason === "ErrImagePull") {
656
772
  await this.deletePod(name).catch(() => { });
657
773
  return { ok: false, error: new RemoteExecutionError("connect_failed", `image pull failed for ${this.cfg.image}: ${waiting.message ?? waiting.reason}`) };
658
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
+ }
659
780
  if (Date.now() > deadline) {
660
781
  if (cache) {
661
782
  const bound = await this.cacheClaimBound(cache, signal);
@@ -669,6 +790,9 @@ export class RemoteK8sExecutionEnv {
669
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)` };
670
791
  }
671
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
+ }
672
796
  return { ok: false, error: new RemoteExecutionError("timeout", `pod not Running within ${this.cfg.readyTimeoutMs}ms (phase=${phase ?? "?"})`) };
673
797
  }
674
798
  await new Promise((r) => setTimeout(r, 1000));
@@ -679,6 +803,7 @@ export class RemoteK8sExecutionEnv {
679
803
  }
680
804
  this.podName = name;
681
805
  this.podIP = podIP;
806
+ this.leaseRef = leaseName !== undefined && broker !== undefined ? { namespace: ns, podName: name, leaseSecretName: leaseName, origins: [...broker.origins] } : undefined;
682
807
  const askHome = this.homeSeat.asks;
683
808
  const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}${askHome ? ` && { ${LANE_HOME_PROBE_COMMAND} || true; }` : ""}`);
684
809
  if (!mk.ok || mk.value.exitCode !== 0) {
@@ -701,23 +826,110 @@ export class RemoteK8sExecutionEnv {
701
826
  this.logger?.warn("workspace_cache_fresh", { space: incarnationLogPrefix(cache.spaceKey), claim: cache.claim.name, size: cache.size });
702
827
  return ok(this.workspaceHandle());
703
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
+ }
704
917
  presignSnapshot(req) {
705
918
  const s3 = this.cfg.s3Snapshot;
706
- const prefix = s3.keyPrefix ?? "kata";
707
919
  const [label, partition] = req.lane === "space" ? ["spaceIncarnation", req.space.incarnation] : ["sessionId", this.cfg.sessionId ?? "_"];
708
920
  const snapshotId = "snapshotId" in req ? req.snapshotId : undefined;
709
921
  const objectName = "object" in req ? req.object : undefined;
710
- const safe = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
711
922
  const segments = [
712
923
  [label, partition],
713
924
  ...(snapshotId !== undefined ? [["snapshotId", snapshotId]] : []),
714
925
  ...(objectName !== undefined ? [["object", objectName]] : []),
715
- ["keyPrefix", prefix],
716
926
  ];
717
927
  for (const [label, seg] of segments) {
718
- if (!safe.test(seg))
928
+ if (!SNAPSHOT_KEY_SEGMENT.test(seg))
719
929
  return { ok: false, error: new RemoteExecutionError("connect_failed", `unsafe snapshot key segment (${label}): refusing to build a traversing object key`) };
720
930
  }
931
+ if (snapshotKeyPrefixProblem(this.snapshotKeyPrefix()) !== undefined)
932
+ return { ok: false, error: new RemoteExecutionError("connect_failed", "unsafe snapshot key segment (keyPrefix): refusing to build a traversing object key") };
721
933
  const common = {
722
934
  endpoint: s3.endpoint,
723
935
  bucket: s3.bucket,
@@ -732,8 +944,11 @@ export class RemoteK8sExecutionEnv {
732
944
  const object = "pointer" in req ? req.pointer : "object" in req ? req.object : `${req.snapshotId}.tar.gz`;
733
945
  return { ok: true, value: presignS3Url({ ...common, key: `${base}${object}`, method: req.method }) };
734
946
  }
947
+ snapshotKeyPrefix() {
948
+ return this.cfg.s3Snapshot?.keyPrefix ?? WORKSPACE_SNAPSHOT_PREFIX_DEFAULT;
949
+ }
735
950
  snapshotBase(req) {
736
- const prefix = this.cfg.s3Snapshot.keyPrefix ?? "kata";
951
+ const prefix = this.snapshotKeyPrefix();
737
952
  return req.lane === "space" ? `${prefix}/${req.space.incarnation}/space/` : `${prefix}/${this.cfg.sessionId ?? "_"}/`;
738
953
  }
739
954
  async reclaimSpaceSnapshots(spaceKey, deadline) {
@@ -825,12 +1040,25 @@ export class RemoteK8sExecutionEnv {
825
1040
  catch (e) {
826
1041
  return spaceReadFailed(what, `: ${this.redactUrl(errorMessageOf(e), url.value)}`, e);
827
1042
  }
1043
+ if (isNoSuchBucketAnswer(res.status, body))
1044
+ return this.refuseMissingBucket(space);
828
1045
  if (res.status === 404)
829
1046
  return ok(undefined);
830
1047
  if (!res.ok)
831
1048
  return spaceReadFailed(what, ` (HTTP ${res.status})`);
832
1049
  return ok(body.trim());
833
1050
  }
1051
+ refuseMissingBucket(space) {
1052
+ const bucket = this.cfg.s3Snapshot.bucket;
1053
+ this.spaceLine("workspace_snapshot_bucket_missing", space, {
1054
+ bucket,
1055
+ code: SNAPSHOT_BUCKET_MISSING,
1056
+ effect: "the workspace snapshot bucket does not exist in the object store (the server never creates buckets) — this run is refused rather than started on a workspace that could be neither restored nor saved",
1057
+ });
1058
+ return this.holdRefusal(spaceReadFailed("pointer", ` (${SNAPSHOT_BUCKET_MISSING}): the workspace snapshot bucket "${bucket}" (WORKSPACE_SNAPSHOT_BUCKET; default ${WORKSPACE_SNAPSHOT_BUCKET_DEFAULT}) does not exist in the object store — ` +
1059
+ `refusing to start this run on a workspace that could be neither restored nor saved. Ways out: create the bucket, or set WORKSPACE_SNAPSHOT_BUCKET to one that exists, ` +
1060
+ "or set WORKSPACE_PERSIST=off for a deployment that keeps no workspace between runs").error);
1061
+ }
834
1062
  async readLatest(space, signal) {
835
1063
  const r = await this.readPointer(space, "latest", signal);
836
1064
  return r.ok ? ok(r.value === undefined ? undefined : r.value) : r;
@@ -1045,7 +1273,7 @@ export class RemoteK8sExecutionEnv {
1045
1273
  });
1046
1274
  }
1047
1275
  async dropDanglingLatest(space, snapshotId) {
1048
- const prefix = this.cfg.s3Snapshot?.keyPrefix ?? "kata";
1276
+ const prefix = this.snapshotKeyPrefix();
1049
1277
  const dropped = await this.writePointer(space, "latest", null);
1050
1278
  this.spaceLine("workspace_latest_dangling", space, {
1051
1279
  key: `${prefix}/${space.incarnation}/space/${snapshotId}.tar.gz`,
@@ -1073,7 +1301,27 @@ export class RemoteK8sExecutionEnv {
1073
1301
  return this.connect();
1074
1302
  }
1075
1303
  async deletePod(name) {
1076
- 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
+ }
1077
1325
  }
1078
1326
  async destroy() {
1079
1327
  await this.disposeBackgroundShells().catch(() => { });
@@ -1861,6 +2109,37 @@ export function createSpaceReclaimer(config, deps) {
1861
2109
  export async function checkSandboxEgressPolicy(config, deps) {
1862
2110
  return new RemoteK8sExecutionEnv(config, deps).egressPolicyGaps();
1863
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
+ }
1864
2143
  export function k8sExecutionEnvFactory(config, deps) {
1865
2144
  return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId }, deps);
1866
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,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How this server reads an S3-compatible store's ANSWER — the one place a "the bucket itself is not there" reading is spelled.
3
+ *
4
+ * A 404 from an object store means two different things: `NoSuchKey` (the bucket is there, this object is not — a space's first run,
5
+ * a pointer never written) and `NoSuchBucket` (the bucket was never created — a deployment error no run can fix by retrying). Reading
6
+ * the second as the first turns a missing bucket into a silently empty workspace on every run. The ONLY evidence of a missing bucket is
7
+ * a 404 whose body carries the S3 error code `NoSuchBucket`: a bare 404, a 404 from a proxy or a wrong path, a 403 (credentials that
8
+ * cannot list ≠ no bucket) and a 5xx prove nothing about the bucket. Every consumer asks this one predicate: the boot probe of a
9
+ * configured bucket (`MinioBlobBackend.probeBucket`) and the runtime read of the workspace `latest` pointer (`remote-env-k8s.ts`).
10
+ */
11
+ /** The code a run's refusal / operator line carries when the store answered "no such bucket" (the `workspace_snapshot_bucket_missing` line's `code`). */
12
+ export declare const SNAPSHOT_BUCKET_MISSING = "snapshot_bucket_missing";
13
+ /** Does this answer prove the bucket is missing? `status` = the HTTP status, `body` = the answer's text (an S3 error document). */
14
+ export declare function isNoSuchBucketAnswer(status: number, body: string): boolean;
15
+ //# sourceMappingURL=s3-answer.d.ts.map
@@ -0,0 +1,5 @@
1
+ export const SNAPSHOT_BUCKET_MISSING = "snapshot_bucket_missing";
2
+ export function isNoSuchBucketAnswer(status, body) {
3
+ return status === 404 && /<Code>NoSuchBucket<\/Code>/.test(body);
4
+ }
5
+ //# sourceMappingURL=s3-answer.js.map