@runuai/host 0.9.14 → 0.9.43

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 (97) hide show
  1. package/README.md +22 -5
  2. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  3. package/db/migrations/0015_host_settings.sql +9 -0
  4. package/db/migrations/0016_task_environment.sql +2 -0
  5. package/db/migrations/meta/_journal.json +21 -0
  6. package/db/schema.ts +80 -30
  7. package/images/standard/Dockerfile +36 -10
  8. package/images/standard/README.md +63 -18
  9. package/images/standard/container/corepack-version +1 -0
  10. package/images/standard/container/uai-init +308 -38
  11. package/images/standard/container/uai-materialize-runtimes +1527 -0
  12. package/lib/agent-cli.ts +33 -2
  13. package/lib/agent.ts +46 -7
  14. package/lib/agents/claude.ts +13 -8
  15. package/lib/agents/codex.ts +11 -6
  16. package/lib/agents/cursor.ts +39 -29
  17. package/lib/agents/durable-proc.ts +20 -27
  18. package/lib/agents/factory.ts +9 -25
  19. package/lib/agents/grok.ts +43 -30
  20. package/lib/agents/kimi.ts +44 -29
  21. package/lib/agents/opencode.ts +43 -31
  22. package/lib/agents/proc.ts +149 -114
  23. package/lib/agents/transport.ts +62 -50
  24. package/lib/agents/types.ts +6 -4
  25. package/lib/apple-runtime-recycle.ts +236 -0
  26. package/lib/apple-uninstall-teardown.ts +224 -0
  27. package/lib/browser-testing.ts +233 -93
  28. package/lib/codex-auth.ts +40 -6
  29. package/lib/command-db.ts +20 -0
  30. package/lib/container-runtime.ts +1338 -0
  31. package/lib/db.ts +1 -0
  32. package/lib/docker-exec.ts +87 -5
  33. package/lib/engine-accounts.ts +68 -5
  34. package/lib/engine-login.ts +1952 -0
  35. package/lib/enrollment-state.ts +251 -0
  36. package/lib/env-file.ts +155 -0
  37. package/lib/env.ts +4 -0
  38. package/lib/git-diff.ts +98 -32
  39. package/lib/git-identity.ts +199 -87
  40. package/lib/github-tokens.ts +202 -91
  41. package/lib/host-cloud-url.ts +62 -0
  42. package/lib/host-config.ts +279 -0
  43. package/lib/host-logs.ts +962 -0
  44. package/lib/keyed-promise-tail.ts +23 -0
  45. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  46. package/lib/managed-activation-watcher.ts +72 -0
  47. package/lib/managed-install-owner-watcher.ts +55 -0
  48. package/lib/managed-operation-drain.ts +49 -0
  49. package/lib/managed-runtime.ts +3644 -0
  50. package/lib/managed-update-scheduler.ts +125 -0
  51. package/lib/mcp-gateway.ts +450 -23
  52. package/lib/orchestrator.ts +3051 -200
  53. package/lib/preview-sidecar.ts +68 -14
  54. package/lib/release-manifest.ts +708 -0
  55. package/lib/release-trust.ts +28 -0
  56. package/lib/runtime-activation-tail.ts +232 -0
  57. package/lib/runtime-archive.ts +1086 -0
  58. package/lib/runtime-authority.ts +79 -0
  59. package/lib/runtime-guard.ts +36 -0
  60. package/lib/runtime-provider-state.ts +169 -0
  61. package/lib/runtime-state.ts +232 -12
  62. package/lib/skills.ts +24 -3
  63. package/lib/ssh.ts +18 -0
  64. package/lib/standard-image.ts +1104 -141
  65. package/lib/stopped-task-status-queue.ts +44 -0
  66. package/lib/task-container-cli.ts +269 -0
  67. package/lib/task-diff.ts +66 -46
  68. package/lib/task-environment/apple-container.ts +757 -0
  69. package/lib/task-environment/docker.ts +956 -0
  70. package/lib/task-environment/index.ts +364 -0
  71. package/lib/task-environment/legacy-adoption.ts +459 -0
  72. package/lib/task-environment/registry.ts +58 -0
  73. package/lib/task-environment/types.ts +408 -0
  74. package/lib/task-identity.ts +19 -0
  75. package/lib/task-inventory.ts +585 -0
  76. package/lib/tunnel-registry.ts +135 -19
  77. package/lib/tunnel-runtime.ts +235 -0
  78. package/package.json +1 -1
  79. package/scripts/agent/_common.sh +123 -3
  80. package/scripts/agent/task-down.sh +146 -38
  81. package/scripts/agent/task-status.sh +19 -3
  82. package/scripts/agent/task-up.sh +1405 -107
  83. package/scripts/install/darwin.ts +848 -50
  84. package/scripts/install/linux.ts +838 -35
  85. package/scripts/install/types.ts +43 -0
  86. package/scripts/install/util.ts +215 -8
  87. package/scripts/install/win.ts +12 -0
  88. package/src/apple-tunnel-route.ts +104 -0
  89. package/src/cli.ts +1464 -72
  90. package/src/event-outbox.ts +83 -4
  91. package/src/index.ts +766 -42
  92. package/src/main.ts +1398 -255
  93. package/src/paths.ts +17 -1
  94. package/src/protocol.ts +695 -1
  95. package/src/runtime-bootstrap.ts +165 -0
  96. package/src/ui/server.ts +46 -10
  97. package/src/ui/types.ts +37 -0
