@moondeck/station 0.0.1 → 1.0.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 (326) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -2
  3. package/bin/uai-host.mjs +130 -0
  4. package/db/migrations/0000_host_tasks.sql +12 -0
  5. package/db/migrations/0001_host_ui.sql +11 -0
  6. package/db/migrations/0002_host_github_tokens.sql +8 -0
  7. package/db/migrations/0003_host_ssh_keys.sql +8 -0
  8. package/db/migrations/0004_host_owner_name.sql +1 -0
  9. package/db/migrations/0005_host_env_vars.sql +5 -0
  10. package/db/migrations/0006_host_project_env.sql +9 -0
  11. package/db/migrations/0007_host_github_token_kind.sql +7 -0
  12. package/db/migrations/0008_host_mcp_connections.sql +18 -0
  13. package/db/migrations/0009_host_agent_sessions.sql +15 -0
  14. package/db/migrations/0010_host_engine_accounts.sql +13 -0
  15. package/db/migrations/0011_codex_thread_id.sql +5 -0
  16. package/db/migrations/0012_agent_session_attach_key.sql +4 -0
  17. package/db/migrations/0013_github_credential_generations.sql +11 -0
  18. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  19. package/db/migrations/0015_host_settings.sql +9 -0
  20. package/db/migrations/0016_task_environment.sql +2 -0
  21. package/db/migrations/0017_engine_account_scope.sql +7 -0
  22. package/db/migrations/0018_engine_account_position.sql +3 -0
  23. package/db/migrations/0019_engine_account_store_ref.sql +3 -0
  24. package/db/migrations/0020_user_credential_store_ref.sql +6 -0
  25. package/db/migrations/0021_project_env_store_ref.sql +3 -0
  26. package/db/migrations/0022_task_project_ids.sql +5 -0
  27. package/db/migrations/0023_project_env_org.sql +4 -0
  28. package/db/migrations/0024_task_env_mode.sql +9 -0
  29. package/db/migrations/0025_project_env_authority.sql +147 -0
  30. package/db/migrations/0026_project_env_replica.sql +150 -0
  31. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  32. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  33. package/db/migrations/0029_task_shared_files_mode.sql +5 -0
  34. package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
  35. package/db/migrations/meta/_journal.json +223 -0
  36. package/db/schema.ts +850 -0
  37. package/images/standard/Dockerfile +347 -0
  38. package/images/standard/README.md +167 -0
  39. package/images/standard/container/code-server-settings.json +63 -0
  40. package/images/standard/container/corepack-version +1 -0
  41. package/images/standard/container/uai-init +704 -0
  42. package/images/standard/container/uai-materialize-runtimes +1527 -0
  43. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  44. package/images/standard/tool-versions +2 -0
  45. package/lib/__fixtures__/skill-index/SKILL.md +10 -0
  46. package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
  47. package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
  48. package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
  49. package/lib/agent-cli.ts +1507 -0
  50. package/lib/agent-errors.ts +22 -0
  51. package/lib/agent.ts +672 -0
  52. package/lib/agents/attach-compatibility.ts +51 -0
  53. package/lib/agents/claude.ts +918 -0
  54. package/lib/agents/codex-token-usage.fixture.json +130 -0
  55. package/lib/agents/codex.ts +1392 -0
  56. package/lib/agents/cursor-period-usage.ts +253 -0
  57. package/lib/agents/cursor.ts +562 -0
  58. package/lib/agents/dispatch.ts +78 -0
  59. package/lib/agents/durable-proc.ts +334 -0
  60. package/lib/agents/factory.ts +57 -0
  61. package/lib/agents/grok.ts +381 -0
  62. package/lib/agents/kimi.ts +329 -0
  63. package/lib/agents/machine-durable.ts +604 -0
  64. package/lib/agents/mock.ts +133 -0
  65. package/lib/agents/mode.ts +64 -0
  66. package/lib/agents/opencode.ts +508 -0
  67. package/lib/agents/output-source.ts +3 -0
  68. package/lib/agents/proc.ts +221 -0
  69. package/lib/agents/rate-limit.ts +337 -0
  70. package/lib/agents/registry.ts +182 -0
  71. package/lib/agents/session-state.ts +197 -0
  72. package/lib/agents/skill-index-preamble.fixture.ts +26 -0
  73. package/lib/agents/transport.ts +741 -0
  74. package/lib/agents/types.ts +254 -0
  75. package/lib/agents/uai.ts +106 -0
  76. package/lib/agents/usage.ts +44 -0
  77. package/lib/apple-one-off.ts +277 -0
  78. package/lib/apple-runtime-recycle.ts +237 -0
  79. package/lib/apple-uninstall-teardown.ts +224 -0
  80. package/lib/assignment-evidence.ts +94 -0
  81. package/lib/attachments.ts +48 -0
  82. package/lib/browser-testing.ts +1537 -0
  83. package/lib/capture-flow.ts +332 -0
  84. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  85. package/lib/cloud-credential-host.ts +512 -0
  86. package/lib/cloud-credential-leases.ts +2587 -0
  87. package/lib/cloud-credential-runtime.ts +1357 -0
  88. package/lib/cloud-state.ts +56 -0
  89. package/lib/codex-auth.ts +456 -0
  90. package/lib/command-db.ts +310 -0
  91. package/lib/compose-network-recovery.ts +283 -0
  92. package/lib/connection-secret-redaction.ts +25 -0
  93. package/lib/container-runtime.ts +1453 -0
  94. package/lib/credential-cleanup-inventory.ts +490 -0
  95. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  96. package/lib/credential-consumers/engines.ts +797 -0
  97. package/lib/credential-consumers/files.ts +934 -0
  98. package/lib/credential-consumers/github.ts +702 -0
  99. package/lib/credential-consumers/mcp.ts +389 -0
  100. package/lib/credential-consumers/task-files.ts +127 -0
  101. package/lib/credential-consumers/task-secrets.ts +10 -0
  102. package/lib/credential-store-pickup.ts +149 -0
  103. package/lib/credential-store-scope.ts +112 -0
  104. package/lib/credential-store.ts +932 -0
  105. package/lib/credentials-activation-authority.ts +709 -0
  106. package/lib/credentials-purge.ts +1006 -0
  107. package/lib/custom-connection-capture.ts +395 -0
  108. package/lib/custom-connection-launch.ts +384 -0
  109. package/lib/custom-connection-runtime.ts +141 -0
  110. package/lib/db.ts +80 -0
  111. package/lib/disk-pressure.ts +42 -0
  112. package/lib/docker-exec.ts +156 -0
  113. package/lib/engine-accounts.ts +1970 -0
  114. package/lib/engine-connect.ts +321 -0
  115. package/lib/engine-credential-env.ts +52 -0
  116. package/lib/engine-login.ts +2933 -0
  117. package/lib/engines.ts +1342 -0
  118. package/lib/enrollment-state.ts +251 -0
  119. package/lib/env-file.ts +246 -0
  120. package/lib/env-schema.ts +891 -0
  121. package/lib/env.ts +195 -0
  122. package/lib/fs-aliases.ts +49 -0
  123. package/lib/git-clone-credentials.ts +334 -0
  124. package/lib/git-diff.ts +530 -0
  125. package/lib/git-identity.ts +715 -0
  126. package/lib/github-git-auth.ts +207 -0
  127. package/lib/github-origin.ts +28 -0
  128. package/lib/github-tokens.ts +2439 -0
  129. package/lib/host-cloud-url.ts +62 -0
  130. package/lib/host-config.ts +279 -0
  131. package/lib/host-env.ts +424 -0
  132. package/lib/host-logs.ts +965 -0
  133. package/lib/internal-connections.ts +5 -0
  134. package/lib/keyed-promise-tail.ts +23 -0
  135. package/lib/launch-mode.ts +25 -0
  136. package/lib/lease.ts +239 -0
  137. package/lib/legacy-host.ts +769 -0
  138. package/lib/legacy-runtime-retirement.ts +424 -0
  139. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  140. package/lib/machine-exec.ts +238 -0
  141. package/lib/machine-gateway-tunnel.ts +344 -0
  142. package/lib/machine-keys.ts +65 -0
  143. package/lib/machine-project-env-loader.ts +152 -0
  144. package/lib/machine-provider-aws.ts +923 -0
  145. package/lib/machine-provider-local.ts +338 -0
  146. package/lib/machine-provider.ts +90 -0
  147. package/lib/managed-activation-watcher.ts +72 -0
  148. package/lib/managed-install-owner-watcher.ts +55 -0
  149. package/lib/managed-operation-drain.ts +57 -0
  150. package/lib/managed-runtime.ts +4296 -0
  151. package/lib/managed-update-scheduler.ts +125 -0
  152. package/lib/mcp-capture.ts +865 -0
  153. package/lib/mcp-config-lock.ts +2 -0
  154. package/lib/mcp-connections.ts +959 -0
  155. package/lib/mcp-gateway.ts +2594 -0
  156. package/lib/obs.ts +512 -0
  157. package/lib/openrouter-budget.ts +247 -0
  158. package/lib/orchestrator.ts +11409 -0
  159. package/lib/person-capture.ts +327 -0
  160. package/lib/preview-ports.ts +85 -0
  161. package/lib/preview-sidecar.ts +331 -0
  162. package/lib/project-env-authority.ts +1319 -0
  163. package/lib/project-env-feature.ts +74 -0
  164. package/lib/project-env-legacy-tags.ts +130 -0
  165. package/lib/project-env-loader.ts +242 -0
  166. package/lib/project-env-replica.ts +2094 -0
  167. package/lib/project-env-schema-capture.ts +342 -0
  168. package/lib/project-env-staging.ts +148 -0
  169. package/lib/project-env-task-policy.ts +176 -0
  170. package/lib/project-env-task-replica.ts +1752 -0
  171. package/lib/release-feed.ts +61 -0
  172. package/lib/release-manifest.ts +710 -0
  173. package/lib/release-trust.ts +46 -0
  174. package/lib/repo-clone.ts +38 -0
  175. package/lib/routing/shadow.ts +261 -0
  176. package/lib/runner-boot.ts +10 -0
  177. package/lib/runtime-activation-tail.ts +279 -0
  178. package/lib/runtime-archive.ts +1216 -0
  179. package/lib/runtime-authority.ts +166 -0
  180. package/lib/runtime-guard.ts +36 -0
  181. package/lib/runtime-provider-state.ts +169 -0
  182. package/lib/runtime-reconnect.ts +86 -0
  183. package/lib/runtime-state.ts +1033 -0
  184. package/lib/safe-http.ts +364 -0
  185. package/lib/secrets-proxy/address.ts +95 -0
  186. package/lib/secrets-proxy/audit.ts +30 -0
  187. package/lib/secrets-proxy/ca-store.ts +200 -0
  188. package/lib/secrets-proxy/ca.ts +470 -0
  189. package/lib/secrets-proxy/container-steps.ts +138 -0
  190. package/lib/secrets-proxy/der.ts +273 -0
  191. package/lib/secrets-proxy/engine.ts +1633 -0
  192. package/lib/secrets-proxy/host.ts +93 -0
  193. package/lib/secrets-proxy/hostname.ts +57 -0
  194. package/lib/secrets-proxy/placeholders.ts +196 -0
  195. package/lib/secrets-proxy/policy.ts +150 -0
  196. package/lib/secrets-proxy/readiness.ts +148 -0
  197. package/lib/secrets-proxy/runtime.ts +582 -0
  198. package/lib/secrets-proxy/scrub.ts +559 -0
  199. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  200. package/lib/secrets-proxy/task-scripts.ts +262 -0
  201. package/lib/secrets-proxy/task-trust.ts +605 -0
  202. package/lib/secrets-proxy/trust-watch.ts +80 -0
  203. package/lib/secrets.ts +71 -0
  204. package/lib/shared-files-s3.ts +976 -0
  205. package/lib/shared-files.ts +156 -0
  206. package/lib/sidecar-assign.ts +49 -0
  207. package/lib/sidecar-guard.ts +58 -0
  208. package/lib/sidecar-resume.ts +178 -0
  209. package/lib/sidecar.ts +69 -0
  210. package/lib/signin-box-credentials.ts +465 -0
  211. package/lib/signin-box.ts +114 -0
  212. package/lib/skill-index-example.fixture.ts +56 -0
  213. package/lib/skill-layout.ts +417 -0
  214. package/lib/skills.ts +489 -0
  215. package/lib/ssh.ts +401 -0
  216. package/lib/standard-image-cleanup.ts +117 -0
  217. package/lib/standard-image-pin.ts +207 -0
  218. package/lib/standard-image.ts +1697 -0
  219. package/lib/stopped-task-status-queue.ts +120 -0
  220. package/lib/task-container-cli.ts +269 -0
  221. package/lib/task-credential-cleanup.ts +752 -0
  222. package/lib/task-diff-sandbox.ts +464 -0
  223. package/lib/task-diff.ts +91 -0
  224. package/lib/task-env-forwarder.ts +228 -0
  225. package/lib/task-env-source.ts +249 -0
  226. package/lib/task-environment/anchor.ts +31 -0
  227. package/lib/task-environment/apple-container.ts +860 -0
  228. package/lib/task-environment/docker.ts +1344 -0
  229. package/lib/task-environment/index.ts +466 -0
  230. package/lib/task-environment/legacy-adoption.ts +467 -0
  231. package/lib/task-environment/machine-environment.ts +61 -0
  232. package/lib/task-environment/machine-task-up.ts +1227 -0
  233. package/lib/task-environment/machine.ts +974 -0
  234. package/lib/task-environment/registry.ts +59 -0
  235. package/lib/task-environment/self-identity.ts +20 -0
  236. package/lib/task-environment/self.ts +566 -0
  237. package/lib/task-environment/types.ts +642 -0
  238. package/lib/task-environment/workspace-files.ts +685 -0
  239. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  240. package/lib/task-files-machine-retirement-outbox.ts +592 -0
  241. package/lib/task-files-machine-retirement.ts +116 -0
  242. package/lib/task-files-readiness.ts +127 -0
  243. package/lib/task-gateway-port.ts +22 -0
  244. package/lib/task-gateway-protocol.ts +190 -0
  245. package/lib/task-gateway-recovery.ts +47 -0
  246. package/lib/task-gateway-state.ts +512 -0
  247. package/lib/task-identity.ts +19 -0
  248. package/lib/task-inventory.ts +682 -0
  249. package/lib/task-machine-credentials-activation.ts +95 -0
  250. package/lib/task-machine-swap-safety.ts +729 -0
  251. package/lib/task-person-credentials.ts +76 -0
  252. package/lib/task-project-ops-host.ts +147 -0
  253. package/lib/task-project-ops.ts +1062 -0
  254. package/lib/task-secret-staging.ts +16 -0
  255. package/lib/task-status.ts +46 -0
  256. package/lib/task-token.ts +46 -0
  257. package/lib/token-broker-adopt.ts +344 -0
  258. package/lib/token-broker-lambda.ts +105 -0
  259. package/lib/token-broker-local.ts +392 -0
  260. package/lib/token-broker-revision.ts +310 -0
  261. package/lib/token-broker-ssm.ts +525 -0
  262. package/lib/token-broker.ts +218 -0
  263. package/lib/token-lanes/openai-oauth.ts +123 -0
  264. package/lib/token-lanes/registry.ts +45 -0
  265. package/lib/token-lanes/types.ts +163 -0
  266. package/lib/token-lanes/xai-oauth.ts +165 -0
  267. package/lib/transcript.ts +128 -0
  268. package/lib/tunnel-registry.ts +220 -0
  269. package/lib/tunnel-runtime.ts +235 -0
  270. package/lib/ulid.ts +16 -0
  271. package/lib/user-bus-env.ts +116 -0
  272. package/lib/user-credentials-store.ts +435 -0
  273. package/lib/vnode-pressure.ts +142 -0
  274. package/package.json +115 -8
  275. package/runner/runner.mjs +442 -0
  276. package/scripts/agent/_common.sh +633 -0
  277. package/scripts/agent/env-hook.sh +626 -0
  278. package/scripts/agent/moondeck-cli.sh +5 -0
  279. package/scripts/agent/task-down.sh +291 -0
  280. package/scripts/agent/task-status.sh +70 -0
  281. package/scripts/agent/task-up.sh +3761 -0
  282. package/scripts/agent/uai-env-exec.sh +26 -0
  283. package/scripts/install/darwin.ts +1048 -0
  284. package/scripts/install/linux.ts +958 -0
  285. package/scripts/install/types.ts +78 -0
  286. package/scripts/install/util.ts +254 -0
  287. package/scripts/install/win.ts +143 -0
  288. package/src/activation-respawn.ts +155 -0
  289. package/src/apple-tunnel-route.ts +104 -0
  290. package/src/capture-wire.ts +102 -0
  291. package/src/cli.ts +2583 -0
  292. package/src/cloud-credentials.ts +2144 -0
  293. package/src/cloud-origins.ts +23 -0
  294. package/src/credential-location.ts +248 -0
  295. package/src/credential-wire-validation.ts +25 -0
  296. package/src/custom-connections.ts +237 -0
  297. package/src/engine-account-paths.ts +11 -0
  298. package/src/engine-models.ts +39 -0
  299. package/src/engine-usage.ts +104 -0
  300. package/src/event-outbox.ts +185 -0
  301. package/src/index.ts +2899 -0
  302. package/src/load-env.ts +97 -0
  303. package/src/machine-tunnel-route.ts +156 -0
  304. package/src/main.ts +6349 -0
  305. package/src/never-proxied-domains.ts +31 -0
  306. package/src/outbound-frame.ts +31 -0
  307. package/src/paths.ts +81 -0
  308. package/src/project-env-latest.ts +130 -0
  309. package/src/project-env-policy.ts +396 -0
  310. package/src/project-env-wording.ts +53 -0
  311. package/src/protocol.ts +5545 -0
  312. package/src/runtime-bootstrap.ts +165 -0
  313. package/src/station-identity.ts +96 -0
  314. package/src/task-env-names.ts +28 -0
  315. package/src/task-secrets.ts +75 -0
  316. package/src/tunnel-dial.ts +102 -0
  317. package/src/ui/server.ts +852 -0
  318. package/src/ui/types.ts +173 -0
  319. package/src/workspace-path-policy.ts +41 -0
  320. package/ui/app.js +1102 -0
  321. package/ui/index.html +90 -0
  322. package/ui/style.css +685 -0
  323. package/ui/uai-favicon.svg +21 -0
  324. package/ui/uai-logo-black.svg +9 -0
  325. package/ui/uai-wheel.svg +9 -0
  326. package/bin/moondeck-station.mjs +0 -3
