@bitkyc08/opencodex 2.11.1 → 2.12.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 (175) hide show
  1. package/README.md +1 -1
  2. package/bin/ocx.mjs +4 -0
  3. package/gui/dist/assets/index-BNVYzdn0.css +1 -0
  4. package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/command-code.ts +94 -5
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-tool-schema.ts +80 -15
  10. package/src/adapters/google.ts +24 -7
  11. package/src/adapters/openai-chat.ts +233 -305
  12. package/src/adapters/openai-responses.ts +62 -0
  13. package/src/adapters/tool-catalog-nudge.ts +2 -11
  14. package/src/claude/outbound.ts +42 -14
  15. package/src/cli/claude.ts +61 -9
  16. package/src/cli/codex-shim-autorestore.ts +2 -0
  17. package/src/cli/codex-shim-readiness.ts +8 -1
  18. package/src/cli/config-command.ts +4 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +15 -0
  21. package/src/cli/index.ts +27 -11
  22. package/src/cli/lab.ts +367 -0
  23. package/src/cli/provider.ts +9 -1
  24. package/src/clients/config-export.ts +100 -3
  25. package/src/codex/app-server-processes.ts +11 -8
  26. package/src/codex/auth-api.ts +92 -28
  27. package/src/codex/auth-collision.ts +5 -3
  28. package/src/codex/catalog/metadata.ts +7 -3
  29. package/src/codex/catalog/provider-fetch.ts +191 -47
  30. package/src/codex/catalog.ts +2 -2
  31. package/src/codex/history-job.ts +83 -18
  32. package/src/codex/history-migration-guardian.ts +9 -16
  33. package/src/codex/history-provider.ts +178 -1
  34. package/src/codex/history-transition.ts +5 -5
  35. package/src/codex/history-worker.ts +22 -2
  36. package/src/codex/native-profile-store.ts +1 -1
  37. package/src/codex/plan.ts +15 -0
  38. package/src/codex/quota.ts +6 -6
  39. package/src/codex/routing.ts +3 -3
  40. package/src/codex/subagent-model-fallback.ts +246 -22
  41. package/src/codex/user-identity.ts +63 -12
  42. package/src/config.ts +265 -32
  43. package/src/images/loop.ts +3 -1
  44. package/src/images/plan.ts +23 -13
  45. package/src/integrations/omp-yaml-source.ts +225 -0
  46. package/src/integrations/ownership.ts +5 -5
  47. package/src/integrations/state.ts +45 -6
  48. package/src/integrations/writer.ts +38 -3
  49. package/src/lab/artifacts/sanitize.ts +586 -0
  50. package/src/lab/artifacts/secure-fs.ts +475 -0
  51. package/src/lab/artifacts/store.ts +310 -0
  52. package/src/lab/conformance/assertion.ts +325 -0
  53. package/src/lab/conformance/digest.ts +22 -0
  54. package/src/lab/conformance/executor.ts +741 -0
  55. package/src/lab/conformance/fixture-provider.ts +27 -0
  56. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  57. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  58. package/src/lab/conformance/harness-budget.ts +47 -0
  59. package/src/lab/conformance/index.ts +5 -0
  60. package/src/lab/conformance/jcs.ts +24 -0
  61. package/src/lab/conformance/json-pointer.ts +39 -0
  62. package/src/lab/conformance/manifest.ts +180 -0
  63. package/src/lab/conformance/mcp-stub.ts +179 -0
  64. package/src/lab/conformance/negative-controls.ts +160 -0
  65. package/src/lab/conformance/observation.ts +355 -0
  66. package/src/lab/conformance/runner.ts +57 -0
  67. package/src/lab/conformance/sse-normalize.ts +59 -0
  68. package/src/lab/conformance/suite-manifest.ts +78 -0
  69. package/src/lab/conformance/types.ts +191 -0
  70. package/src/lab/constants.ts +126 -0
  71. package/src/lab/digest.ts +64 -0
  72. package/src/lab/events/errors.ts +9 -0
  73. package/src/lab/events/limits.ts +117 -0
  74. package/src/lab/events/types.ts +228 -0
  75. package/src/lab/events/validate.ts +780 -0
  76. package/src/lab/index.ts +36 -0
  77. package/src/lab/ledger/artifact-refs.ts +127 -0
  78. package/src/lab/ledger/invalidation.ts +136 -0
  79. package/src/lab/ledger/purge.ts +241 -0
  80. package/src/lab/ledger/store.ts +263 -0
  81. package/src/lab/live/credential-lease.ts +53 -0
  82. package/src/lab/live/destination.ts +155 -0
  83. package/src/lab/live/executor.ts +311 -0
  84. package/src/lab/live/inert-tools.ts +56 -0
  85. package/src/lab/live/manifest.ts +85 -0
  86. package/src/lab/live/mcp-loopback.ts +57 -0
  87. package/src/lab/live/runner.ts +19 -0
  88. package/src/lab/live/sandbox.ts +61 -0
  89. package/src/lab/live/suite-manifest.ts +41 -0
  90. package/src/lab/live/transport.ts +116 -0
  91. package/src/lab/live/types.ts +197 -0
  92. package/src/lab/observe/from-conformance.ts +344 -0
  93. package/src/lab/observe/from-live.ts +103 -0
  94. package/src/lab/paths.ts +65 -0
  95. package/src/lab/projection/rebuild.ts +477 -0
  96. package/src/lab/projection/schema.ts +122 -0
  97. package/src/lab/projection/verdicts.ts +438 -0
  98. package/src/lab/projection/verification.ts +332 -0
  99. package/src/lab/query/catalog.ts +72 -0
  100. package/src/lab/query/connection.ts +107 -0
  101. package/src/lab/query/constants.ts +4 -0
  102. package/src/lab/query/cursor.ts +132 -0
  103. package/src/lab/query/dto-map.ts +277 -0
  104. package/src/lab/query/errors.ts +22 -0
  105. package/src/lab/query/index.ts +34 -0
  106. package/src/lab/query/queries.ts +444 -0
  107. package/src/lab/query/types.ts +266 -0
  108. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  109. package/src/lab/subject/installation-salt.ts +81 -0
  110. package/src/lab/subject/route-subject.ts +73 -0
  111. package/src/lib/bounded-body.ts +104 -0
  112. package/src/lib/lab-live-execution-authority.ts +13 -0
  113. package/src/lib/lab-live-host.ts +30 -0
  114. package/src/lib/lab-live-pinned-sender.ts +45 -0
  115. package/src/lib/pinned-http.ts +36 -7
  116. package/src/oauth/index.ts +20 -1
  117. package/src/oauth/local-token-detect.ts +11 -2
  118. package/src/oauth/login-cli.ts +38 -4
  119. package/src/providers/alibaba-region-migration.ts +1 -1
  120. package/src/providers/codex-capacity.ts +7 -4
  121. package/src/providers/command-code-efforts.ts +5 -2
  122. package/src/providers/derive.ts +105 -0
  123. package/src/providers/label.ts +1 -1
  124. package/src/providers/openai-tiers.ts +59 -3
  125. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  126. package/src/providers/quota.ts +67 -21
  127. package/src/providers/registry.ts +118 -5
  128. package/src/router.ts +15 -1
  129. package/src/routing/analytics.ts +8 -7
  130. package/src/routing/evaluator.ts +51 -5
  131. package/src/routing/profile.ts +10 -1
  132. package/src/routing/trace.ts +42 -6
  133. package/src/server/auth-cors.ts +15 -0
  134. package/src/server/background-lifecycle.ts +169 -0
  135. package/src/server/chat-completions.ts +18 -3
  136. package/src/server/claude-messages.ts +1 -0
  137. package/src/server/images.ts +78 -11
  138. package/src/server/index.ts +72 -18
  139. package/src/server/lifecycle.ts +6 -0
  140. package/src/server/live.ts +5 -1
  141. package/src/server/management/agent-settings-routes.ts +41 -24
  142. package/src/server/management/body.ts +6 -0
  143. package/src/server/management/config-routes.ts +51 -8
  144. package/src/server/management/lab-routes.ts +388 -0
  145. package/src/server/management/logs-usage-routes.ts +19 -1
  146. package/src/server/management/model-rows.ts +25 -11
  147. package/src/server/management/native-integration-routes.ts +3 -3
  148. package/src/server/management/provider-routes.ts +5 -0
  149. package/src/server/management/shared.ts +8 -1
  150. package/src/server/management/usage-summary-cache.ts +2 -0
  151. package/src/server/management/vision-sidecar-options.ts +125 -0
  152. package/src/server/management-api.ts +2 -0
  153. package/src/server/request-decompress.ts +106 -5
  154. package/src/server/responses/collaboration.ts +4 -1
  155. package/src/server/responses/core.ts +117 -31
  156. package/src/server/responses-json-events.ts +50 -12
  157. package/src/server/search.ts +23 -7
  158. package/src/server/ws-bridge.ts +3 -1
  159. package/src/service.ts +40 -20
  160. package/src/types.ts +74 -0
  161. package/src/update/job.ts +10 -2
  162. package/src/usage/cost.ts +0 -0
  163. package/src/usage/expected-prices.ts +9 -0
  164. package/src/usage/log.ts +9 -9
  165. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  166. package/src/usage/user-cost-overlays.ts +314 -0
  167. package/src/vision/describe.ts +2 -2
  168. package/src/vision/eligibility.ts +229 -0
  169. package/src/vision/index.ts +20 -2
  170. package/src/web-search/executor.ts +2 -2
  171. package/src/web-search/index.ts +4 -2
  172. package/src/web-search/loop.ts +3 -1
  173. package/src/web-search/parse.ts +72 -3
  174. package/gui/dist/assets/index-BA1dgo4q.css +0 -1
  175. package/gui/dist/assets/index-DDZpgzKk.js +0 -70
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Descriptor/handle-bound, no-follow artifact I/O for the Compatibility Lab store.
3
+ *
4
+ * POSIX runtimes use directory-relative `dir` opens. Windows uses the same pinned
5
+ * directory identity checks as other reviewed OpenCodex bounded readers, because
6
+ * directory-relative child opens are not durable there.
7
+ */
8
+ import {
9
+ closeSync,
10
+ constants as fsConstants,
11
+ existsSync,
12
+ fstatSync,
13
+ fsyncSync,
14
+ lstatSync,
15
+ mkdirSync,
16
+ openSync,
17
+ readSync,
18
+ renameSync,
19
+ unlinkSync,
20
+ writeSync,
21
+ type Stats,
22
+ } from "node:fs";
23
+ import { join } from "node:path";
24
+ import {
25
+ ARTIFACT_FILENAME_EXT,
26
+ MAX_BYTES_PER_ARTIFACT,
27
+ } from "../constants";
28
+ import { artifactBytesDigest, isSha256Hex } from "../digest";
29
+
30
+ export class ArtifactFsError extends Error {
31
+ readonly code: string;
32
+ constructor(code: string, message: string) {
33
+ super(message);
34
+ this.name = "ArtifactFsError";
35
+ this.code = code;
36
+ }
37
+ }
38
+
39
+ export function harnessFailure(message: string, code = "harness_failure"): never {
40
+ throw new ArtifactFsError(code, message);
41
+ }
42
+
43
+ const O_RDONLY = fsConstants.O_RDONLY;
44
+ const O_RDWR = fsConstants.O_RDWR;
45
+ const O_CREAT = fsConstants.O_CREAT;
46
+ const O_EXCL = fsConstants.O_EXCL;
47
+ const O_NOFOLLOW = (fsConstants as { O_NOFOLLOW?: number }).O_NOFOLLOW;
48
+ const O_DIRECTORY = (fsConstants as { O_DIRECTORY?: number }).O_DIRECTORY;
49
+
50
+ type ArtifactIoMode = "dirfd" | "win32_pinned";
51
+
52
+ let artifactIoMode: ArtifactIoMode | null = null;
53
+
54
+ export function assertDigestName(digest: string): string {
55
+ if (!isSha256Hex(digest)) harnessFailure("artifact digest must be lowercase sha256 hex");
56
+ if (digest.includes("/") || digest.includes("\\") || digest.includes(":") || digest.includes("..")) {
57
+ harnessFailure("artifact digest must not contain path separators");
58
+ }
59
+ return digest;
60
+ }
61
+
62
+ export function digestFileName(digest: string): string {
63
+ return `${assertDigestName(digest)}${ARTIFACT_FILENAME_EXT}`;
64
+ }
65
+
66
+ function platformSupportsNoFollow(): boolean {
67
+ return typeof O_NOFOLLOW === "number" && O_NOFOLLOW !== 0;
68
+ }
69
+
70
+ function openFlags(base: number, noFollow: boolean): number {
71
+ if (noFollow && platformSupportsNoFollow()) return base | O_NOFOLLOW!;
72
+ return base;
73
+ }
74
+
75
+ function assertRegularFileStats(stats: Stats, label: string): void {
76
+ if (
77
+ !stats.isFile() ||
78
+ stats.isSymbolicLink() ||
79
+ stats.isDirectory() ||
80
+ stats.isFIFO() ||
81
+ stats.isSocket() ||
82
+ stats.isCharacterDevice() ||
83
+ stats.isBlockDevice()
84
+ ) {
85
+ harnessFailure(`${label}: not a regular file`, "artifact_unsafe_target");
86
+ }
87
+ if (stats.nlink !== 1) {
88
+ harnessFailure(`${label}: hard links prohibited (nlink=${stats.nlink})`, "artifact_unsafe_target");
89
+ }
90
+ }
91
+
92
+ function assertDirectoryStats(stats: Stats, label: string): void {
93
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
94
+ harnessFailure(`${label}: must be a real directory (no symlink/reparse redirection)`, "artifact_unsafe_target");
95
+ }
96
+ }
97
+
98
+ function identityOf(stats: Stats): string {
99
+ return `${stats.dev}:${stats.ino}`;
100
+ }
101
+
102
+ function assertRelativeName(name: string): void {
103
+ if (name.includes("..") || name.includes("/") || name.includes("\\") || name.includes("\0")) {
104
+ harnessFailure("invalid relative artifact name", "artifact_unsafe_target");
105
+ }
106
+ }
107
+
108
+ export interface TrustedArtifactDir {
109
+ path: string;
110
+ fd: number;
111
+ identity: string;
112
+ }
113
+
114
+ type OpenSyncWithDir = (
115
+ path: string,
116
+ flags: number,
117
+ mode: number,
118
+ options: { dir: number },
119
+ ) => number;
120
+
121
+ type RenameSyncWithDir = (from: string, to: string, options: { dir: number }) => void;
122
+ type UnlinkSyncWithDir = (path: string, options: { dir: number }) => void;
123
+
124
+ function detectArtifactIoMode(dir: TrustedArtifactDir): ArtifactIoMode {
125
+ if (artifactIoMode !== null) return artifactIoMode;
126
+ if (process.platform === "win32") {
127
+ artifactIoMode = "win32_pinned";
128
+ return artifactIoMode;
129
+ }
130
+ const probe = `.dirfd-probe-${process.pid}`;
131
+ const finalName = `${probe}.ok`;
132
+ try {
133
+ const openWithDir = openSync as unknown as OpenSyncWithDir;
134
+ const fd = openWithDir(probe, O_CREAT | O_EXCL | O_RDWR, 0o600, { dir: dir.fd });
135
+ closeSync(fd);
136
+ (renameSync as unknown as RenameSyncWithDir)(probe, finalName, { dir: dir.fd });
137
+ if (!existsSync(join(dir.path, finalName))) {
138
+ artifactIoMode = "win32_pinned";
139
+ return artifactIoMode;
140
+ }
141
+ (unlinkSync as unknown as UnlinkSyncWithDir)(finalName, { dir: dir.fd });
142
+ artifactIoMode = "dirfd";
143
+ } catch {
144
+ artifactIoMode = "win32_pinned";
145
+ }
146
+ return artifactIoMode;
147
+ }
148
+
149
+ function childPath(dir: TrustedArtifactDir, name: string): string {
150
+ revalidateDir(dir);
151
+ assertRelativeName(name);
152
+ return join(dir.path, name);
153
+ }
154
+
155
+ function assertOpenedPathMatchesDescriptor(dir: TrustedArtifactDir, name: string, fd: number): void {
156
+ if (detectArtifactIoMode(dir) !== "win32_pinned") return;
157
+ const opened = fstatSync(fd);
158
+ assertRegularFileStats(opened, "artifact fd");
159
+ const pathEntry = lstatSync(childPath(dir, name));
160
+ if (
161
+ !pathEntry.isFile() ||
162
+ pathEntry.isSymbolicLink() ||
163
+ pathEntry.dev !== opened.dev ||
164
+ pathEntry.ino !== opened.ino
165
+ ) {
166
+ closeSync(fd);
167
+ harnessFailure("artifact path identity mismatch after open", "artifact_unsafe_target");
168
+ }
169
+ }
170
+
171
+ function openAtDir(dir: TrustedArtifactDir, name: string, flags: number, mode = 0): number {
172
+ revalidateDir(dir);
173
+ assertRelativeName(name);
174
+ const mode_ = detectArtifactIoMode(dir);
175
+ if (mode_ === "dirfd") {
176
+ return (openSync as unknown as OpenSyncWithDir)(name, flags, mode, { dir: dir.fd });
177
+ }
178
+ const fd = openSync(childPath(dir, name), flags, mode);
179
+ assertOpenedPathMatchesDescriptor(dir, name, fd);
180
+ return fd;
181
+ }
182
+
183
+ function renameAtDir(dir: TrustedArtifactDir, from: string, to: string): void {
184
+ revalidateDir(dir);
185
+ assertRelativeName(from);
186
+ assertRelativeName(to);
187
+ if (detectArtifactIoMode(dir) === "dirfd") {
188
+ (renameSync as unknown as RenameSyncWithDir)(from, to, { dir: dir.fd });
189
+ return;
190
+ }
191
+ renameSync(childPath(dir, from), childPath(dir, to));
192
+ }
193
+
194
+ function unlinkAtDir(dir: TrustedArtifactDir, name: string): void {
195
+ revalidateDir(dir);
196
+ assertRelativeName(name);
197
+ if (detectArtifactIoMode(dir) === "dirfd") {
198
+ (unlinkSync as unknown as UnlinkSyncWithDir)(name, { dir: dir.fd });
199
+ return;
200
+ }
201
+ unlinkSync(childPath(dir, name));
202
+ }
203
+
204
+ function revalidateDir(dir: TrustedArtifactDir): void {
205
+ const stats = fstatSync(dir.fd);
206
+ assertDirectoryStats(stats, "artifacts dir");
207
+ if (identityOf(stats) !== dir.identity) {
208
+ harnessFailure("artifacts directory identity changed", "artifact_unsafe_target");
209
+ }
210
+ }
211
+
212
+ export function openTrustedArtifactDir(artifactsDir: string): TrustedArtifactDir {
213
+ const abs = artifactsDir.replace(/[\\/]+$/, "");
214
+ if (abs.includes("\0")) harnessFailure("NUL in artifacts path", "artifact_unsafe_target");
215
+ mkdirSync(abs, { recursive: true, mode: 0o700 });
216
+
217
+ let fd: number;
218
+ if (typeof O_DIRECTORY === "number") {
219
+ try {
220
+ fd = openSync(abs, openFlags(O_RDONLY | O_DIRECTORY, true));
221
+ } catch {
222
+ harnessFailure("failed to open artifacts directory with O_DIRECTORY", "artifact_unsafe_target");
223
+ }
224
+ } else {
225
+ fd = openSync(abs, O_RDONLY);
226
+ }
227
+
228
+ const stats = fstatSync(fd);
229
+ assertDirectoryStats(stats, "artifacts dir");
230
+ const trusted = { path: abs, fd, identity: identityOf(stats) };
231
+ detectArtifactIoMode(trusted);
232
+ return trusted;
233
+ }
234
+
235
+ export function closeTrustedArtifactDir(dir: TrustedArtifactDir): void {
236
+ try {
237
+ closeSync(dir.fd);
238
+ } catch {
239
+ /* ignore */
240
+ }
241
+ }
242
+
243
+ export interface StoredArtifactBytes {
244
+ digest: string;
245
+ bytes: Uint8Array;
246
+ byteCount: number;
247
+ }
248
+
249
+ export interface ReadArtifactOptions {
250
+ expectedByteCount?: number;
251
+ contentDigest?: (bytes: Uint8Array) => string;
252
+ }
253
+
254
+ function readAllFromFd(fd: number, size: number): Buffer {
255
+ const buf = Buffer.alloc(size);
256
+ let offset = 0;
257
+ while (offset < buf.length) {
258
+ const n = readSync(fd, buf, offset, buf.length - offset, offset);
259
+ if (n <= 0) break;
260
+ offset += n;
261
+ }
262
+ if (offset !== size) harnessFailure("short read from artifact descriptor", "artifact_mismatch");
263
+ return buf;
264
+ }
265
+
266
+ function isRawMissingError(err: unknown): boolean {
267
+ return !!err && typeof err === "object" && "code" in err && (err as { code: string }).code === "ENOENT";
268
+ }
269
+
270
+ function isMissingArtifactError(err: unknown): boolean {
271
+ return isRawMissingError(err) || (err instanceof ArtifactFsError && err.code === "artifact_missing");
272
+ }
273
+
274
+ /**
275
+ * Returns true when a clean regular digest target appeared after the caller's
276
+ * missing read. The final readback still verifies byte count and digest, so a
277
+ * concurrent or hostile wrong-content regular file fails closed.
278
+ */
279
+ function artifactTargetAlreadyPublished(dir: TrustedArtifactDir, name: string): boolean {
280
+ revalidateDir(dir);
281
+ assertRelativeName(name);
282
+ try {
283
+ const stats = lstatSync(childPath(dir, name));
284
+ if (stats.isSymbolicLink()) {
285
+ harnessFailure("artifact target is a symbolic link", "artifact_unsafe_target");
286
+ }
287
+ assertRegularFileStats(stats, "artifact create target");
288
+ return true;
289
+ } catch (err) {
290
+ if (isRawMissingError(err)) return false;
291
+ if (err instanceof ArtifactFsError) throw err;
292
+ harnessFailure(
293
+ `artifact create target check failed: ${err instanceof Error ? err.message : String(err)}`,
294
+ "artifact_unsafe_target",
295
+ );
296
+ }
297
+ }
298
+
299
+ function writeAll(fd: number, bytes: Uint8Array): void {
300
+ let offset = 0;
301
+ while (offset < bytes.byteLength) {
302
+ const n = writeSync(fd, bytes, offset, bytes.byteLength - offset);
303
+ if (n <= 0) harnessFailure("artifact write made no progress", "artifact_mismatch");
304
+ offset += n;
305
+ }
306
+ }
307
+
308
+ function writeTempArtifact(
309
+ dir: TrustedArtifactDir,
310
+ tmpName: string,
311
+ bytes: Uint8Array,
312
+ digest: string,
313
+ contentDigest: (b: Uint8Array) => string,
314
+ ): void {
315
+ let fd: number | null = null;
316
+ try {
317
+ fd = openAtDir(dir, tmpName, openFlags(O_RDWR | O_CREAT | O_EXCL, true), 0o600);
318
+ writeAll(fd, bytes);
319
+ fsyncSync(fd);
320
+ const stats = fstatSync(fd);
321
+ assertRegularFileStats(stats, "artifact temp");
322
+ if (stats.size !== bytes.byteLength) {
323
+ harnessFailure("size mismatch after write", "artifact_mismatch");
324
+ }
325
+ const buf = readAllFromFd(fd, bytes.byteLength);
326
+ if (contentDigest(buf) !== digest) {
327
+ harnessFailure("digest mismatch on same descriptor", "artifact_mismatch");
328
+ }
329
+ closeSync(fd);
330
+ fd = null;
331
+ renameAtDir(dir, tmpName, digestFileName(digest));
332
+ } catch (err) {
333
+ if (fd !== null) {
334
+ try { closeSync(fd); } catch { /* ignore */ }
335
+ }
336
+ try {
337
+ unlinkAtDir(dir, tmpName);
338
+ } catch {
339
+ /* ignore cleanup */
340
+ }
341
+ if (err instanceof ArtifactFsError) throw err;
342
+ harnessFailure(`artifact write failed: ${err instanceof Error ? err.message : String(err)}`);
343
+ }
344
+ }
345
+
346
+ export function readArtifactBytes(
347
+ dir: TrustedArtifactDir,
348
+ digest: string,
349
+ expectedByteCountOrOpts?: number | ReadArtifactOptions,
350
+ ): StoredArtifactBytes {
351
+ const opts: ReadArtifactOptions =
352
+ typeof expectedByteCountOrOpts === "number"
353
+ ? { expectedByteCount: expectedByteCountOrOpts }
354
+ : expectedByteCountOrOpts ?? {};
355
+ const contentDigest = opts.contentDigest ?? artifactBytesDigest;
356
+ revalidateDir(dir);
357
+ assertDigestName(digest);
358
+ const name = digestFileName(digest);
359
+
360
+ let fd: number | null = null;
361
+ try {
362
+ fd = openAtDir(dir, name, openFlags(O_RDONLY, true));
363
+ const stats = fstatSync(fd);
364
+ assertRegularFileStats(stats, "artifact fd");
365
+ if (opts.expectedByteCount !== undefined && stats.size !== opts.expectedByteCount) {
366
+ harnessFailure("artifact size mismatch on descriptor", "artifact_mismatch");
367
+ }
368
+ if (stats.size > MAX_BYTES_PER_ARTIFACT) {
369
+ harnessFailure("artifact exceeds ceiling", "artifact_mismatch");
370
+ }
371
+ const buf = readAllFromFd(fd, stats.size);
372
+ const got = contentDigest(buf);
373
+ if (got !== digest) harnessFailure("artifact digest mismatch on descriptor", "artifact_mismatch");
374
+ return { digest, bytes: new Uint8Array(buf), byteCount: stats.size };
375
+ } catch (err) {
376
+ if (isRawMissingError(err)) {
377
+ harnessFailure(`artifact missing: ${digest}`, "artifact_missing");
378
+ }
379
+ if (err instanceof ArtifactFsError) throw err;
380
+ harnessFailure(`artifact read failed: ${err instanceof Error ? err.message : String(err)}`);
381
+ } finally {
382
+ if (fd !== null) closeSync(fd);
383
+ }
384
+ harnessFailure("artifact read failed");
385
+ }
386
+
387
+ export function putArtifactBytes(
388
+ dir: TrustedArtifactDir,
389
+ bytes: Uint8Array,
390
+ expectedDigest?: string,
391
+ ): StoredArtifactBytes {
392
+ revalidateDir(dir);
393
+ if (bytes.byteLength > MAX_BYTES_PER_ARTIFACT) {
394
+ harnessFailure(`artifact exceeds ${MAX_BYTES_PER_ARTIFACT} bytes`);
395
+ }
396
+ const digest = artifactBytesDigest(bytes);
397
+ if (expectedDigest !== undefined) {
398
+ assertDigestName(expectedDigest);
399
+ if (digest !== expectedDigest) {
400
+ harnessFailure("artifact digest mismatch before write", "artifact_mismatch");
401
+ }
402
+ }
403
+
404
+ try {
405
+ return readArtifactBytes(dir, digest, bytes.byteLength);
406
+ } catch (err) {
407
+ if (!isMissingArtifactError(err)) throw err;
408
+ }
409
+
410
+ if (!artifactTargetAlreadyPublished(dir, digestFileName(digest))) {
411
+ const tmpName = `.tmp-${digest}-${process.pid}-${Date.now()}.partial`;
412
+ writeTempArtifact(dir, tmpName, bytes, digest, artifactBytesDigest);
413
+ }
414
+ return readArtifactBytes(dir, digest, bytes.byteLength);
415
+ }
416
+
417
+ export function putNamedDigestBytes(
418
+ dir: TrustedArtifactDir,
419
+ digest: string,
420
+ bytes: Uint8Array,
421
+ contentDigest: (b: Uint8Array) => string,
422
+ ): StoredArtifactBytes {
423
+ revalidateDir(dir);
424
+ assertDigestName(digest);
425
+ if (bytes.byteLength > MAX_BYTES_PER_ARTIFACT) {
426
+ harnessFailure(`artifact exceeds ${MAX_BYTES_PER_ARTIFACT} bytes`);
427
+ }
428
+ if (contentDigest(bytes) !== digest) {
429
+ harnessFailure("named artifact content digest mismatch before write", "artifact_mismatch");
430
+ }
431
+
432
+ try {
433
+ return readArtifactBytes(dir, digest, { expectedByteCount: bytes.byteLength, contentDigest });
434
+ } catch (err) {
435
+ if (!isMissingArtifactError(err)) throw err;
436
+ }
437
+
438
+ if (!artifactTargetAlreadyPublished(dir, digestFileName(digest))) {
439
+ const tmpName = `.tmp-${digest}-${process.pid}-${Date.now()}.partial`;
440
+ writeTempArtifact(dir, tmpName, bytes, digest, contentDigest);
441
+ }
442
+ return readArtifactBytes(dir, digest, { expectedByteCount: bytes.byteLength, contentDigest });
443
+ }
444
+
445
+ export function deleteArtifactBytes(dir: TrustedArtifactDir, digest: string): void {
446
+ revalidateDir(dir);
447
+ assertDigestName(digest);
448
+ const name = digestFileName(digest);
449
+ try {
450
+ unlinkAtDir(dir, name);
451
+ } catch (err) {
452
+ if (isRawMissingError(err)) return;
453
+ if (err instanceof ArtifactFsError) throw err;
454
+ harnessFailure(`artifact delete failed: ${err instanceof Error ? err.message : String(err)}`);
455
+ }
456
+ }
457
+
458
+ export function artifactExists(dir: TrustedArtifactDir, digest: string): boolean {
459
+ revalidateDir(dir);
460
+ assertDigestName(digest);
461
+ try {
462
+ const fd = openAtDir(dir, digestFileName(digest), openFlags(O_RDONLY, true));
463
+ try {
464
+ const stats = fstatSync(fd);
465
+ assertRegularFileStats(stats, "artifact exists");
466
+ return true;
467
+ } finally {
468
+ closeSync(fd);
469
+ }
470
+ } catch (err) {
471
+ if (isMissingArtifactError(err)) return false;
472
+ if (err instanceof ArtifactFsError) throw err;
473
+ harnessFailure(`artifact exists check failed: ${err instanceof Error ? err.message : String(err)}`);
474
+ }
475
+ }