@@ -0,0 +1,1086 @@
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
+ /** Plugin directory == plugin binary name in Apple's signed pkg payload. */
32
+ const APPLE_CONTAINER_PLUGINS = Object.freeze([
33
+ "container-core-images",
34
+ "container-network-vmnet",
35
+ "container-runtime-linux",
36
+ "k8s",
37
+ "machine-apiserver",
38
+ ]);
39
+ const STANDARD_IMAGE_ASSETS = Object.freeze([
40
+ Object.freeze({ path: "Dockerfile", mode: 0o644 as const }),
41
+ Object.freeze({ path: "README.md", mode: 0o644 as const }),
42
+ Object.freeze({ path: "container/code-server-settings.json", mode: 0o644 as const }),
43
+ Object.freeze({ path: "container/corepack-version", mode: 0o644 as const }),
44
+ Object.freeze({ path: "container/uai-init", mode: 0o755 as const }),
45
+ Object.freeze({ path: "container/uai-materialize-runtimes", mode: 0o755 as const }),
46
+ Object.freeze({ path: "tool-versions", mode: 0o644 as const }),
47
+ ]);
48
+ const RUNTIME_ASSET_ROOTS = Object.freeze([
49
+ "apple-container",
50
+ "host-assets",
51
+ "kernel",
52
+ "licenses",
53
+ ]);
54
+ export const REQUIRED_RUNTIME_LICENSES = Object.freeze([
55
+ "Container-LICENSE",
56
+ "Kernel-GPL-2.0",
57
+ ]);
58
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
59
+ const SHA256 = /^[a-f0-9]{64}$/;
60
+
61
+ interface RuntimeDescriptorCommon {
62
+ readonly hostVersion: string;
63
+ readonly platform: string;
64
+ readonly node: {
65
+ readonly version: string;
66
+ readonly modules: string;
67
+ };
68
+ readonly requirements: Readonly<Record<string, string>>;
69
+ readonly layout: {
70
+ readonly agent: "agent";
71
+ readonly launcher: "bin/uai-host";
72
+ readonly node: "node";
73
+ };
74
+ }
75
+
76
+ export interface RuntimeDescriptorV1 extends RuntimeDescriptorCommon {
77
+ readonly schemaVersion: 1;
78
+ }
79
+
80
+ export interface BundledRuntimeDescriptor {
81
+ readonly assetFingerprint: string;
82
+ readonly versions: {
83
+ readonly container: string;
84
+ readonly kata: string;
85
+ readonly guestKernel: string;
86
+ };
87
+ readonly requirements: {
88
+ readonly os: "darwin";
89
+ readonly arch: "arm64";
90
+ readonly minVersion: string;
91
+ };
92
+ readonly teamIdentifier: string;
93
+ readonly layout: {
94
+ readonly assetSourceRoot: "agent/bundled-runtime";
95
+ readonly assetManifest: "agent/bundled-runtime/assets.json";
96
+ readonly containerCli: "agent/bundled-runtime/apple-container/bin/container";
97
+ readonly apiserver: "agent/bundled-runtime/apple-container/bin/container-apiserver";
98
+ readonly plugins: "agent/bundled-runtime/apple-container/libexec/container/plugins";
99
+ readonly kernel: "agent/bundled-runtime/kernel/vmlinux";
100
+ readonly standardImage: "agent/bundled-runtime/host-assets/standard-image";
101
+ readonly licenses: "agent/bundled-runtime/licenses";
102
+ };
103
+ }
104
+
105
+ export interface RuntimeCapabilityDescriptorV3 {
106
+ readonly capabilitySchemaVersion: 3;
107
+ readonly bundledRuntime: BundledRuntimeDescriptor;
108
+ }
109
+
110
+ export type RuntimeDescriptor = RuntimeDescriptorV1;
111
+ export type ResolvedRuntimeDescriptor = RuntimeDescriptor & (
112
+ | { readonly capabilitySchemaVersion: 1 }
113
+ | RuntimeCapabilityDescriptorV3
114
+ );
115
+
116
+ export interface RuntimeAssetManifestEntry {
117
+ readonly path: string;
118
+ readonly type: "file";
119
+ readonly mode: 0o644 | 0o755;
120
+ readonly size: number;
121
+ readonly sha256: string;
122
+ }
123
+
124
+ export interface RuntimeAssetManifest {
125
+ readonly schemaVersion: 1;
126
+ readonly fingerprint: string;
127
+ readonly entries: readonly RuntimeAssetManifestEntry[];
128
+ }
129
+
130
+ interface ArchiveEntry {
131
+ readonly memberPath: string;
132
+ readonly type: "0" | "2" | "5";
133
+ readonly mode: number;
134
+ readonly data: Buffer;
135
+ readonly linkTarget: string;
136
+ readonly resolvedLinkTarget: string | null;
137
+ }
138
+
139
+ export interface InspectedRuntimeArchive {
140
+ readonly descriptor: RuntimeDescriptor;
141
+ readonly capabilities:
142
+ | { readonly capabilitySchemaVersion: 1 }
143
+ | RuntimeCapabilityDescriptorV3;
144
+ readonly memberCount: number;
145
+ }
146
+
147
+ /**
148
+ * Read, count and hash one already-downloaded regular file through one open
149
+ * descriptor. Extraction consumes the exact returned bytes; there is no
150
+ * verify-by-name/reopen window in which the pathname could be swapped.
151
+ */
152
+ export async function readVerifiedArtifactFile(
153
+ archivePath: string,
154
+ artifact: ReleaseArtifact,
155
+ ): Promise<Buffer> {
156
+ const noFollow = fsConstants.O_NOFOLLOW ?? 0;
157
+ const handle = await open(archivePath, fsConstants.O_RDONLY | noFollow);
158
+ try {
159
+ const before = await handle.stat();
160
+ if (!before.isFile()) throw new Error("runtime archive must be a regular file");
161
+ if (before.size > MAX_RELEASE_ARTIFACT_BYTES) {
162
+ throw new Error(`runtime archive exceeds ${MAX_RELEASE_ARTIFACT_BYTES} bytes`);
163
+ }
164
+
165
+ const hash = createHash("sha256");
166
+ const chunks: Buffer[] = [];
167
+ let size = 0;
168
+ for await (const raw of handle.createReadStream({ autoClose: false })) {
169
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
170
+ size += chunk.length;
171
+ if (size > MAX_RELEASE_ARTIFACT_BYTES) {
172
+ throw new Error(`runtime archive exceeds ${MAX_RELEASE_ARTIFACT_BYTES} bytes`);
173
+ }
174
+ hash.update(chunk);
175
+ chunks.push(chunk);
176
+ }
177
+ const after = await handle.stat();
178
+ if (
179
+ before.dev !== after.dev ||
180
+ before.ino !== after.ino ||
181
+ before.size !== after.size ||
182
+ size !== after.size
183
+ ) {
184
+ throw new Error("runtime archive changed while it was being verified");
185
+ }
186
+ verifyReleaseArtifactIntegrity(artifact, {
187
+ size,
188
+ sha256: hash.digest(),
189
+ });
190
+ return Buffer.concat(chunks, size);
191
+ } finally {
192
+ await handle.close();
193
+ }
194
+ }
195
+
196
+ export async function extractVerifiedRuntimeArchive(
197
+ archiveBytes: Buffer,
198
+ destination: string,
199
+ expected: { version: string; platform: string },
200
+ ): Promise<InspectedRuntimeArchive> {
201
+ const { entries, descriptor, capabilities } = inspectRuntimeArchive(
202
+ archiveBytes,
203
+ expected,
204
+ );
205
+ // The caller gives us a fresh, private staging path. `recursive: false`
206
+ // refuses to merge archive contents into any pre-existing tree.
207
+ await mkdir(destination, { mode: 0o700, recursive: false });
208
+ const directories = [destination];
209
+
210
+ for (const entry of entries) {
211
+ const output = path.join(destination, ...entry.memberPath.split("/"));
212
+ if (entry.type === "5") {
213
+ await mkdir(output, { mode: 0o755, recursive: false });
214
+ await chmod(output, 0o755);
215
+ directories.push(output);
216
+ } else if (entry.type === "0") {
217
+ const handle = await open(output, "wx", entry.mode);
218
+ try {
219
+ await handle.writeFile(entry.data);
220
+ await handle.chmod(entry.mode);
221
+ await handle.sync();
222
+ } finally {
223
+ await handle.close();
224
+ }
225
+ }
226
+ }
227
+ // Create links only after every regular target exists. Archive validation
228
+ // has already rejected absolute/escaping targets and any child below a link.
229
+ for (const entry of entries) {
230
+ if (entry.type !== "2") continue;
231
+ const output = path.join(destination, ...entry.memberPath.split("/"));
232
+ await symlink(entry.linkTarget, output);
233
+ }
234
+ // Persist children before parents so activation never points at a directory
235
+ // whose archive contents only existed in the page cache at power loss.
236
+ for (const directory of directories.reverse()) {
237
+ await syncDirectory(directory);
238
+ }
239
+ return { descriptor, capabilities, memberCount: entries.length };
240
+ }
241
+
242
+ export function inspectRuntimeArchive(
243
+ archiveBytes: Buffer,
244
+ expected: { version: string; platform: string },
245
+ ): {
246
+ entries: readonly ArchiveEntry[];
247
+ descriptor: RuntimeDescriptor;
248
+ capabilities:
249
+ | { readonly capabilitySchemaVersion: 1 }
250
+ | RuntimeCapabilityDescriptorV3;
251
+ } {
252
+ assertCanonicalGzipHeader(archiveBytes);
253
+ const tar = gunzipSync(archiveBytes, { maxOutputLength: MAX_UNCOMPRESSED_BYTES });
254
+ // The release builder emits a single deterministic gzip stream. Rebuilding
255
+ // it under the packaged, exactly-pinned Node rejects concatenated streams,
256
+ // trailing bytes and alternate encodings before any filesystem mutation.
257
+ // Modern Node always writes a zero gzip mtime (the option was removed from
258
+ // its public typings); the builder runs the same pinned Node patch.
259
+ const canonical = gzipSync(tar, { level: 9 });
260
+ canonical[9] = 255;
261
+ if (!archiveBytes.equals(canonical)) {
262
+ throw new Error("runtime archive gzip stream is not canonical");
263
+ }
264
+
265
+ const entries: ArchiveEntry[] = [];
266
+ const byPath = new Map<string, ArchiveEntry>();
267
+ let offset = 0;
268
+ let pendingPax: { path?: string; linkpath?: string } | null = null;
269
+ let previousMember: string | null = null;
270
+ let terminated = false;
271
+
272
+ while (offset + BLOCK_SIZE <= tar.length) {
273
+ const header = tar.subarray(offset, offset + BLOCK_SIZE);
274
+ if (header.equals(ZERO_BLOCK)) {
275
+ if (
276
+ offset + 2 * BLOCK_SIZE !== tar.length ||
277
+ !tar.subarray(offset + BLOCK_SIZE).equals(ZERO_BLOCK)
278
+ ) {
279
+ throw new Error("runtime archive has trailing bytes or an invalid terminator");
280
+ }
281
+ offset += 2 * BLOCK_SIZE;
282
+ terminated = true;
283
+ break;
284
+ }
285
+
286
+ validateHeaderChecksum(header);
287
+ if (parseString(header, 257, 6) !== "ustar") {
288
+ throw new Error("runtime archive is not ustar");
289
+ }
290
+ const name = parseString(header, 0, 100);
291
+ const prefix = parseString(header, 345, 155);
292
+ const headerPath = prefix ? `${prefix}/${name}` : name;
293
+ const mode = parseOctal(header, 100, 8, "mode");
294
+ const uid = parseOctal(header, 108, 8, "uid");
295
+ const gid = parseOctal(header, 116, 8, "gid");
296
+ const size = parseOctal(header, 124, 12, "size");
297
+ const mtime = parseOctal(header, 136, 12, "mtime");
298
+ const type = (parseString(header, 156, 1) || "0") as string;
299
+ const rawLinkTarget = parseString(header, 157, 100);
300
+ if (
301
+ uid !== 0 ||
302
+ gid !== 0 ||
303
+ mtime !== 0 ||
304
+ parseString(header, 265, 32) !== "root" ||
305
+ parseString(header, 297, 32) !== "root" ||
306
+ parseOctal(header, 329, 8, "devmajor") !== 0 ||
307
+ parseOctal(header, 337, 8, "devminor") !== 0
308
+ ) {
309
+ throw new Error("runtime archive metadata is not canonical");
310
+ }
311
+ if (!Number.isSafeInteger(size) || size < 0 || size > MAX_UNCOMPRESSED_BYTES) {
312
+ throw new Error("runtime archive member size is invalid");
313
+ }
314
+ const dataStart = offset + BLOCK_SIZE;
315
+ const dataEnd = dataStart + size;
316
+ const paddedEnd = dataStart + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
317
+ if (dataEnd > tar.length || paddedEnd > tar.length) {
318
+ throw new Error("runtime archive member exceeds input");
319
+ }
320
+ if (tar.subarray(dataEnd, paddedEnd).some((byte) => byte !== 0)) {
321
+ throw new Error(`runtime archive member padding is not zero: ${headerPath}`);
322
+ }
323
+ const data = tar.subarray(dataStart, dataEnd);
324
+ offset = paddedEnd;
325
+
326
+ if (type === "x") {
327
+ if (pendingPax) throw new Error("runtime archive has consecutive pax headers");
328
+ if (size > MAX_PAX_BYTES || mode !== 0o644) {
329
+ throw new Error("runtime archive pax header is invalid");
330
+ }
331
+ pendingPax = parsePax(data);
332
+ continue;
333
+ }
334
+ if (type !== "0" && type !== "2" && type !== "5") {
335
+ throw new Error(`unsupported runtime archive member type: ${type}`);
336
+ }
337
+ const rawMemberPath = pendingPax?.path ?? headerPath;
338
+ if ((type === "5") !== rawMemberPath.endsWith("/")) {
339
+ throw new Error(`runtime archive member path/type mismatch: ${rawMemberPath}`);
340
+ }
341
+ const memberPath = safeMemberPath(rawMemberPath);
342
+ if (
343
+ previousMember !== null &&
344
+ Buffer.compare(Buffer.from(previousMember), Buffer.from(memberPath)) >= 0
345
+ ) {
346
+ throw new Error(`runtime archive members are not strictly sorted: ${memberPath}`);
347
+ }
348
+ previousMember = memberPath;
349
+ if (byPath.has(memberPath)) {
350
+ throw new Error(`duplicate runtime archive member: ${memberPath}`);
351
+ }
352
+ if (entries.length >= MAX_ARCHIVE_MEMBERS) {
353
+ throw new Error(`runtime archive exceeds ${MAX_ARCHIVE_MEMBERS} members`);
354
+ }
355
+ if (type !== "0" && size !== 0) {
356
+ throw new Error(`non-file runtime archive member carries data: ${memberPath}`);
357
+ }
358
+ const expectedMode = type === "5" ? 0o755 : type === "2" ? 0o777 : mode;
359
+ if (
360
+ mode !== expectedMode ||
361
+ (type === "0" && mode !== 0o644 && mode !== 0o755)
362
+ ) {
363
+ throw new Error(`runtime archive member mode is invalid: ${memberPath}`);
364
+ }
365
+ const linkTarget = pendingPax?.linkpath ?? rawLinkTarget;
366
+ const resolvedLinkTarget =
367
+ type === "2" ? safeLinkTarget(memberPath, linkTarget) : null;
368
+ const entry: ArchiveEntry = {
369
+ memberPath,
370
+ type,
371
+ mode,
372
+ data,
373
+ linkTarget,
374
+ resolvedLinkTarget,
375
+ };
376
+ entries.push(entry);
377
+ byPath.set(memberPath, entry);
378
+ pendingPax = null;
379
+ }
380
+
381
+ if (!terminated || offset !== tar.length || pendingPax) {
382
+ throw new Error("runtime archive is truncated");
383
+ }
384
+ validateEntryGraph(entries, byPath);
385
+ const descriptorEntry = byPath.get("runtime.json");
386
+ if (!descriptorEntry || descriptorEntry.data.length > MAX_PAX_BYTES) {
387
+ throw new Error("runtime archive descriptor is missing or too large");
388
+ }
389
+ let rawDescriptor: unknown;
390
+ try {
391
+ rawDescriptor = JSON.parse(descriptorEntry.data.toString("utf8"));
392
+ } catch {
393
+ throw new Error("runtime archive descriptor is invalid JSON");
394
+ }
395
+ const descriptor = validateRuntimeDescriptor(rawDescriptor, expected);
396
+ const capabilities = validateEntryLayout(entries, byPath, descriptor);
397
+ return { entries, descriptor, capabilities };
398
+ }
399
+
400
+ function validateEntryGraph(
401
+ entries: readonly ArchiveEntry[],
402
+ byPath: ReadonlyMap<string, ArchiveEntry>,
403
+ ): void {
404
+ for (const entry of entries) {
405
+ const parts = entry.memberPath.split("/");
406
+ for (let length = 1; length < parts.length; length += 1) {
407
+ const ancestor = parts.slice(0, length).join("/");
408
+ const parent = byPath.get(ancestor);
409
+ if (!parent || parent.type !== "5") {
410
+ throw new Error(`runtime archive parent is not a directory: ${ancestor}`);
411
+ }
412
+ }
413
+ if (
414
+ entry.type === "2" &&
415
+ (!entry.resolvedLinkTarget || !byPath.has(entry.resolvedLinkTarget))
416
+ ) {
417
+ throw new Error(`runtime archive symlink target does not exist: ${entry.memberPath}`);
418
+ }
419
+ }
420
+ }
421
+
422
+ function validateEntryLayout(
423
+ entries: readonly ArchiveEntry[],
424
+ byPath: ReadonlyMap<string, ArchiveEntry>,
425
+ descriptor: RuntimeDescriptor,
426
+ ): { readonly capabilitySchemaVersion: 1 } | RuntimeCapabilityDescriptorV3 {
427
+ const topLevel = new Set(entries.map((entry) => entry.memberPath.split("/")[0]!));
428
+ if (
429
+ [...topLevel].sort().join("\0") !== [...BASE_TOP_LEVEL].sort().join("\0")
430
+ ) {
431
+ throw new Error("runtime archive top-level layout is invalid");
432
+ }
433
+ const requiredFiles = [
434
+ "agent/bin/uai-host.mjs",
435
+ "agent/scripts/agent/task-up.sh",
436
+ "agent/scripts/agent/task-down.sh",
437
+ "agent/scripts/agent/task-status.sh",
438
+ "bin/uai-host",
439
+ "node/bin/node",
440
+ "runtime.json",
441
+ ];
442
+ const requiredDirectories = ["agent", "bin", "node"];
443
+ const executables = [
444
+ "bin/uai-host",
445
+ "node/bin/node",
446
+ "agent/scripts/agent/task-up.sh",
447
+ "agent/scripts/agent/task-down.sh",
448
+ "agent/scripts/agent/task-status.sh",
449
+ ];
450
+ const capabilities = validateArchivedRuntimeCapability(byPath, descriptor);
451
+ if (capabilities.capabilitySchemaVersion === 3) {
452
+ const bundled = capabilities.bundledRuntime;
453
+ const pluginBinaries = APPLE_CONTAINER_PLUGINS.map(
454
+ (plugin) => `${bundled.layout.plugins}/${plugin}/bin/${plugin}`,
455
+ );
456
+ requiredFiles.push(
457
+ BUNDLED_RUNTIME_CAPABILITY,
458
+ bundled.layout.assetManifest,
459
+ bundled.layout.containerCli,
460
+ bundled.layout.apiserver,
461
+ ...pluginBinaries,
462
+ bundled.layout.kernel,
463
+ ...STANDARD_IMAGE_ASSETS.map(
464
+ ({ path: relative }) => `${bundled.layout.standardImage}/${relative}`,
465
+ ),
466
+ ...REQUIRED_RUNTIME_LICENSES.map(
467
+ (license) => `${bundled.layout.licenses}/${license}`,
468
+ ),
469
+ );
470
+ requiredDirectories.push(
471
+ BUNDLED_RUNTIME_ROOT,
472
+ `${BUNDLED_RUNTIME_ROOT}/apple-container`,
473
+ `${BUNDLED_RUNTIME_ROOT}/apple-container/bin`,
474
+ `${BUNDLED_RUNTIME_ROOT}/apple-container/libexec`,
475
+ `${BUNDLED_RUNTIME_ROOT}/apple-container/libexec/container`,
476
+ bundled.layout.plugins,
477
+ ...APPLE_CONTAINER_PLUGINS.flatMap((plugin) => [
478
+ `${bundled.layout.plugins}/${plugin}`,
479
+ `${bundled.layout.plugins}/${plugin}/bin`,
480
+ ]),
481
+ `${BUNDLED_RUNTIME_ROOT}/kernel`,
482
+ `${BUNDLED_RUNTIME_ROOT}/host-assets`,
483
+ bundled.layout.standardImage,
484
+ `${bundled.layout.standardImage}/container`,
485
+ bundled.layout.licenses,
486
+ );
487
+ validateArchivedRuntimeAssets(byPath, capabilities);
488
+ executables.push(
489
+ bundled.layout.containerCli,
490
+ bundled.layout.apiserver,
491
+ ...pluginBinaries,
492
+ );
493
+ }
494
+ for (const required of requiredFiles) {
495
+ if (byPath.get(required)?.type !== "0") {
496
+ throw new Error(`runtime archive is missing regular file ${required}`);
497
+ }
498
+ }
499
+ for (const required of requiredDirectories) {
500
+ if (byPath.get(required)?.type !== "5") {
501
+ throw new Error(`runtime archive is missing directory ${required}`);
502
+ }
503
+ }
504
+ for (const executable of executables) {
505
+ if (byPath.get(executable)?.mode !== 0o755) {
506
+ throw new Error(`runtime archive executable mode is invalid: ${executable}`);
507
+ }
508
+ }
509
+ return capabilities;
510
+ }
511
+
512
+ function validateArchivedRuntimeCapability(
513
+ byPath: ReadonlyMap<string, ArchiveEntry>,
514
+ descriptor: RuntimeDescriptor,
515
+ ): { readonly capabilitySchemaVersion: 1 } | RuntimeCapabilityDescriptorV3 {
516
+ const hasBundledSubtree = [...byPath.keys()].some(
517
+ (member) =>
518
+ member === BUNDLED_RUNTIME_ROOT ||
519
+ member.startsWith(`${BUNDLED_RUNTIME_ROOT}/`),
520
+ );
521
+ if (!descriptor.platform.startsWith("darwin-")) {
522
+ if (hasBundledSubtree) {
523
+ throw new Error("Linux runtime archive must not contain bundled Darwin assets");
524
+ }
525
+ return { capabilitySchemaVersion: 1 };
526
+ }
527
+ if (!hasBundledSubtree) return { capabilitySchemaVersion: 1 };
528
+ const entry = byPath.get(BUNDLED_RUNTIME_CAPABILITY);
529
+ if (entry?.type !== "0" || entry.data.length > MAX_PAX_BYTES) {
530
+ throw new Error("runtime capability descriptor is missing or too large");
531
+ }
532
+ let rawCapability: unknown;
533
+ try {
534
+ rawCapability = JSON.parse(entry.data.toString("utf8"));
535
+ } catch {
536
+ throw new Error("runtime capability descriptor is invalid JSON");
537
+ }
538
+ return validateRuntimeCapabilityDescriptor(rawCapability, descriptor.platform);
539
+ }
540
+
541
+ export function validateRuntimeDescriptor(
542
+ value: unknown,
543
+ expected: { version: string; platform: string },
544
+ ): RuntimeDescriptor {
545
+ const object = strictObject(
546
+ value,
547
+ ["schemaVersion", "hostVersion", "platform", "node", "requirements", "layout"],
548
+ "runtime descriptor",
549
+ );
550
+ if (object.schemaVersion !== 1) {
551
+ throw new Error("unsupported runtime descriptor schema");
552
+ }
553
+ if (object.hostVersion !== expected.version || !VERSION.test(expected.version)) {
554
+ throw new Error("runtime descriptor version does not match the authenticated release");
555
+ }
556
+ if (object.platform !== expected.platform) {
557
+ throw new Error("runtime descriptor platform does not match this host");
558
+ }
559
+ const node = strictObject(object.node, ["version", "modules"], "runtime descriptor node");
560
+ if (typeof node.version !== "string" || !VERSION.test(node.version)) {
561
+ throw new Error("runtime descriptor Node version is invalid");
562
+ }
563
+ if (typeof node.modules !== "string" || !/^\d+$/.test(node.modules)) {
564
+ throw new Error("runtime descriptor Node ABI is invalid");
565
+ }
566
+ const layout = strictObject(
567
+ object.layout,
568
+ ["agent", "launcher", "node"],
569
+ "runtime descriptor layout",
570
+ );
571
+ if (
572
+ layout.agent !== "agent" ||
573
+ layout.launcher !== "bin/uai-host" ||
574
+ layout.node !== "node"
575
+ ) {
576
+ throw new Error("runtime descriptor layout is invalid");
577
+ }
578
+ const requirements = strictStringObject(object.requirements, "runtime requirements");
579
+ validateRequirements(expected.platform, requirements);
580
+ return {
581
+ schemaVersion: 1,
582
+ hostVersion: expected.version,
583
+ platform: expected.platform,
584
+ node: { version: node.version, modules: node.modules },
585
+ requirements,
586
+ layout: { agent: "agent", launcher: "bin/uai-host", node: "node" } as const,
587
+ };
588
+ }
589
+
590
+ export function validateRuntimeCapabilityDescriptor(
591
+ value: unknown,
592
+ platform: string,
593
+ ): RuntimeCapabilityDescriptorV3 {
594
+ // ADR-106: the Apple container payload exists only on Apple Silicon.
595
+ if (platform !== "darwin-arm64") {
596
+ throw new Error("bundled runtime capability is supported only on darwin-arm64");
597
+ }
598
+ const object = strictObject(
599
+ value,
600
+ ["capabilitySchemaVersion", "bundledRuntime"],
601
+ "runtime capability descriptor",
602
+ );
603
+ if (object.capabilitySchemaVersion !== 3) {
604
+ throw new Error("unsupported runtime capability schema");
605
+ }
606
+ return {
607
+ capabilitySchemaVersion: 3,
608
+ bundledRuntime: validateBundledRuntimeDescriptor(object.bundledRuntime),
609
+ };
610
+ }
611
+
612
+ function validateBundledRuntimeDescriptor(
613
+ value: unknown,
614
+ ): BundledRuntimeDescriptor {
615
+ const object = strictObject(
616
+ value,
617
+ ["assetFingerprint", "versions", "requirements", "teamIdentifier", "layout"],
618
+ "bundled runtime descriptor",
619
+ );
620
+ if (typeof object.assetFingerprint !== "string" || !SHA256.test(object.assetFingerprint)) {
621
+ throw new Error("bundled runtime asset fingerprint is invalid");
622
+ }
623
+ const versions = strictObject(
624
+ object.versions,
625
+ ["container", "kata", "guestKernel"],
626
+ "bundled runtime versions",
627
+ );
628
+ for (const [name, version] of Object.entries(versions)) {
629
+ if (typeof version !== "string" || !VERSION.test(version)) {
630
+ throw new Error(`bundled runtime ${name} version is invalid`);
631
+ }
632
+ }
633
+ const requirements = strictObject(
634
+ object.requirements,
635
+ ["os", "arch", "minVersion"],
636
+ "bundled runtime requirements",
637
+ );
638
+ if (
639
+ requirements.os !== "darwin" ||
640
+ requirements.arch !== "arm64" ||
641
+ typeof requirements.minVersion !== "string" ||
642
+ !/^\d+\.\d+$/.test(requirements.minVersion)
643
+ ) {
644
+ throw new Error("bundled runtime requirements are invalid");
645
+ }
646
+ if (
647
+ typeof object.teamIdentifier !== "string" ||
648
+ !/^[A-Z0-9]{10}$/.test(object.teamIdentifier)
649
+ ) {
650
+ throw new Error("bundled runtime signing team identifier is invalid");
651
+ }
652
+ const layout = strictObject(
653
+ object.layout,
654
+ [
655
+ "assetSourceRoot",
656
+ "assetManifest",
657
+ "containerCli",
658
+ "apiserver",
659
+ "plugins",
660
+ "kernel",
661
+ "standardImage",
662
+ "licenses",
663
+ ],
664
+ "bundled runtime layout",
665
+ );
666
+ const expectedLayout = {
667
+ assetSourceRoot: BUNDLED_RUNTIME_ROOT,
668
+ assetManifest: `${BUNDLED_RUNTIME_ROOT}/assets.json`,
669
+ containerCli: `${BUNDLED_RUNTIME_ROOT}/apple-container/bin/container`,
670
+ apiserver: `${BUNDLED_RUNTIME_ROOT}/apple-container/bin/container-apiserver`,
671
+ plugins: `${BUNDLED_RUNTIME_ROOT}/apple-container/libexec/container/plugins`,
672
+ kernel: `${BUNDLED_RUNTIME_ROOT}/kernel/vmlinux`,
673
+ standardImage: `${BUNDLED_RUNTIME_ROOT}/host-assets/standard-image`,
674
+ licenses: `${BUNDLED_RUNTIME_ROOT}/licenses`,
675
+ } as const;
676
+ for (const [name, expectedValue] of Object.entries(expectedLayout)) {
677
+ if (layout[name] !== expectedValue) {
678
+ throw new Error(`bundled runtime layout ${name} is invalid`);
679
+ }
680
+ }
681
+ return {
682
+ assetFingerprint: object.assetFingerprint,
683
+ versions: {
684
+ container: versions.container as string,
685
+ kata: versions.kata as string,
686
+ guestKernel: versions.guestKernel as string,
687
+ },
688
+ requirements: {
689
+ os: "darwin",
690
+ arch: "arm64",
691
+ minVersion: requirements.minVersion,
692
+ },
693
+ teamIdentifier: object.teamIdentifier,
694
+ layout: expectedLayout,
695
+ };
696
+ }
697
+
698
+ function compareNames(left: string, right: string): number {
699
+ return Buffer.compare(Buffer.from(left), Buffer.from(right));
700
+ }
701
+
702
+ function runtimeAssetFingerprint(entries: readonly RuntimeAssetManifestEntry[]): string {
703
+ return createHash("sha256").update(JSON.stringify(entries)).digest("hex");
704
+ }
705
+
706
+ export function validateRuntimeAssetManifest(
707
+ value: unknown,
708
+ expectedFingerprint: string,
709
+ ): RuntimeAssetManifest {
710
+ const object = strictObject(
711
+ value,
712
+ ["schemaVersion", "fingerprint", "entries"],
713
+ "runtime asset manifest",
714
+ );
715
+ if (object.schemaVersion !== 1) {
716
+ throw new Error("unsupported runtime asset manifest schema");
717
+ }
718
+ if (
719
+ typeof object.fingerprint !== "string" ||
720
+ !SHA256.test(object.fingerprint) ||
721
+ object.fingerprint !== expectedFingerprint
722
+ ) {
723
+ throw new Error("runtime asset manifest fingerprint does not match runtime descriptor");
724
+ }
725
+ if (
726
+ !Array.isArray(object.entries) ||
727
+ object.entries.length === 0 ||
728
+ object.entries.length > MAX_RUNTIME_ASSET_ENTRIES
729
+ ) {
730
+ throw new Error("runtime asset manifest entry count is invalid");
731
+ }
732
+ const entries: RuntimeAssetManifestEntry[] = [];
733
+ let totalBytes = 0;
734
+ let previous: string | null = null;
735
+ let hasAppleContainer = false;
736
+ let hasKernel = false;
737
+ let hasHostAssets = false;
738
+ for (const rawEntry of object.entries) {
739
+ const entry = strictObject(
740
+ rawEntry,
741
+ ["path", "type", "mode", "size", "sha256"],
742
+ "runtime asset manifest entry",
743
+ );
744
+ const entryPath = entry.path;
745
+ if (
746
+ typeof entryPath !== "string" ||
747
+ entryPath.length === 0 ||
748
+ Buffer.byteLength(entryPath) > MAX_MEMBER_PATH_BYTES ||
749
+ entryPath.startsWith("/") ||
750
+ /[\x00-\x1f\x7f]/.test(entryPath) ||
751
+ entryPath.split("/").some((part) => !part || part === "." || part === "..") ||
752
+ path.posix.normalize(entryPath) !== entryPath ||
753
+ !RUNTIME_ASSET_ROOTS.some((root) => entryPath.startsWith(`${root}/`))
754
+ ) {
755
+ throw new Error(`runtime asset manifest path is invalid: ${String(entryPath)}`);
756
+ }
757
+ if (previous !== null && compareNames(previous, entryPath) >= 0) {
758
+ throw new Error("runtime asset manifest entries must be unique and bytewise sorted");
759
+ }
760
+ previous = entryPath;
761
+ if (entry.type !== "file") {
762
+ throw new Error(`runtime asset manifest entry is not a regular file: ${entryPath}`);
763
+ }
764
+ if (entry.mode !== 0o644 && entry.mode !== 0o755) {
765
+ throw new Error(`runtime asset manifest mode is invalid: ${entryPath}`);
766
+ }
767
+ if (!Number.isSafeInteger(entry.size) || (entry.size as number) < 0) {
768
+ throw new Error(`runtime asset manifest size is invalid: ${entryPath}`);
769
+ }
770
+ totalBytes += entry.size as number;
771
+ if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_RUNTIME_ASSET_BYTES) {
772
+ throw new Error(`runtime assets exceed ${MAX_RUNTIME_ASSET_BYTES} bytes`);
773
+ }
774
+ if (typeof entry.sha256 !== "string" || !SHA256.test(entry.sha256)) {
775
+ throw new Error(`runtime asset manifest sha256 is invalid: ${entryPath}`);
776
+ }
777
+ hasAppleContainer ||= entryPath.startsWith("apple-container/");
778
+ hasKernel ||= entryPath.startsWith("kernel/");
779
+ hasHostAssets ||= entryPath.startsWith("host-assets/");
780
+ entries.push({
781
+ path: entryPath,
782
+ type: "file",
783
+ mode: entry.mode,
784
+ size: entry.size as number,
785
+ sha256: entry.sha256,
786
+ });
787
+ }
788
+ if (!hasAppleContainer || !hasKernel || !hasHostAssets) {
789
+ throw new Error(
790
+ "runtime asset manifest must include Apple container, guest kernel, and host files",
791
+ );
792
+ }
793
+ const entryPaths = new Set(entries.map((entry) => entry.path));
794
+ for (const required of [
795
+ "apple-container/bin/container",
796
+ "apple-container/bin/container-apiserver",
797
+ ...APPLE_CONTAINER_PLUGINS.map(
798
+ (plugin) =>
799
+ `apple-container/libexec/container/plugins/${plugin}/bin/${plugin}`,
800
+ ),
801
+ "kernel/vmlinux",
802
+ ...STANDARD_IMAGE_ASSETS.map(
803
+ ({ path: relative }) => `host-assets/standard-image/${relative}`,
804
+ ),
805
+ ]) {
806
+ if (!entryPaths.has(required)) {
807
+ throw new Error(`runtime asset manifest is missing ${required}`);
808
+ }
809
+ }
810
+ for (const { path: relative, mode } of STANDARD_IMAGE_ASSETS) {
811
+ const assetPath = `host-assets/standard-image/${relative}`;
812
+ if (entries.find((entry) => entry.path === assetPath)?.mode !== mode) {
813
+ throw new Error(`runtime asset manifest mode is invalid: ${assetPath}`);
814
+ }
815
+ }
816
+ if (runtimeAssetFingerprint(entries) !== object.fingerprint) {
817
+ throw new Error("runtime asset manifest fingerprint is invalid for its entries");
818
+ }
819
+ return { schemaVersion: 1, fingerprint: object.fingerprint, entries };
820
+ }
821
+
822
+ function validateArchivedRuntimeAssets(
823
+ byPath: ReadonlyMap<string, ArchiveEntry>,
824
+ capabilities: RuntimeCapabilityDescriptorV3,
825
+ ): void {
826
+ const manifestEntry = byPath.get(capabilities.bundledRuntime.layout.assetManifest);
827
+ if (
828
+ manifestEntry?.type !== "0" ||
829
+ manifestEntry.data.length > MAX_RUNTIME_ASSET_MANIFEST_BYTES
830
+ ) {
831
+ throw new Error("runtime asset manifest is missing or too large");
832
+ }
833
+ let rawManifest: unknown;
834
+ try {
835
+ rawManifest = JSON.parse(manifestEntry.data.toString("utf8"));
836
+ } catch {
837
+ throw new Error("runtime asset manifest is invalid JSON");
838
+ }
839
+ const manifest = validateRuntimeAssetManifest(
840
+ rawManifest,
841
+ capabilities.bundledRuntime.assetFingerprint,
842
+ );
843
+ const actual: RuntimeAssetManifestEntry[] = [];
844
+ for (const [memberPath, entry] of byPath) {
845
+ if (
846
+ !RUNTIME_ASSET_ROOTS.some(
847
+ (root) =>
848
+ memberPath === `${BUNDLED_RUNTIME_ROOT}/${root}` ||
849
+ memberPath.startsWith(`${BUNDLED_RUNTIME_ROOT}/${root}/`),
850
+ )
851
+ ) {
852
+ continue;
853
+ }
854
+ if (entry.type === "5") continue;
855
+ if (entry.type !== "0") {
856
+ throw new Error(`runtime asset is not a regular file: ${memberPath}`);
857
+ }
858
+ actual.push({
859
+ path: memberPath.slice(`${BUNDLED_RUNTIME_ROOT}/`.length),
860
+ type: "file",
861
+ mode: entry.mode as 0o644 | 0o755,
862
+ size: entry.data.length,
863
+ sha256: createHash("sha256").update(entry.data).digest("hex"),
864
+ });
865
+ }
866
+ actual.sort((left, right) => compareNames(left.path, right.path));
867
+ if (JSON.stringify(actual) !== JSON.stringify(manifest.entries)) {
868
+ throw new Error("runtime asset manifest does not close the archived asset tree");
869
+ }
870
+ validateArchivedBundledSubtreeClosure(byPath, capabilities, manifest);
871
+ }
872
+
873
+ function expectedBundledRuntimeFiles(
874
+ capabilities: RuntimeCapabilityDescriptorV3,
875
+ manifest: RuntimeAssetManifest,
876
+ ): ReadonlySet<string> {
877
+ const bundled = capabilities.bundledRuntime;
878
+ // Every staged payload file is a manifest entry; only the two metadata
879
+ // documents live outside the digest closure.
880
+ return new Set([
881
+ BUNDLED_RUNTIME_CAPABILITY,
882
+ bundled.layout.assetManifest,
883
+ ...manifest.entries.map(
884
+ (entry) => `${bundled.layout.assetSourceRoot}/${entry.path}`,
885
+ ),
886
+ ]);
887
+ }
888
+
889
+ function validateArchivedBundledSubtreeClosure(
890
+ byPath: ReadonlyMap<string, ArchiveEntry>,
891
+ capabilities: RuntimeCapabilityDescriptorV3,
892
+ manifest: RuntimeAssetManifest,
893
+ ): void {
894
+ const expected = expectedBundledRuntimeFiles(capabilities, manifest);
895
+ const actual = new Set<string>();
896
+ for (const [memberPath, entry] of byPath) {
897
+ if (!memberPath.startsWith(`${BUNDLED_RUNTIME_ROOT}/`)) continue;
898
+ if (entry.type === "5") {
899
+ if (![...expected].some((file) => file.startsWith(`${memberPath}/`))) {
900
+ throw new Error(
901
+ `runtime bundled subtree has an unexpected directory: ${memberPath}`,
902
+ );
903
+ }
904
+ continue;
905
+ }
906
+ if (entry.type !== "0") {
907
+ throw new Error(`runtime bundled subtree has an unsafe entry: ${memberPath}`);
908
+ }
909
+ actual.add(memberPath);
910
+ }
911
+ if (
912
+ [...actual].sort(compareNames).join("\0") !==
913
+ [...expected].sort(compareNames).join("\0")
914
+ ) {
915
+ throw new Error("runtime bundled subtree is not closed by its descriptor and manifest");
916
+ }
917
+ }
918
+
919
+ function validateRequirements(platform: string, value: Readonly<Record<string, string>>): void {
920
+ const [os, arch] = platform.split("-");
921
+ if (value.os !== os || value.arch !== arch) {
922
+ throw new Error("runtime descriptor requirements do not match its platform");
923
+ }
924
+ const keys = Object.keys(value).sort().join("\0");
925
+ if (os === "darwin") {
926
+ if (keys !== ["arch", "minVersion", "os"].join("\0") || value.minVersion !== "13.5") {
927
+ throw new Error("runtime descriptor macOS requirements are invalid");
928
+ }
929
+ } else if (
930
+ os !== "linux" ||
931
+ keys !== ["arch", "libc", "minKernel", "minLibcVersion", "os"].join("\0") ||
932
+ value.libc !== "glibc" ||
933
+ value.minKernel !== "4.18" ||
934
+ value.minLibcVersion !== "2.29"
935
+ ) {
936
+ throw new Error("runtime descriptor Linux requirements are invalid");
937
+ }
938
+ }
939
+
940
+ function strictObject(
941
+ value: unknown,
942
+ expectedKeys: readonly string[],
943
+ label: string,
944
+ ): Record<string, unknown> {
945
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
946
+ throw new Error(`${label} must be an object`);
947
+ }
948
+ const object = value as Record<string, unknown>;
949
+ if (Object.getPrototypeOf(object) !== Object.prototype) {
950
+ throw new Error(`${label} must be a plain object`);
951
+ }
952
+ if (Object.keys(object).sort().join("\0") !== [...expectedKeys].sort().join("\0")) {
953
+ throw new Error(`${label} fields are invalid`);
954
+ }
955
+ return object;
956
+ }
957
+
958
+ function strictStringObject(value: unknown, label: string): Readonly<Record<string, string>> {
959
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
960
+ throw new Error(`${label} must be an object`);
961
+ }
962
+ const output: Record<string, string> = {};
963
+ for (const [key, item] of Object.entries(value)) {
964
+ if (typeof item !== "string") throw new Error(`${label}.${key} must be a string`);
965
+ output[key] = item;
966
+ }
967
+ return output;
968
+ }
969
+
970
+ function assertCanonicalGzipHeader(bytes: Buffer): void {
971
+ if (
972
+ bytes.length < 18 ||
973
+ bytes[0] !== 0x1f ||
974
+ bytes[1] !== 0x8b ||
975
+ bytes[2] !== 8 ||
976
+ bytes[3] !== 0 ||
977
+ bytes.readUInt32LE(4) !== 0 ||
978
+ bytes[8] !== 2 ||
979
+ bytes[9] !== 255
980
+ ) {
981
+ throw new Error("runtime archive gzip header is not canonical");
982
+ }
983
+ }
984
+
985
+ function validateHeaderChecksum(header: Buffer): void {
986
+ const expected = parseOctal(header, 148, 8, "checksum");
987
+ const copy = Buffer.from(header);
988
+ copy.fill(0x20, 148, 156);
989
+ const actual = copy.reduce((sum, byte) => sum + byte, 0);
990
+ if (actual !== expected) throw new Error("runtime archive header checksum mismatch");
991
+ }
992
+
993
+ function parseString(block: Buffer, offset: number, length: number): string {
994
+ const end = block.indexOf(0, offset);
995
+ const stop = end === -1 || end > offset + length ? offset + length : end;
996
+ const bytes = block.subarray(offset, stop);
997
+ const decoded = bytes.toString("utf8");
998
+ if (!Buffer.from(decoded, "utf8").equals(bytes)) {
999
+ throw new Error("runtime archive header contains invalid UTF-8");
1000
+ }
1001
+ return decoded;
1002
+ }
1003
+
1004
+ function parseOctal(block: Buffer, offset: number, length: number, label: string): number {
1005
+ const raw = parseString(block, offset, length).trim();
1006
+ if (!/^[0-7]+$/.test(raw)) throw new Error(`runtime archive has invalid ${label}`);
1007
+ const value = Number.parseInt(raw, 8);
1008
+ if (!Number.isSafeInteger(value)) throw new Error(`runtime archive has invalid ${label}`);
1009
+ return value;
1010
+ }
1011
+
1012
+ function parsePax(body: Buffer): { path?: string; linkpath?: string } {
1013
+ const values: { path?: string; linkpath?: string } = {};
1014
+ let offset = 0;
1015
+ while (offset < body.length) {
1016
+ const space = body.indexOf(0x20, offset);
1017
+ if (space === -1) throw new Error("runtime archive pax record has no length");
1018
+ const rawLength = body.subarray(offset, space).toString("ascii");
1019
+ if (!/^[1-9]\d*$/.test(rawLength)) throw new Error("runtime archive pax length is invalid");
1020
+ const length = Number.parseInt(rawLength, 10);
1021
+ if (!Number.isSafeInteger(length) || offset + length > body.length) {
1022
+ throw new Error("runtime archive pax boundary is invalid");
1023
+ }
1024
+ const record = body.subarray(space + 1, offset + length);
1025
+ if (record.at(-1) !== 0x0a) throw new Error("runtime archive pax terminator is invalid");
1026
+ const text = record.subarray(0, -1).toString("utf8");
1027
+ const equals = text.indexOf("=");
1028
+ if (equals <= 0) throw new Error("runtime archive pax record is invalid");
1029
+ const key = text.slice(0, equals);
1030
+ const value = text.slice(equals + 1);
1031
+ if ((key !== "path" && key !== "linkpath") || Object.hasOwn(values, key)) {
1032
+ throw new Error(`unsupported or duplicate runtime archive pax key: ${key}`);
1033
+ }
1034
+ values[key] = value;
1035
+ offset += length;
1036
+ }
1037
+ return values;
1038
+ }
1039
+
1040
+ function safeMemberPath(value: string): string {
1041
+ const normalizedValue = value.endsWith("/") ? value.slice(0, -1) : value;
1042
+ if (
1043
+ !normalizedValue ||
1044
+ Buffer.byteLength(normalizedValue) > MAX_MEMBER_PATH_BYTES ||
1045
+ normalizedValue.startsWith("/") ||
1046
+ /[\x00-\x1f\x7f]/.test(normalizedValue) ||
1047
+ normalizedValue
1048
+ .split("/")
1049
+ .some((part) => !part || part === "." || part === "..") ||
1050
+ path.posix.normalize(normalizedValue) !== normalizedValue
1051
+ ) {
1052
+ throw new Error(`unsafe runtime archive member path: ${JSON.stringify(value)}`);
1053
+ }
1054
+ return normalizedValue;
1055
+ }
1056
+
1057
+ function safeLinkTarget(memberPath: string, target: string): string {
1058
+ if (
1059
+ !target ||
1060
+ Buffer.byteLength(target) > MAX_MEMBER_PATH_BYTES ||
1061
+ target.startsWith("/") ||
1062
+ /[\x00-\x1f\x7f]/.test(target)
1063
+ ) {
1064
+ throw new Error(`unsafe runtime archive symlink target: ${JSON.stringify(target)}`);
1065
+ }
1066
+ const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(memberPath), target));
1067
+ if (resolved === ".." || resolved.startsWith("../") || resolved.startsWith("/")) {
1068
+ throw new Error(`runtime archive symlink escapes the payload: ${memberPath}`);
1069
+ }
1070
+ return resolved;
1071
+ }
1072
+
1073
+ async function syncDirectory(directory: string): Promise<void> {
1074
+ let handle;
1075
+ try {
1076
+ handle = await open(directory, fsConstants.O_RDONLY);
1077
+ await handle.sync();
1078
+ } catch (error) {
1079
+ const code = (error as NodeJS.ErrnoException).code;
1080
+ if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR") {
1081
+ throw error;
1082
+ }
1083
+ } finally {
1084
+ await handle?.close();
1085
+ }
1086
+ }