@codyswann/lisa 4.70.4 → 4.71.1

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 (139) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
  3. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
  4. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
  5. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
  6. package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
  22. package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
  23. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
  24. package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
  25. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
  26. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
  27. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
  28. package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
  29. package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
  30. package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
  31. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +300 -0
  32. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
  33. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
  34. package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
  35. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
  36. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
  37. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
  38. package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
  39. package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
  40. package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
  41. package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
  42. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
  43. package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
  44. package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
  45. package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
  46. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +254 -0
  47. package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
  48. package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
  49. package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
  50. package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
  51. package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
  52. package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
  53. package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
  54. package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
  55. package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
  56. package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
  57. package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
  58. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
  59. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
  60. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
  61. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
  62. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
  63. package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
  64. package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
  65. package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
  66. package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
  67. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
  68. package/dist/codex/scripts/block-no-verify.sh +89 -5
  69. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  70. package/dist/core/lisa-owned-hash-ledger.js +604 -0
  71. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  72. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  73. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  74. package/dist/core/upstream-evidence-manifest.js +159 -11
  75. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  76. package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
  77. package/package.json +4 -4
  78. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
  81. package/plugins/lisa/hooks/block-no-verify.sh +89 -5
  82. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
  83. package/plugins/lisa-agy/plugin.json +1 -1
  84. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  87. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
  91. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
  93. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-agy/plugin.json +1 -1
  96. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  111. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  116. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-rails-agy/plugin.json +1 -1
  121. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  126. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  130. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  131. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  133. package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
  134. package/plugins/src/base/hooks/block-no-verify.sh +89 -5
  135. package/scripts/check-shipped-surface-removals.mjs +96 -24
  136. package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
  137. package/scripts/two-channel-couplings.json +31 -7
  138. package/typescript/copy-contents/.husky/commit-msg +18 -0
  139. package/typescript/copy-overwrite/.prettierignore +2 -0
