@prismatic-io/lux 0.0.2-preview.27 → 0.0.2-preview.29
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/authoring.d.ts +1 -0
- package/lib/authoring.d.ts.map +1 -1
- package/lib/authoring.js +1 -0
- package/lib/authoring.js.map +1 -1
- package/lib/container/driver.d.ts +27 -0
- package/lib/container/driver.d.ts.map +1 -0
- package/lib/container/driver.js +197 -0
- package/lib/container/driver.js.map +1 -0
- package/lib/container/protocol.d.ts +12 -0
- package/lib/container/protocol.d.ts.map +1 -0
- package/lib/container/protocol.js +38 -0
- package/lib/container/protocol.js.map +1 -0
- package/lib/container/resources.d.ts +12 -0
- package/lib/container/resources.d.ts.map +1 -0
- package/lib/container/resources.js +133 -0
- package/lib/container/resources.js.map +1 -0
- package/lib/container/worker.d.ts +2 -0
- package/lib/container/worker.d.ts.map +1 -0
- package/lib/container/worker.js +85 -0
- package/lib/container/worker.js.map +1 -0
- package/lib/core/case.d.ts +80 -0
- package/lib/core/case.d.ts.map +1 -1
- package/lib/core/case.js +8 -1
- package/lib/core/case.js.map +1 -1
- package/lib/core/driver-execution.d.ts +83 -0
- package/lib/core/driver-execution.d.ts.map +1 -0
- package/lib/core/driver-execution.js +72 -0
- package/lib/core/driver-execution.js.map +1 -0
- package/lib/core/driver.d.ts +5 -1
- package/lib/core/driver.d.ts.map +1 -1
- package/lib/core/driver.js +5 -1
- package/lib/core/driver.js.map +1 -1
- package/lib/core/harness-catalogs/cursor.js +1 -1
- package/lib/core/harness-catalogs/cursor.js.map +1 -1
- package/lib/core/harness-matrix.d.ts +84 -0
- package/lib/core/harness-matrix.d.ts.map +1 -1
- package/lib/core/harness-matrix.js +11 -1
- package/lib/core/harness-matrix.js.map +1 -1
- package/lib/core/run.d.ts +40 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/drivers/antigravity/artifacts.d.ts +2 -0
- package/lib/drivers/antigravity/artifacts.d.ts.map +1 -0
- package/lib/drivers/antigravity/artifacts.js +33 -0
- package/lib/drivers/antigravity/artifacts.js.map +1 -0
- package/lib/drivers/antigravity/config.d.ts +27 -0
- package/lib/drivers/antigravity/config.d.ts.map +1 -1
- package/lib/drivers/antigravity/config.js +35 -0
- package/lib/drivers/antigravity/config.js.map +1 -1
- package/lib/drivers/antigravity/index.d.ts +35 -0
- package/lib/drivers/antigravity/index.d.ts.map +1 -1
- package/lib/drivers/antigravity/index.js +42 -20
- package/lib/drivers/antigravity/index.js.map +1 -1
- package/lib/drivers/antigravity/runtime.d.ts +7 -0
- package/lib/drivers/antigravity/runtime.d.ts.map +1 -0
- package/lib/drivers/antigravity/runtime.js +78 -0
- package/lib/drivers/antigravity/runtime.js.map +1 -0
- package/lib/drivers/copilot/config.d.ts +3 -3
- package/lib/drivers/copilot/config.js +5 -5
- package/lib/drivers/copilot/config.js.map +1 -1
- package/lib/drivers/copilot/index.d.ts +4 -4
- package/lib/drivers/copilot/index.d.ts.map +1 -1
- package/lib/drivers/copilot/index.js +13 -3
- package/lib/drivers/copilot/index.js.map +1 -1
- package/lib/drivers/cursor/config.js +1 -1
- package/lib/drivers/cursor/config.js.map +1 -1
- package/lib/drivers/cursor/index.d.ts.map +1 -1
- package/lib/drivers/cursor/index.js +8 -3
- package/lib/drivers/cursor/index.js.map +1 -1
- package/lib/drivers/shared/acp-session.d.ts +4 -0
- package/lib/drivers/shared/acp-session.d.ts.map +1 -0
- package/lib/drivers/shared/acp-session.js +23 -0
- package/lib/drivers/shared/acp-session.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/config.d.ts +122 -1
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +2 -1
- package/lib/orchestrator/config.js.map +1 -1
- package/lib/orchestrator/doctor.d.ts.map +1 -1
- package/lib/orchestrator/doctor.js +3 -1
- package/lib/orchestrator/doctor.js.map +1 -1
- package/lib/orchestrator/driver-capabilities.d.ts +2 -1
- package/lib/orchestrator/driver-capabilities.d.ts.map +1 -1
- package/lib/orchestrator/driver-capabilities.js +5 -2
- package/lib/orchestrator/driver-capabilities.js.map +1 -1
- package/lib/orchestrator/driver-identity.d.ts.map +1 -1
- package/lib/orchestrator/driver-identity.js +11 -1
- package/lib/orchestrator/driver-identity.js.map +1 -1
- package/lib/orchestrator/experiment-context.d.ts +34 -0
- package/lib/orchestrator/experiment-context.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +16 -3
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/package.json +1 -1
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/authoring.ts +6 -0
- package/src/container/driver.ts +211 -0
- package/src/container/protocol.ts +48 -0
- package/src/container/resources.ts +153 -0
- package/src/container/worker.ts +107 -0
- package/src/core/case.ts +8 -1
- package/src/core/driver-execution.ts +84 -0
- package/src/core/driver.ts +9 -1
- package/src/core/harness-catalogs/cursor.ts +1 -1
- package/src/core/harness-matrix.ts +19 -1
- package/src/drivers/antigravity/README.md +108 -10
- package/src/drivers/antigravity/artifacts.ts +37 -0
- package/src/drivers/antigravity/config.ts +35 -0
- package/src/drivers/antigravity/index.ts +41 -22
- package/src/drivers/antigravity/runtime.ts +88 -0
- package/src/drivers/copilot/README.md +19 -11
- package/src/drivers/copilot/config.ts +5 -5
- package/src/drivers/copilot/index.ts +20 -4
- package/src/drivers/cursor/README.md +14 -8
- package/src/drivers/cursor/config.ts +1 -1
- package/src/drivers/cursor/index.ts +11 -7
- package/src/drivers/shared/acp-session.ts +40 -0
- package/src/index.ts +6 -0
- package/src/orchestrator/config.ts +4 -1
- package/src/orchestrator/doctor.ts +3 -1
- package/src/orchestrator/driver-capabilities.ts +6 -1
- package/src/orchestrator/driver-identity.ts +11 -1
- package/src/orchestrator/orchestrator.ts +19 -3
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
|
+
import type { AgentDriver, Artifact, DriverEventMap, StartContext } from "../core/driver.js";
|
|
4
|
+
import type { ContainerExecution } from "../core/driver-execution.js";
|
|
5
|
+
import { TypedEmitter } from "../core/emitter.js";
|
|
6
|
+
import {
|
|
7
|
+
type Answer,
|
|
8
|
+
DoneSchema,
|
|
9
|
+
ErrorEventSchema,
|
|
10
|
+
InterruptSchema,
|
|
11
|
+
ProgressSchema,
|
|
12
|
+
type ReadyState,
|
|
13
|
+
ReadyStateSchema,
|
|
14
|
+
} from "../core/events.js";
|
|
15
|
+
import { type ManagedProcess, spawnManagedProcess } from "../core/exec.js";
|
|
16
|
+
import { walkArtifacts } from "../drivers/shared/artifacts.js";
|
|
17
|
+
import { LUX_VERSION } from "../orchestrator/package-version.js";
|
|
18
|
+
import { type Message, readMessages, writeMessage } from "./protocol.js";
|
|
19
|
+
import { prepareContainer } from "./resources.js";
|
|
20
|
+
|
|
21
|
+
export class ContainerDriver extends TypedEmitter<DriverEventMap> implements AgentDriver {
|
|
22
|
+
private process: ManagedProcess | undefined;
|
|
23
|
+
private resources: Awaited<ReturnType<typeof prepareContainer>> | undefined;
|
|
24
|
+
private sequence = 0;
|
|
25
|
+
private pending = new Map<
|
|
26
|
+
number,
|
|
27
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void }
|
|
28
|
+
>();
|
|
29
|
+
private closing: Promise<void> | undefined;
|
|
30
|
+
private context: StartContext | undefined;
|
|
31
|
+
private terminal = false;
|
|
32
|
+
private readonly abort = () => {
|
|
33
|
+
void this.close().catch(() => {});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
private readonly name: string;
|
|
37
|
+
private readonly config: unknown;
|
|
38
|
+
private readonly execution: ContainerExecution;
|
|
39
|
+
constructor(name: string, config: unknown, execution: ContainerExecution) {
|
|
40
|
+
super();
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.execution = execution;
|
|
44
|
+
JSON.stringify(config, (_key, value: unknown) => {
|
|
45
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint")
|
|
46
|
+
throw new Error("container driver configuration must be JSON serializable");
|
|
47
|
+
return value;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fail = (error: Error): void => {
|
|
52
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
53
|
+
this.pending.clear();
|
|
54
|
+
if (!this.terminal && !this.closing) {
|
|
55
|
+
this.terminal = true;
|
|
56
|
+
void this.emit("error", { exitReason: "error", reason: error.message }).catch(() => {});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
private receive = (message: Message): void => {
|
|
61
|
+
if (message.event) {
|
|
62
|
+
const dispatch = async () => {
|
|
63
|
+
switch (message.event) {
|
|
64
|
+
case "interrupt":
|
|
65
|
+
await this.emit("interrupt", InterruptSchema.parse(message.value));
|
|
66
|
+
break;
|
|
67
|
+
case "progress":
|
|
68
|
+
await this.emit("progress", ProgressSchema.parse(message.value));
|
|
69
|
+
break;
|
|
70
|
+
case "done":
|
|
71
|
+
this.terminal = true;
|
|
72
|
+
await this.emit("done", DoneSchema.parse(message.value));
|
|
73
|
+
break;
|
|
74
|
+
case "error":
|
|
75
|
+
this.terminal = true;
|
|
76
|
+
await this.emit("error", ErrorEventSchema.parse(message.value));
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
throw new Error("unknown container event");
|
|
80
|
+
}
|
|
81
|
+
if (this.process) writeMessage(this.process.child.stdin, { id: message.id, ack: true });
|
|
82
|
+
};
|
|
83
|
+
void dispatch().catch(this.fail);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const pending = this.pending.get(message.id);
|
|
87
|
+
if (!pending) return;
|
|
88
|
+
this.pending.delete(message.id);
|
|
89
|
+
if (message.error !== undefined) pending.reject(new Error(message.error));
|
|
90
|
+
else pending.resolve(message.value);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
private rpc = (method: string, value?: unknown, timeoutMs = 30_000): Promise<unknown> =>
|
|
94
|
+
new Promise((resolvePromise, reject) => {
|
|
95
|
+
if (!this.process) {
|
|
96
|
+
reject(new Error("container process is unavailable"));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const id = ++this.sequence;
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
this.pending.delete(id);
|
|
102
|
+
reject(new Error(`container ${method} timed out`));
|
|
103
|
+
}, timeoutMs);
|
|
104
|
+
this.pending.set(id, {
|
|
105
|
+
resolve: (result) => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
resolvePromise(result);
|
|
108
|
+
},
|
|
109
|
+
reject: (error) => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
reject(error);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
writeMessage(this.process.child.stdin, { id, method, value });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
this.pending.get(id)?.reject(error as Error);
|
|
118
|
+
this.pending.delete(id);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
async start(ctx: StartContext): Promise<ReadyState> {
|
|
123
|
+
this.context = ctx;
|
|
124
|
+
if (ctx.abortSignal.aborted) throw new Error("container run aborted before startup");
|
|
125
|
+
const raw = this.config as { cwd?: string; cwdMode?: string };
|
|
126
|
+
if (raw?.cwdMode === "runDir")
|
|
127
|
+
throw new Error("container drivers must use the artifacts workspace, not cwdMode: runDir");
|
|
128
|
+
if (raw?.cwd && (await realpath(raw.cwd)) !== (await realpath(ctx.artifactsDir)))
|
|
129
|
+
throw new Error("container driver cwd must be the mounted artifacts workspace");
|
|
130
|
+
this.resources = await prepareContainer(this.execution, ctx.artifactsDir);
|
|
131
|
+
try {
|
|
132
|
+
this.process = spawnManagedProcess(this.resources.command, this.resources.args, {
|
|
133
|
+
env: this.resources.env,
|
|
134
|
+
});
|
|
135
|
+
readMessages(this.process.child.stdout, this.receive, this.fail);
|
|
136
|
+
// Harness stderr can contain secrets. The normalized driver protocol is the evidence surface.
|
|
137
|
+
this.process.child.stderr.resume();
|
|
138
|
+
void this.process.exited.then(() => this.fail(new Error("container worker exited")));
|
|
139
|
+
ctx.abortSignal.addEventListener("abort", this.abort, { once: true });
|
|
140
|
+
await this.process.spawned;
|
|
141
|
+
if (ctx.abortSignal.aborted) throw new Error("container run aborted during startup");
|
|
142
|
+
const { abortSignal: _signal, ...context } = ctx;
|
|
143
|
+
const ready = ReadyStateSchema.parse(
|
|
144
|
+
await this.rpc(
|
|
145
|
+
"start",
|
|
146
|
+
{
|
|
147
|
+
version: LUX_VERSION,
|
|
148
|
+
name: this.name,
|
|
149
|
+
config: this.config,
|
|
150
|
+
module: this.execution.driverModule,
|
|
151
|
+
context: {
|
|
152
|
+
...context,
|
|
153
|
+
artifactsDir: this.resources.root,
|
|
154
|
+
...(context.fixtures
|
|
155
|
+
? { fixtures: { ...context.fixtures, rootDir: this.resources.root } }
|
|
156
|
+
: {}),
|
|
157
|
+
runDir: "/tmp/lux-run",
|
|
158
|
+
runsRoot: "/tmp/lux-runs",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
this.execution.startupTimeoutMs ?? 60_000,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
...ready,
|
|
166
|
+
detail: { ...ready.detail, execution: "container", image: this.resources.image },
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await this.close();
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async respond(answer: Answer): Promise<void> {
|
|
175
|
+
await this.rpc("respond", answer);
|
|
176
|
+
}
|
|
177
|
+
async quiesce(): Promise<void> {
|
|
178
|
+
await this.rpc("quiesce");
|
|
179
|
+
}
|
|
180
|
+
async collect(): Promise<Artifact[]> {
|
|
181
|
+
const root = this.resources?.root;
|
|
182
|
+
if (!root) return [];
|
|
183
|
+
try {
|
|
184
|
+
const artifacts = (await this.rpc("collect")) as Artifact[];
|
|
185
|
+
for (const artifact of artifacts) {
|
|
186
|
+
const path = resolve(artifact.root ?? root, artifact.path);
|
|
187
|
+
if (path !== root && !path.startsWith(`${root}${sep}`))
|
|
188
|
+
throw new Error("container driver returned artifacts outside its mounted workspace");
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
await this.close();
|
|
192
|
+
}
|
|
193
|
+
return walkArtifacts(root);
|
|
194
|
+
}
|
|
195
|
+
close(): Promise<void> {
|
|
196
|
+
this.closing ??= (async () => {
|
|
197
|
+
this.context?.abortSignal.removeEventListener("abort", this.abort);
|
|
198
|
+
try {
|
|
199
|
+
if (this.process) {
|
|
200
|
+
await this.rpc("close", undefined, 5_000).catch(() => {});
|
|
201
|
+
this.process.child.stdin.end();
|
|
202
|
+
}
|
|
203
|
+
await this.resources?.cleanup();
|
|
204
|
+
} finally {
|
|
205
|
+
await this.process?.terminate();
|
|
206
|
+
this.fail(new Error("container driver closed"));
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
return this.closing;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
export type Message = {
|
|
4
|
+
id: number;
|
|
5
|
+
method?: string;
|
|
6
|
+
event?: string;
|
|
7
|
+
value?: unknown;
|
|
8
|
+
error?: string;
|
|
9
|
+
ack?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export const writeMessage = (stream: Writable, message: Message): void => {
|
|
12
|
+
const line = JSON.stringify(message);
|
|
13
|
+
if (line.length > 8_000_000) throw new Error("container protocol message exceeds 8 MB");
|
|
14
|
+
stream.write(`${line}\n`);
|
|
15
|
+
};
|
|
16
|
+
export const readMessages = (
|
|
17
|
+
stream: Readable,
|
|
18
|
+
receive: (message: Message) => void,
|
|
19
|
+
fail: (error: Error) => void,
|
|
20
|
+
): void => {
|
|
21
|
+
let buffer = "";
|
|
22
|
+
stream.setEncoding("utf8");
|
|
23
|
+
stream.on("data", (chunk: string) => {
|
|
24
|
+
try {
|
|
25
|
+
buffer += chunk;
|
|
26
|
+
for (;;) {
|
|
27
|
+
const newline = buffer.indexOf("\n");
|
|
28
|
+
if (newline < 0) break;
|
|
29
|
+
if (newline > 8_000_000) throw new Error("container protocol message exceeds 8 MB");
|
|
30
|
+
const message: unknown = JSON.parse(buffer.slice(0, newline));
|
|
31
|
+
buffer = buffer.slice(newline + 1);
|
|
32
|
+
if (
|
|
33
|
+
!message ||
|
|
34
|
+
typeof message !== "object" ||
|
|
35
|
+
!("id" in message) ||
|
|
36
|
+
!Number.isSafeInteger(message.id)
|
|
37
|
+
)
|
|
38
|
+
throw new Error("invalid container protocol message");
|
|
39
|
+
receive(message as Message);
|
|
40
|
+
}
|
|
41
|
+
if (buffer.length > 8_000_000) throw new Error("container protocol buffer exceeds 8 MB");
|
|
42
|
+
} catch (error) {
|
|
43
|
+
buffer = "";
|
|
44
|
+
stream.pause();
|
|
45
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import type { ContainerExecution } from "../core/driver-execution.js";
|
|
6
|
+
import { exec } from "../core/exec.js";
|
|
7
|
+
|
|
8
|
+
const mount = (source: string, target: string, readOnly = false): string => {
|
|
9
|
+
if ([source, target].some((path) => !isAbsolute(path) || /[,\r\n\0]/u.test(path)))
|
|
10
|
+
throw new Error(
|
|
11
|
+
"container mount paths must be absolute and contain no commas or control characters",
|
|
12
|
+
);
|
|
13
|
+
return `type=bind,source=${source},target=${target}${readOnly ? ",readonly" : ""}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const prepareContainer = async (config: ContainerExecution, workspace: string) => {
|
|
17
|
+
const command = config.command ?? "docker";
|
|
18
|
+
const inspected = await exec(command, ["image", "inspect", "--format", "{{.Id}}", config.image], {
|
|
19
|
+
timeoutMs: config.startupTimeoutMs ?? 30_000,
|
|
20
|
+
maxBuffer: 4096,
|
|
21
|
+
});
|
|
22
|
+
if (inspected.exitCode !== 0 || !/^sha256:[a-f0-9]{64}$/u.test(inspected.stdout.trim()))
|
|
23
|
+
throw new Error(
|
|
24
|
+
"container image is unavailable; build or pull the configured image before running Lux",
|
|
25
|
+
);
|
|
26
|
+
const image = inspected.stdout.trim();
|
|
27
|
+
const home = await mkdtemp(join(tmpdir(), "lux-container-home-"));
|
|
28
|
+
const name = `lux-${randomUUID()}`;
|
|
29
|
+
const cleanup = async () => {
|
|
30
|
+
// Killing the Docker client does not stop the daemon-owned container.
|
|
31
|
+
const removed = await exec(command, ["rm", "--force", name], {
|
|
32
|
+
timeoutMs: 15_000,
|
|
33
|
+
maxBuffer: 4096,
|
|
34
|
+
});
|
|
35
|
+
if (removed.exitCode !== 0 && !removed.stderr.includes("No such container"))
|
|
36
|
+
throw new Error(
|
|
37
|
+
`failed to remove owned container '${name}'; private home retained at ${home}`,
|
|
38
|
+
);
|
|
39
|
+
await rm(home, { recursive: true, force: true });
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
await chmod(home, 0o700);
|
|
43
|
+
for (const file of config.homeFiles ?? []) {
|
|
44
|
+
const target = join(home, file.destination);
|
|
45
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
46
|
+
if (file.source.kind === "file") {
|
|
47
|
+
if (!(await lstat(file.source.path)).isFile())
|
|
48
|
+
throw new Error("container home credential source must be a regular file");
|
|
49
|
+
await copyFile(file.source.path, target);
|
|
50
|
+
} else {
|
|
51
|
+
if (process.platform !== "darwin")
|
|
52
|
+
throw new Error("container Keychain credentials require macOS");
|
|
53
|
+
const result = await exec(
|
|
54
|
+
"/usr/bin/security",
|
|
55
|
+
["find-generic-password", "-s", file.source.service, "-a", file.source.account, "-w"],
|
|
56
|
+
{ timeoutMs: 10_000, maxBuffer: 1_000_000 },
|
|
57
|
+
);
|
|
58
|
+
if (result.exitCode !== 0 || result.stdoutTruncated)
|
|
59
|
+
throw new Error("could not read the explicitly configured container Keychain credential");
|
|
60
|
+
const value = result.stdout.trimEnd();
|
|
61
|
+
await writeFile(
|
|
62
|
+
target,
|
|
63
|
+
file.source.encoding === "go-keyring-base64" && value.startsWith("go-keyring-base64:")
|
|
64
|
+
? Buffer.from(value.slice("go-keyring-base64:".length), "base64")
|
|
65
|
+
: value,
|
|
66
|
+
{ mode: 0o600 },
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
await chmod(target, 0o600);
|
|
70
|
+
}
|
|
71
|
+
const root = await realpath(workspace);
|
|
72
|
+
const args = [
|
|
73
|
+
"run",
|
|
74
|
+
"--init",
|
|
75
|
+
"-i",
|
|
76
|
+
"--pull",
|
|
77
|
+
"never",
|
|
78
|
+
"--name",
|
|
79
|
+
name,
|
|
80
|
+
"--read-only",
|
|
81
|
+
"--cap-drop",
|
|
82
|
+
"ALL",
|
|
83
|
+
"--security-opt",
|
|
84
|
+
"no-new-privileges",
|
|
85
|
+
"--network",
|
|
86
|
+
config.network ?? "bridge",
|
|
87
|
+
"--memory",
|
|
88
|
+
config.memory ?? "4g",
|
|
89
|
+
"--cpus",
|
|
90
|
+
String(config.cpus ?? 2),
|
|
91
|
+
"--pids-limit",
|
|
92
|
+
String(config.pidsLimit ?? 512),
|
|
93
|
+
"--user",
|
|
94
|
+
config.user ?? `${process.getuid?.() ?? 1000}:${process.getgid?.() ?? 1000}`,
|
|
95
|
+
"--tmpfs",
|
|
96
|
+
"/tmp:rw,nosuid,nodev,exec,size=1g,mode=1777",
|
|
97
|
+
"--workdir",
|
|
98
|
+
root,
|
|
99
|
+
"--mount",
|
|
100
|
+
mount(root, root),
|
|
101
|
+
"--mount",
|
|
102
|
+
mount(await realpath(home), "/home/lux"),
|
|
103
|
+
"--env",
|
|
104
|
+
"HOME=/home/lux",
|
|
105
|
+
"--env",
|
|
106
|
+
"npm_config_yes=true",
|
|
107
|
+
];
|
|
108
|
+
for (const entry of config.mounts ?? []) {
|
|
109
|
+
const source = await realpath(entry.source);
|
|
110
|
+
const target = resolve(entry.target ?? source);
|
|
111
|
+
if (
|
|
112
|
+
[
|
|
113
|
+
"/",
|
|
114
|
+
"/home",
|
|
115
|
+
"/home/lux",
|
|
116
|
+
"/tmp",
|
|
117
|
+
"/opt",
|
|
118
|
+
"/opt/lux",
|
|
119
|
+
"/usr",
|
|
120
|
+
"/bin",
|
|
121
|
+
"/etc",
|
|
122
|
+
"/proc",
|
|
123
|
+
"/sys",
|
|
124
|
+
"/dev",
|
|
125
|
+
].some(
|
|
126
|
+
(reserved) =>
|
|
127
|
+
target === reserved || (reserved !== "/" && target.startsWith(`${reserved}/`)),
|
|
128
|
+
) ||
|
|
129
|
+
target === root ||
|
|
130
|
+
root.startsWith(`${target}/`) ||
|
|
131
|
+
target.startsWith(`${root}/`)
|
|
132
|
+
)
|
|
133
|
+
throw new Error(
|
|
134
|
+
"additional container mounts must not overlap the workspace or runtime paths",
|
|
135
|
+
);
|
|
136
|
+
args.push("--mount", mount(source, target, entry.readOnly ?? true));
|
|
137
|
+
}
|
|
138
|
+
const env = { ...process.env };
|
|
139
|
+
for (const key of new Set([...(config.passEnv ?? []), ...Object.keys(config.env ?? {})])) {
|
|
140
|
+
if (["HOME", "NODE_OPTIONS", "NODE_PATH", "LD_PRELOAD", "LD_LIBRARY_PATH"].includes(key))
|
|
141
|
+
throw new Error(`reserved container environment variable '${key}'`);
|
|
142
|
+
const value = config.env?.[key] ?? process.env[key];
|
|
143
|
+
if (value === undefined) throw new Error(`container environment variable '${key}' is unset`);
|
|
144
|
+
env[key] = value;
|
|
145
|
+
args.push("--env", key);
|
|
146
|
+
}
|
|
147
|
+
args.push("--entrypoint", "node", image, config.worker ?? "/opt/lux/lib/container/worker.js");
|
|
148
|
+
return { command, args, env, image, root, cleanup };
|
|
149
|
+
} catch (error) {
|
|
150
|
+
await rm(home, { recursive: true, force: true });
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import type { AgentDriver, DriverPlugin, StartContext } from "../core/driver.js";
|
|
3
|
+
import { AnswerSchema } from "../core/events.js";
|
|
4
|
+
import { buildRegistryFromConfig, LuxConfigSchema } from "../orchestrator/config.js";
|
|
5
|
+
import { LUX_VERSION } from "../orchestrator/package-version.js";
|
|
6
|
+
import { validatePluginConfig } from "../orchestrator/validate-plugin-config.js";
|
|
7
|
+
import { type Message, readMessages, writeMessage } from "./protocol.js";
|
|
8
|
+
|
|
9
|
+
let driver: AgentDriver | undefined;
|
|
10
|
+
let eventId = 0;
|
|
11
|
+
const acknowledgements = new Map<number, () => void>();
|
|
12
|
+
const abort = new AbortController();
|
|
13
|
+
const dispatch = async (message: Message): Promise<unknown> => {
|
|
14
|
+
if (message.method === "start") {
|
|
15
|
+
if (driver) throw new Error("container driver already started");
|
|
16
|
+
const request = message.value as {
|
|
17
|
+
version: string;
|
|
18
|
+
name: string;
|
|
19
|
+
module?: string;
|
|
20
|
+
config: unknown;
|
|
21
|
+
context: Omit<StartContext, "abortSignal">;
|
|
22
|
+
};
|
|
23
|
+
if (request.version !== LUX_VERSION)
|
|
24
|
+
throw new Error("container worker and host Lux versions differ; rebuild the image");
|
|
25
|
+
const plugin: DriverPlugin | undefined = request.module
|
|
26
|
+
? (await import(request.module)).default
|
|
27
|
+
: buildRegistryFromConfig({
|
|
28
|
+
...LuxConfigSchema.parse({}),
|
|
29
|
+
rootDir: process.cwd(),
|
|
30
|
+
configPath: null,
|
|
31
|
+
}).getDriver(request.name);
|
|
32
|
+
if (!plugin || plugin.name !== request.name)
|
|
33
|
+
throw new Error("container driver plugin is unavailable");
|
|
34
|
+
driver = await plugin.create(
|
|
35
|
+
validatePluginConfig(
|
|
36
|
+
plugin.configSchema,
|
|
37
|
+
request.config,
|
|
38
|
+
`container driver '${request.name}'`,
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
for (const event of ["interrupt", "progress", "done", "error"] as const) {
|
|
42
|
+
driver.on(
|
|
43
|
+
event,
|
|
44
|
+
(value) =>
|
|
45
|
+
new Promise<void>((resolve) => {
|
|
46
|
+
const id = ++eventId;
|
|
47
|
+
acknowledgements.set(id, resolve);
|
|
48
|
+
writeMessage(process.stdout, { id, event, value });
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
await mkdir("/tmp/lux-run", { recursive: true });
|
|
53
|
+
await mkdir("/tmp/lux-runs", { recursive: true });
|
|
54
|
+
return driver.start({
|
|
55
|
+
...request.context,
|
|
56
|
+
isolatedHome: "/home/lux",
|
|
57
|
+
abortSignal: abort.signal,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!driver) throw new Error("container driver has not started");
|
|
61
|
+
switch (message.method) {
|
|
62
|
+
case "respond":
|
|
63
|
+
return driver.respond(AnswerSchema.parse(message.value));
|
|
64
|
+
case "quiesce":
|
|
65
|
+
return driver.quiesce?.();
|
|
66
|
+
case "collect":
|
|
67
|
+
return driver.collect();
|
|
68
|
+
case "close":
|
|
69
|
+
return driver.close();
|
|
70
|
+
default:
|
|
71
|
+
throw new Error("unknown container RPC method");
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
readMessages(
|
|
75
|
+
process.stdin,
|
|
76
|
+
(message) => {
|
|
77
|
+
if (message.ack) {
|
|
78
|
+
acknowledgements.get(message.id)?.();
|
|
79
|
+
acknowledgements.delete(message.id);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
void dispatch(message).then(
|
|
83
|
+
(value) => writeMessage(process.stdout, { id: message.id, value }),
|
|
84
|
+
(error: unknown) =>
|
|
85
|
+
writeMessage(process.stdout, {
|
|
86
|
+
id: message.id,
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
() => {
|
|
92
|
+
abort.abort();
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
process.stdin.destroy();
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
process.stdin.on("end", () => {
|
|
98
|
+
abort.abort();
|
|
99
|
+
for (const resolve of acknowledgements.values()) resolve();
|
|
100
|
+
void driver?.close().finally(() => {
|
|
101
|
+
process.exitCode = 0;
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
process.stdout.on("error", () => {
|
|
105
|
+
abort.abort();
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
});
|
package/src/core/case.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { AssertionPlugin } from "./assertion.js";
|
|
3
3
|
import { collectEvalCaseExtensions, setEvalCaseExtensions } from "./case-authoring-state.js";
|
|
4
4
|
import { DriverCapabilitySchema } from "./driver.js";
|
|
5
|
+
import { DriverExecutionSchema } from "./driver-execution.js";
|
|
5
6
|
import { EvaluationMetadataSchema } from "./evaluation-clusters.js";
|
|
6
7
|
import { HarnessMatrixSchema } from "./harness-matrix.js";
|
|
7
8
|
import type { LifecycleHooksPlugin } from "./lifecycle-hooks.js";
|
|
@@ -20,7 +21,13 @@ export type FixturesRef = z.infer<typeof FixturesRefSchema>;
|
|
|
20
21
|
// `defineEvalCase` discriminate the config type on `name`.
|
|
21
22
|
export const DriverRefSchema = z.union([
|
|
22
23
|
z.string(),
|
|
23
|
-
z
|
|
24
|
+
z
|
|
25
|
+
.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
config: z.unknown().optional(),
|
|
28
|
+
execution: DriverExecutionSchema.optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict(),
|
|
24
31
|
]);
|
|
25
32
|
export type DriverRef = z.infer<typeof DriverRefSchema>;
|
|
26
33
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const homePath = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(1)
|
|
6
|
+
.refine(
|
|
7
|
+
(value) =>
|
|
8
|
+
!value.startsWith("/") &&
|
|
9
|
+
!value.includes("\\") &&
|
|
10
|
+
value.split("/").every((part) => part !== ".." && part !== "." && part !== ""),
|
|
11
|
+
"must be a relative path within the container home",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const ContainerExecutionSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
kind: z.literal("container"),
|
|
17
|
+
/** A locally available image containing Lux and the selected Linux harness. */
|
|
18
|
+
image: z.string().min(1),
|
|
19
|
+
command: z.string().min(1).optional(),
|
|
20
|
+
/** The worker shipped with the same Lux build as the host. */
|
|
21
|
+
worker: z.string().min(1).optional(),
|
|
22
|
+
/** Optional module in the image exporting a custom DriverPlugin as default. */
|
|
23
|
+
driverModule: z.string().min(1).optional(),
|
|
24
|
+
mounts: z
|
|
25
|
+
.array(
|
|
26
|
+
z
|
|
27
|
+
.object({
|
|
28
|
+
source: z.string().min(1),
|
|
29
|
+
target: z.string().min(1).optional(),
|
|
30
|
+
readOnly: z.boolean().optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict(),
|
|
33
|
+
)
|
|
34
|
+
.optional(),
|
|
35
|
+
/** Only these host environment variables enter the container. */
|
|
36
|
+
passEnv: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).optional(),
|
|
37
|
+
env: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string()).optional(),
|
|
38
|
+
/** Private copies, never host-home mounts; deleted after the run. */
|
|
39
|
+
homeFiles: z
|
|
40
|
+
.array(
|
|
41
|
+
z
|
|
42
|
+
.object({
|
|
43
|
+
destination: homePath,
|
|
44
|
+
source: z.discriminatedUnion("kind", [
|
|
45
|
+
z.object({ kind: z.literal("file"), path: z.string().min(1) }).strict(),
|
|
46
|
+
z
|
|
47
|
+
.object({
|
|
48
|
+
kind: z.literal("keychain"),
|
|
49
|
+
service: z.string().min(1),
|
|
50
|
+
account: z.string().min(1),
|
|
51
|
+
encoding: z.enum(["utf8", "go-keyring-base64"]).optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict(),
|
|
54
|
+
]),
|
|
55
|
+
})
|
|
56
|
+
.strict(),
|
|
57
|
+
)
|
|
58
|
+
.max(32)
|
|
59
|
+
.optional(),
|
|
60
|
+
network: z.enum(["bridge", "none"]).optional(),
|
|
61
|
+
user: z.string().min(1).optional(),
|
|
62
|
+
memory: z
|
|
63
|
+
.string()
|
|
64
|
+
.regex(/^[1-9][0-9]*[bkmgt]?$/i)
|
|
65
|
+
.optional(),
|
|
66
|
+
cpus: z.number().positive().optional(),
|
|
67
|
+
pidsLimit: z.number().int().positive().optional(),
|
|
68
|
+
startupTimeoutMs: z.number().int().positive().optional(),
|
|
69
|
+
})
|
|
70
|
+
.strict();
|
|
71
|
+
|
|
72
|
+
export const DriverExecutionSchema = z.discriminatedUnion("kind", [
|
|
73
|
+
z.object({ kind: z.literal("host") }).strict(),
|
|
74
|
+
ContainerExecutionSchema,
|
|
75
|
+
]);
|
|
76
|
+
export type DriverExecution = z.infer<typeof DriverExecutionSchema>;
|
|
77
|
+
export type ContainerExecution = z.infer<typeof ContainerExecutionSchema>;
|
|
78
|
+
|
|
79
|
+
export const driverExecutionOf = (reference: unknown): DriverExecution | undefined => {
|
|
80
|
+
if (!reference || typeof reference !== "object" || !("execution" in reference)) return undefined;
|
|
81
|
+
return reference.execution === undefined
|
|
82
|
+
? undefined
|
|
83
|
+
: DriverExecutionSchema.parse(reference.execution);
|
|
84
|
+
};
|
package/src/core/driver.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { DriverExecution } from "./driver-execution.js";
|
|
2
3
|
import type { EventHandler } from "./emitter.js";
|
|
3
4
|
import type { Answer, Done, ErrorEvent, Interrupt, Progress, ReadyState } from "./events.js";
|
|
4
5
|
import type { ReasoningEffort } from "./harness-catalog.js";
|
|
@@ -28,6 +29,8 @@ export type ResolvedFixtures = z.infer<typeof ResolvedFixturesSchema>;
|
|
|
28
29
|
export type StartContext = {
|
|
29
30
|
/** Stable selected case id, available to drivers that multiplex scenarios. */
|
|
30
31
|
caseId?: string;
|
|
32
|
+
/** Private home owned by an outer execution boundary; never the caller’s home. */
|
|
33
|
+
isolatedHome?: string;
|
|
31
34
|
prompt: string;
|
|
32
35
|
fixtures?: ResolvedFixtures | undefined;
|
|
33
36
|
/** Authored fixture values selected for this case. */
|
|
@@ -139,4 +142,9 @@ export const defineDriver = <S extends z.ZodType>(
|
|
|
139
142
|
export const driverRef = <S extends z.ZodType>(
|
|
140
143
|
plugin: { name: string; configSchema: S },
|
|
141
144
|
config: z.input<S>,
|
|
142
|
-
|
|
145
|
+
execution?: DriverExecution,
|
|
146
|
+
): { name: string; config: z.input<S>; execution?: DriverExecution } => ({
|
|
147
|
+
name: plugin.name,
|
|
148
|
+
config,
|
|
149
|
+
...(execution ? { execution } : {}),
|
|
150
|
+
});
|
|
@@ -4,7 +4,7 @@ export const cursorCatalog = defineHarnessCatalog({
|
|
|
4
4
|
driver: "cursor",
|
|
5
5
|
verified: "2026-09-18",
|
|
6
6
|
sources: [
|
|
7
|
-
"agent --list-models (account-scoped, needs a login)",
|
|
7
|
+
"cursor-agent --list-models (account-scoped, needs a login)",
|
|
8
8
|
"https://cursor.com/docs/models",
|
|
9
9
|
"https://cursor.com/docs/cli/acp",
|
|
10
10
|
],
|