@prismatic-io/lux 0.0.2-preview.21 → 0.0.2-preview.22
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/lib/assertions/rubric/index.d.ts.map +1 -1
- package/lib/assertions/rubric/index.js +1 -0
- package/lib/assertions/rubric/index.js.map +1 -1
- package/lib/assertions/rubric/internal.d.ts.map +1 -1
- package/lib/assertions/rubric/internal.js +42 -8
- package/lib/assertions/rubric/internal.js.map +1 -1
- package/lib/drivers/antigravity/config.d.ts +7 -1
- package/lib/drivers/antigravity/config.d.ts.map +1 -1
- package/lib/drivers/antigravity/config.js +8 -2
- package/lib/drivers/antigravity/config.js.map +1 -1
- package/lib/drivers/antigravity/events.d.ts.map +1 -1
- package/lib/drivers/antigravity/events.js +54 -6
- package/lib/drivers/antigravity/events.js.map +1 -1
- package/lib/drivers/antigravity/index.d.ts +8 -0
- package/lib/drivers/antigravity/index.d.ts.map +1 -1
- package/lib/drivers/antigravity/index.js +4 -3
- package/lib/drivers/antigravity/index.js.map +1 -1
- package/lib/drivers/codex/config.d.ts +1 -0
- package/lib/drivers/codex/config.d.ts.map +1 -1
- package/lib/drivers/codex/config.js +16 -2
- package/lib/drivers/codex/config.js.map +1 -1
- package/lib/drivers/codex/index.d.ts.map +1 -1
- package/lib/drivers/codex/index.js +2 -2
- package/lib/drivers/codex/index.js.map +1 -1
- package/lib/drivers/subprocess/index.js +1 -1
- package/lib/drivers/subprocess/index.js.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +15 -3
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/lib/orchestrator/run-workspace.d.ts +10 -0
- package/lib/orchestrator/run-workspace.d.ts.map +1 -0
- package/lib/orchestrator/run-workspace.js +58 -0
- package/lib/orchestrator/run-workspace.js.map +1 -0
- package/package.json +1 -1
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/assertions/rubric/index.ts +1 -0
- package/src/assertions/rubric/internal.ts +35 -7
- package/src/drivers/antigravity/README.md +18 -6
- package/src/drivers/antigravity/config.ts +8 -2
- package/src/drivers/antigravity/events.ts +61 -6
- package/src/drivers/antigravity/index.ts +7 -3
- package/src/drivers/codex/config.ts +23 -2
- package/src/drivers/codex/index.ts +2 -1
- package/src/drivers/subprocess/index.ts +1 -1
- package/src/orchestrator/run-execution.ts +16 -3
- package/src/orchestrator/run-workspace.ts +62 -0
|
@@ -232,19 +232,47 @@ export const summarizeEvents = (
|
|
|
232
232
|
evidence?: RubricEvidence,
|
|
233
233
|
): string => {
|
|
234
234
|
const lines: string[] = [];
|
|
235
|
-
const
|
|
235
|
+
const groups: RunEvent[][] = [];
|
|
236
|
+
let previous: RunEvent | undefined;
|
|
237
|
+
for (const event of events) {
|
|
238
|
+
if (eventSelected(event, evidence)) {
|
|
239
|
+
const sameSpeaker =
|
|
240
|
+
event.type === "progress" &&
|
|
241
|
+
previous?.type === "progress" &&
|
|
242
|
+
event.kind === "agent-message" &&
|
|
243
|
+
previous.kind === "agent-message" &&
|
|
244
|
+
event.agent?.id === previous.agent?.id;
|
|
245
|
+
if (sameSpeaker && groups.length > 0) groups.at(-1)?.push(event);
|
|
246
|
+
else groups.push([event]);
|
|
247
|
+
}
|
|
248
|
+
previous = event;
|
|
249
|
+
}
|
|
236
250
|
// Grading only the tail would silently hide the start of a long run from
|
|
237
251
|
// the judge; make the omission explicit rather than invisible.
|
|
238
|
-
if (
|
|
239
|
-
lines.push(`[... ${
|
|
252
|
+
if (groups.length > cap) {
|
|
253
|
+
lines.push(`[... ${groups.length - cap} earlier events omitted ...]`);
|
|
240
254
|
}
|
|
241
|
-
const
|
|
255
|
+
const tailGroups = cap === 0 ? [] : groups.slice(-cap);
|
|
256
|
+
const tail = tailGroups.flat();
|
|
242
257
|
const agentTextAllotments = allotAgentText(tail, evidence?.maxAgentChars ?? 30_000);
|
|
243
258
|
const toolDetail = evidence?.toolDetail ?? "names";
|
|
244
259
|
const toolText = allotToolText(tail, evidence?.maxToolChars ?? 20_000, toolDetail);
|
|
245
|
-
for (const
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
for (const group of tailGroups) {
|
|
261
|
+
if (group.length > 1) {
|
|
262
|
+
const parts: string[] = [];
|
|
263
|
+
for (const event of group) {
|
|
264
|
+
if (event.type !== "progress") continue;
|
|
265
|
+
const text = (event.payload as { text?: unknown })?.text;
|
|
266
|
+
if (typeof text !== "string") continue;
|
|
267
|
+
const take = agentTextAllotments.get(event) ?? 0;
|
|
268
|
+
if (take > 0) parts.push(text.slice(0, take));
|
|
269
|
+
if (take < text.length && parts.at(-1) !== "… [truncated]") parts.push("… [truncated]");
|
|
270
|
+
}
|
|
271
|
+
lines.push(`agent: ${parts.join("")}`);
|
|
272
|
+
} else if (group[0]) {
|
|
273
|
+
const line = eventLine(group[0], agentTextAllotments, toolText.allotments, toolText.rendered);
|
|
274
|
+
if (line !== null) lines.push(line);
|
|
275
|
+
}
|
|
248
276
|
}
|
|
249
277
|
return lines.join("\n");
|
|
250
278
|
};
|
|
@@ -27,18 +27,30 @@ Lux writes one `user` event with the case prompt, closes stdin, and reads
|
|
|
27
27
|
`init`, `step_update`, and `result` events. Tool steps become paired
|
|
28
28
|
`tool-call` and `tool-result` events keyed by step index, response deltas become
|
|
29
29
|
`agent-message` progress, other steps stay visible as `antigravity-step`
|
|
30
|
-
progress, and the final `result` usage is recorded as agent usage.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
progress, and the final `result` usage is recorded as agent usage. The terminal
|
|
31
|
+
response is also retained as message evidence when it was not already streamed.
|
|
32
|
+
Both `DONE` and `ERROR` tool steps close the corresponding tool call; failed
|
|
33
|
+
steps retain their error message. A result with `denied_actions` is an error even
|
|
34
|
+
when Antigravity reports `SUCCESS`. `WAITING` and other non-success statuses are
|
|
35
|
+
also errors. Configure the required scoped permissions in Antigravity before a
|
|
36
|
+
headless evaluation; Lux does not retry a denial with broader permissions.
|
|
37
|
+
|
|
38
|
+
Each run creates a fresh Antigravity project and passes its canonical working
|
|
39
|
+
directory through `--add-dir` and an explicit task-workspace prompt preamble.
|
|
40
|
+
Set `project` to reuse an explicitly configured
|
|
41
|
+
project, and `addDirs` for additional reference directories. Process cwd alone
|
|
42
|
+
does not select an Antigravity project. Project selection does not grant tool
|
|
43
|
+
permissions: Antigravity 1.2.10 can still deny workspace writes in headless mode.
|
|
44
|
+
`skipPermissions` remains false by default and must be explicitly enabled to
|
|
45
|
+
bypass tool permission checks.
|
|
34
46
|
|
|
35
47
|
The headless protocol carries no user questions or permission prompts, so the
|
|
36
48
|
driver is not interactive and declares no isolation: Antigravity still reads
|
|
37
49
|
its user settings and conversation history. Controlled experiments are not
|
|
38
50
|
declared.
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
Regression tests cover the `ERROR` tool step followed by `SUCCESS` with denied
|
|
53
|
+
actions observed in an authenticated Antigravity 1.2.10 session.
|
|
42
54
|
|
|
43
55
|
Protocol references:
|
|
44
56
|
|
|
@@ -8,9 +8,12 @@ export const AntigravityDriverConfigSchema = z
|
|
|
8
8
|
model: z.string().min(1),
|
|
9
9
|
reasoningEffort: z.enum(antigravityCatalog.efforts).optional(),
|
|
10
10
|
agent: z.string().min(1).optional(),
|
|
11
|
+
/** Use an existing project; otherwise each run creates a fresh project. */
|
|
12
|
+
project: z.string().min(1).optional(),
|
|
13
|
+
addDirs: z.array(z.string().min(1)).default([]),
|
|
11
14
|
/**
|
|
12
15
|
* Auto-approve every tool permission. Off by default: a headless turn that
|
|
13
|
-
* needs approval
|
|
16
|
+
* needs approval can report WAITING or SUCCESS with denied_actions; both fail.
|
|
14
17
|
*/
|
|
15
18
|
skipPermissions: z.boolean().default(false),
|
|
16
19
|
/** Enable Antigravity's terminal sandbox restrictions. */
|
|
@@ -26,13 +29,16 @@ export const AntigravityDriverConfigSchema = z
|
|
|
26
29
|
|
|
27
30
|
export type AntigravityDriverConfig = z.infer<typeof AntigravityDriverConfigSchema>;
|
|
28
31
|
|
|
29
|
-
export const buildAntigravityArgs = (config: AntigravityDriverConfig): string[] => [
|
|
32
|
+
export const buildAntigravityArgs = (config: AntigravityDriverConfig, cwd?: string): string[] => [
|
|
30
33
|
"--input-format",
|
|
31
34
|
"stream-json",
|
|
32
35
|
"--output-format",
|
|
33
36
|
"stream-json",
|
|
34
37
|
"--model",
|
|
35
38
|
config.model,
|
|
39
|
+
...(config.project ? ["--project", config.project] : ["--new-project"]),
|
|
40
|
+
...(cwd ? ["--add-dir", cwd] : []),
|
|
41
|
+
...config.addDirs.flatMap((directory) => ["--add-dir", directory]),
|
|
36
42
|
...(config.reasoningEffort ? ["--effort", config.reasoningEffort] : []),
|
|
37
43
|
...(config.agent ? ["--agent", config.agent] : []),
|
|
38
44
|
...(config.skipPermissions ? ["--dangerously-skip-permissions"] : []),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import {
|
|
3
4
|
type ParsedDriverEvent,
|
|
@@ -38,6 +39,9 @@ const ResultSchema = z.looseObject({
|
|
|
38
39
|
status: z.string().optional(),
|
|
39
40
|
response: z.string().optional(),
|
|
40
41
|
error: z.string().optional(),
|
|
42
|
+
denied_actions: z
|
|
43
|
+
.array(z.looseObject({ action: z.string(), display_name: z.string().optional() }))
|
|
44
|
+
.optional(),
|
|
41
45
|
num_turns: z.number().optional(),
|
|
42
46
|
usage: z.unknown().optional(),
|
|
43
47
|
});
|
|
@@ -64,8 +68,8 @@ const parseUsage = (raw: unknown): TokenUsage | null => {
|
|
|
64
68
|
const usage = UsageSchema.safeParse(raw).data;
|
|
65
69
|
if (!usage) return null;
|
|
66
70
|
return {
|
|
67
|
-
input:
|
|
68
|
-
output: usage.output_tokens
|
|
71
|
+
input: usage.input_tokens,
|
|
72
|
+
output: usage.output_tokens,
|
|
69
73
|
cacheRead: usage.cache_read_tokens,
|
|
70
74
|
cacheCreation: 0,
|
|
71
75
|
};
|
|
@@ -125,7 +129,7 @@ const toolStepEvents = (
|
|
|
125
129
|
),
|
|
126
130
|
});
|
|
127
131
|
}
|
|
128
|
-
if (step.state === "DONE") {
|
|
132
|
+
if (step.state === "DONE" || step.state === "ERROR") {
|
|
129
133
|
openTools.delete(step.step_index);
|
|
130
134
|
const error = step.tool_info?.error;
|
|
131
135
|
events.push({
|
|
@@ -136,8 +140,12 @@ const toolStepEvents = (
|
|
|
136
140
|
name,
|
|
137
141
|
toolUseId,
|
|
138
142
|
content: step.tool_info?.output ?? null,
|
|
139
|
-
isError: error !== undefined,
|
|
140
|
-
text:
|
|
143
|
+
isError: step.state === "ERROR" || error !== undefined,
|
|
144
|
+
text:
|
|
145
|
+
error?.message ??
|
|
146
|
+
(step.state === "ERROR"
|
|
147
|
+
? "Antigravity tool failed"
|
|
148
|
+
: outputText(step.tool_info?.output)),
|
|
141
149
|
},
|
|
142
150
|
ts,
|
|
143
151
|
),
|
|
@@ -179,6 +187,21 @@ const resultEvent = (
|
|
|
179
187
|
conversationId: string | undefined,
|
|
180
188
|
usage: TokenUsage | null,
|
|
181
189
|
): ParsedDriverEvent => {
|
|
190
|
+
if (result?.denied_actions?.length) {
|
|
191
|
+
return {
|
|
192
|
+
tag: "error",
|
|
193
|
+
error: {
|
|
194
|
+
exitReason: "error",
|
|
195
|
+
reason: `Antigravity could not complete because tool permissions were denied: ${result.denied_actions.map(({ action }) => action).join(", ")}`,
|
|
196
|
+
summary: {
|
|
197
|
+
deniedActions: result.denied_actions,
|
|
198
|
+
...(result.response !== undefined ? { response: result.response } : {}),
|
|
199
|
+
...(conversationId ? { conversationId } : {}),
|
|
200
|
+
...(usage ? { usage } : { usageAvailability: "unavailable" }),
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
182
205
|
if (result?.status === "SUCCESS") {
|
|
183
206
|
return {
|
|
184
207
|
tag: "done",
|
|
@@ -211,6 +234,8 @@ export const makeParseEvent = (config: AntigravityDriverConfig, cliVersion?: str
|
|
|
211
234
|
const openTools = new Set<number>();
|
|
212
235
|
let conversationId: string | undefined;
|
|
213
236
|
let usage: TokenUsage | null = null;
|
|
237
|
+
let responseStep: number | undefined;
|
|
238
|
+
let responseHash = createHash("sha256");
|
|
214
239
|
return (line: string): ParsedDriverEvent | ParsedDriverEvent[] | null => {
|
|
215
240
|
let raw: unknown;
|
|
216
241
|
try {
|
|
@@ -228,8 +253,38 @@ export const makeParseEvent = (config: AntigravityDriverConfig, cliVersion?: str
|
|
|
228
253
|
if (event.event === "step_update") {
|
|
229
254
|
if (!event.step_update) return null;
|
|
230
255
|
usage = parseUsage(event.step_update.usage) ?? usage;
|
|
256
|
+
if (event.step_update.step_type === "agent_response") {
|
|
257
|
+
if (responseStep !== event.step_update.step_index) {
|
|
258
|
+
responseHash = createHash("sha256");
|
|
259
|
+
responseStep = event.step_update.step_index;
|
|
260
|
+
}
|
|
261
|
+
responseHash.update(event.step_update.text_delta ?? "");
|
|
262
|
+
}
|
|
231
263
|
return stepEvents(event.step_update, openTools, Date.now());
|
|
232
264
|
}
|
|
233
|
-
|
|
265
|
+
const terminal = resultEvent(
|
|
266
|
+
event.result,
|
|
267
|
+
conversationId,
|
|
268
|
+
parseUsage(event.result?.usage) ?? usage,
|
|
269
|
+
);
|
|
270
|
+
if (
|
|
271
|
+
event.result?.response &&
|
|
272
|
+
createHash("sha256").update(event.result.response).digest("hex") !==
|
|
273
|
+
responseHash.copy().digest("hex")
|
|
274
|
+
) {
|
|
275
|
+
return [
|
|
276
|
+
{
|
|
277
|
+
tag: "progress",
|
|
278
|
+
progress: {
|
|
279
|
+
id: "antigravity-final-response",
|
|
280
|
+
kind: "agent-message",
|
|
281
|
+
payload: { text: event.result.response },
|
|
282
|
+
ts: Date.now(),
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
terminal,
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
return terminal;
|
|
234
289
|
};
|
|
235
290
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
1
2
|
import {
|
|
2
3
|
type AgentDriver,
|
|
3
4
|
type Answer,
|
|
@@ -42,17 +43,20 @@ class AntigravityDriver implements AgentDriver {
|
|
|
42
43
|
|
|
43
44
|
async start(ctx: StartContext): Promise<ReadyState> {
|
|
44
45
|
if (this.inner) throw new Error("Antigravity driver cannot be started twice");
|
|
45
|
-
this.cwd = this.config.cwd ?? ctx.artifactsDir;
|
|
46
|
+
this.cwd = await realpath(this.config.cwd ?? ctx.artifactsDir);
|
|
46
47
|
this.artifactsDir = ctx.artifactsDir;
|
|
47
48
|
const cliVersion = await discoverCliVersion(this.config.command);
|
|
48
49
|
const inner = await subprocessDriver.create({
|
|
49
50
|
command: this.config.command,
|
|
50
|
-
args: buildAntigravityArgs(this.config),
|
|
51
|
+
args: buildAntigravityArgs(this.config, this.cwd),
|
|
51
52
|
cwd: this.cwd,
|
|
52
53
|
env: this.config.env,
|
|
53
54
|
protocol: "custom",
|
|
54
55
|
parseEvent: makeParseEvent(this.config, cliVersion),
|
|
55
|
-
formatPrompt:
|
|
56
|
+
formatPrompt: (prompt) =>
|
|
57
|
+
formatAntigravityPrompt(
|
|
58
|
+
`The task workspace is ${JSON.stringify(this.cwd)}. Treat that directory as the current directory for this task and create task artifacts there.\n\n${prompt}`,
|
|
59
|
+
),
|
|
56
60
|
closeStdinAfterPrompt: true,
|
|
57
61
|
idleTimeoutMs: this.config.idleTimeoutMs,
|
|
58
62
|
maxLineBytes: this.config.maxLineBytes,
|
|
@@ -115,6 +115,27 @@ const tomlValue = (value: CodexConfigValue): string => {
|
|
|
115
115
|
.join(",")}}`;
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
// Codex shell snapshots can replace the launching process's PATH. Pin only
|
|
119
|
+
// executable-search variables here; never put other environment secrets in argv.
|
|
120
|
+
export const codexConfigOverrides = (
|
|
121
|
+
config: CodexDriverConfig,
|
|
122
|
+
): Record<string, CodexConfigValue> => {
|
|
123
|
+
const authored = config.config ?? {};
|
|
124
|
+
const paths = Object.fromEntries(
|
|
125
|
+
Object.entries(config.env ?? {}).filter(
|
|
126
|
+
([key]) => key.toUpperCase() === "PATH" || key.toUpperCase() === "PATHEXT",
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
if (Object.keys(paths).length === 0) return authored;
|
|
130
|
+
const object = (value: CodexConfigValue | undefined): Record<string, CodexConfigValue> =>
|
|
131
|
+
value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
132
|
+
const policy = object(authored.shell_environment_policy);
|
|
133
|
+
return {
|
|
134
|
+
...authored,
|
|
135
|
+
shell_environment_policy: { ...policy, set: { ...paths, ...object(policy.set) } },
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
118
139
|
export const buildExecArgs = (config: CodexDriverConfig): string[] => {
|
|
119
140
|
const args = ["exec", "--json", "--color", "never", "--sandbox", config.sandbox];
|
|
120
141
|
args.push("--model", config.model);
|
|
@@ -125,7 +146,7 @@ export const buildExecArgs = (config: CodexDriverConfig): string[] => {
|
|
|
125
146
|
if (config.ignoreRules) args.push("--ignore-rules");
|
|
126
147
|
if (config.strictConfig) args.push("--strict-config");
|
|
127
148
|
for (const directory of config.addDirs ?? []) args.push("--add-dir", directory);
|
|
128
|
-
for (const [key, value] of Object.entries(config
|
|
149
|
+
for (const [key, value] of Object.entries(codexConfigOverrides(config))) {
|
|
129
150
|
args.push("--config", `${key}=${tomlValue(value)}`);
|
|
130
151
|
}
|
|
131
152
|
args.push(...effortArgs(config));
|
|
@@ -137,7 +158,7 @@ export const buildExecArgs = (config: CodexDriverConfig): string[] => {
|
|
|
137
158
|
export const buildAppServerArgs = (config: CodexDriverConfig): string[] => {
|
|
138
159
|
const args = ["app-server", "--stdio"];
|
|
139
160
|
if (config.strictConfig) args.push("--strict-config");
|
|
140
|
-
for (const [key, value] of Object.entries(config
|
|
161
|
+
for (const [key, value] of Object.entries(codexConfigOverrides(config))) {
|
|
141
162
|
args.push("--config", `${key}=${tomlValue(value)}`);
|
|
142
163
|
}
|
|
143
164
|
args.push(...effortArgs(config));
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
type CodexConfigValue,
|
|
42
42
|
type CodexDriverConfig,
|
|
43
43
|
CodexDriverConfigSchema,
|
|
44
|
+
codexConfigOverrides,
|
|
44
45
|
} from "./config.js";
|
|
45
46
|
|
|
46
47
|
import { makeParseEvent } from "./exec-events.js";
|
|
@@ -189,7 +190,7 @@ class CodexAppServerDriverImpl implements AgentDriver {
|
|
|
189
190
|
? await Promise.all(this.config.isolation.allowRead.map((path) => realpath(path)))
|
|
190
191
|
: [];
|
|
191
192
|
const threadConfig: Record<string, CodexConfigValue> = {
|
|
192
|
-
...(this.config
|
|
193
|
+
...codexConfigOverrides(this.config),
|
|
193
194
|
...(this.config.reasoningEffort
|
|
194
195
|
? { model_reasoning_effort: this.config.reasoningEffort }
|
|
195
196
|
: {}),
|
|
@@ -148,7 +148,7 @@ class SubprocessDriverImpl implements AgentDriver {
|
|
|
148
148
|
throw new Error(`subprocess fixture values exceed ${MAX_FIXTURE_VALUES_BYTES} bytes`);
|
|
149
149
|
}
|
|
150
150
|
const managed = spawnManagedProcess(this.config.command, this.config.args ?? [], {
|
|
151
|
-
cwd: this.config.cwd ?? ctx.
|
|
151
|
+
cwd: this.config.cwd ?? ctx.artifactsDir,
|
|
152
152
|
env: {
|
|
153
153
|
...(this.config.inheritEnv === false ? {} : process.env),
|
|
154
154
|
...this.config.env,
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type RunFailurePhase,
|
|
54
54
|
type RunLifecycleEffects,
|
|
55
55
|
} from "./run-lifecycle.js";
|
|
56
|
+
import { RunWorkspace } from "./run-workspace.js";
|
|
56
57
|
import { isRunTraceLimit, type RunTraceLimitFailure } from "./trace-limits.js";
|
|
57
58
|
|
|
58
59
|
/** Sentinel `casePath` for a pre-loaded case that has no file behind it. */
|
|
@@ -313,6 +314,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
313
314
|
private metadata: RunMetadata | undefined;
|
|
314
315
|
private usage: RunUsage | undefined;
|
|
315
316
|
private run: Run | undefined;
|
|
317
|
+
private workspace: RunWorkspace | undefined;
|
|
316
318
|
|
|
317
319
|
private constructor(
|
|
318
320
|
args: RunWithPluginsArgs,
|
|
@@ -369,10 +371,11 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
369
371
|
const { request, evalCase, casePath, driver } = this.args;
|
|
370
372
|
this.lifecycle("run-loop");
|
|
371
373
|
if (request.abortSignal?.aborted) return;
|
|
374
|
+
this.workspace = await RunWorkspace.create();
|
|
372
375
|
const staged = await stageFixtures(evalCase.fixtures, {
|
|
373
376
|
...(request.fixturesRoot ? { fixturesRoot: request.fixturesRoot } : {}),
|
|
374
377
|
casePath,
|
|
375
|
-
artifactsDir: this.
|
|
378
|
+
artifactsDir: this.workspace.path,
|
|
376
379
|
});
|
|
377
380
|
this.fixturesHash = staged?.hash;
|
|
378
381
|
if (request.abortSignal?.aborted) return;
|
|
@@ -387,7 +390,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
387
390
|
...(this.args.fixtureValues.length > 0 ? { fixtureValues: this.args.fixtureValues } : {}),
|
|
388
391
|
runsRoot: resolve(request.runsRoot),
|
|
389
392
|
runDir: this.runDir.path,
|
|
390
|
-
artifactsDir: this.
|
|
393
|
+
artifactsDir: this.workspace.path,
|
|
391
394
|
abortSignal: request.abortSignal ?? new AbortController().signal,
|
|
392
395
|
});
|
|
393
396
|
this.append({ type: "ready", ...this.ready });
|
|
@@ -463,6 +466,12 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
463
466
|
}
|
|
464
467
|
|
|
465
468
|
async close(primaryError: unknown | null): Promise<void> {
|
|
469
|
+
if (this.workspace) {
|
|
470
|
+
await closeQuietly(async () => {
|
|
471
|
+
await this.args.driver.close();
|
|
472
|
+
await this.workspace?.archive(this.runDir.artifactsDir);
|
|
473
|
+
}, `run workspace ${this.workspace.path}`);
|
|
474
|
+
}
|
|
466
475
|
if (primaryError instanceof Error) {
|
|
467
476
|
(primaryError as Error & RunDirCarrier).luxRunDir = this.runDir.path;
|
|
468
477
|
}
|
|
@@ -567,7 +576,11 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
567
576
|
if (!this.ready) return { artifacts: [] };
|
|
568
577
|
try {
|
|
569
578
|
await this.args.driver.quiesce?.();
|
|
570
|
-
|
|
579
|
+
const artifacts = await this.args.driver.collect();
|
|
580
|
+
return {
|
|
581
|
+
artifacts:
|
|
582
|
+
(await this.workspace?.archive(this.runDir.artifactsDir, artifacts)) ?? artifacts,
|
|
583
|
+
};
|
|
571
584
|
} catch (error) {
|
|
572
585
|
const reason = errorMessage(error);
|
|
573
586
|
this.runDir.events.append({
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { cp, lstat, mkdir, mkdtemp, realpath, rename, rm, rmdir } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import type { Artifact } from "../core/index.js";
|
|
5
|
+
|
|
6
|
+
/** Keep package-manager discovery and repository ignore rules outside subject workspaces. */
|
|
7
|
+
export class RunWorkspace {
|
|
8
|
+
private archived = false;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
|
|
11
|
+
private constructor(path: string) {
|
|
12
|
+
this.path = path;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static async create(): Promise<RunWorkspace> {
|
|
16
|
+
const root = join(tmpdir(), `lux-workspaces-${process.getuid?.() ?? "user"}`);
|
|
17
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
18
|
+
const metadata = await lstat(root);
|
|
19
|
+
if (
|
|
20
|
+
!metadata.isDirectory() ||
|
|
21
|
+
metadata.isSymbolicLink() ||
|
|
22
|
+
(process.getuid && metadata.uid !== process.getuid())
|
|
23
|
+
) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Lux workspace root must be a real directory owned by the current user: ${root}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return new RunWorkspace(await realpath(await mkdtemp(join(root, "run-"))));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async archive(destination: string, artifacts: Artifact[] = []): Promise<Artifact[]> {
|
|
32
|
+
if (!this.archived) {
|
|
33
|
+
const metadata = await lstat(this.path);
|
|
34
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
35
|
+
throw new Error(`run workspace must remain a real directory: ${this.path}`);
|
|
36
|
+
}
|
|
37
|
+
await rmdir(destination).catch((error: NodeJS.ErrnoException) => {
|
|
38
|
+
if (error.code !== "ENOENT") throw error;
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
await rename(this.path, destination);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if ((error as NodeJS.ErrnoException).code !== "EXDEV") throw error;
|
|
44
|
+
await cp(this.path, destination, { recursive: true, verbatimSymlinks: true });
|
|
45
|
+
await rm(this.path, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
this.archived = true;
|
|
48
|
+
}
|
|
49
|
+
return artifacts.map((artifact) => {
|
|
50
|
+
if (!artifact.root) return artifact;
|
|
51
|
+
const path = relative(this.path, resolve(artifact.root));
|
|
52
|
+
if (path === "") {
|
|
53
|
+
const { root: _root, ...captured } = artifact;
|
|
54
|
+
return captured;
|
|
55
|
+
}
|
|
56
|
+
if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) {
|
|
57
|
+
return artifact;
|
|
58
|
+
}
|
|
59
|
+
return { ...artifact, root: join(destination, path) };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|