@@ -0,0 +1,1216 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import {
4
+ chmod,
5
+ mkdir,
6
+ open,
7
+ symlink,
8
+ } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { gzipSync, gunzipSync } from "node:zlib";
11
+
12
+ import {
13
+ MAX_RELEASE_ARTIFACT_BYTES,
14
+ type ReleaseArtifact,
15
+ verifyReleaseArtifactIntegrity,
16
+ } from "./release-manifest";
17
+
18
+ const BLOCK_SIZE = 512;
19
+ const MAX_UNCOMPRESSED_BYTES = 1024 * 1024 * 1024;
20
+ const MAX_ARCHIVE_MEMBERS = 100_000;
21
+ const MAX_MEMBER_PATH_BYTES = 4096;
22
+ const MAX_PAX_BYTES = 64 * 1024;
23
+ const MAX_RUNTIME_ASSET_MANIFEST_BYTES = 2 * 1024 * 1024;
24
+ export const MAX_RUNTIME_ASSET_ENTRIES = 10_000;
25
+ export const MAX_RUNTIME_ASSET_BYTES = 512 * 1024 * 1024;
26
+ const ZERO_BLOCK = Buffer.alloc(BLOCK_SIZE);
27
+ const BASE_TOP_LEVEL = Object.freeze(["agent", "bin", "node", "runtime.json"]);
28
+ export const BUNDLED_RUNTIME_ROOT = "agent/bundled-runtime" as const;
29
+ export const BUNDLED_RUNTIME_CAPABILITY =
30
+ "agent/bundled-runtime/capability.json" as const;
31
+ /**
32
+ * Cross-platform managed-runtime generation capability. Keep this outside the
33
+ * Darwin-only bundled subtree: the 0.9.148 validator tolerates additive files
34
+ * below `agent/`, so it can install the first capability-4 generation without
35
+ * understanding the marker. That generation then uses it to fence every
36
+ * later update and rollback after project-env activation.
37
+ */
38
+ export const MANAGED_RUNTIME_CAPABILITY =
39
+ "agent/managed-runtime-capability.json" as const;
40
+ export const MANAGED_RUNTIME_CAPABILITY_VERSION = 4 as const;
41
+ /**
42
+ * ADR-154 §13's cutover marker: this generation was built at or after the
43
+ * release that moved credentials to the cloud. It is a second file rather
44
+ * than capability 5 because the 0.9.149 and 0.9.150 validator accepts exactly
45
+ * capability 4, so the cutover release must still carry
46
+ * `{"capabilitySchemaVersion":4}` or no owned runner could install it. Like
47
+ * that marker in its day, this additive `agent/` file is ignored by every
48
+ * older validator. A generation without it predates the cutover and resolves
49
+ * to credentials capability 0.
50
+ */
51
+ export const MANAGED_RUNTIME_CREDENTIALS =
52
+ "agent/managed-runtime-credentials.json" as const;
53
+ export const MANAGED_RUNTIME_CREDENTIALS_VERSION = 1 as const;
54
+ /** Plugin directory == plugin binary name in Apple's signed pkg payload. */
55
+ const APPLE_CONTAINER_PLUGINS = Object.freeze([
56
+ "container-core-images",
57
+ "container-network-vmnet",
58
+ "container-runtime-linux",
59
+ "k8s",
60
+ "machine-apiserver",
61
+ ]);
62
+ const STANDARD_IMAGE_ASSETS = Object.freeze([
63
+ Object.freeze({ path: "Dockerfile", mode: 0o644 as const }),
64
+ Object.freeze({ path: "README.md", mode: 0o644 as const }),
65
+ Object.freeze({ path: "container/code-server-settings.json", mode: 0o644 as const }),
66
+ Object.freeze({ path: "container/corepack-version", mode: 0o644 as const }),
67
+ Object.freeze({ path: "container/uai-init", mode: 0o755 as const }),
68
+ Object.freeze({ path: "container/uai-materialize-runtimes", mode: 0o755 as const }),
69
+ Object.freeze({ path: "container/uai-task-forwarder.mjs", mode: 0o644 as const }),
70
+ Object.freeze({ path: "tool-versions", mode: 0o644 as const }),
71
+ ]);
72
+ const RUNTIME_ASSET_ROOTS = Object.freeze([
73
+ "apple-container",
74
+ "host-assets",
75
+ "kernel",
76
+ "licenses",
77
+ ]);
78
+ export const REQUIRED_RUNTIME_LICENSES = Object.freeze([
79
+ "Container-LICENSE",
80
+ "Kernel-GPL-2.0",
81
+ ]);
82
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
83
+ const SHA256 = /^[a-f0-9]{64}$/;
84
+
85
+ interface RuntimeDescriptorCommon {
86
+ readonly hostVersion: string;
87
+ readonly platform: string;
88
+ readonly node: {
89
+ readonly version: string;
90
+ readonly modules: string;
91
+ };
92
+ readonly requirements: Readonly<Record<string, string>>;
93
+ readonly layout: {
94
+ readonly agent: "agent";
95
+ readonly launcher: "bin/uai-host";
96
+ readonly node: "node";
97
+ };
98
+ }
99
+
100
+ export interface RuntimeDescriptorV1 extends RuntimeDescriptorCommon {
101
+ readonly schemaVersion: 1;
102
+ }
103
+
104
+ export interface BundledRuntimeDescriptor {
105
+ readonly assetFingerprint: string;
106
+ readonly versions: {
107
+ readonly container: string;
108
+ readonly kata: string;
109
+ readonly guestKernel: string;
110
+ };
111
+ readonly requirements: {
112
+ readonly os: "darwin";
113
+ readonly arch: "arm64";
114
+ readonly minVersion: string;
115
+ };
116
+ readonly teamIdentifier: string;
117
+ readonly layout: {
118
+ readonly assetSourceRoot: "agent/bundled-runtime";
119
+ readonly assetManifest: "agent/bundled-runtime/assets.json";
120
+ readonly containerCli: "agent/bundled-runtime/apple-container/bin/container";
121
+ readonly apiserver: "agent/bundled-runtime/apple-container/bin/container-apiserver";
122
+ readonly plugins: "agent/bundled-runtime/apple-container/libexec/container/plugins";
123
+ readonly kernel: "agent/bundled-runtime/kernel/vmlinux";
124
+ readonly standardImage: "agent/bundled-runtime/host-assets/standard-image";
125
+ readonly licenses: "agent/bundled-runtime/licenses";
126
+ };
127
+ }
128
+
129
+ export interface RuntimeCapabilityDescriptorV3 {
130
+ readonly capabilitySchemaVersion: 3;
131
+ readonly bundledRuntime: BundledRuntimeDescriptor;
132
+ }
133
+
134
+ export interface ManagedRuntimeCapabilityDescriptorV4 {
135
+ readonly capabilitySchemaVersion: 4;
136
+ }
137
+
138
+ export interface ManagedRuntimeCredentialsDescriptor {
139
+ readonly credentialsCapabilityVersion: number;
140
+ }
141
+
142
+ export type RuntimePlatformCapabilities =
143
+ | { readonly capabilitySchemaVersion: 1 }
144
+ | RuntimeCapabilityDescriptorV3;
145
+
146
+ export type ResolvedRuntimeCapabilities = RuntimePlatformCapabilities & {
147
+ /** Effective rollback/update floor carried by this whole generation. */
148
+ readonly runtimeCapabilityVersion: 1 | 3 | 4;
149
+ /** ADR-154 cutover marker; 0 for a generation built before the cutover. */
150
+ readonly credentialsCapabilityVersion: number;
151
+ };
152
+
153
+ export type RuntimeDescriptor = RuntimeDescriptorV1;
154
+ export type ResolvedRuntimeDescriptor = RuntimeDescriptor &
155
+ ResolvedRuntimeCapabilities;
156
+
157
+ export interface RuntimeAssetManifestEntry {
158
+ readonly path: string;
159
+ readonly type: "file";
160
+ readonly mode: 0o644 | 0o755;
161
+ readonly size: number;
162
+ readonly sha256: string;
163
+ }
164
+
165
+ export interface RuntimeAssetManifest {
166
+ readonly schemaVersion: 1;
167
+ readonly fingerprint: string;
168
+ readonly entries: readonly RuntimeAssetManifestEntry[];
169
+ }
170
+
171
+ interface ArchiveEntry {
172
+ readonly memberPath: string;
173
+ readonly type: "0" | "2" | "5";
174
+ readonly mode: number;
175
+ readonly data: Buffer;
176
+ readonly linkTarget: string;
177
+ readonly resolvedLinkTarget: string | null;
178
+ }
179
+
180
+ export interface InspectedRuntimeArchive {
181
+ readonly descriptor: RuntimeDescriptor;
182
+ readonly capabilities: ResolvedRuntimeCapabilities;
183
+ readonly memberCount: number;
184
+ }
185
+
186
+ /**
187
+ * Read, count and hash one already-downloaded regular file through one open
188
+ * descriptor. Extraction consumes the exact returned bytes; there is no
189
+ * verify-by-name/reopen window in which the pathname could be swapped.
190
+ */
191
+ export async function readVerifiedArtifactFile(
192
+ archivePath: string,
193
+ artifact: ReleaseArtifact,
194
+ ): Promise<Buffer> {
195
+ const noFollow = fsConstants.O_NOFOLLOW ?? 0;
196
+ const handle = await open(archivePath, fsConstants.O_RDONLY | noFollow);
197
+ try {
198
+ const before = await handle.stat();
199
+ if (!before.isFile()) throw new Error("runtime archive must be a regular file");
200
+ if (before.size > MAX_RELEASE_ARTIFACT_BYTES) {
201
+ throw new Error(`runtime archive exceeds ${MAX_RELEASE_ARTIFACT_BYTES} bytes`);
202
+ }
203
+
204
+ const hash = createHash("sha256");
205
+ const chunks: Buffer[] = [];
206
+ let size = 0;
207
+ for await (const raw of handle.createReadStream({ autoClose: false })) {
208
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
209
+ size += chunk.length;
210
+ if (size > MAX_RELEASE_ARTIFACT_BYTES) {
211
+ throw new Error(`runtime archive exceeds ${MAX_RELEASE_ARTIFACT_BYTES} bytes`);
212
+ }
213
+ hash.update(chunk);
214
+ chunks.push(chunk);
215
+ }
216
+ const after = await handle.stat();
217
+ if (
218
+ before.dev !== after.dev ||
219
+ before.ino !== after.ino ||
220
+ before.size !== after.size ||
221
+ size !== after.size
222
+ ) {
223
+ throw new Error("runtime archive changed while it was being verified");
224
+ }
225
+ verifyReleaseArtifactIntegrity(artifact, {
226
+ size,
227
+ sha256: hash.digest(),
228
+ });
229
+ return Buffer.concat(chunks, size);
230
+ } finally {
231
+ await handle.close();
232
+ }
233
+ }
234
+
235
+ export async function extractVerifiedRuntimeArchive(
236
+ archiveBytes: Buffer,
237
+ destination: string,
238
+ expected: { version: string; platform: string },
239
+ ): Promise<InspectedRuntimeArchive> {
240
+ const { entries, descriptor, capabilities } = inspectRuntimeArchive(
241
+ archiveBytes,
242
+ expected,
243
+ );
244
+ // The caller gives us a fresh, private staging path. `recursive: false`
245
+ // refuses to merge archive contents into any pre-existing tree.
246
+ await mkdir(destination, { mode: 0o700, recursive: false });
247
+ const directories = [destination];
248
+
249
+ for (const entry of entries) {
250
+ const output = path.join(destination, ...entry.memberPath.split("/"));
251
+ if (entry.type === "5") {
252
+ await mkdir(output, { mode: 0o755, recursive: false });
253
+ await chmod(output, 0o755);
254
+ directories.push(output);
255
+ } else if (entry.type === "0") {
256
+ const handle = await open(output, "wx", entry.mode);
257
+ try {
258
+ await handle.writeFile(entry.data);
259
+ await handle.chmod(entry.mode);
260
+ await handle.sync();
261
+ } finally {
262
+ await handle.close();
263
+ }
264
+ }
265
+ }
266
+ // Create links only after every regular target exists. Archive validation
267
+ // has already rejected absolute/escaping targets and any child below a link.
268
+ for (const entry of entries) {
269
+ if (entry.type !== "2") continue;
270
+ const output = path.join(destination, ...entry.memberPath.split("/"));
271
+ await symlink(entry.linkTarget, output);
272
+ }
273
+ // Persist children before parents so activation never points at a directory
274
+ // whose archive contents only existed in the page cache at power loss.
275
+ for (const directory of directories.reverse()) {
276
+ await syncDirectory(directory);
277
+ }
278
+ return { descriptor, capabilities, memberCount: entries.length };
279
+ }
280
+
281
+ export function inspectRuntimeArchive(
282
+ archiveBytes: Buffer,
283
+ expected: { version: string; platform: string },
284
+ ): {
285
+ entries: readonly ArchiveEntry[];
286
+ descriptor: RuntimeDescriptor;
287
+ capabilities: ResolvedRuntimeCapabilities;
288
+ } {
289
+ assertCanonicalGzipHeader(archiveBytes);
290
+ const tar = gunzipSync(archiveBytes, { maxOutputLength: MAX_UNCOMPRESSED_BYTES });
291
+ // The release builder emits a single deterministic gzip stream. Rebuilding
292
+ // it under the packaged, exactly-pinned Node rejects concatenated streams,
293
+ // trailing bytes and alternate encodings before any filesystem mutation.
294
+ // Modern Node always writes a zero gzip mtime (the option was removed from
295
+ // its public typings); the builder runs the same pinned Node patch.
296
+ const canonical = gzipSync(tar, { level: 9 });
297
+ canonical[9] = 255;
298
+ if (!archiveBytes.equals(canonical)) {
299
+ throw new Error("runtime archive gzip stream is not canonical");
300
+ }
301
+
302
+ const entries: ArchiveEntry[] = [];
303
+ const byPath = new Map<string, ArchiveEntry>();
304
+ let offset = 0;
305
+ let pendingPax: { path?: string; linkpath?: string } | null = null;
306
+ let previousMember: string | null = null;
307
+ let terminated = false;
308
+
309
+ while (offset + BLOCK_SIZE <= tar.length) {
310
+ const header = tar.subarray(offset, offset + BLOCK_SIZE);
311
+ if (header.equals(ZERO_BLOCK)) {
312
+ if (
313
+ offset + 2 * BLOCK_SIZE !== tar.length ||
314
+ !tar.subarray(offset + BLOCK_SIZE).equals(ZERO_BLOCK)
315
+ ) {
316
+ throw new Error("runtime archive has trailing bytes or an invalid terminator");
317
+ }
318
+ offset += 2 * BLOCK_SIZE;
319
+ terminated = true;
320
+ break;
321
+ }
322
+
323
+ validateHeaderChecksum(header);
324
+ if (parseString(header, 257, 6) !== "ustar") {
325
+ throw new Error("runtime archive is not ustar");
326
+ }
327
+ const name = parseString(header, 0, 100);
328
+ const prefix = parseString(header, 345, 155);
329
+ const headerPath = prefix ? `${prefix}/${name}` : name;
330
+ const mode = parseOctal(header, 100, 8, "mode");
331
+ const uid = parseOctal(header, 108, 8, "uid");
332
+ const gid = parseOctal(header, 116, 8, "gid");
333
+ const size = parseOctal(header, 124, 12, "size");
334
+ const mtime = parseOctal(header, 136, 12, "mtime");
335
+ const type = (parseString(header, 156, 1) || "0") as string;
336
+ const rawLinkTarget = parseString(header, 157, 100);
337
+ if (
338
+ uid !== 0 ||
339
+ gid !== 0 ||
340
+ mtime !== 0 ||
341
+ parseString(header, 265, 32) !== "root" ||
342
+ parseString(header, 297, 32) !== "root" ||
343
+ parseOctal(header, 329, 8, "devmajor") !== 0 ||
344
+ parseOctal(header, 337, 8, "devminor") !== 0
345
+ ) {
346
+ throw new Error("runtime archive metadata is not canonical");
347
+ }
348
+ if (!Number.isSafeInteger(size) || size < 0 || size > MAX_UNCOMPRESSED_BYTES) {
349
+ throw new Error("runtime archive member size is invalid");
350
+ }
351
+ const dataStart = offset + BLOCK_SIZE;
352
+ const dataEnd = dataStart + size;
353
+ const paddedEnd = dataStart + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
354
+ if (dataEnd > tar.length || paddedEnd > tar.length) {
355
+ throw new Error("runtime archive member exceeds input");
356
+ }
357
+ if (tar.subarray(dataEnd, paddedEnd).some((byte) => byte !== 0)) {
358
+ throw new Error(`runtime archive member padding is not zero: ${headerPath}`);
359
+ }
360
+ const data = tar.subarray(dataStart, dataEnd);
361
+ offset = paddedEnd;
362
+
363
+ if (type === "x") {
364
+ if (pendingPax) throw new Error("runtime archive has consecutive pax headers");
365
+ if (size > MAX_PAX_BYTES || mode !== 0o644) {
366
+ throw new Error("runtime archive pax header is invalid");
367
+ }
368
+ pendingPax = parsePax(data);
369
+ continue;
370
+ }
371
+ if (type !== "0" && type !== "2" && type !== "5") {
372
+ throw new Error(`unsupported runtime archive member type: ${type}`);
373
+ }
374
+ const rawMemberPath = pendingPax?.path ?? headerPath;
375
+ if ((type === "5") !== rawMemberPath.endsWith("/")) {
376
+ throw new Error(`runtime archive member path/type mismatch: ${rawMemberPath}`);
377
+ }
378
+ const memberPath = safeMemberPath(rawMemberPath);
379
+ if (
380
+ previousMember !== null &&
381
+ Buffer.compare(Buffer.from(previousMember), Buffer.from(memberPath)) >= 0
382
+ ) {
383
+ throw new Error(`runtime archive members are not strictly sorted: ${memberPath}`);
384
+ }
385
+ previousMember = memberPath;
386
+ if (byPath.has(memberPath)) {
387
+ throw new Error(`duplicate runtime archive member: ${memberPath}`);
388
+ }
389
+ if (entries.length >= MAX_ARCHIVE_MEMBERS) {
390
+ throw new Error(`runtime archive exceeds ${MAX_ARCHIVE_MEMBERS} members`);
391
+ }
392
+ if (type !== "0" && size !== 0) {
393
+ throw new Error(`non-file runtime archive member carries data: ${memberPath}`);
394
+ }
395
+ const expectedMode = type === "5" ? 0o755 : type === "2" ? 0o777 : mode;
396
+ if (
397
+ mode !== expectedMode ||
398
+ (type === "0" && mode !== 0o644 && mode !== 0o755)
399
+ ) {
400
+ throw new Error(`runtime archive member mode is invalid: ${memberPath}`);
401
+ }
402
+ const linkTarget = pendingPax?.linkpath ?? rawLinkTarget;
403
+ const resolvedLinkTarget =
404
+ type === "2" ? safeLinkTarget(memberPath, linkTarget) : null;
405
+ const entry: ArchiveEntry = {
406
+ memberPath,
407
+ type,
408
+ mode,
409
+ data,
410
+ linkTarget,
411
+ resolvedLinkTarget,
412
+ };
413
+ entries.push(entry);
414
+ byPath.set(memberPath, entry);
415
+ pendingPax = null;
416
+ }
417
+
418
+ if (!terminated || offset !== tar.length || pendingPax) {
419
+ throw new Error("runtime archive is truncated");
420
+ }
421
+ validateEntryGraph(entries, byPath);
422
+ const descriptorEntry = byPath.get("runtime.json");
423
+ if (!descriptorEntry || descriptorEntry.data.length > MAX_PAX_BYTES) {
424
+ throw new Error("runtime archive descriptor is missing or too large");
425
+ }
426
+ let rawDescriptor: unknown;
427
+ try {
428
+ rawDescriptor = JSON.parse(descriptorEntry.data.toString("utf8"));
429
+ } catch {
430
+ throw new Error("runtime archive descriptor is invalid JSON");
431
+ }
432
+ const descriptor = validateRuntimeDescriptor(rawDescriptor, expected);
433
+ const capabilities = validateEntryLayout(entries, byPath, descriptor);
434
+ return { entries, descriptor, capabilities };
435
+ }
436
+
437
+ function validateEntryGraph(
438
+ entries: readonly ArchiveEntry[],
439
+ byPath: ReadonlyMap<string, ArchiveEntry>,
440
+ ): void {
441
+ for (const entry of entries) {
442
+ const parts = entry.memberPath.split("/");
443
+ for (let length = 1; length < parts.length; length += 1) {
444
+ const ancestor = parts.slice(0, length).join("/");
445
+ const parent = byPath.get(ancestor);
446
+ if (!parent || parent.type !== "5") {
447
+ throw new Error(`runtime archive parent is not a directory: ${ancestor}`);
448
+ }
449
+ }
450
+ if (
451
+ entry.type === "2" &&
452
+ (!entry.resolvedLinkTarget || !byPath.has(entry.resolvedLinkTarget))
453
+ ) {
454
+ throw new Error(`runtime archive symlink target does not exist: ${entry.memberPath}`);
455
+ }
456
+ }
457
+ }
458
+
459
+ function validateEntryLayout(
460
+ entries: readonly ArchiveEntry[],
461
+ byPath: ReadonlyMap<string, ArchiveEntry>,
462
+ descriptor: RuntimeDescriptor,
463
+ ): ResolvedRuntimeCapabilities {
464
+ const topLevel = new Set(entries.map((entry) => entry.memberPath.split("/")[0]!));
465
+ if (
466
+ [...topLevel].sort().join("\0") !== [...BASE_TOP_LEVEL].sort().join("\0")
467
+ ) {
468
+ throw new Error("runtime archive top-level layout is invalid");
469
+ }
470
+ const requiredFiles = [
471
+ "agent/bin/uai-host.mjs",
472
+ "agent/scripts/agent/task-up.sh",
473
+ "agent/scripts/agent/task-down.sh",
474
+ "agent/scripts/agent/task-status.sh",
475
+ "bin/uai-host",
476
+ "node/bin/node",
477
+ "runtime.json",
478
+ ];
479
+ const requiredDirectories = ["agent", "bin", "node"];
480
+ const executables = [
481
+ "bin/uai-host",
482
+ "node/bin/node",
483
+ "agent/scripts/agent/task-up.sh",
484
+ "agent/scripts/agent/task-down.sh",
485
+ "agent/scripts/agent/task-status.sh",
486
+ ];
487
+ const platformCapabilities = validateArchivedRuntimeCapability(
488
+ byPath,
489
+ descriptor,
490
+ );
491
+ const runtimeCapabilityVersion = validateArchivedManagedRuntimeCapability(
492
+ byPath,
493
+ platformCapabilities.capabilitySchemaVersion,
494
+ );
495
+ const credentialsCapabilityVersion =
496
+ validateArchivedManagedRuntimeCredentials(byPath);
497
+ if (platformCapabilities.capabilitySchemaVersion === 3) {
498
+ const bundled = platformCapabilities.bundledRuntime;
499
+ const pluginBinaries = APPLE_CONTAINER_PLUGINS.map(
500
+ (plugin) => `${bundled.layout.plugins}/${plugin}/bin/${plugin}`,
501
+ );
502
+ requiredFiles.push(
503
+ BUNDLED_RUNTIME_CAPABILITY,
504
+ bundled.layout.assetManifest,
505
+ bundled.layout.containerCli,
506
+ bundled.layout.apiserver,
507
+ ...pluginBinaries,
508
+ bundled.layout.kernel,
509
+ ...STANDARD_IMAGE_ASSETS.map(
510
+ ({ path: relative }) => `${bundled.layout.standardImage}/${relative}`,
511
+ ),
512
+ ...REQUIRED_RUNTIME_LICENSES.map(
513
+ (license) => `${bundled.layout.licenses}/${license}`,
514
+ ),
515
+ );
516
+ requiredDirectories.push(
517
+ BUNDLED_RUNTIME_ROOT,
518
+ `${BUNDLED_RUNTIME_ROOT}/apple-container`,
519
+ `${BUNDLED_RUNTIME_ROOT}/apple-container/bin`,
520
+ `${BUNDLED_RUNTIME_ROOT}/apple-container/libexec`,
521
+ `${BUNDLED_RUNTIME_ROOT}/apple-container/libexec/container`,
522
+ bundled.layout.plugins,
523
+ ...APPLE_CONTAINER_PLUGINS.flatMap((plugin) => [
524
+ `${bundled.layout.plugins}/${plugin}`,
525
+ `${bundled.layout.plugins}/${plugin}/bin`,
526
+ ]),
527
+ `${BUNDLED_RUNTIME_ROOT}/kernel`,
528
+ `${BUNDLED_RUNTIME_ROOT}/host-assets`,
529
+ bundled.layout.standardImage,
530
+ `${bundled.layout.standardImage}/container`,
531
+ bundled.layout.licenses,
532
+ );
533
+ validateArchivedRuntimeAssets(byPath, platformCapabilities);
534
+ executables.push(
535
+ bundled.layout.containerCli,
536
+ bundled.layout.apiserver,
537
+ ...pluginBinaries,
538
+ );
539
+ }
540
+ for (const required of requiredFiles) {
541
+ if (byPath.get(required)?.type !== "0") {
542
+ throw new Error(`runtime archive is missing regular file ${required}`);
543
+ }
544
+ }
545
+ for (const required of requiredDirectories) {
546
+ if (byPath.get(required)?.type !== "5") {
547
+ throw new Error(`runtime archive is missing directory ${required}`);
548
+ }
549
+ }
550
+ for (const executable of executables) {
551
+ if (byPath.get(executable)?.mode !== 0o755) {
552
+ throw new Error(`runtime archive executable mode is invalid: ${executable}`);
553
+ }
554
+ }
555
+ return {
556
+ ...platformCapabilities,
557
+ runtimeCapabilityVersion,
558
+ credentialsCapabilityVersion,
559
+ };
560
+ }
561
+
562
+ function validateArchivedManagedRuntimeCredentials(
563
+ byPath: ReadonlyMap<string, ArchiveEntry>,
564
+ ): number {
565
+ const entry = byPath.get(MANAGED_RUNTIME_CREDENTIALS);
566
+ if (entry === undefined) return 0;
567
+ if (entry.type !== "0" || entry.data.length > MAX_PAX_BYTES) {
568
+ throw new Error(
569
+ "managed runtime credentials descriptor is not a regular file or is too large",
570
+ );
571
+ }
572
+ let raw: unknown;
573
+ try {
574
+ raw = JSON.parse(entry.data.toString("utf8"));
575
+ } catch {
576
+ throw new Error("managed runtime credentials descriptor is invalid JSON");
577
+ }
578
+ return validateManagedRuntimeCredentialsDescriptor(raw)
579
+ .credentialsCapabilityVersion;
580
+ }
581
+
582
+ function validateArchivedManagedRuntimeCapability(
583
+ byPath: ReadonlyMap<string, ArchiveEntry>,
584
+ legacyCapabilityVersion: 1 | 3,
585
+ ): 1 | 3 | 4 {
586
+ const entry = byPath.get(MANAGED_RUNTIME_CAPABILITY);
587
+ if (entry === undefined) return legacyCapabilityVersion;
588
+ if (entry.type !== "0" || entry.data.length > MAX_PAX_BYTES) {
589
+ throw new Error(
590
+ "managed runtime capability descriptor is missing or too large",
591
+ );
592
+ }
593
+ let raw: unknown;
594
+ try {
595
+ raw = JSON.parse(entry.data.toString("utf8"));
596
+ } catch {
597
+ throw new Error("managed runtime capability descriptor is invalid JSON");
598
+ }
599
+ return validateManagedRuntimeCapabilityDescriptor(raw).capabilitySchemaVersion;
600
+ }
601
+
602
+ function validateArchivedRuntimeCapability(
603
+ byPath: ReadonlyMap<string, ArchiveEntry>,
604
+ descriptor: RuntimeDescriptor,
605
+ ): { readonly capabilitySchemaVersion: 1 } | RuntimeCapabilityDescriptorV3 {
606
+ const hasBundledSubtree = [...byPath.keys()].some(
607
+ (member) =>
608
+ member === BUNDLED_RUNTIME_ROOT ||
609
+ member.startsWith(`${BUNDLED_RUNTIME_ROOT}/`),
610
+ );
611
+ if (!descriptor.platform.startsWith("darwin-")) {
612
+ if (hasBundledSubtree) {
613
+ throw new Error("Linux runtime archive must not contain bundled Darwin assets");
614
+ }
615
+ return { capabilitySchemaVersion: 1 };
616
+ }
617
+ if (!hasBundledSubtree) return { capabilitySchemaVersion: 1 };
618
+ const entry = byPath.get(BUNDLED_RUNTIME_CAPABILITY);
619
+ if (entry?.type !== "0" || entry.data.length > MAX_PAX_BYTES) {
620
+ throw new Error("runtime capability descriptor is missing or too large");
621
+ }
622
+ let rawCapability: unknown;
623
+ try {
624
+ rawCapability = JSON.parse(entry.data.toString("utf8"));
625
+ } catch {
626
+ throw new Error("runtime capability descriptor is invalid JSON");
627
+ }
628
+ return validateRuntimeCapabilityDescriptor(rawCapability, descriptor.platform);
629
+ }
630
+
631
+ export function validateRuntimeDescriptor(
632
+ value: unknown,
633
+ expected: { version: string; platform: string },
634
+ ): RuntimeDescriptor {
635
+ const object = strictObject(
636
+ value,
637
+ ["schemaVersion", "hostVersion", "platform", "node", "requirements", "layout"],
638
+ "runtime descriptor",
639
+ );
640
+ if (object.schemaVersion !== 1) {
641
+ throw new Error("unsupported runtime descriptor schema");
642
+ }
643
+ if (object.hostVersion !== expected.version || !VERSION.test(expected.version)) {
644
+ throw new Error("runtime descriptor version does not match the authenticated release");
645
+ }
646
+ if (object.platform !== expected.platform) {
647
+ throw new Error("runtime descriptor platform does not match this host");
648
+ }
649
+ const node = strictObject(object.node, ["version", "modules"], "runtime descriptor node");
650
+ if (typeof node.version !== "string" || !VERSION.test(node.version)) {
651
+ throw new Error("runtime descriptor Node version is invalid");
652
+ }
653
+ if (typeof node.modules !== "string" || !/^\d+$/.test(node.modules)) {
654
+ throw new Error("runtime descriptor Node ABI is invalid");
655
+ }
656
+ const layout = strictObject(
657
+ object.layout,
658
+ ["agent", "launcher", "node"],
659
+ "runtime descriptor layout",
660
+ );
661
+ if (
662
+ layout.agent !== "agent" ||
663
+ layout.launcher !== "bin/uai-host" ||
664
+ layout.node !== "node"
665
+ ) {
666
+ throw new Error("runtime descriptor layout is invalid");
667
+ }
668
+ const requirements = strictStringObject(object.requirements, "runtime requirements");
669
+ validateRequirements(expected.platform, requirements);
670
+ return {
671
+ schemaVersion: 1,
672
+ hostVersion: expected.version,
673
+ platform: expected.platform,
674
+ node: { version: node.version, modules: node.modules },
675
+ requirements,
676
+ layout: { agent: "agent", launcher: "bin/uai-host", node: "node" } as const,
677
+ };
678
+ }
679
+
680
+ export function validateRuntimeCapabilityDescriptor(
681
+ value: unknown,
682
+ platform: string,
683
+ ): RuntimeCapabilityDescriptorV3 {
684
+ // ADR-106: the Apple container payload exists only on Apple Silicon.
685
+ if (platform !== "darwin-arm64") {
686
+ throw new Error("bundled runtime capability is supported only on darwin-arm64");
687
+ }
688
+ const object = strictObject(
689
+ value,
690
+ ["capabilitySchemaVersion", "bundledRuntime"],
691
+ "runtime capability descriptor",
692
+ );
693
+ if (object.capabilitySchemaVersion !== 3) {
694
+ throw new Error("unsupported runtime capability schema");
695
+ }
696
+ return {
697
+ capabilitySchemaVersion: 3,
698
+ bundledRuntime: validateBundledRuntimeDescriptor(object.bundledRuntime),
699
+ };
700
+ }
701
+
702
+ export function validateManagedRuntimeCapabilityDescriptor(
703
+ value: unknown,
704
+ ): ManagedRuntimeCapabilityDescriptorV4 {
705
+ const object = strictObject(
706
+ value,
707
+ ["capabilitySchemaVersion"],
708
+ "managed runtime capability descriptor",
709
+ );
710
+ if (
711
+ object.capabilitySchemaVersion !== MANAGED_RUNTIME_CAPABILITY_VERSION
712
+ ) {
713
+ throw new Error("unsupported managed runtime capability schema");
714
+ }
715
+ return {
716
+ capabilitySchemaVersion: MANAGED_RUNTIME_CAPABILITY_VERSION,
717
+ };
718
+ }
719
+
720
+ export function validateManagedRuntimeCredentialsDescriptor(
721
+ value: unknown,
722
+ ): ManagedRuntimeCredentialsDescriptor {
723
+ const object = strictObject(
724
+ value,
725
+ ["credentialsCapabilityVersion"],
726
+ "managed runtime credentials descriptor",
727
+ );
728
+ // Any version from 1 up, unlike the exact match above: that exact match is
729
+ // why the cutover needed this second file, and a later release must be able
730
+ // to raise this one in place. The floor compares it numerically.
731
+ const version = object.credentialsCapabilityVersion;
732
+ if (
733
+ typeof version !== "number" ||
734
+ !Number.isSafeInteger(version) ||
735
+ version < 1
736
+ ) {
737
+ throw new Error("unsupported managed runtime credentials descriptor");
738
+ }
739
+ return { credentialsCapabilityVersion: version };
740
+ }
741
+
742
+ function validateBundledRuntimeDescriptor(
743
+ value: unknown,
744
+ ): BundledRuntimeDescriptor {
745
+ const object = strictObject(
746
+ value,
747
+ ["assetFingerprint", "versions", "requirements", "teamIdentifier", "layout"],
748
+ "bundled runtime descriptor",
749
+ );
750
+ if (typeof object.assetFingerprint !== "string" || !SHA256.test(object.assetFingerprint)) {
751
+ throw new Error("bundled runtime asset fingerprint is invalid");
752
+ }
753
+ const versions = strictObject(
754
+ object.versions,
755
+ ["container", "kata", "guestKernel"],
756
+ "bundled runtime versions",
757
+ );
758
+ for (const [name, version] of Object.entries(versions)) {
759
+ if (typeof version !== "string" || !VERSION.test(version)) {
760
+ throw new Error(`bundled runtime ${name} version is invalid`);
761
+ }
762
+ }
763
+ const requirements = strictObject(
764
+ object.requirements,
765
+ ["os", "arch", "minVersion"],
766
+ "bundled runtime requirements",
767
+ );
768
+ if (
769
+ requirements.os !== "darwin" ||
770
+ requirements.arch !== "arm64" ||
771
+ typeof requirements.minVersion !== "string" ||
772
+ !/^\d+\.\d+$/.test(requirements.minVersion)
773
+ ) {
774
+ throw new Error("bundled runtime requirements are invalid");
775
+ }
776
+ if (
777
+ typeof object.teamIdentifier !== "string" ||
778
+ !/^[A-Z0-9]{10}$/.test(object.teamIdentifier)
779
+ ) {
780
+ throw new Error("bundled runtime signing team identifier is invalid");
781
+ }
782
+ const layout = strictObject(
783
+ object.layout,
784
+ [
785
+ "assetSourceRoot",
786
+ "assetManifest",
787
+ "containerCli",
788
+ "apiserver",
789
+ "plugins",
790
+ "kernel",
791
+ "standardImage",
792
+ "licenses",
793
+ ],
794
+ "bundled runtime layout",
795
+ );
796
+ const expectedLayout = {
797
+ assetSourceRoot: BUNDLED_RUNTIME_ROOT,
798
+ assetManifest: `${BUNDLED_RUNTIME_ROOT}/assets.json`,
799
+ containerCli: `${BUNDLED_RUNTIME_ROOT}/apple-container/bin/container`,
800
+ apiserver: `${BUNDLED_RUNTIME_ROOT}/apple-container/bin/container-apiserver`,
801
+ plugins: `${BUNDLED_RUNTIME_ROOT}/apple-container/libexec/container/plugins`,
802
+ kernel: `${BUNDLED_RUNTIME_ROOT}/kernel/vmlinux`,
803
+ standardImage: `${BUNDLED_RUNTIME_ROOT}/host-assets/standard-image`,
804
+ licenses: `${BUNDLED_RUNTIME_ROOT}/licenses`,
805
+ } as const;
806
+ for (const [name, expectedValue] of Object.entries(expectedLayout)) {
807
+ if (layout[name] !== expectedValue) {
808
+ throw new Error(`bundled runtime layout ${name} is invalid`);
809
+ }
810
+ }
811
+ return {
812
+ assetFingerprint: object.assetFingerprint,
813
+ versions: {
814
+ container: versions.container as string,
815
+ kata: versions.kata as string,
816
+ guestKernel: versions.guestKernel as string,
817
+ },
818
+ requirements: {
819
+ os: "darwin",
820
+ arch: "arm64",
821
+ minVersion: requirements.minVersion,
822
+ },
823
+ teamIdentifier: object.teamIdentifier,
824
+ layout: expectedLayout,
825
+ };
826
+ }
827
+
828
+ function compareNames(left: string, right: string): number {
829
+ return Buffer.compare(Buffer.from(left), Buffer.from(right));
830
+ }
831
+
832
+ function runtimeAssetFingerprint(entries: readonly RuntimeAssetManifestEntry[]): string {
833
+ return createHash("sha256").update(JSON.stringify(entries)).digest("hex");
834
+ }
835
+
836
+ export function validateRuntimeAssetManifest(
837
+ value: unknown,
838
+ expectedFingerprint: string,
839
+ ): RuntimeAssetManifest {
840
+ const object = strictObject(
841
+ value,
842
+ ["schemaVersion", "fingerprint", "entries"],
843
+ "runtime asset manifest",
844
+ );
845
+ if (object.schemaVersion !== 1) {
846
+ throw new Error("unsupported runtime asset manifest schema");
847
+ }
848
+ if (
849
+ typeof object.fingerprint !== "string" ||
850
+ !SHA256.test(object.fingerprint) ||
851
+ object.fingerprint !== expectedFingerprint
852
+ ) {
853
+ throw new Error("runtime asset manifest fingerprint does not match runtime descriptor");
854
+ }
855
+ if (
856
+ !Array.isArray(object.entries) ||
857
+ object.entries.length === 0 ||
858
+ object.entries.length > MAX_RUNTIME_ASSET_ENTRIES
859
+ ) {
860
+ throw new Error("runtime asset manifest entry count is invalid");
861
+ }
862
+ const entries: RuntimeAssetManifestEntry[] = [];
863
+ let totalBytes = 0;
864
+ let previous: string | null = null;
865
+ let hasAppleContainer = false;
866
+ let hasKernel = false;
867
+ let hasHostAssets = false;
868
+ for (const rawEntry of object.entries) {
869
+ const entry = strictObject(
870
+ rawEntry,
871
+ ["path", "type", "mode", "size", "sha256"],
872
+ "runtime asset manifest entry",
873
+ );
874
+ const entryPath = entry.path;
875
+ if (
876
+ typeof entryPath !== "string" ||
877
+ entryPath.length === 0 ||
878
+ Buffer.byteLength(entryPath) > MAX_MEMBER_PATH_BYTES ||
879
+ entryPath.startsWith("/") ||
880
+ /[\x00-\x1f\x7f]/.test(entryPath) ||
881
+ entryPath.split("/").some((part) => !part || part === "." || part === "..") ||
882
+ path.posix.normalize(entryPath) !== entryPath ||
883
+ !RUNTIME_ASSET_ROOTS.some((root) => entryPath.startsWith(`${root}/`))
884
+ ) {
885
+ throw new Error(`runtime asset manifest path is invalid: ${String(entryPath)}`);
886
+ }
887
+ if (previous !== null && compareNames(previous, entryPath) >= 0) {
888
+ throw new Error("runtime asset manifest entries must be unique and bytewise sorted");
889
+ }
890
+ previous = entryPath;
891
+ if (entry.type !== "file") {
892
+ throw new Error(`runtime asset manifest entry is not a regular file: ${entryPath}`);
893
+ }
894
+ if (entry.mode !== 0o644 && entry.mode !== 0o755) {
895
+ throw new Error(`runtime asset manifest mode is invalid: ${entryPath}`);
896
+ }
897
+ if (!Number.isSafeInteger(entry.size) || (entry.size as number) < 0) {
898
+ throw new Error(`runtime asset manifest size is invalid: ${entryPath}`);
899
+ }
900
+ totalBytes += entry.size as number;
901
+ if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_RUNTIME_ASSET_BYTES) {
902
+ throw new Error(`runtime assets exceed ${MAX_RUNTIME_ASSET_BYTES} bytes`);
903
+ }
904
+ if (typeof entry.sha256 !== "string" || !SHA256.test(entry.sha256)) {
905
+ throw new Error(`runtime asset manifest sha256 is invalid: ${entryPath}`);
906
+ }
907
+ hasAppleContainer ||= entryPath.startsWith("apple-container/");
908
+ hasKernel ||= entryPath.startsWith("kernel/");
909
+ hasHostAssets ||= entryPath.startsWith("host-assets/");
910
+ entries.push({
911
+ path: entryPath,
912
+ type: "file",
913
+ mode: entry.mode,
914
+ size: entry.size as number,
915
+ sha256: entry.sha256,
916
+ });
917
+ }
918
+ if (!hasAppleContainer || !hasKernel || !hasHostAssets) {
919
+ throw new Error(
920
+ "runtime asset manifest must include Apple container, guest kernel, and host files",
921
+ );
922
+ }
923
+ const entryPaths = new Set(entries.map((entry) => entry.path));
924
+ for (const required of [
925
+ "apple-container/bin/container",
926
+ "apple-container/bin/container-apiserver",
927
+ ...APPLE_CONTAINER_PLUGINS.map(
928
+ (plugin) =>
929
+ `apple-container/libexec/container/plugins/${plugin}/bin/${plugin}`,
930
+ ),
931
+ "kernel/vmlinux",
932
+ ...STANDARD_IMAGE_ASSETS.map(
933
+ ({ path: relative }) => `host-assets/standard-image/${relative}`,
934
+ ),
935
+ ]) {
936
+ if (!entryPaths.has(required)) {
937
+ throw new Error(`runtime asset manifest is missing ${required}`);
938
+ }
939
+ }
940
+ for (const { path: relative, mode } of STANDARD_IMAGE_ASSETS) {
941
+ const assetPath = `host-assets/standard-image/${relative}`;
942
+ if (entries.find((entry) => entry.path === assetPath)?.mode !== mode) {
943
+ throw new Error(`runtime asset manifest mode is invalid: ${assetPath}`);
944
+ }
945
+ }
946
+ if (runtimeAssetFingerprint(entries) !== object.fingerprint) {
947
+ throw new Error("runtime asset manifest fingerprint is invalid for its entries");
948
+ }
949
+ return { schemaVersion: 1, fingerprint: object.fingerprint, entries };
950
+ }
951
+
952
+ function validateArchivedRuntimeAssets(
953
+ byPath: ReadonlyMap<string, ArchiveEntry>,
954
+ capabilities: RuntimeCapabilityDescriptorV3,
955
+ ): void {
956
+ const manifestEntry = byPath.get(capabilities.bundledRuntime.layout.assetManifest);
957
+ if (
958
+ manifestEntry?.type !== "0" ||
959
+ manifestEntry.data.length > MAX_RUNTIME_ASSET_MANIFEST_BYTES
960
+ ) {
961
+ throw new Error("runtime asset manifest is missing or too large");
962
+ }
963
+ let rawManifest: unknown;
964
+ try {
965
+ rawManifest = JSON.parse(manifestEntry.data.toString("utf8"));
966
+ } catch {
967
+ throw new Error("runtime asset manifest is invalid JSON");
968
+ }
969
+ const manifest = validateRuntimeAssetManifest(
970
+ rawManifest,
971
+ capabilities.bundledRuntime.assetFingerprint,
972
+ );
973
+ const actual: RuntimeAssetManifestEntry[] = [];
974
+ for (const [memberPath, entry] of byPath) {
975
+ if (
976
+ !RUNTIME_ASSET_ROOTS.some(
977
+ (root) =>
978
+ memberPath === `${BUNDLED_RUNTIME_ROOT}/${root}` ||
979
+ memberPath.startsWith(`${BUNDLED_RUNTIME_ROOT}/${root}/`),
980
+ )
981
+ ) {
982
+ continue;
983
+ }
984
+ if (entry.type === "5") continue;
985
+ if (entry.type !== "0") {
986
+ throw new Error(`runtime asset is not a regular file: ${memberPath}`);
987
+ }
988
+ actual.push({
989
+ path: memberPath.slice(`${BUNDLED_RUNTIME_ROOT}/`.length),
990
+ type: "file",
991
+ mode: entry.mode as 0o644 | 0o755,
992
+ size: entry.data.length,
993
+ sha256: createHash("sha256").update(entry.data).digest("hex"),
994
+ });
995
+ }
996
+ actual.sort((left, right) => compareNames(left.path, right.path));
997
+ if (JSON.stringify(actual) !== JSON.stringify(manifest.entries)) {
998
+ throw new Error("runtime asset manifest does not close the archived asset tree");
999
+ }
1000
+ validateArchivedBundledSubtreeClosure(byPath, capabilities, manifest);
1001
+ }
1002
+
1003
+ function expectedBundledRuntimeFiles(
1004
+ capabilities: RuntimeCapabilityDescriptorV3,
1005
+ manifest: RuntimeAssetManifest,
1006
+ ): ReadonlySet<string> {
1007
+ const bundled = capabilities.bundledRuntime;
1008
+ // Every staged payload file is a manifest entry; only the two metadata
1009
+ // documents live outside the digest closure.
1010
+ return new Set([
1011
+ BUNDLED_RUNTIME_CAPABILITY,
1012
+ bundled.layout.assetManifest,
1013
+ ...manifest.entries.map(
1014
+ (entry) => `${bundled.layout.assetSourceRoot}/${entry.path}`,
1015
+ ),
1016
+ ]);
1017
+ }
1018
+
1019
+ function validateArchivedBundledSubtreeClosure(
1020
+ byPath: ReadonlyMap<string, ArchiveEntry>,
1021
+ capabilities: RuntimeCapabilityDescriptorV3,
1022
+ manifest: RuntimeAssetManifest,
1023
+ ): void {
1024
+ const expected = expectedBundledRuntimeFiles(capabilities, manifest);
1025
+ const actual = new Set<string>();
1026
+ for (const [memberPath, entry] of byPath) {
1027
+ if (!memberPath.startsWith(`${BUNDLED_RUNTIME_ROOT}/`)) continue;
1028
+ if (entry.type === "5") {
1029
+ if (![...expected].some((file) => file.startsWith(`${memberPath}/`))) {
1030
+ throw new Error(
1031
+ `runtime bundled subtree has an unexpected directory: ${memberPath}`,
1032
+ );
1033
+ }
1034
+ continue;
1035
+ }
1036
+ if (entry.type !== "0") {
1037
+ throw new Error(`runtime bundled subtree has an unsafe entry: ${memberPath}`);
1038
+ }
1039
+ actual.add(memberPath);
1040
+ }
1041
+ if (
1042
+ [...actual].sort(compareNames).join("\0") !==
1043
+ [...expected].sort(compareNames).join("\0")
1044
+ ) {
1045
+ throw new Error("runtime bundled subtree is not closed by its descriptor and manifest");
1046
+ }
1047
+ }
1048
+
1049
+ function validateRequirements(platform: string, value: Readonly<Record<string, string>>): void {
1050
+ const [os, arch] = platform.split("-");
1051
+ if (value.os !== os || value.arch !== arch) {
1052
+ throw new Error("runtime descriptor requirements do not match its platform");
1053
+ }
1054
+ const keys = Object.keys(value).sort().join("\0");
1055
+ if (os === "darwin") {
1056
+ if (keys !== ["arch", "minVersion", "os"].join("\0") || value.minVersion !== "13.5") {
1057
+ throw new Error("runtime descriptor macOS requirements are invalid");
1058
+ }
1059
+ } else if (
1060
+ os !== "linux" ||
1061
+ keys !== ["arch", "libc", "minKernel", "minLibcVersion", "os"].join("\0") ||
1062
+ value.libc !== "glibc" ||
1063
+ value.minKernel !== "4.18" ||
1064
+ value.minLibcVersion !== "2.29"
1065
+ ) {
1066
+ throw new Error("runtime descriptor Linux requirements are invalid");
1067
+ }
1068
+ }
1069
+
1070
+ function strictObject(
1071
+ value: unknown,
1072
+ expectedKeys: readonly string[],
1073
+ label: string,
1074
+ ): Record<string, unknown> {
1075
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1076
+ throw new Error(`${label} must be an object`);
1077
+ }
1078
+ const object = value as Record<string, unknown>;
1079
+ if (Object.getPrototypeOf(object) !== Object.prototype) {
1080
+ throw new Error(`${label} must be a plain object`);
1081
+ }
1082
+ if (Object.keys(object).sort().join("\0") !== [...expectedKeys].sort().join("\0")) {
1083
+ throw new Error(`${label} fields are invalid`);
1084
+ }
1085
+ return object;
1086
+ }
1087
+
1088
+ function strictStringObject(value: unknown, label: string): Readonly<Record<string, string>> {
1089
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1090
+ throw new Error(`${label} must be an object`);
1091
+ }
1092
+ const output: Record<string, string> = {};
1093
+ for (const [key, item] of Object.entries(value)) {
1094
+ if (typeof item !== "string") throw new Error(`${label}.${key} must be a string`);
1095
+ output[key] = item;
1096
+ }
1097
+ return output;
1098
+ }
1099
+
1100
+ function assertCanonicalGzipHeader(bytes: Buffer): void {
1101
+ if (
1102
+ bytes.length < 18 ||
1103
+ bytes[0] !== 0x1f ||
1104
+ bytes[1] !== 0x8b ||
1105
+ bytes[2] !== 8 ||
1106
+ bytes[3] !== 0 ||
1107
+ bytes.readUInt32LE(4) !== 0 ||
1108
+ bytes[8] !== 2 ||
1109
+ bytes[9] !== 255
1110
+ ) {
1111
+ throw new Error("runtime archive gzip header is not canonical");
1112
+ }
1113
+ }
1114
+
1115
+ function validateHeaderChecksum(header: Buffer): void {
1116
+ const expected = parseOctal(header, 148, 8, "checksum");
1117
+ const copy = Buffer.from(header);
1118
+ copy.fill(0x20, 148, 156);
1119
+ const actual = copy.reduce((sum, byte) => sum + byte, 0);
1120
+ if (actual !== expected) throw new Error("runtime archive header checksum mismatch");
1121
+ }
1122
+
1123
+ function parseString(block: Buffer, offset: number, length: number): string {
1124
+ const end = block.indexOf(0, offset);
1125
+ const stop = end === -1 || end > offset + length ? offset + length : end;
1126
+ const bytes = block.subarray(offset, stop);
1127
+ const decoded = bytes.toString("utf8");
1128
+ if (!Buffer.from(decoded, "utf8").equals(bytes)) {
1129
+ throw new Error("runtime archive header contains invalid UTF-8");
1130
+ }
1131
+ return decoded;
1132
+ }
1133
+
1134
+ function parseOctal(block: Buffer, offset: number, length: number, label: string): number {
1135
+ const raw = parseString(block, offset, length).trim();
1136
+ if (!/^[0-7]+$/.test(raw)) throw new Error(`runtime archive has invalid ${label}`);
1137
+ const value = Number.parseInt(raw, 8);
1138
+ if (!Number.isSafeInteger(value)) throw new Error(`runtime archive has invalid ${label}`);
1139
+ return value;
1140
+ }
1141
+
1142
+ function parsePax(body: Buffer): { path?: string; linkpath?: string } {
1143
+ const values: { path?: string; linkpath?: string } = {};
1144
+ let offset = 0;
1145
+ while (offset < body.length) {
1146
+ const space = body.indexOf(0x20, offset);
1147
+ if (space === -1) throw new Error("runtime archive pax record has no length");
1148
+ const rawLength = body.subarray(offset, space).toString("ascii");
1149
+ if (!/^[1-9]\d*$/.test(rawLength)) throw new Error("runtime archive pax length is invalid");
1150
+ const length = Number.parseInt(rawLength, 10);
1151
+ if (!Number.isSafeInteger(length) || offset + length > body.length) {
1152
+ throw new Error("runtime archive pax boundary is invalid");
1153
+ }
1154
+ const record = body.subarray(space + 1, offset + length);
1155
+ if (record.at(-1) !== 0x0a) throw new Error("runtime archive pax terminator is invalid");
1156
+ const text = record.subarray(0, -1).toString("utf8");
1157
+ const equals = text.indexOf("=");
1158
+ if (equals <= 0) throw new Error("runtime archive pax record is invalid");
1159
+ const key = text.slice(0, equals);
1160
+ const value = text.slice(equals + 1);
1161
+ if ((key !== "path" && key !== "linkpath") || Object.hasOwn(values, key)) {
1162
+ throw new Error(`unsupported or duplicate runtime archive pax key: ${key}`);
1163
+ }
1164
+ values[key] = value;
1165
+ offset += length;
1166
+ }
1167
+ return values;
1168
+ }
1169
+
1170
+ function safeMemberPath(value: string): string {
1171
+ const normalizedValue = value.endsWith("/") ? value.slice(0, -1) : value;
1172
+ if (
1173
+ !normalizedValue ||
1174
+ Buffer.byteLength(normalizedValue) > MAX_MEMBER_PATH_BYTES ||
1175
+ normalizedValue.startsWith("/") ||
1176
+ /[\x00-\x1f\x7f]/.test(normalizedValue) ||
1177
+ normalizedValue
1178
+ .split("/")
1179
+ .some((part) => !part || part === "." || part === "..") ||
1180
+ path.posix.normalize(normalizedValue) !== normalizedValue
1181
+ ) {
1182
+ throw new Error(`unsafe runtime archive member path: ${JSON.stringify(value)}`);
1183
+ }
1184
+ return normalizedValue;
1185
+ }
1186
+
1187
+ function safeLinkTarget(memberPath: string, target: string): string {
1188
+ if (
1189
+ !target ||
1190
+ Buffer.byteLength(target) > MAX_MEMBER_PATH_BYTES ||
1191
+ target.startsWith("/") ||
1192
+ /[\x00-\x1f\x7f]/.test(target)
1193
+ ) {
1194
+ throw new Error(`unsafe runtime archive symlink target: ${JSON.stringify(target)}`);
1195
+ }
1196
+ const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(memberPath), target));
1197
+ if (resolved === ".." || resolved.startsWith("../") || resolved.startsWith("/")) {
1198
+ throw new Error(`runtime archive symlink escapes the payload: ${memberPath}`);
1199
+ }
1200
+ return resolved;
1201
+ }
1202
+
1203
+ async function syncDirectory(directory: string): Promise<void> {
1204
+ let handle;
1205
+ try {
1206
+ handle = await open(directory, fsConstants.O_RDONLY);
1207
+ await handle.sync();
1208
+ } catch (error) {
1209
+ const code = (error as NodeJS.ErrnoException).code;
1210
+ if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR") {
1211
+ throw error;
1212
+ }
1213
+ } finally {
1214
+ await handle?.close();
1215
+ }
1216
+ }