@@ -0,0 +1,221 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Public immutable archives are checked before the trusted daemon loads them. @module npm-updater */
5
+ import { openSync, closeSync, writeSync, fsyncSync, constants } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { required, keys } from "./npm-update-contract.mjs";
8
+ import { inspectRuntimeArchive } from "./npm-update-runtime-archive.mjs";
9
+ import { dockerClient } from "./npm-update-worker-lifecycle.mjs";
10
+ import { namespaceRecords } from "./npm-update-orchestrator.mjs";
11
+
12
+ /** Only exact official public assets grant archive transport authority. */
13
+ export function validateArchiveTransport(archive, platform) {
14
+ keys(archive, ["url", "sha256", "bytes", "uncompressedBytes", "config"]);
15
+ required(
16
+ typeof archive.sha256 === "string" &&
17
+ /^[a-f0-9]{64}$/.test(archive.sha256) &&
18
+ typeof archive.config === "string" &&
19
+ /^sha256:[a-f0-9]{64}$/.test(archive.config),
20
+ "runtime archive/config identity is unresolved"
21
+ );
22
+ required(
23
+ Number.isSafeInteger(archive.bytes) &&
24
+ archive.bytes > 0 &&
25
+ archive.bytes < 1_000_000_000 &&
26
+ Number.isSafeInteger(archive.uncompressedBytes) &&
27
+ archive.uncompressedBytes > 0 &&
28
+ archive.uncompressedBytes < 1_500_000_000,
29
+ "runtime archive exceeds supported asset bounds"
30
+ );
31
+ required(
32
+ ["linux/arm64", "linux/amd64"].includes(platform),
33
+ "unsupported archive platform"
34
+ );
35
+ const url = new URL(archive.url);
36
+ const path = new RegExp(
37
+ `^/CodySwannGT/lisa/releases/download/v[0-9]+\\.[0-9]+\\.[0-9]+/npm-updater-gate-${platform.replace("/", "-")}-${archive.sha256}\\.tar\\.gz$`
38
+ );
39
+ required(
40
+ url.protocol === "https:" &&
41
+ url.hostname === "github.com" &&
42
+ !url.port &&
43
+ !url.username &&
44
+ !url.password &&
45
+ !url.search &&
46
+ !url.hash &&
47
+ path.test(url.pathname),
48
+ "runtime archive is not an immutable official public asset"
49
+ );
50
+ return archive;
51
+ }
52
+
53
+ /** Redirects stay within GitHub's public asset services, with no authorization headers. */
54
+ async function publicResponse(initialUrl, signal) {
55
+ let url = initialUrl;
56
+ for (let redirects = 0; redirects <= 3; redirects++) {
57
+ const parsed = new URL(url);
58
+ required(
59
+ parsed.protocol === "https:" &&
60
+ ["github.com", "release-assets.githubusercontent.com"].includes(
61
+ parsed.hostname
62
+ ) &&
63
+ !parsed.username &&
64
+ !parsed.password &&
65
+ !parsed.port,
66
+ "runtime asset redirect is foreign"
67
+ );
68
+ const response = await fetch(url, {
69
+ redirect: "manual",
70
+ signal,
71
+ headers: { Accept: "application/octet-stream" },
72
+ });
73
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
74
+ const next = response.headers.get("location");
75
+ await response.body?.cancel();
76
+ required(next, "runtime asset redirect is missing");
77
+ url = new URL(next, url).href;
78
+ } else {
79
+ if (!response.ok || response.status !== 200 || !response.body) {
80
+ await response.body?.cancel();
81
+ required(false, "anonymous runtime asset download failed");
82
+ }
83
+ return response;
84
+ }
85
+ }
86
+ throw Error("runtime asset redirect limit exceeded");
87
+ }
88
+
89
+ /** Only pinned bytes enter fresh private storage, with a bounded total deadline. */
90
+ async function writeDownload(file, response, expectedBytes, signal) {
91
+ const reader = response.body.getReader();
92
+ let fd;
93
+ let length = 0;
94
+ let expired = false;
95
+ const deadline = () => {
96
+ expired = true;
97
+ reader.cancel().catch(() => {});
98
+ };
99
+ const timer = setTimeout(deadline, 180_000);
100
+ signal.addEventListener("abort", deadline, { once: true });
101
+ try {
102
+ required(!signal.aborted, "runtime asset deadline exceeded");
103
+ fd = openSync(
104
+ file,
105
+ constants.O_CREAT |
106
+ constants.O_EXCL |
107
+ constants.O_WRONLY |
108
+ constants.O_NOFOLLOW,
109
+ 0o600
110
+ );
111
+ while (true) {
112
+ const { done, value } = await reader.read();
113
+ if (done) break;
114
+ length += value.length;
115
+ required(
116
+ length <= expectedBytes,
117
+ "runtime asset expanded beyond its byte bound"
118
+ );
119
+ let offset = 0;
120
+ while (offset < value.length) {
121
+ const count = writeSync(fd, value, offset);
122
+ required(count > 0, "runtime asset write failed");
123
+ offset += count;
124
+ }
125
+ }
126
+ required(!expired && !signal.aborted, "runtime asset deadline exceeded");
127
+ required(length === expectedBytes, "runtime asset download was incomplete");
128
+ fsyncSync(fd);
129
+ } finally {
130
+ clearTimeout(timer);
131
+ signal.removeEventListener("abort", deadline);
132
+ await reader.cancel().catch(() => {});
133
+ reader.releaseLock();
134
+ if (fd !== undefined) closeSync(fd);
135
+ }
136
+ }
137
+
138
+ /** No daemon load can consume an archive before whole-byte and graph verification. */
139
+ export async function downloadRuntimeArchive(root, entry) {
140
+ const archive = validateArchiveTransport(entry.archive, entry.platform);
141
+ required(
142
+ namespaceRecords(root).length === 0,
143
+ "runtime download requires completed worker recovery"
144
+ );
145
+ const file = join(root, `runtime-${archive.sha256}.tar.gz`);
146
+ const signal = AbortSignal.timeout(180_000);
147
+ const response = await publicResponse(archive.url, signal);
148
+ try {
149
+ const declared = response.headers.get("content-length");
150
+ required(
151
+ declared === null || declared === String(archive.bytes),
152
+ "runtime asset declared byte length differs"
153
+ );
154
+ await writeDownload(file, response, archive.bytes, signal);
155
+ } finally {
156
+ await response.body.cancel().catch(() => {});
157
+ }
158
+ await inspectRuntimeArchive(file, {
159
+ ...archive,
160
+ image: entry.image,
161
+ platform: entry.platform,
162
+ });
163
+ return file;
164
+ }
165
+
166
+ /** Loading tagless data must preserve all foreign image names and bind the actual index. */
167
+ export async function loadRuntimeImage(boundary, entry, file) {
168
+ validateArchiveTransport(entry.archive, entry.platform);
169
+ required(
170
+ namespaceRecords(boundary.root).length === 0,
171
+ "runtime load requires completed worker recovery"
172
+ );
173
+ await inspectRuntimeArchive(file, {
174
+ ...entry.archive,
175
+ image: entry.image,
176
+ platform: entry.platform,
177
+ });
178
+ const docker = dockerClient(boundary);
179
+ const census = async () =>
180
+ (
181
+ await docker([
182
+ "image",
183
+ "ls",
184
+ "--no-trunc",
185
+ "--format",
186
+ "{{.ID}} {{.Repository}}:{{.Tag}}",
187
+ ])
188
+ ).stdout
189
+ .toString()
190
+ .trim()
191
+ .split("\n")
192
+ .filter(Boolean)
193
+ .sort();
194
+ const before = await census();
195
+ await docker(["image", "load", "--input", file], { timeout: 180_000 });
196
+ const values = JSON.parse(
197
+ (await docker(["image", "inspect", entry.image])).stdout.toString()
198
+ );
199
+ required(
200
+ Array.isArray(values) &&
201
+ values.length === 1 &&
202
+ values[0].Id === entry.image &&
203
+ values[0].Descriptor?.digest === entry.image &&
204
+ `${values[0].Os}/${values[0].Architecture}` === entry.platform,
205
+ "loaded runtime index/platform differs"
206
+ );
207
+ const after = await census();
208
+ required(
209
+ before.every(value => after.includes(value)) &&
210
+ after.every(
211
+ value =>
212
+ before.includes(value) || value === `${entry.image} <none>:<none>`
213
+ ),
214
+ "runtime load changed foreign image/tag census"
215
+ );
216
+ return {
217
+ image: entry.image,
218
+ config: entry.archive.config,
219
+ platform: entry.platform,
220
+ };
221
+ }
@@ -0,0 +1,284 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Closed runtime identities make missing platform and process policy actionable refusals. @module npm-updater */
5
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
6
+ import { sha256 } from "./github-attestation-verifier.mjs";
7
+ import { required, keys } from "./npm-update-contract.mjs";
8
+ import { validateArchiveTransport } from "./npm-update-runtime-transport.mjs";
9
+
10
+ const HEX = /^[a-f0-9]{64}$/;
11
+ const TOOLS = [
12
+ "node",
13
+ "npm",
14
+ "git",
15
+ "shell",
16
+ "bash",
17
+ "bun",
18
+ "gh",
19
+ "gitleaks",
20
+ "ruby",
21
+ "bundle",
22
+ "timeout",
23
+ "supervisor",
24
+ ];
25
+ const DENIED = new Set([
26
+ "ptrace",
27
+ "process_vm_readv",
28
+ "process_vm_writev",
29
+ "mount",
30
+ "umount2",
31
+ "pivot_root",
32
+ "setns",
33
+ "unshare",
34
+ "bpf",
35
+ "userfaultfd",
36
+ "open_by_handle_at",
37
+ ]);
38
+
39
+ /** A manifest's hash cannot make an allowing process-memory profile safe. */
40
+ function securityProfile(seccomp) {
41
+ keys(seccomp, ["sha256", "profile"]);
42
+ const profile = seccomp.profile;
43
+ required(
44
+ typeof seccomp.sha256 === "string" &&
45
+ HEX.test(seccomp.sha256) &&
46
+ sha256(`${canonicalJson(profile)}\n`) === seccomp.sha256,
47
+ "runtime kernel policy bytes differ"
48
+ );
49
+ required(
50
+ profile.defaultAction === "SCMP_ACT_ERRNO" &&
51
+ Array.isArray(profile.syscalls) &&
52
+ profile.syscalls.length > 0 &&
53
+ profile.syscalls.length <= 512,
54
+ "runtime kernel policy is not fail closed"
55
+ );
56
+ required(
57
+ Object.keys(profile).every(name =>
58
+ ["defaultAction", "defaultErrnoRet", "archMap", "syscalls"].includes(name)
59
+ ) &&
60
+ (profile.defaultErrnoRet === undefined || profile.defaultErrnoRet === 1),
61
+ "runtime kernel policy contains foreign authority"
62
+ );
63
+ if (profile.archMap !== undefined) {
64
+ required(
65
+ Array.isArray(profile.archMap) && profile.archMap.length <= 16,
66
+ "unbounded runtime architecture map"
67
+ );
68
+ for (const mapping of profile.archMap) {
69
+ keys(mapping, ["architecture", "subArchitectures"]);
70
+ required(
71
+ typeof mapping.architecture === "string" &&
72
+ /^SCMP_ARCH_[A-Z0-9_]+$/.test(mapping.architecture) &&
73
+ (mapping.subArchitectures === null ||
74
+ (Array.isArray(mapping.subArchitectures) &&
75
+ mapping.subArchitectures.length <= 8 &&
76
+ mapping.subArchitectures.every(
77
+ value =>
78
+ typeof value === "string" &&
79
+ /^SCMP_ARCH_[A-Z0-9_]+$/.test(value)
80
+ ))),
81
+ "invalid runtime architecture map"
82
+ );
83
+ }
84
+ }
85
+ for (const rule of profile.syscalls) {
86
+ syscallRule(rule);
87
+ required(
88
+ Array.isArray(rule.names) &&
89
+ rule.names.length > 0 &&
90
+ rule.names.length <= 512 &&
91
+ rule.names.every(
92
+ name => typeof name === "string" && /^[a-z0-9_]+$/.test(name)
93
+ ),
94
+ "invalid runtime syscall inventory"
95
+ );
96
+ required(
97
+ ["SCMP_ACT_ALLOW", "SCMP_ACT_ERRNO"].includes(rule.action),
98
+ "unsupported runtime syscall action"
99
+ );
100
+ required(
101
+ rule.action !== "SCMP_ACT_ALLOW" ||
102
+ rule.names.every(name => !DENIED.has(name)),
103
+ "runtime permits process-memory or namespace authority"
104
+ );
105
+ }
106
+ }
107
+
108
+ /** Only reviewed argument/capability filters can constrain an allowed syscall. */
109
+ function syscallRule(rule) {
110
+ required(
111
+ rule &&
112
+ typeof rule === "object" &&
113
+ !Array.isArray(rule) &&
114
+ Object.keys(rule).every(name =>
115
+ [
116
+ "names",
117
+ "action",
118
+ "args",
119
+ "comment",
120
+ "errnoRet",
121
+ "includes",
122
+ "excludes",
123
+ ].includes(name)
124
+ ),
125
+ "foreign runtime syscall control"
126
+ );
127
+ required(
128
+ rule.errnoRet === undefined ||
129
+ (Number.isSafeInteger(rule.errnoRet) &&
130
+ rule.errnoRet > 0 &&
131
+ rule.errnoRet <= 4095),
132
+ "invalid runtime syscall errno"
133
+ );
134
+ if (rule.args !== undefined) {
135
+ required(
136
+ Array.isArray(rule.args) && rule.args.length <= 6,
137
+ "unbounded runtime syscall argument policy"
138
+ );
139
+ for (const argument of rule.args) {
140
+ keys(argument, ["index", "op", "value"]);
141
+ required(
142
+ Number.isSafeInteger(argument.index) &&
143
+ argument.index >= 0 &&
144
+ argument.index <= 5 &&
145
+ ["SCMP_CMP_EQ", "SCMP_CMP_NE", "SCMP_CMP_MASKED_EQ"].includes(
146
+ argument.op
147
+ ) &&
148
+ Number.isSafeInteger(argument.value) &&
149
+ argument.value >= 0,
150
+ "invalid runtime syscall argument policy"
151
+ );
152
+ }
153
+ }
154
+ syscallFilters(rule);
155
+ }
156
+
157
+ /** Capability filters are closed independently of syscall argument predicates. */
158
+ function syscallFilters(rule) {
159
+ for (const name of ["includes", "excludes"])
160
+ if (rule[name] !== undefined) {
161
+ const filter = rule[name];
162
+ required(
163
+ filter &&
164
+ typeof filter === "object" &&
165
+ !Array.isArray(filter) &&
166
+ Object.keys(filter).every(key =>
167
+ ["arches", "caps", "minKernel"].includes(key)
168
+ ),
169
+ "foreign runtime syscall filter"
170
+ );
171
+ for (const key of ["arches", "caps"])
172
+ if (filter[key] !== undefined)
173
+ required(
174
+ Array.isArray(filter[key]) &&
175
+ filter[key].length <= 64 &&
176
+ filter[key].every(
177
+ value => typeof value === "string" && /^\w+$/.test(value)
178
+ ),
179
+ "invalid runtime syscall filter"
180
+ );
181
+ required(
182
+ filter.minKernel === undefined ||
183
+ (typeof filter.minKernel === "string" &&
184
+ /^\d+\.\d+(?:\.\d+)?$/.test(filter.minKernel)),
185
+ "invalid runtime kernel bound"
186
+ );
187
+ }
188
+ }
189
+
190
+ /** Typed tool records describe required identities, not evidence that an image ran. */
191
+ export function toolIdentity(tools) {
192
+ keys(tools, TOOLS);
193
+ for (const name of TOOLS) {
194
+ const tool = tools[name];
195
+ keys(tool, ["path", "version", "sha256"]);
196
+ required(
197
+ typeof tool.path === "string" &&
198
+ /^\/(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+$/.test(tool.path) &&
199
+ !tool.path.split("/").includes(".."),
200
+ "invalid qualified tool path"
201
+ );
202
+ required(
203
+ typeof tool.version === "string" &&
204
+ tool.version.length > 0 &&
205
+ tool.version.length <= 128 &&
206
+ typeof tool.sha256 === "string" &&
207
+ HEX.test(tool.sha256),
208
+ "unqualified runtime tool identity"
209
+ );
210
+ }
211
+ required(
212
+ tools.node.version === "22.23.3" && tools.npm.version === "11.21.0",
213
+ "unsupported runtime Node/npm identity"
214
+ );
215
+ }
216
+
217
+ /** A fixed workload service grants no arbitrary Docker or production credential configuration. */
218
+ function serviceProfiles(services) {
219
+ required(
220
+ Array.isArray(services) && services.length <= 2,
221
+ "runtime service inventory exceeds bound"
222
+ );
223
+ for (const service of services) {
224
+ keys(service, ["profile", "platform", "image", "port"]);
225
+ required(
226
+ service.profile === "mysql" &&
227
+ ["linux/arm64", "linux/amd64"].includes(service.platform) &&
228
+ service.port === 3306 &&
229
+ typeof service.image === "string" &&
230
+ /^sha256:[a-f0-9]{64}$/.test(service.image),
231
+ "unsupported or unqualified runtime service"
232
+ );
233
+ }
234
+ required(
235
+ new Set(services.map(service => service.platform)).size === services.length,
236
+ "ambiguous runtime service identity"
237
+ );
238
+ }
239
+
240
+ /** Closed immutable identities refuse a missing platform; native execution is never a fallback. */
241
+ export function validateRuntime(value, platform) {
242
+ keys(value, [
243
+ "version",
244
+ "recipeSha256",
245
+ "supervisorSha256",
246
+ "platforms",
247
+ "seccomp",
248
+ "services",
249
+ ]);
250
+ required(
251
+ value.version === 1 &&
252
+ [value.recipeSha256, value.supervisorSha256].every(
253
+ hash => typeof hash === "string" && HEX.test(hash)
254
+ ),
255
+ "unqualified runtime build recipe or supervisor"
256
+ );
257
+ securityProfile(value.seccomp);
258
+ serviceProfiles(value.services);
259
+ required(
260
+ Array.isArray(value.platforms) &&
261
+ value.platforms.length > 0 &&
262
+ value.platforms.length <= 2,
263
+ "runtime platform identity is unavailable"
264
+ );
265
+ for (const entry of value.platforms) {
266
+ keys(entry, ["platform", "image", "tools", "archive"]);
267
+ required(
268
+ ["linux/arm64", "linux/amd64"].includes(entry.platform) &&
269
+ typeof entry.image === "string" &&
270
+ /^sha256:[a-f0-9]{64}$/.test(entry.image),
271
+ "unqualified platform image"
272
+ );
273
+ toolIdentity(entry.tools);
274
+ validateArchiveTransport(entry.archive, entry.platform);
275
+ }
276
+ const matching = value.platforms.filter(entry => entry.platform === platform);
277
+ required(
278
+ matching.length === 1 &&
279
+ new Set(value.platforms.map(entry => entry.platform)).size ===
280
+ value.platforms.length,
281
+ "runtime platform is absent or ambiguous"
282
+ );
283
+ return matching[0];
284
+ }