@vostride/agent-qa-runner-local 0.0.1 → 0.2.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.
- package/LICENSE.md +105 -0
- package/NOTICE.md +11 -0
- package/dist/index.cjs +1112 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +311 -0
- package/dist/index.d.ts +311 -0
- package/dist/index.js +1073 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ARTIFACT_FILE: () => ARTIFACT_FILE,
|
|
34
|
+
BUILD_ORDINALS_FILE: () => BUILD_ORDINALS_FILE,
|
|
35
|
+
BUILD_TAGS_FILE: () => BUILD_TAGS_FILE,
|
|
36
|
+
BuildOrdinalReservationError: () => BuildOrdinalReservationError,
|
|
37
|
+
CACHE_SCHEMA_VERSION: () => CACHE_SCHEMA_VERSION,
|
|
38
|
+
DockerHookExecutor: () => DockerHookExecutor,
|
|
39
|
+
EVENTS_FILE: () => EVENTS_FILE,
|
|
40
|
+
FsActionCache: () => FsActionCache,
|
|
41
|
+
FsEventLog: () => FsEventLog,
|
|
42
|
+
FsEvidenceArtifactStore: () => FsEvidenceArtifactStore,
|
|
43
|
+
FsHandoffStore: () => FsHandoffStore,
|
|
44
|
+
FsRunArtifactStore: () => FsRunArtifactStore,
|
|
45
|
+
FsStorageStateStore: () => FsStorageStateStore,
|
|
46
|
+
HANDOFF_FILE: () => HANDOFF_FILE,
|
|
47
|
+
RUNS_SUBDIR: () => RUNS_SUBDIR,
|
|
48
|
+
RUN_EVIDENCE_BLOBS_SUBDIR: () => RUN_EVIDENCE_BLOBS_SUBDIR,
|
|
49
|
+
STORAGE_STATES_SUBDIR: () => STORAGE_STATES_SUBDIR,
|
|
50
|
+
SharpScreenshotCompressor: () => SharpScreenshotCompressor,
|
|
51
|
+
alignToWindow: () => alignToWindow,
|
|
52
|
+
checkDockerAvailable: () => checkDockerAvailable,
|
|
53
|
+
flattenOntoWhite: () => flattenOntoWhite,
|
|
54
|
+
getImageDimensions: () => getImageDimensions,
|
|
55
|
+
latestBuildOrdinal: () => latestBuildOrdinal,
|
|
56
|
+
loadCompletedRunHandoff: () => loadCompletedRunHandoff,
|
|
57
|
+
nextBuildOrdinal: () => nextBuildOrdinal,
|
|
58
|
+
readBuildTagMap: () => readBuildTagMap,
|
|
59
|
+
recordBuildTag: () => recordBuildTag,
|
|
60
|
+
reserveBuildOrdinalForProducts: () => reserveBuildOrdinalForProducts,
|
|
61
|
+
resolveBranch: () => resolveBranch,
|
|
62
|
+
resolveBuildTag: () => resolveBuildTag,
|
|
63
|
+
shouldWrite: () => shouldWrite,
|
|
64
|
+
slugBranch: () => import_agent_qa_schema.slugBranch
|
|
65
|
+
});
|
|
66
|
+
module.exports = __toCommonJS(index_exports);
|
|
67
|
+
|
|
68
|
+
// src/fs-action-cache.ts
|
|
69
|
+
var import_promises = require("fs/promises");
|
|
70
|
+
var import_node_path2 = require("path");
|
|
71
|
+
|
|
72
|
+
// src/branch-resolver.ts
|
|
73
|
+
var import_node_child_process = require("child_process");
|
|
74
|
+
var import_node_fs = require("fs");
|
|
75
|
+
var import_node_path = require("path");
|
|
76
|
+
var import_agent_qa_schema = require("@vostride/agent-qa-schema");
|
|
77
|
+
var HEAD_REF_RE = /^ref:\s*refs\/heads\/(.+?)\s*$/m;
|
|
78
|
+
var GITDIR_RE = /^gitdir:\s*(.+)$/m;
|
|
79
|
+
function resolveBranch(opts = {}) {
|
|
80
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
81
|
+
const readHead = opts.readHead ?? defaultReadHead;
|
|
82
|
+
const runSymbolicRef = opts.runSymbolicRef ?? defaultRunSymbolicRef;
|
|
83
|
+
const defaultBranch = parseSymbolicRef(safe(() => runSymbolicRef(cwd))) ?? opts.configuredDefault ?? "main";
|
|
84
|
+
const currentBranch = parseHeadBranch(safe(() => readHead(cwd))) ?? defaultBranch;
|
|
85
|
+
return { currentBranch, defaultBranch };
|
|
86
|
+
}
|
|
87
|
+
function parseHeadBranch(head) {
|
|
88
|
+
if (head === null) return null;
|
|
89
|
+
const match = HEAD_REF_RE.exec(head);
|
|
90
|
+
return match ? match[1] : null;
|
|
91
|
+
}
|
|
92
|
+
function parseSymbolicRef(output) {
|
|
93
|
+
if (output === null) return null;
|
|
94
|
+
const trimmed = output.trim();
|
|
95
|
+
if (trimmed === "") return null;
|
|
96
|
+
const name = trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
97
|
+
return name.length > 0 ? name : null;
|
|
98
|
+
}
|
|
99
|
+
function defaultReadHead(cwd) {
|
|
100
|
+
try {
|
|
101
|
+
const gitPath = (0, import_node_path.join)(cwd, ".git");
|
|
102
|
+
const stat = (0, import_node_fs.statSync)(gitPath);
|
|
103
|
+
if (stat.isDirectory()) {
|
|
104
|
+
return (0, import_node_fs.readFileSync)((0, import_node_path.join)(gitPath, "HEAD"), "utf-8");
|
|
105
|
+
}
|
|
106
|
+
const pointer = (0, import_node_fs.readFileSync)(gitPath, "utf-8");
|
|
107
|
+
const match = GITDIR_RE.exec(pointer);
|
|
108
|
+
if (match === null) return null;
|
|
109
|
+
const gitdir = (0, import_node_path.resolve)(cwd, match[1].trim());
|
|
110
|
+
return (0, import_node_fs.readFileSync)((0, import_node_path.join)(gitdir, "HEAD"), "utf-8");
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
var DEFAULT_GIT_SYMBOLIC_REF_TIMEOUT_MS = 5e3;
|
|
116
|
+
function runGitSymbolicRef(cwd, options = {}) {
|
|
117
|
+
const exec = options.exec ?? import_node_child_process.execFileSync;
|
|
118
|
+
const timeout = options.timeoutMs ?? DEFAULT_GIT_SYMBOLIC_REF_TIMEOUT_MS;
|
|
119
|
+
try {
|
|
120
|
+
return exec("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
121
|
+
cwd,
|
|
122
|
+
encoding: "utf-8",
|
|
123
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
124
|
+
timeout
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function defaultRunSymbolicRef(cwd) {
|
|
131
|
+
return runGitSymbolicRef(cwd);
|
|
132
|
+
}
|
|
133
|
+
function safe(read) {
|
|
134
|
+
try {
|
|
135
|
+
return read();
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/write-policy.ts
|
|
142
|
+
function shouldWrite(policy) {
|
|
143
|
+
if (policy.disableCache) return false;
|
|
144
|
+
const protectedNow = policy.protectedBranches.includes(policy.currentBranch);
|
|
145
|
+
return !protectedNow || policy.saveCache || policy.isCI;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/fs-action-cache.ts
|
|
149
|
+
var CACHE_SCHEMA_VERSION = 2;
|
|
150
|
+
var FsActionCache = class {
|
|
151
|
+
root;
|
|
152
|
+
policy;
|
|
153
|
+
constructor(config) {
|
|
154
|
+
this.root = config.root;
|
|
155
|
+
this.policy = config.policy;
|
|
156
|
+
}
|
|
157
|
+
async get(key) {
|
|
158
|
+
if (this.policy.disableCache) return null;
|
|
159
|
+
const onCurrent = await this.readEntry(this.policy.currentBranch, key);
|
|
160
|
+
if (onCurrent !== null) return onCurrent;
|
|
161
|
+
if (this.policy.currentBranch !== this.policy.defaultBranch) {
|
|
162
|
+
return this.readEntry(this.policy.defaultBranch, key);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
async set(key, value) {
|
|
167
|
+
if (!shouldWrite(this.policy)) return;
|
|
168
|
+
const dir = this.branchDir(this.policy.currentBranch);
|
|
169
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
170
|
+
const envelope = { schemaVersion: CACHE_SCHEMA_VERSION, value };
|
|
171
|
+
await (0, import_promises.writeFile)(this.entryPath(dir, key), `${JSON.stringify(envelope, null, 2)}
|
|
172
|
+
`, "utf-8");
|
|
173
|
+
}
|
|
174
|
+
async invalidate(key) {
|
|
175
|
+
const dir = this.branchDir(this.policy.currentBranch);
|
|
176
|
+
try {
|
|
177
|
+
await (0, import_promises.unlink)(this.entryPath(dir, key));
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err.code !== "ENOENT") throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
branchDir(branch) {
|
|
183
|
+
return (0, import_node_path2.join)(this.root, (0, import_agent_qa_schema.slugBranch)(branch));
|
|
184
|
+
}
|
|
185
|
+
entryPath(dir, key) {
|
|
186
|
+
return (0, import_node_path2.join)(dir, `${key}.json`);
|
|
187
|
+
}
|
|
188
|
+
async readEntry(branch, key) {
|
|
189
|
+
let raw;
|
|
190
|
+
try {
|
|
191
|
+
raw = await (0, import_promises.readFile)(this.entryPath(this.branchDir(branch), key), "utf-8");
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err.code === "ENOENT") return null;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
let envelope;
|
|
197
|
+
try {
|
|
198
|
+
envelope = JSON.parse(raw);
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (envelope?.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
203
|
+
return envelope.value ?? null;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/storage-state-store.ts
|
|
208
|
+
var import_promises2 = require("fs/promises");
|
|
209
|
+
var import_node_path3 = require("path");
|
|
210
|
+
var import_agent_qa_schema2 = require("@vostride/agent-qa-schema");
|
|
211
|
+
var STORAGE_STATES_SUBDIR = "storage-states";
|
|
212
|
+
var FsStorageStateStore = class {
|
|
213
|
+
root;
|
|
214
|
+
constructor(config) {
|
|
215
|
+
this.root = config.root;
|
|
216
|
+
}
|
|
217
|
+
async save(name, state) {
|
|
218
|
+
const dir = this.storageStatesDir();
|
|
219
|
+
await (0, import_promises2.mkdir)(dir, { recursive: true });
|
|
220
|
+
const serialized = JSON.stringify(state);
|
|
221
|
+
await (0, import_promises2.writeFile)(this.entryPath(name), `${serialized}
|
|
222
|
+
`, "utf-8");
|
|
223
|
+
return { name, fingerprint: (0, import_agent_qa_schema2.sha256hex16)(serialized) };
|
|
224
|
+
}
|
|
225
|
+
async load(name) {
|
|
226
|
+
let raw;
|
|
227
|
+
try {
|
|
228
|
+
raw = await (0, import_promises2.readFile)(this.entryPath(name), "utf-8");
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err.code === "ENOENT") return void 0;
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
return JSON.parse(raw);
|
|
235
|
+
} catch {
|
|
236
|
+
return void 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
storageStatesDir() {
|
|
240
|
+
return (0, import_node_path3.join)(this.root, STORAGE_STATES_SUBDIR);
|
|
241
|
+
}
|
|
242
|
+
entryPath(name) {
|
|
243
|
+
return (0, import_node_path3.join)(this.storageStatesDir(), `${(0, import_agent_qa_schema.slugBranch)(name)}.json`);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/event-log.ts
|
|
248
|
+
var import_node_fs2 = require("fs");
|
|
249
|
+
var import_node_path4 = require("path");
|
|
250
|
+
var RUNS_SUBDIR = "runs";
|
|
251
|
+
var EVENTS_FILE = "events.jsonl";
|
|
252
|
+
var FsEventLog = class {
|
|
253
|
+
dir;
|
|
254
|
+
file;
|
|
255
|
+
constructor(config) {
|
|
256
|
+
this.dir = (0, import_node_path4.join)(config.root, RUNS_SUBDIR, (0, import_agent_qa_schema.slugBranch)(config.runId));
|
|
257
|
+
this.file = (0, import_node_path4.join)(this.dir, EVENTS_FILE);
|
|
258
|
+
}
|
|
259
|
+
append(envelope) {
|
|
260
|
+
(0, import_node_fs2.mkdirSync)(this.dir, { recursive: true });
|
|
261
|
+
(0, import_node_fs2.appendFileSync)(this.file, `${JSON.stringify(envelope)}
|
|
262
|
+
`);
|
|
263
|
+
}
|
|
264
|
+
read(fromSeq) {
|
|
265
|
+
let raw;
|
|
266
|
+
try {
|
|
267
|
+
raw = (0, import_node_fs2.readFileSync)(this.file, "utf-8");
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (err.code === "ENOENT") return [];
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
const out = [];
|
|
273
|
+
for (const line of raw.split("\n")) {
|
|
274
|
+
if (line.length === 0) continue;
|
|
275
|
+
let parsed;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(line);
|
|
278
|
+
} catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (parsed.seq >= fromSeq) out.push(parsed);
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/handoff-store.ts
|
|
288
|
+
var import_node_fs3 = require("fs");
|
|
289
|
+
var import_node_path5 = require("path");
|
|
290
|
+
var import_agent_qa_schema3 = require("@vostride/agent-qa-schema");
|
|
291
|
+
var HANDOFF_FILE = "handoff.json";
|
|
292
|
+
var FsHandoffStore = class {
|
|
293
|
+
dir;
|
|
294
|
+
file;
|
|
295
|
+
constructor(config) {
|
|
296
|
+
this.dir = (0, import_node_path5.join)(config.root, RUNS_SUBDIR, (0, import_agent_qa_schema.slugBranch)(config.runId));
|
|
297
|
+
this.file = (0, import_node_path5.join)(this.dir, HANDOFF_FILE);
|
|
298
|
+
}
|
|
299
|
+
save(handoff) {
|
|
300
|
+
(0, import_node_fs3.mkdirSync)(this.dir, { recursive: true });
|
|
301
|
+
(0, import_node_fs3.writeFileSync)(this.file, `${JSON.stringify(handoff)}
|
|
302
|
+
`, "utf-8");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
function loadCompletedRunHandoff(filePath) {
|
|
306
|
+
const raw = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
307
|
+
return (0, import_agent_qa_schema3.guardCompletedRunHandoff)(JSON.parse(raw));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/run-artifact-store.ts
|
|
311
|
+
var import_node_fs4 = require("fs");
|
|
312
|
+
var import_node_path6 = require("path");
|
|
313
|
+
var import_agent_qa_schema4 = require("@vostride/agent-qa-schema");
|
|
314
|
+
var ARTIFACT_FILE = "artifact.json";
|
|
315
|
+
var FsRunArtifactStore = class {
|
|
316
|
+
dir;
|
|
317
|
+
file;
|
|
318
|
+
constructor(config) {
|
|
319
|
+
this.dir = (0, import_node_path6.join)(config.root, RUNS_SUBDIR, (0, import_agent_qa_schema.slugBranch)(config.runId));
|
|
320
|
+
this.file = (0, import_node_path6.join)(this.dir, ARTIFACT_FILE);
|
|
321
|
+
}
|
|
322
|
+
stage(document) {
|
|
323
|
+
const persisted = this.merge({ schemaVersion: import_agent_qa_schema4.RUN_ARTIFACT_SCHEMA_VERSION }, document);
|
|
324
|
+
this.write(persisted);
|
|
325
|
+
}
|
|
326
|
+
finalize(patch) {
|
|
327
|
+
const base = this.readExisting() ?? { schemaVersion: import_agent_qa_schema4.RUN_ARTIFACT_SCHEMA_VERSION };
|
|
328
|
+
const persisted = this.merge(base, patch);
|
|
329
|
+
this.write(persisted);
|
|
330
|
+
}
|
|
331
|
+
/** Shallow-merge a stage/finalize input onto the base, always dropping the top-level runtime. */
|
|
332
|
+
merge(base, input) {
|
|
333
|
+
const { runtime: _dropRuntime, ...rest } = input;
|
|
334
|
+
const merged = {
|
|
335
|
+
...base,
|
|
336
|
+
...rest,
|
|
337
|
+
schemaVersion: import_agent_qa_schema4.RUN_ARTIFACT_SCHEMA_VERSION
|
|
338
|
+
};
|
|
339
|
+
delete merged.runtime;
|
|
340
|
+
return merged;
|
|
341
|
+
}
|
|
342
|
+
readExisting() {
|
|
343
|
+
try {
|
|
344
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(this.file, "utf-8"));
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (err.code === "ENOENT") return void 0;
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
write(document) {
|
|
351
|
+
(0, import_node_fs4.mkdirSync)(this.dir, { recursive: true });
|
|
352
|
+
(0, import_node_fs4.writeFileSync)(this.file, `${JSON.stringify(document)}
|
|
353
|
+
`, "utf-8");
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/evidence-artifact-store.ts
|
|
358
|
+
var import_node_crypto = require("crypto");
|
|
359
|
+
var import_node_fs5 = require("fs");
|
|
360
|
+
var import_node_path7 = require("path");
|
|
361
|
+
var import_node_fs6 = require("fs");
|
|
362
|
+
var import_node_path8 = require("path");
|
|
363
|
+
var import_agent_qa_schema5 = require("@vostride/agent-qa-schema");
|
|
364
|
+
var import_agent_qa_schema6 = require("@vostride/agent-qa-schema");
|
|
365
|
+
var RUN_EVIDENCE_BLOBS_SUBDIR = "blobs";
|
|
366
|
+
var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
367
|
+
var DIRECTORY_SYNC_TOLERATED_CODES = ["EINVAL", "EPERM", "ENOTSUP", "EOPNOTSUPP", "EBADF", "EISDIR"];
|
|
368
|
+
var FsEvidenceArtifactStore = class {
|
|
369
|
+
dir;
|
|
370
|
+
durabilityRoot;
|
|
371
|
+
constructor(config) {
|
|
372
|
+
this.dir = (0, import_node_path7.join)(config.root, RUNS_SUBDIR, (0, import_agent_qa_schema.slugBranch)(config.runId), RUN_EVIDENCE_BLOBS_SUBDIR);
|
|
373
|
+
this.durabilityRoot = (0, import_node_path7.dirname)((0, import_node_path7.join)(config.root, "."));
|
|
374
|
+
}
|
|
375
|
+
persistDecisionScreenshot(bytes) {
|
|
376
|
+
const sha = (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
377
|
+
(0, import_node_fs5.mkdirSync)(this.dir, { recursive: true });
|
|
378
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path7.join)(this.dir, sha), bytes);
|
|
379
|
+
return { id: `decision-screenshot:${sha}`, sha };
|
|
380
|
+
}
|
|
381
|
+
persistEvidenceDocument(document) {
|
|
382
|
+
const bytes = Buffer.from(JSON.stringify(document));
|
|
383
|
+
const parsed = import_agent_qa_schema6.EvidenceDocumentSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
384
|
+
if (parsed.kind === "variables") throw new Error("Variable evidence is stored inline");
|
|
385
|
+
const sha = (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
386
|
+
const prefix = parsed.kind === "aria" ? "aria-state:" : `${parsed.kind}-evidence:`;
|
|
387
|
+
const ref = import_agent_qa_schema6.EvidenceArtifactRefSchema.parse({ id: `${prefix}${sha}`, sha });
|
|
388
|
+
(0, import_node_fs5.mkdirSync)(this.dir, { recursive: true });
|
|
389
|
+
const target = (0, import_node_path7.join)(this.dir, sha);
|
|
390
|
+
const existing = this.getBlob(sha);
|
|
391
|
+
if (existing !== void 0 && Buffer.from(existing).equals(bytes)) {
|
|
392
|
+
this.syncPath(target);
|
|
393
|
+
this.syncDirectories();
|
|
394
|
+
return ref;
|
|
395
|
+
}
|
|
396
|
+
const temporary = (0, import_node_path7.join)(this.dir, `.${sha}.${(0, import_node_crypto.randomUUID)()}.tmp`);
|
|
397
|
+
const fd = (0, import_node_fs5.openSync)(temporary, "wx", 384);
|
|
398
|
+
let opened = true;
|
|
399
|
+
try {
|
|
400
|
+
(0, import_node_fs5.writeFileSync)(fd, bytes);
|
|
401
|
+
(0, import_node_fs5.fsyncSync)(fd);
|
|
402
|
+
(0, import_node_fs5.closeSync)(fd);
|
|
403
|
+
opened = false;
|
|
404
|
+
(0, import_node_fs5.renameSync)(temporary, target);
|
|
405
|
+
this.syncDirectories();
|
|
406
|
+
return ref;
|
|
407
|
+
} finally {
|
|
408
|
+
if (opened) (0, import_node_fs5.closeSync)(fd);
|
|
409
|
+
try {
|
|
410
|
+
(0, import_node_fs5.unlinkSync)(temporary);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error.code !== "ENOENT") throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
syncDirectories() {
|
|
417
|
+
for (let path = this.dir; ; path = (0, import_node_path7.dirname)(path)) {
|
|
418
|
+
try {
|
|
419
|
+
this.syncPath(path);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
const code = error.code;
|
|
422
|
+
if (!DIRECTORY_SYNC_TOLERATED_CODES.some((tolerated) => tolerated === code)) throw error;
|
|
423
|
+
}
|
|
424
|
+
if (path === this.durabilityRoot || (0, import_node_path7.dirname)(path) === path) return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
syncPath(path) {
|
|
428
|
+
const fd = (0, import_node_fs5.openSync)(path, "r");
|
|
429
|
+
try {
|
|
430
|
+
(0, import_node_fs5.fsyncSync)(fd);
|
|
431
|
+
} finally {
|
|
432
|
+
(0, import_node_fs5.closeSync)(fd);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
hasBlob(sha) {
|
|
436
|
+
if (!SHA256_HEX_RE.test(sha)) return false;
|
|
437
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path7.join)(this.dir, sha));
|
|
438
|
+
}
|
|
439
|
+
persistRecording(stagingPath) {
|
|
440
|
+
this.assertRecordingStagingPath(stagingPath);
|
|
441
|
+
let recordingTemp;
|
|
442
|
+
try {
|
|
443
|
+
const byteLength = (0, import_node_fs6.statSync)(stagingPath).size;
|
|
444
|
+
if (byteLength === 0) throw new Error("Cannot persist an empty recording");
|
|
445
|
+
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
446
|
+
const fd = (0, import_node_fs5.openSync)(stagingPath, "r");
|
|
447
|
+
try {
|
|
448
|
+
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
449
|
+
let total = 0;
|
|
450
|
+
for (; ; ) {
|
|
451
|
+
const count = (0, import_node_fs6.readSync)(fd, chunk, 0, chunk.length, null);
|
|
452
|
+
if (count === 0) break;
|
|
453
|
+
total += count;
|
|
454
|
+
hash.update(chunk.subarray(0, count));
|
|
455
|
+
}
|
|
456
|
+
if (total !== byteLength) throw new Error("Recording changed while hashing");
|
|
457
|
+
} finally {
|
|
458
|
+
(0, import_node_fs5.closeSync)(fd);
|
|
459
|
+
}
|
|
460
|
+
const sha = hash.digest("hex");
|
|
461
|
+
const result = {
|
|
462
|
+
ref: { id: `${import_agent_qa_schema5.RECORDING_REF_PREFIX}${sha}`, sha },
|
|
463
|
+
byteLength
|
|
464
|
+
};
|
|
465
|
+
(0, import_node_fs5.mkdirSync)(this.dir, { recursive: true });
|
|
466
|
+
const target = (0, import_node_path7.join)(this.dir, sha);
|
|
467
|
+
if (this.blobSize(sha) !== void 0) {
|
|
468
|
+
this.syncPath(target);
|
|
469
|
+
this.syncDirectories();
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
recordingTemp = (0, import_node_path7.join)(this.dir, `.${sha}.${(0, import_node_crypto.randomUUID)()}.tmp`);
|
|
473
|
+
try {
|
|
474
|
+
(0, import_node_fs5.renameSync)(stagingPath, recordingTemp);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error.code !== "EXDEV") throw error;
|
|
477
|
+
(0, import_node_fs6.copyFileSync)(stagingPath, recordingTemp);
|
|
478
|
+
}
|
|
479
|
+
(0, import_node_fs6.chmodSync)(recordingTemp, 384);
|
|
480
|
+
this.syncPath(recordingTemp);
|
|
481
|
+
(0, import_node_fs5.renameSync)(recordingTemp, target);
|
|
482
|
+
this.syncDirectories();
|
|
483
|
+
return result;
|
|
484
|
+
} finally {
|
|
485
|
+
try {
|
|
486
|
+
if (recordingTemp !== void 0) this.unlinkRecordingStaging(recordingTemp);
|
|
487
|
+
} finally {
|
|
488
|
+
this.unlinkRecordingStaging(stagingPath);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
blobSize(sha) {
|
|
493
|
+
if (!SHA256_HEX_RE.test(sha)) return void 0;
|
|
494
|
+
try {
|
|
495
|
+
return (0, import_node_fs6.statSync)((0, import_node_path7.join)(this.dir, sha)).size;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error.code === "ENOENT") return void 0;
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
readBlobRange(sha, start, end) {
|
|
502
|
+
if (!SHA256_HEX_RE.test(sha)) return void 0;
|
|
503
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) return void 0;
|
|
504
|
+
let fd;
|
|
505
|
+
try {
|
|
506
|
+
fd = (0, import_node_fs5.openSync)((0, import_node_path7.join)(this.dir, sha), "r");
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error.code === "ENOENT") return void 0;
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const size = (0, import_node_fs6.fstatSync)(fd).size;
|
|
513
|
+
if (start >= size) return void 0;
|
|
514
|
+
const length = Math.min(end, size - 1) - start + 1;
|
|
515
|
+
const bytes = new Uint8Array(length);
|
|
516
|
+
let offset = 0;
|
|
517
|
+
while (offset < length) {
|
|
518
|
+
const count = (0, import_node_fs6.readSync)(fd, bytes, offset, length - offset, start + offset);
|
|
519
|
+
if (count === 0) throw new Error("Recording blob ended before the requested range");
|
|
520
|
+
offset += count;
|
|
521
|
+
}
|
|
522
|
+
return bytes;
|
|
523
|
+
} finally {
|
|
524
|
+
(0, import_node_fs5.closeSync)(fd);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
discardRecording(stagingPath) {
|
|
528
|
+
this.assertRecordingStagingPath(stagingPath);
|
|
529
|
+
this.unlinkRecordingStaging(stagingPath);
|
|
530
|
+
}
|
|
531
|
+
assertRecordingStagingPath(stagingPath) {
|
|
532
|
+
const inside = (dir, path) => {
|
|
533
|
+
const tail = (0, import_node_path8.relative)(dir, path);
|
|
534
|
+
return tail === "" || tail !== ".." && !tail.startsWith(`..${import_node_path8.sep}`) && !(0, import_node_path8.isAbsolute)(tail);
|
|
535
|
+
};
|
|
536
|
+
if (inside((0, import_node_path8.resolve)(this.dir), (0, import_node_path8.resolve)(stagingPath))) {
|
|
537
|
+
throw new Error("Recording staging path must be outside the evidence blob directory");
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
if (inside((0, import_node_fs6.realpathSync)(this.dir), (0, import_node_fs6.realpathSync)(stagingPath))) {
|
|
541
|
+
throw new Error("Recording staging path must be outside the evidence blob directory");
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error.code !== "ENOENT") throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
unlinkRecordingStaging(path) {
|
|
548
|
+
try {
|
|
549
|
+
(0, import_node_fs5.unlinkSync)(path);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (error.code !== "ENOENT") throw error;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
getBlob(sha) {
|
|
555
|
+
if (!SHA256_HEX_RE.test(sha)) return void 0;
|
|
556
|
+
try {
|
|
557
|
+
return new Uint8Array((0, import_node_fs5.readFileSync)((0, import_node_path7.join)(this.dir, sha)));
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (err.code === "ENOENT") return void 0;
|
|
560
|
+
throw err;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// src/screenshot-compressor.ts
|
|
566
|
+
var import_sharp = __toESM(require("sharp"), 1);
|
|
567
|
+
var STARTING_QUALITY = 70;
|
|
568
|
+
var QUALITY_REDUCTION = 0.8;
|
|
569
|
+
var MIN_QUALITY = 10;
|
|
570
|
+
async function compressScreenshot(buffer, opts, logger) {
|
|
571
|
+
const originalKB = (buffer.length / 1024).toFixed(1);
|
|
572
|
+
const meta = await (0, import_sharp.default)(buffer).metadata();
|
|
573
|
+
const srcWidth = meta.width ?? opts.actionSpaceWidth;
|
|
574
|
+
const srcHeight = meta.height ?? 0;
|
|
575
|
+
const targetEdge = opts.effectiveResolution;
|
|
576
|
+
let resizePipeline = (0, import_sharp.default)(buffer);
|
|
577
|
+
if (srcWidth > targetEdge || srcHeight > targetEdge) {
|
|
578
|
+
resizePipeline = resizePipeline.resize({
|
|
579
|
+
width: targetEdge,
|
|
580
|
+
height: targetEdge,
|
|
581
|
+
fit: "inside",
|
|
582
|
+
withoutEnlargement: true,
|
|
583
|
+
kernel: "lanczos3"
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const afterResize = await resizePipeline.toBuffer({ resolveWithObject: true });
|
|
587
|
+
const resizedWidth = afterResize.info.width;
|
|
588
|
+
const resizedHeight = afterResize.info.height;
|
|
589
|
+
if (!opts.maxBytes) {
|
|
590
|
+
return { buffer: afterResize.data, imageWidth: resizedWidth, imageHeight: resizedHeight };
|
|
591
|
+
}
|
|
592
|
+
const maxBytes = opts.maxBytes;
|
|
593
|
+
const targetKB = (maxBytes / 1024).toFixed(1);
|
|
594
|
+
let quality = STARTING_QUALITY;
|
|
595
|
+
while (quality > MIN_QUALITY) {
|
|
596
|
+
const { data: data2, info: info2 } = await (0, import_sharp.default)(afterResize.data).jpeg({ quality, mozjpeg: true }).toBuffer({ resolveWithObject: true });
|
|
597
|
+
if (info2.size <= maxBytes) {
|
|
598
|
+
logger?.debug("Compressed screenshot", {
|
|
599
|
+
originalKB: parseFloat(originalKB),
|
|
600
|
+
compressedKB: parseFloat((info2.size / 1024).toFixed(1)),
|
|
601
|
+
targetKB: parseFloat(targetKB),
|
|
602
|
+
quality
|
|
603
|
+
});
|
|
604
|
+
return { buffer: data2, imageWidth: resizedWidth, imageHeight: resizedHeight };
|
|
605
|
+
}
|
|
606
|
+
quality = Math.max(MIN_QUALITY, Math.round(quality * QUALITY_REDUCTION));
|
|
607
|
+
}
|
|
608
|
+
const { data, info } = await (0, import_sharp.default)(afterResize.data).jpeg({ quality: MIN_QUALITY, mozjpeg: true }).toBuffer({ resolveWithObject: true });
|
|
609
|
+
logger?.debug("Compressed screenshot (exceeded target)", {
|
|
610
|
+
originalKB: parseFloat(originalKB),
|
|
611
|
+
compressedKB: parseFloat((info.size / 1024).toFixed(1)),
|
|
612
|
+
targetKB: parseFloat(targetKB),
|
|
613
|
+
quality: MIN_QUALITY
|
|
614
|
+
});
|
|
615
|
+
return { buffer: data, imageWidth: resizedWidth, imageHeight: resizedHeight };
|
|
616
|
+
}
|
|
617
|
+
var SharpScreenshotCompressor = class {
|
|
618
|
+
constructor(logger) {
|
|
619
|
+
this.logger = logger;
|
|
620
|
+
}
|
|
621
|
+
async compress(screenshot, options) {
|
|
622
|
+
return compressScreenshot(screenshot, options, this.logger);
|
|
623
|
+
}
|
|
624
|
+
async dimensions(screenshot) {
|
|
625
|
+
const meta = await (0, import_sharp.default)(screenshot).metadata();
|
|
626
|
+
return { width: meta.width ?? 0, height: meta.height ?? 0 };
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
async function alignToWindow(buffer, opts) {
|
|
630
|
+
return (0, import_sharp.default)(buffer).resize({ width: opts.width, height: opts.height, kernel: "lanczos3", fit: "fill" }).png().toBuffer();
|
|
631
|
+
}
|
|
632
|
+
async function getImageDimensions(buffer) {
|
|
633
|
+
const meta = await (0, import_sharp.default)(buffer).metadata();
|
|
634
|
+
if (!meta.width || !meta.height) return void 0;
|
|
635
|
+
return { width: meta.width, height: meta.height };
|
|
636
|
+
}
|
|
637
|
+
async function flattenOntoWhite(buffer) {
|
|
638
|
+
const meta = await (0, import_sharp.default)(buffer).metadata();
|
|
639
|
+
if (!meta.hasAlpha) return buffer;
|
|
640
|
+
return (0, import_sharp.default)(buffer).flatten({ background: "#ffffff" }).png().toBuffer();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/build-ordinal.ts
|
|
644
|
+
var import_node_crypto2 = require("crypto");
|
|
645
|
+
var import_node_fs7 = require("fs");
|
|
646
|
+
var import_node_path9 = require("path");
|
|
647
|
+
var BUILD_ORDINALS_FILE = "build-ordinals.json";
|
|
648
|
+
var BuildOrdinalReservationError = class extends Error {
|
|
649
|
+
name = "BuildOrdinalReservationError";
|
|
650
|
+
code;
|
|
651
|
+
product;
|
|
652
|
+
value;
|
|
653
|
+
constructor(code, product, value) {
|
|
654
|
+
const message = code === "invalid-state" ? `${BUILD_ORDINALS_FILE} must contain a JSON object` : code === "missing-counter" ? `Missing build ordinal counter for product ${JSON.stringify(product)}` : code === "invalid-counter" ? `Build ordinal counter for product ${JSON.stringify(product)} must be a non-negative safe integer` : `Build ordinal counter for product ${JSON.stringify(product)} is exhausted`;
|
|
655
|
+
super(message);
|
|
656
|
+
this.code = code;
|
|
657
|
+
this.product = product;
|
|
658
|
+
this.value = value;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
var BUILD_ORDINALS_LOCK_FILE = ".build-ordinals.lock";
|
|
662
|
+
var COUNTER_LOCK_POLL_MS = 5;
|
|
663
|
+
var COUNTER_LOCK_WAIT = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
664
|
+
function fsyncDirectory(dir) {
|
|
665
|
+
let fd;
|
|
666
|
+
try {
|
|
667
|
+
fd = (0, import_node_fs7.openSync)(dir, "r");
|
|
668
|
+
(0, import_node_fs7.fsyncSync)(fd);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
const code = error.code;
|
|
671
|
+
if (code !== "EINVAL" && code !== "EISDIR" && code !== "EPERM" && code !== "ENOTSUP") {
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
if (fd !== void 0) (0, import_node_fs7.closeSync)(fd);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function durableExclusiveFile(file, text) {
|
|
679
|
+
const fd = (0, import_node_fs7.openSync)(file, "wx");
|
|
680
|
+
try {
|
|
681
|
+
(0, import_node_fs7.writeFileSync)(fd, text, "utf8");
|
|
682
|
+
(0, import_node_fs7.fsyncSync)(fd);
|
|
683
|
+
} finally {
|
|
684
|
+
(0, import_node_fs7.closeSync)(fd);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function parsedLockOwner(file) {
|
|
688
|
+
try {
|
|
689
|
+
const value = JSON.parse((0, import_node_fs7.readFileSync)(file, "utf8"));
|
|
690
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
691
|
+
const owner = value;
|
|
692
|
+
return typeof owner.pid === "number" && Number.isSafeInteger(owner.pid) && owner.pid > 0 && typeof owner.token === "string" && owner.token !== "" ? { pid: owner.pid, token: owner.token } : void 0;
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (error.code === "ENOENT") return void 0;
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function processIsAlive(pid) {
|
|
699
|
+
try {
|
|
700
|
+
process.kill(pid, 0);
|
|
701
|
+
return true;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
const code = error.code;
|
|
704
|
+
if (code === "ESRCH") return false;
|
|
705
|
+
if (code === "EPERM") return true;
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function withCounterLock(root, operation) {
|
|
710
|
+
(0, import_node_fs7.mkdirSync)(root, { recursive: true });
|
|
711
|
+
const lock = (0, import_node_path9.join)(root, BUILD_ORDINALS_LOCK_FILE);
|
|
712
|
+
const owner = { pid: process.pid, token: (0, import_node_crypto2.randomUUID)() };
|
|
713
|
+
const ownerFile = `${lock}.owner-${owner.token}`;
|
|
714
|
+
durableExclusiveFile(ownerFile, `${JSON.stringify(owner)}
|
|
715
|
+
`);
|
|
716
|
+
let acquired = false;
|
|
717
|
+
try {
|
|
718
|
+
while (!acquired) {
|
|
719
|
+
try {
|
|
720
|
+
(0, import_node_fs7.linkSync)(ownerFile, lock);
|
|
721
|
+
fsyncDirectory(root);
|
|
722
|
+
acquired = true;
|
|
723
|
+
} catch (error) {
|
|
724
|
+
if (error.code !== "EEXIST") throw error;
|
|
725
|
+
const current = parsedLockOwner(lock);
|
|
726
|
+
if (current !== void 0 && !processIsAlive(current.pid)) {
|
|
727
|
+
const quarantine = `${lock}.dead-${current.token}-${(0, import_node_crypto2.randomUUID)()}`;
|
|
728
|
+
try {
|
|
729
|
+
(0, import_node_fs7.renameSync)(lock, quarantine);
|
|
730
|
+
fsyncDirectory(root);
|
|
731
|
+
(0, import_node_fs7.rmSync)(quarantine, { force: true });
|
|
732
|
+
fsyncDirectory(root);
|
|
733
|
+
continue;
|
|
734
|
+
} catch (recoveryError) {
|
|
735
|
+
if (recoveryError.code === "ENOENT") continue;
|
|
736
|
+
throw recoveryError;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
Atomics.wait(COUNTER_LOCK_WAIT, 0, 0, COUNTER_LOCK_POLL_MS);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return operation();
|
|
743
|
+
} finally {
|
|
744
|
+
if (acquired && parsedLockOwner(lock)?.token === owner.token) {
|
|
745
|
+
(0, import_node_fs7.rmSync)(lock, { force: true });
|
|
746
|
+
fsyncDirectory(root);
|
|
747
|
+
}
|
|
748
|
+
(0, import_node_fs7.rmSync)(ownerFile, { force: true });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function persistOrdinals(file, map, beforePersist) {
|
|
752
|
+
const temporary = `${file}.tmp-${(0, import_node_crypto2.randomUUID)()}`;
|
|
753
|
+
let fd;
|
|
754
|
+
try {
|
|
755
|
+
fd = (0, import_node_fs7.openSync)(temporary, "wx");
|
|
756
|
+
(0, import_node_fs7.writeFileSync)(fd, `${JSON.stringify(map)}
|
|
757
|
+
`, "utf8");
|
|
758
|
+
(0, import_node_fs7.fsyncSync)(fd);
|
|
759
|
+
(0, import_node_fs7.closeSync)(fd);
|
|
760
|
+
fd = void 0;
|
|
761
|
+
beforePersist?.();
|
|
762
|
+
(0, import_node_fs7.renameSync)(temporary, file);
|
|
763
|
+
fsyncDirectory((0, import_node_path9.dirname)(file));
|
|
764
|
+
} finally {
|
|
765
|
+
if (fd !== void 0) (0, import_node_fs7.closeSync)(fd);
|
|
766
|
+
(0, import_node_fs7.rmSync)(temporary, { force: true });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function nextBuildOrdinal(opts) {
|
|
770
|
+
return withCounterLock(opts.root, () => {
|
|
771
|
+
const file = (0, import_node_path9.join)(opts.root, BUILD_ORDINALS_FILE);
|
|
772
|
+
const map = readOrdinals(file, opts.product, true);
|
|
773
|
+
const current = Object.hasOwn(map, opts.product) ? map[opts.product] : 0;
|
|
774
|
+
if (current === Number.MAX_SAFE_INTEGER) {
|
|
775
|
+
throw new BuildOrdinalReservationError("counter-exhausted", opts.product, current);
|
|
776
|
+
}
|
|
777
|
+
const next = current + 1;
|
|
778
|
+
map[opts.product] = next;
|
|
779
|
+
persistOrdinals(file, map);
|
|
780
|
+
return next;
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
async function reserveBuildOrdinalForProducts(opts) {
|
|
784
|
+
const products = Object.freeze([...new Set(opts.products)].sort());
|
|
785
|
+
if (products.length === 0) return void 0;
|
|
786
|
+
return withCounterLock(opts.root, () => {
|
|
787
|
+
const file = (0, import_node_path9.join)(opts.root, BUILD_ORDINALS_FILE);
|
|
788
|
+
const map = readOrdinals(file, products[0], false);
|
|
789
|
+
let maximum = 0;
|
|
790
|
+
for (const product of products) {
|
|
791
|
+
if (!Object.hasOwn(map, product)) {
|
|
792
|
+
throw new BuildOrdinalReservationError("missing-counter", product);
|
|
793
|
+
}
|
|
794
|
+
const current = map[product];
|
|
795
|
+
if (current === Number.MAX_SAFE_INTEGER) {
|
|
796
|
+
throw new BuildOrdinalReservationError("counter-exhausted", product, current);
|
|
797
|
+
}
|
|
798
|
+
maximum = Math.max(maximum, current);
|
|
799
|
+
}
|
|
800
|
+
const ordinal = maximum + 1;
|
|
801
|
+
for (const product of products) map[product] = ordinal;
|
|
802
|
+
const reservation = Object.freeze({ products, ordinal });
|
|
803
|
+
persistOrdinals(file, map, () => opts.beforePersist?.(reservation));
|
|
804
|
+
return ordinal;
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function readOrdinals(file, product, missingIsEmpty) {
|
|
808
|
+
let raw;
|
|
809
|
+
try {
|
|
810
|
+
raw = (0, import_node_fs7.readFileSync)(file, "utf-8");
|
|
811
|
+
} catch (error) {
|
|
812
|
+
if (error.code === "ENOENT") {
|
|
813
|
+
if (missingIsEmpty) return /* @__PURE__ */ Object.create(null);
|
|
814
|
+
throw new BuildOrdinalReservationError("missing-counter", product);
|
|
815
|
+
}
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
let parsed;
|
|
819
|
+
try {
|
|
820
|
+
parsed = JSON.parse(raw);
|
|
821
|
+
} catch {
|
|
822
|
+
throw new BuildOrdinalReservationError("invalid-state", product);
|
|
823
|
+
}
|
|
824
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
825
|
+
throw new BuildOrdinalReservationError("invalid-state", product, parsed);
|
|
826
|
+
}
|
|
827
|
+
const map = /* @__PURE__ */ Object.create(null);
|
|
828
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
829
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
830
|
+
throw new BuildOrdinalReservationError("invalid-counter", key, value);
|
|
831
|
+
}
|
|
832
|
+
map[key] = value;
|
|
833
|
+
}
|
|
834
|
+
return map;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/build-tag-map.ts
|
|
838
|
+
var import_node_fs8 = require("fs");
|
|
839
|
+
var import_node_path10 = require("path");
|
|
840
|
+
var BUILD_TAGS_FILE = "build-tags.json";
|
|
841
|
+
async function recordBuildTag(opts) {
|
|
842
|
+
const map = readBuildTagMap({ root: opts.root });
|
|
843
|
+
const productTags = Object.prototype.hasOwnProperty.call(map, opts.product) ? map[opts.product] : void 0;
|
|
844
|
+
if (productTags && Object.prototype.hasOwnProperty.call(productTags, opts.tag)) return;
|
|
845
|
+
const nextMap = {
|
|
846
|
+
...map,
|
|
847
|
+
[opts.product]: {
|
|
848
|
+
...productTags,
|
|
849
|
+
[opts.tag]: opts.ordinal
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
const file = (0, import_node_path10.join)(opts.root, BUILD_TAGS_FILE);
|
|
853
|
+
const tmp = `${file}.tmp`;
|
|
854
|
+
(0, import_node_fs8.writeFileSync)(tmp, `${JSON.stringify(nextMap)}
|
|
855
|
+
`, "utf-8");
|
|
856
|
+
(0, import_node_fs8.renameSync)(tmp, file);
|
|
857
|
+
}
|
|
858
|
+
function resolveBuildTag(opts) {
|
|
859
|
+
return readBuildTagMap({ root: opts.root })[opts.product]?.[opts.tag];
|
|
860
|
+
}
|
|
861
|
+
function latestBuildOrdinal(opts) {
|
|
862
|
+
const parsed = readJsonRecord((0, import_node_path10.join)(opts.root, BUILD_ORDINALS_FILE));
|
|
863
|
+
const value = parsed?.[opts.product];
|
|
864
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
865
|
+
}
|
|
866
|
+
function readBuildTagMap(opts) {
|
|
867
|
+
const parsed = readJsonRecord((0, import_node_path10.join)(opts.root, BUILD_TAGS_FILE));
|
|
868
|
+
if (!parsed) return {};
|
|
869
|
+
const products = [];
|
|
870
|
+
for (const [product, tags] of Object.entries(parsed)) {
|
|
871
|
+
if (!isRecord(tags)) continue;
|
|
872
|
+
const validTags = Object.fromEntries(
|
|
873
|
+
Object.entries(tags).filter(
|
|
874
|
+
(entry) => typeof entry[1] === "number" && Number.isFinite(entry[1])
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
products.push([product, validTags]);
|
|
878
|
+
}
|
|
879
|
+
return Object.fromEntries(products);
|
|
880
|
+
}
|
|
881
|
+
function readJsonRecord(file) {
|
|
882
|
+
try {
|
|
883
|
+
const parsed = JSON.parse((0, import_node_fs8.readFileSync)(file, "utf-8"));
|
|
884
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
885
|
+
} catch {
|
|
886
|
+
return void 0;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function isRecord(value) {
|
|
890
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/hook-executor.ts
|
|
894
|
+
var import_node_child_process2 = require("child_process");
|
|
895
|
+
var import_promises3 = require("fs/promises");
|
|
896
|
+
var import_node_path11 = require("path");
|
|
897
|
+
var import_node_os = require("os");
|
|
898
|
+
var import_agent_qa_runner = require("@vostride/agent-qa-runner");
|
|
899
|
+
var DEFAULT_DOCKER_BIN = "docker";
|
|
900
|
+
var DockerHookExecutor = class {
|
|
901
|
+
dockerBin;
|
|
902
|
+
pullPolicy;
|
|
903
|
+
constructor(config = {}) {
|
|
904
|
+
this.dockerBin = config.dockerBin ?? DEFAULT_DOCKER_BIN;
|
|
905
|
+
this.pullPolicy = config.pullPolicy ?? "if-not-present";
|
|
906
|
+
}
|
|
907
|
+
async execute(hook, opts) {
|
|
908
|
+
const dockerBin = this.dockerBin;
|
|
909
|
+
const pullPolicy = this.pullPolicy;
|
|
910
|
+
const start = Date.now();
|
|
911
|
+
const image = import_agent_qa_runner.RUNTIME_IMAGE_MAP[hook.runtime];
|
|
912
|
+
if (pullPolicy === "always" || pullPolicy === "if-not-present") {
|
|
913
|
+
const needsPull = pullPolicy === "always" || !await imageExists(dockerBin, image);
|
|
914
|
+
if (needsPull) {
|
|
915
|
+
await pullImage(dockerBin, image);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
let workDir;
|
|
919
|
+
try {
|
|
920
|
+
workDir = await (0, import_promises3.mkdtemp)((0, import_node_path11.join)((0, import_node_os.tmpdir)(), "agent-qa-hook-"));
|
|
921
|
+
await (0, import_promises3.mkdir)((0, import_node_path11.join)(workDir, "tmp"), { recursive: true });
|
|
922
|
+
await (0, import_promises3.cp)(hook.file, (0, import_node_path11.join)(workDir, (0, import_node_path11.basename)(hook.file)));
|
|
923
|
+
for (const dep of hook.deps) {
|
|
924
|
+
await (0, import_promises3.cp)(dep, (0, import_node_path11.join)(workDir, (0, import_node_path11.basename)(dep)));
|
|
925
|
+
}
|
|
926
|
+
if (hook.packageFile) {
|
|
927
|
+
await (0, import_promises3.cp)(hook.packageFile, (0, import_node_path11.join)(workDir, (0, import_node_path11.basename)(hook.packageFile)));
|
|
928
|
+
}
|
|
929
|
+
if (opts.authState) {
|
|
930
|
+
const authStateDir = (0, import_node_path11.join)(workDir, import_agent_qa_runner.AUTH_STATE_HOOK_WORKSPACE_DIR);
|
|
931
|
+
await (0, import_promises3.mkdir)(authStateDir, { recursive: true });
|
|
932
|
+
await (0, import_promises3.cp)(opts.authState.storageStatePath, (0, import_node_path11.join)(authStateDir, import_agent_qa_runner.AUTH_STATE_HOOK_STORAGE_STATE_FILENAME));
|
|
933
|
+
}
|
|
934
|
+
const args = buildDockerArgs({
|
|
935
|
+
image,
|
|
936
|
+
workDir,
|
|
937
|
+
hook,
|
|
938
|
+
env: opts.env,
|
|
939
|
+
secretEnv: opts.secretEnv,
|
|
940
|
+
authState: opts.authState
|
|
941
|
+
});
|
|
942
|
+
const cmd = getHookCommand(hook.runtime, (0, import_node_path11.basename)(hook.file));
|
|
943
|
+
args.push(...cmd);
|
|
944
|
+
const { stdout, stderr, exitCode } = await execDocker(dockerBin, args, hook.timeout);
|
|
945
|
+
let variables = {};
|
|
946
|
+
try {
|
|
947
|
+
const envContent = await (0, import_promises3.readFile)((0, import_node_path11.join)(workDir, "tmp", "agent-qa.env"), "utf-8");
|
|
948
|
+
variables = (0, import_agent_qa_runner.parseEnvFile)(envContent);
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
variables = (0, import_agent_qa_runner.stripReservedAuthStateHookEnv)(filterSecretVariables(variables, opts.secretEnv));
|
|
952
|
+
const redactedStdout = (0, import_agent_qa_runner.redactSecretValue)(stdout, opts.secretRedactor);
|
|
953
|
+
const redactedStderr = (0, import_agent_qa_runner.redactSecretValue)(stderr, opts.secretRedactor);
|
|
954
|
+
const output = redactedStdout.trim();
|
|
955
|
+
return {
|
|
956
|
+
success: exitCode === 0,
|
|
957
|
+
variables,
|
|
958
|
+
output: output || redactedStderr,
|
|
959
|
+
stdout: redactedStdout,
|
|
960
|
+
stderr: redactedStderr,
|
|
961
|
+
duration: Date.now() - start,
|
|
962
|
+
error: exitCode !== 0 ? (0, import_agent_qa_runner.redactSecretValue)(`Hook exited with code ${exitCode}: ${stderr}`, opts.secretRedactor) : void 0
|
|
963
|
+
};
|
|
964
|
+
} catch (err) {
|
|
965
|
+
return {
|
|
966
|
+
success: false,
|
|
967
|
+
variables: {},
|
|
968
|
+
output: "",
|
|
969
|
+
stdout: "",
|
|
970
|
+
stderr: "",
|
|
971
|
+
duration: Date.now() - start,
|
|
972
|
+
error: (0, import_agent_qa_runner.redactSecretValue)(err.message, opts.secretRedactor)
|
|
973
|
+
};
|
|
974
|
+
} finally {
|
|
975
|
+
if (workDir) {
|
|
976
|
+
await (0, import_promises3.rm)(workDir, { recursive: true, force: true }).catch(() => {
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
async function checkDockerAvailable(dockerBin = DEFAULT_DOCKER_BIN) {
|
|
983
|
+
return new Promise((resolve3) => {
|
|
984
|
+
(0, import_node_child_process2.execFile)(dockerBin, ["info", "--format", "{{.ID}}"], { timeout: 5e3 }, (err) => {
|
|
985
|
+
resolve3(!err);
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
function getHookCommand(runtime, entryFile) {
|
|
990
|
+
switch (runtime) {
|
|
991
|
+
case "node":
|
|
992
|
+
return ["node", entryFile];
|
|
993
|
+
case "bun":
|
|
994
|
+
return ["bun", entryFile];
|
|
995
|
+
case "python":
|
|
996
|
+
return ["python3", entryFile];
|
|
997
|
+
case "bash":
|
|
998
|
+
return ["bash", entryFile];
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function buildDockerArgs(input) {
|
|
1002
|
+
const args = [
|
|
1003
|
+
"run",
|
|
1004
|
+
"--rm",
|
|
1005
|
+
"--init",
|
|
1006
|
+
"-v",
|
|
1007
|
+
`${input.workDir}:/workspace`,
|
|
1008
|
+
"-w",
|
|
1009
|
+
"/workspace",
|
|
1010
|
+
"--memory",
|
|
1011
|
+
"512m",
|
|
1012
|
+
"--cpus",
|
|
1013
|
+
"1",
|
|
1014
|
+
"--pids-limit",
|
|
1015
|
+
"256",
|
|
1016
|
+
"--read-only",
|
|
1017
|
+
"-v",
|
|
1018
|
+
`${input.workDir}/tmp:/tmp`
|
|
1019
|
+
];
|
|
1020
|
+
if (!input.hook.network) {
|
|
1021
|
+
args.push("--network", "none");
|
|
1022
|
+
}
|
|
1023
|
+
const envVars = { ...input.env };
|
|
1024
|
+
Object.assign(envVars, input.secretEnv);
|
|
1025
|
+
Object.assign(envVars, (0, import_agent_qa_runner.buildAuthStateHookEnv)(
|
|
1026
|
+
input.authState,
|
|
1027
|
+
import_agent_qa_runner.AUTH_STATE_HOOK_CONTAINER_STORAGE_STATE_PATH
|
|
1028
|
+
));
|
|
1029
|
+
for (const [k, v] of Object.entries(envVars)) {
|
|
1030
|
+
args.push("-e", `${k}=${v}`);
|
|
1031
|
+
}
|
|
1032
|
+
args.push(input.image);
|
|
1033
|
+
return args;
|
|
1034
|
+
}
|
|
1035
|
+
function filterSecretVariables(variables, secretEnv) {
|
|
1036
|
+
const secretValues = new Set(Object.values(secretEnv));
|
|
1037
|
+
if (secretValues.size === 0) return variables;
|
|
1038
|
+
return Object.fromEntries(
|
|
1039
|
+
Object.entries(variables).filter(([_key, value]) => !secretValues.has(value))
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
function imageExists(dockerBin, image) {
|
|
1043
|
+
return new Promise((resolve3) => {
|
|
1044
|
+
(0, import_node_child_process2.execFile)(dockerBin, ["image", "inspect", image], { timeout: 1e4 }, (err) => {
|
|
1045
|
+
resolve3(!err);
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function pullImage(dockerBin, image) {
|
|
1050
|
+
return new Promise((resolve3, reject) => {
|
|
1051
|
+
(0, import_node_child_process2.execFile)(dockerBin, ["pull", image], { timeout: 12e4 }, (err) => {
|
|
1052
|
+
if (err) reject(new Error(`Failed to pull image ${image}: ${err.message}`));
|
|
1053
|
+
else resolve3();
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
function execDocker(dockerBin, args, timeout) {
|
|
1058
|
+
return new Promise((resolve3) => {
|
|
1059
|
+
let stdout = "";
|
|
1060
|
+
let stderr = "";
|
|
1061
|
+
let resolved = false;
|
|
1062
|
+
const _proc = (0, import_node_child_process2.execFile)(dockerBin, args, { timeout, maxBuffer: 4 * 1024 * 1024 }, (err, out, errOut) => {
|
|
1063
|
+
if (resolved) return;
|
|
1064
|
+
resolved = true;
|
|
1065
|
+
stdout = out ?? "";
|
|
1066
|
+
stderr = errOut ?? "";
|
|
1067
|
+
if (err && "killed" in err && err.killed) {
|
|
1068
|
+
resolve3({ stdout, stderr: `Hook timed out after ${timeout}ms`, exitCode: 124 });
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const exitCode = err?.code ?? 0;
|
|
1072
|
+
resolve3({ stdout, stderr, exitCode: typeof exitCode === "number" ? exitCode : 1 });
|
|
1073
|
+
});
|
|
1074
|
+
void _proc;
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1078
|
+
0 && (module.exports = {
|
|
1079
|
+
ARTIFACT_FILE,
|
|
1080
|
+
BUILD_ORDINALS_FILE,
|
|
1081
|
+
BUILD_TAGS_FILE,
|
|
1082
|
+
BuildOrdinalReservationError,
|
|
1083
|
+
CACHE_SCHEMA_VERSION,
|
|
1084
|
+
DockerHookExecutor,
|
|
1085
|
+
EVENTS_FILE,
|
|
1086
|
+
FsActionCache,
|
|
1087
|
+
FsEventLog,
|
|
1088
|
+
FsEvidenceArtifactStore,
|
|
1089
|
+
FsHandoffStore,
|
|
1090
|
+
FsRunArtifactStore,
|
|
1091
|
+
FsStorageStateStore,
|
|
1092
|
+
HANDOFF_FILE,
|
|
1093
|
+
RUNS_SUBDIR,
|
|
1094
|
+
RUN_EVIDENCE_BLOBS_SUBDIR,
|
|
1095
|
+
STORAGE_STATES_SUBDIR,
|
|
1096
|
+
SharpScreenshotCompressor,
|
|
1097
|
+
alignToWindow,
|
|
1098
|
+
checkDockerAvailable,
|
|
1099
|
+
flattenOntoWhite,
|
|
1100
|
+
getImageDimensions,
|
|
1101
|
+
latestBuildOrdinal,
|
|
1102
|
+
loadCompletedRunHandoff,
|
|
1103
|
+
nextBuildOrdinal,
|
|
1104
|
+
readBuildTagMap,
|
|
1105
|
+
recordBuildTag,
|
|
1106
|
+
reserveBuildOrdinalForProducts,
|
|
1107
|
+
resolveBranch,
|
|
1108
|
+
resolveBuildTag,
|
|
1109
|
+
shouldWrite,
|
|
1110
|
+
slugBranch
|
|
1111
|
+
});
|
|
1112
|
+
//# sourceMappingURL=index.cjs.map
|