@prismatic-io/lux 0.0.2-preview.29 → 0.0.2-preview.30
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/answerers/persona/index.d.ts +6 -0
- package/lib/answerers/persona/index.d.ts.map +1 -1
- package/lib/answerers/persona/index.js +5 -0
- package/lib/answerers/persona/index.js.map +1 -1
- package/lib/assertions/core/command-exits-zero.d.ts +2 -0
- package/lib/assertions/core/command-exits-zero.d.ts.map +1 -1
- package/lib/assertions/core/command-exits-zero.js +46 -4
- package/lib/assertions/core/command-exits-zero.js.map +1 -1
- package/lib/assertions/rubric/index.d.ts +16 -0
- package/lib/assertions/rubric/index.d.ts.map +1 -1
- package/lib/assertions/rubric/index.js +34 -3
- package/lib/assertions/rubric/index.js.map +1 -1
- package/lib/assertions/rubric/internal.d.ts +14 -0
- package/lib/assertions/rubric/internal.d.ts.map +1 -1
- package/lib/assertions/rubric/internal.js +122 -12
- package/lib/assertions/rubric/internal.js.map +1 -1
- package/lib/core/agent-environment.d.ts +5 -0
- package/lib/core/agent-environment.d.ts.map +1 -0
- package/lib/core/agent-environment.js +47 -0
- package/lib/core/agent-environment.js.map +1 -0
- package/lib/core/command-verification.d.ts +10 -0
- package/lib/core/command-verification.d.ts.map +1 -0
- package/lib/core/command-verification.js +66 -0
- package/lib/core/command-verification.js.map +1 -0
- package/lib/core/driver.d.ts +7 -0
- package/lib/core/driver.d.ts.map +1 -1
- package/lib/core/driver.js +2 -0
- package/lib/core/driver.js.map +1 -1
- package/lib/core/index.d.ts +2 -0
- package/lib/core/index.d.ts.map +1 -1
- package/lib/core/index.js +2 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/model-cli.d.ts +5 -0
- package/lib/core/model-cli.d.ts.map +1 -1
- package/lib/core/model-cli.js +8 -2
- package/lib/core/model-cli.js.map +1 -1
- package/lib/core/run.d.ts +5 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/core/secure-copy.d.ts.map +1 -0
- package/lib/core/secure-copy.js.map +1 -0
- package/lib/drivers/antigravity/config.d.ts +3 -0
- package/lib/drivers/antigravity/config.d.ts.map +1 -1
- package/lib/drivers/antigravity/config.js +2 -0
- package/lib/drivers/antigravity/config.js.map +1 -1
- package/lib/drivers/antigravity/index.d.ts +4 -0
- package/lib/drivers/antigravity/index.d.ts.map +1 -1
- package/lib/drivers/antigravity/runtime.d.ts.map +1 -1
- package/lib/drivers/antigravity/runtime.js +7 -2
- package/lib/drivers/antigravity/runtime.js.map +1 -1
- package/lib/drivers/claude-code/index.d.ts +18 -4
- package/lib/drivers/claude-code/index.d.ts.map +1 -1
- package/lib/drivers/claude-code/index.js +97 -200
- package/lib/drivers/claude-code/index.js.map +1 -1
- package/lib/drivers/claude-code/isolation.d.ts +8 -0
- package/lib/drivers/claude-code/isolation.d.ts.map +1 -0
- package/lib/drivers/claude-code/isolation.js +199 -0
- package/lib/drivers/claude-code/isolation.js.map +1 -0
- package/lib/drivers/codex/config.d.ts +3 -0
- package/lib/drivers/codex/config.d.ts.map +1 -1
- package/lib/drivers/codex/config.js +2 -0
- package/lib/drivers/codex/config.js.map +1 -1
- package/lib/drivers/codex/index.d.ts +4 -0
- package/lib/drivers/codex/index.d.ts.map +1 -1
- package/lib/drivers/codex/index.js +52 -24
- package/lib/drivers/codex/index.js.map +1 -1
- package/lib/drivers/copilot/config.d.ts +3 -0
- package/lib/drivers/copilot/config.d.ts.map +1 -1
- package/lib/drivers/copilot/config.js +2 -0
- package/lib/drivers/copilot/config.js.map +1 -1
- package/lib/drivers/copilot/index.d.ts +4 -0
- package/lib/drivers/copilot/index.d.ts.map +1 -1
- package/lib/drivers/copilot/index.js +2 -1
- package/lib/drivers/copilot/index.js.map +1 -1
- package/lib/drivers/cursor/config.d.ts +1 -0
- package/lib/drivers/cursor/config.d.ts.map +1 -1
- package/lib/drivers/cursor/config.js +2 -0
- package/lib/drivers/cursor/config.js.map +1 -1
- package/lib/drivers/cursor/index.d.ts +2 -0
- package/lib/drivers/cursor/index.d.ts.map +1 -1
- package/lib/drivers/cursor/index.js +2 -1
- package/lib/drivers/cursor/index.js.map +1 -1
- package/lib/drivers/grok-build/config.d.ts +3 -0
- package/lib/drivers/grok-build/config.d.ts.map +1 -1
- package/lib/drivers/grok-build/config.js +2 -0
- package/lib/drivers/grok-build/config.js.map +1 -1
- package/lib/drivers/grok-build/index.d.ts +4 -0
- package/lib/drivers/grok-build/index.d.ts.map +1 -1
- package/lib/drivers/grok-build/index.js +2 -1
- package/lib/drivers/grok-build/index.js.map +1 -1
- package/lib/drivers/subprocess/index.d.ts +3 -3
- package/lib/drivers/subprocess/index.d.ts.map +1 -1
- package/lib/drivers/subprocess/index.js +5 -2
- package/lib/drivers/subprocess/index.js.map +1 -1
- package/lib/orchestrator/artifact-origin.d.ts +3 -0
- package/lib/orchestrator/artifact-origin.d.ts.map +1 -0
- package/lib/orchestrator/artifact-origin.js +40 -0
- package/lib/orchestrator/artifact-origin.js.map +1 -0
- package/lib/orchestrator/config.d.ts +6 -0
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/experiment-context.d.ts +2 -0
- package/lib/orchestrator/experiment-context.d.ts.map +1 -1
- package/lib/orchestrator/experiment-corpus.d.ts.map +1 -1
- package/lib/orchestrator/experiment-corpus.js +2 -2
- package/lib/orchestrator/experiment-corpus.js.map +1 -1
- package/lib/orchestrator/experiment-identity.d.ts.map +1 -1
- package/lib/orchestrator/experiment-identity.js +4 -4
- package/lib/orchestrator/experiment-identity.js.map +1 -1
- package/lib/orchestrator/fixture-identity.d.ts +1 -0
- package/lib/orchestrator/fixture-identity.d.ts.map +1 -1
- package/lib/orchestrator/fixture-identity.js +6 -1
- package/lib/orchestrator/fixture-identity.js.map +1 -1
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +2 -2
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +14 -3
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/package.json +1 -1
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/answerers/persona/index.ts +5 -0
- package/src/assertions/core/command-exits-zero.ts +57 -4
- package/src/assertions/rubric/index.ts +38 -3
- package/src/assertions/rubric/internal.ts +172 -11
- package/src/core/agent-environment.ts +64 -0
- package/src/core/command-verification.ts +74 -0
- package/src/core/driver.ts +4 -0
- package/src/core/index.ts +2 -0
- package/src/core/model-cli.ts +14 -2
- package/src/drivers/antigravity/config.ts +2 -0
- package/src/drivers/antigravity/runtime.ts +8 -2
- package/src/drivers/claude-code/index.ts +112 -221
- package/src/drivers/claude-code/isolation.ts +228 -0
- package/src/drivers/codex/config.ts +2 -0
- package/src/drivers/codex/index.ts +71 -31
- package/src/drivers/copilot/config.ts +2 -0
- package/src/drivers/copilot/index.ts +2 -1
- package/src/drivers/cursor/config.ts +2 -0
- package/src/drivers/cursor/index.ts +2 -1
- package/src/drivers/grok-build/config.ts +2 -0
- package/src/drivers/grok-build/index.ts +2 -1
- package/src/drivers/subprocess/index.ts +5 -2
- package/src/orchestrator/artifact-origin.ts +50 -0
- package/src/orchestrator/experiment-corpus.ts +10 -7
- package/src/orchestrator/experiment-identity.ts +16 -13
- package/src/orchestrator/fixture-identity.ts +9 -1
- package/src/orchestrator/orchestrator.ts +12 -6
- package/src/orchestrator/run-execution.ts +21 -4
- package/lib/drivers/shared/secure-copy.d.ts.map +0 -1
- package/lib/drivers/shared/secure-copy.js.map +0 -1
- /package/lib/{drivers/shared → core}/secure-copy.d.ts +0 -0
- /package/lib/{drivers/shared → core}/secure-copy.js +0 -0
- /package/src/{drivers/shared → core}/secure-copy.ts +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { cp, mkdir,
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
1
|
+
import { cp, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
4
3
|
import { temporaryDirectory } from "tempy";
|
|
5
4
|
import { z } from "zod";
|
|
5
|
+
import { agentEnvironment, InheritEnvironmentSchema } from "../../core/agent-environment.js";
|
|
6
6
|
import { DurationSchema, durationMs } from "../../core/duration.js";
|
|
7
7
|
import {
|
|
8
8
|
type AgentDriver,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
selectModelAndReasoningEffort,
|
|
21
21
|
TypedEmitter,
|
|
22
22
|
} from "../../core/index.js";
|
|
23
|
+
import { copyRegularFileNoFollow } from "../../core/secure-copy.js";
|
|
23
24
|
import { walkArtifacts } from "../shared/artifacts.js";
|
|
24
25
|
import {
|
|
25
26
|
claudeConfigRoot,
|
|
@@ -27,9 +28,9 @@ import {
|
|
|
27
28
|
configuredClaudeSourceHashes,
|
|
28
29
|
} from "../shared/experiment-behavior.js";
|
|
29
30
|
import { prepareClaudeExperimentConfig } from "../shared/experiment-policy.js";
|
|
30
|
-
import { copyRegularFileNoFollow } from "../shared/secure-copy.js";
|
|
31
31
|
import { subprocessDriver } from "../subprocess/index.js";
|
|
32
32
|
import { runDeferredProcess } from "./deferred-process.js";
|
|
33
|
+
import { isolatedLaunch } from "./isolation.js";
|
|
33
34
|
import { ClaudeCodeLineSchema, makeParseEvent } from "./parse-events.js";
|
|
34
35
|
|
|
35
36
|
const LUX_BOOKKEEPING_FILES = new Set([
|
|
@@ -43,12 +44,23 @@ const LUX_BOOKKEEPING_FILES = new Set([
|
|
|
43
44
|
const commandName = (command: string): string => command.split(/[\\/]/).at(-1) ?? command;
|
|
44
45
|
const ISOLATION_RESERVED_ARGS = new Set([
|
|
45
46
|
"--add-dir",
|
|
47
|
+
"--dangerously-skip-permissions",
|
|
48
|
+
"--allow-dangerously-skip-permissions",
|
|
49
|
+
"--permission-mode",
|
|
50
|
+
"--teleport",
|
|
51
|
+
"--continue",
|
|
52
|
+
"-c",
|
|
53
|
+
"--resume",
|
|
54
|
+
"-r",
|
|
55
|
+
"--fork-session",
|
|
56
|
+
"--session-id",
|
|
46
57
|
"--agent",
|
|
47
58
|
"--agents",
|
|
48
59
|
"--bare",
|
|
49
60
|
"--mcp-config",
|
|
50
61
|
"--no-session-persistence",
|
|
51
62
|
"--plugin-dir",
|
|
63
|
+
"--plugin-dir-no-mcp",
|
|
52
64
|
"--plugin-url",
|
|
53
65
|
"--safe-mode",
|
|
54
66
|
"--setting-sources",
|
|
@@ -64,6 +76,8 @@ const IsolationSchema = z
|
|
|
64
76
|
* and `addDirs` paths are included automatically.
|
|
65
77
|
*/
|
|
66
78
|
allowRead: z.array(z.string()).default([]),
|
|
79
|
+
/** Additional protected trees and files, including other evaluation runs. */
|
|
80
|
+
denyRead: z.array(z.string()).optional(),
|
|
67
81
|
})
|
|
68
82
|
.strict();
|
|
69
83
|
|
|
@@ -76,6 +90,7 @@ export const ClaudeCodeDriverConfigSchema = z
|
|
|
76
90
|
cwdMode: z.enum(["artifacts", "runDir"]).default("artifacts"),
|
|
77
91
|
cwd: z.string().optional(),
|
|
78
92
|
env: z.record(z.string(), z.string()).optional(),
|
|
93
|
+
inheritEnv: InheritEnvironmentSchema.default(true),
|
|
79
94
|
/** Low-cost by default; choose a larger model explicitly when needed. */
|
|
80
95
|
model: z.string().min(1).default(DEFAULT_CLAUDE_MODEL),
|
|
81
96
|
reasoningEffort: z.enum(claudeCodeCatalog.efforts).optional(),
|
|
@@ -126,9 +141,15 @@ export const ClaudeCodeDriverConfigSchema = z
|
|
|
126
141
|
}
|
|
127
142
|
if (config.interactionMode === "defer-resume") {
|
|
128
143
|
const reserved = config.extraArgs?.find((arg) =>
|
|
129
|
-
[
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
[
|
|
145
|
+
"--input-format",
|
|
146
|
+
"--no-session-persistence",
|
|
147
|
+
"--resume",
|
|
148
|
+
"-r",
|
|
149
|
+
"--settings",
|
|
150
|
+
"--permission-prompt-tool",
|
|
151
|
+
"--permission-prompts",
|
|
152
|
+
].some((name) => arg === name || arg.startsWith(`${name}=`)),
|
|
132
153
|
);
|
|
133
154
|
if (reserved) {
|
|
134
155
|
ctx.addIssue({
|
|
@@ -147,11 +168,12 @@ export const ClaudeCodeDriverConfigSchema = z
|
|
|
147
168
|
"isolation requires permissionMode to be omitted or 'default'; Claude Code forces default mode while subprocess environment scrubbing is enabled",
|
|
148
169
|
});
|
|
149
170
|
}
|
|
150
|
-
if (config.interactionMode === "defer-resume") {
|
|
171
|
+
if (config.interactionMode === "defer-resume" && config.pluginDirs?.length) {
|
|
151
172
|
ctx.addIssue({
|
|
152
173
|
code: "custom",
|
|
153
|
-
path: ["
|
|
154
|
-
message:
|
|
174
|
+
path: ["pluginDirs"],
|
|
175
|
+
message:
|
|
176
|
+
"isolated defer-resume cannot load plugin hooks; use addDirs for read-only skill files",
|
|
155
177
|
});
|
|
156
178
|
}
|
|
157
179
|
if (config.cwd || config.cwdMode !== "artifacts") {
|
|
@@ -231,15 +253,33 @@ const buildDeferredArgs = (
|
|
|
231
253
|
config: ClaudeCodeDriverConfig,
|
|
232
254
|
hookPath: string,
|
|
233
255
|
sessionId?: string,
|
|
256
|
+
isolation?: Awaited<ReturnType<typeof isolatedLaunch>>,
|
|
234
257
|
): string[] => {
|
|
235
|
-
|
|
258
|
+
// Native print mode removes AskUserQuestion unless a host permission surface exists.
|
|
259
|
+
// The trusted PreToolUse hook handles the question before that surface is consulted.
|
|
260
|
+
const out = [
|
|
261
|
+
"--print",
|
|
262
|
+
"--output-format",
|
|
263
|
+
"stream-json",
|
|
264
|
+
"--verbose",
|
|
265
|
+
"--permission-prompt-tool",
|
|
266
|
+
"stdio",
|
|
267
|
+
];
|
|
236
268
|
out.push("--model", config.model, ...effortArgs(config));
|
|
237
269
|
if (config.permissionMode) out.push("--permission-mode", config.permissionMode);
|
|
238
270
|
out.push("--allowed-tools", ...allowedTools(config));
|
|
239
271
|
for (const dir of config.pluginDirs ?? []) out.push("--plugin-dir", dir);
|
|
240
272
|
if (config.agent) out.push("--agent", config.agent);
|
|
241
|
-
for (const dir of config.addDirs ?? []) out.push("--add-dir", dir);
|
|
242
|
-
out.push(
|
|
273
|
+
if (!isolation) for (const dir of config.addDirs ?? []) out.push("--add-dir", dir);
|
|
274
|
+
out.push(
|
|
275
|
+
"--settings",
|
|
276
|
+
JSON.stringify({
|
|
277
|
+
...isolation?.settings,
|
|
278
|
+
...deferredHookSettings(config, hookPath),
|
|
279
|
+
...(isolation ? { disableAllHooks: false } : {}),
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
if (isolation) out.push("--strict-mcp-config", "--setting-sources", "");
|
|
243
283
|
if (sessionId) out.push("--resume", sessionId);
|
|
244
284
|
if (config.extraArgs) out.push(...config.extraArgs);
|
|
245
285
|
return out;
|
|
@@ -295,184 +335,6 @@ process.stdin.on("end", () => {
|
|
|
295
335
|
});
|
|
296
336
|
`;
|
|
297
337
|
|
|
298
|
-
const isWithin = (parent: string, child: string): boolean =>
|
|
299
|
-
child === parent || child.startsWith(`${parent}${sep}`);
|
|
300
|
-
|
|
301
|
-
const permissionPath = (tool: "Edit" | "Read", path: string, directory: boolean): string => {
|
|
302
|
-
const absolute = resolve(path).replaceAll("\\", "/");
|
|
303
|
-
return `${tool}(//${absolute.replace(/^\/+/, "")}${directory ? "/**" : ""})`;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Permission rules have no "allow inside deny" override, so isolate a root by
|
|
308
|
-
* denying every sibling around explicitly allowed descendants.
|
|
309
|
-
*/
|
|
310
|
-
const carvedPermissionDenials = async (
|
|
311
|
-
tool: "Edit" | "Read",
|
|
312
|
-
root: string,
|
|
313
|
-
allowed: readonly string[],
|
|
314
|
-
): Promise<string[]> => {
|
|
315
|
-
const normalizedRoot = resolve(root);
|
|
316
|
-
const inside = allowed
|
|
317
|
-
.map((path) => resolve(path))
|
|
318
|
-
.filter((path) => isWithin(normalizedRoot, path));
|
|
319
|
-
if (inside.some((path) => path === normalizedRoot)) return [];
|
|
320
|
-
if (inside.length === 0) return [permissionPath(tool, normalizedRoot, true)];
|
|
321
|
-
|
|
322
|
-
const out: string[] = [];
|
|
323
|
-
const walk = async (dir: string): Promise<void> => {
|
|
324
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
325
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
326
|
-
for (const entry of entries) {
|
|
327
|
-
const path = join(dir, entry.name);
|
|
328
|
-
if (inside.some((allowedPath) => allowedPath === path)) continue;
|
|
329
|
-
if (entry.isDirectory() && inside.some((allowedPath) => isWithin(path, allowedPath))) {
|
|
330
|
-
await walk(path);
|
|
331
|
-
} else {
|
|
332
|
-
out.push(permissionPath(tool, path, entry.isDirectory()));
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
await walk(normalizedRoot);
|
|
337
|
-
return out;
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
const carvedReadDenials = (root: string, allowed: readonly string[]): Promise<string[]> =>
|
|
341
|
-
carvedPermissionDenials("Read", root, allowed);
|
|
342
|
-
|
|
343
|
-
const existingRealpaths = async (paths: readonly string[]): Promise<string[]> => {
|
|
344
|
-
const found = await Promise.all(
|
|
345
|
-
paths.map(async (path) => {
|
|
346
|
-
try {
|
|
347
|
-
return await realpath(resolve(path));
|
|
348
|
-
} catch {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
}),
|
|
352
|
-
);
|
|
353
|
-
return [...new Set(found.filter((path): path is string => path !== null))];
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
const compactRoots = (paths: readonly string[]): string[] =>
|
|
357
|
-
[...new Set(paths)]
|
|
358
|
-
.sort((a, b) => a.length - b.length || a.localeCompare(b))
|
|
359
|
-
.filter((path, index, roots) => !roots.slice(0, index).some((root) => isWithin(root, path)));
|
|
360
|
-
|
|
361
|
-
const isolatedLaunch = async (
|
|
362
|
-
config: ClaudeCodeDriverConfig,
|
|
363
|
-
ctx: StartContext,
|
|
364
|
-
workspace: string,
|
|
365
|
-
): Promise<{ args: string[]; env: Record<string, string> }> => {
|
|
366
|
-
const homeRoots = await existingRealpaths([
|
|
367
|
-
homedir(),
|
|
368
|
-
...(config.env?.HOME ? [config.env.HOME] : []),
|
|
369
|
-
]);
|
|
370
|
-
const sensitiveHomePaths = homeRoots.flatMap((home) => [
|
|
371
|
-
join(home, ".aws"),
|
|
372
|
-
join(home, ".config", "gcloud"),
|
|
373
|
-
join(home, ".docker", "config.json"),
|
|
374
|
-
join(home, ".git-credentials"),
|
|
375
|
-
join(home, ".gnupg"),
|
|
376
|
-
join(home, ".kube"),
|
|
377
|
-
join(home, ".netrc"),
|
|
378
|
-
join(home, ".npmrc"),
|
|
379
|
-
join(home, ".ssh"),
|
|
380
|
-
]);
|
|
381
|
-
const protectedRoots = await existingRealpaths([
|
|
382
|
-
...(ctx.runsRoot ? [ctx.runsRoot] : [ctx.runDir]),
|
|
383
|
-
...(process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []),
|
|
384
|
-
...(config.env?.CLAUDE_CONFIG_DIR ? [config.env.CLAUDE_CONFIG_DIR] : []),
|
|
385
|
-
...sensitiveHomePaths,
|
|
386
|
-
...homeRoots.flatMap((home) => [
|
|
387
|
-
join(home, ".claude", "projects"),
|
|
388
|
-
join(home, ".claude", "memory"),
|
|
389
|
-
]),
|
|
390
|
-
]);
|
|
391
|
-
const denyRoots = compactRoots(protectedRoots);
|
|
392
|
-
const allowed = compactRoots(
|
|
393
|
-
await existingRealpaths([
|
|
394
|
-
...(config.pluginDirs ?? []),
|
|
395
|
-
...(config.addDirs ?? []),
|
|
396
|
-
...(config.isolation?.allowRead ?? []),
|
|
397
|
-
]),
|
|
398
|
-
);
|
|
399
|
-
const overlapping = allowed.find(
|
|
400
|
-
(path) =>
|
|
401
|
-
(path !== workspace && isWithin(path, workspace)) ||
|
|
402
|
-
denyRoots.some((root) => isWithin(path, root)) ||
|
|
403
|
-
protectedRoots.some((root) => isWithin(root, path)),
|
|
404
|
-
);
|
|
405
|
-
if (overlapping) {
|
|
406
|
-
throw new Error(`isolation read carve-out overlaps a protected boundary: ${overlapping}`);
|
|
407
|
-
}
|
|
408
|
-
const readOnlyRoots = allowed.filter((path) => !isWithin(workspace, path));
|
|
409
|
-
const editDenyRoots = compactRoots([...denyRoots, ...readOnlyRoots]);
|
|
410
|
-
const [readDenials, writeDenials] = await Promise.all([
|
|
411
|
-
Promise.all(denyRoots.map((root) => carvedPermissionDenials("Read", root, allowed))),
|
|
412
|
-
Promise.all(editDenyRoots.map((root) => carvedPermissionDenials("Edit", root, []))),
|
|
413
|
-
]);
|
|
414
|
-
const denyRules = [...readDenials.flat(), ...writeDenials.flat()];
|
|
415
|
-
const sandboxSettings = {
|
|
416
|
-
disableAllHooks: true,
|
|
417
|
-
permissions: { deny: denyRules },
|
|
418
|
-
sandbox: {
|
|
419
|
-
enabled: true,
|
|
420
|
-
failIfUnavailable: true,
|
|
421
|
-
allowUnsandboxedCommands: false,
|
|
422
|
-
filesystem: {
|
|
423
|
-
denyRead: denyRoots,
|
|
424
|
-
denyWrite: denyRoots,
|
|
425
|
-
allowRead: allowed,
|
|
426
|
-
},
|
|
427
|
-
// Filesystem isolation does not disable the live docs and package
|
|
428
|
-
// registry access that cases are intended to exercise.
|
|
429
|
-
network: { allowedDomains: ["*"] },
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
const stateDir = join(workspace, ".lux-claude");
|
|
433
|
-
const tmpDir = join(stateDir, "tmp");
|
|
434
|
-
const cacheDir = join(stateDir, "cache");
|
|
435
|
-
const pluginCacheDir = join(stateDir, "plugins");
|
|
436
|
-
await Promise.all([
|
|
437
|
-
mkdir(tmpDir, { recursive: true }),
|
|
438
|
-
mkdir(cacheDir, { recursive: true }),
|
|
439
|
-
mkdir(pluginCacheDir, { recursive: true }),
|
|
440
|
-
]);
|
|
441
|
-
return {
|
|
442
|
-
args: [
|
|
443
|
-
// --add-dir also discovers skills and agents. Isolation treats addDirs
|
|
444
|
-
// as data-only read grants, so expose them through the filesystem policy
|
|
445
|
-
// above without passing the discovery flag to Claude.
|
|
446
|
-
...buildArgs({ ...config, addDirs: undefined }),
|
|
447
|
-
"--no-session-persistence",
|
|
448
|
-
"--strict-mcp-config",
|
|
449
|
-
// Authentication is independent of filesystem setting sources. Keep
|
|
450
|
-
// these empty instead of using --safe-mode: safe mode would also disable
|
|
451
|
-
// the explicitly requested plugin's skills, commands, and agents.
|
|
452
|
-
"--setting-sources",
|
|
453
|
-
"",
|
|
454
|
-
"--settings",
|
|
455
|
-
JSON.stringify(sandboxSettings),
|
|
456
|
-
],
|
|
457
|
-
env: {
|
|
458
|
-
...(config.env ?? {}),
|
|
459
|
-
CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
|
|
460
|
-
CLAUDE_CODE_DISABLE_CLAUDE_MDS: "1",
|
|
461
|
-
CLAUDE_CODE_SKIP_PROMPT_HISTORY: "1",
|
|
462
|
-
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1",
|
|
463
|
-
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
|
|
464
|
-
CLAUDE_CODE_AUTO_CONNECT_IDE: "false",
|
|
465
|
-
CLAUDE_CODE_PLUGIN_CACHE_DIR: pluginCacheDir,
|
|
466
|
-
CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: "1",
|
|
467
|
-
DISABLE_AUTOUPDATER: "1",
|
|
468
|
-
TMPDIR: tmpDir,
|
|
469
|
-
XDG_CACHE_HOME: cacheDir,
|
|
470
|
-
npm_config_cache: join(cacheDir, "npm"),
|
|
471
|
-
BUN_INSTALL_CACHE_DIR: join(cacheDir, "bun"),
|
|
472
|
-
},
|
|
473
|
-
};
|
|
474
|
-
};
|
|
475
|
-
|
|
476
338
|
const formatPrompt = (text: string): string =>
|
|
477
339
|
`${JSON.stringify({
|
|
478
340
|
type: "user",
|
|
@@ -501,9 +363,7 @@ const formatAnswer = (answer: Answer): string => {
|
|
|
501
363
|
class ClaudeCodeDriverImpl implements AgentDriver {
|
|
502
364
|
private inner: AgentDriver | null = null;
|
|
503
365
|
private readonly emitter = new TypedEmitter<DriverEventMap>();
|
|
504
|
-
//
|
|
505
|
-
// so the queue stays type-safe without per-entry casts; the identity pair
|
|
506
|
-
// rides along so off() can drop a queued attach symmetrically.
|
|
366
|
+
// Typed closures retain handler identity so off() can detach queued listeners.
|
|
507
367
|
private pendingAttach: Array<{
|
|
508
368
|
event: keyof DriverEventMap;
|
|
509
369
|
handler: unknown;
|
|
@@ -514,6 +374,8 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
514
374
|
private artifactsDir: string | null = null;
|
|
515
375
|
private runDir: string | null = null;
|
|
516
376
|
private isolatedWorkspace: string | null = null;
|
|
377
|
+
private isolationControlDir: string | null = null;
|
|
378
|
+
private deferredIsolation: Awaited<ReturnType<typeof isolatedLaunch>> | undefined;
|
|
517
379
|
private deferredContext: StartContext | null = null;
|
|
518
380
|
private deferredCwd: string | null = null;
|
|
519
381
|
private deferredHookPath: string | null = null;
|
|
@@ -571,15 +433,20 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
571
433
|
let cwd =
|
|
572
434
|
this.config.cwd ?? (this.config.cwdMode === "artifacts" ? ctx.artifactsDir : ctx.runDir);
|
|
573
435
|
let args = buildArgs(this.config);
|
|
574
|
-
let env = this.config.env;
|
|
436
|
+
let env = agentEnvironment(process.env, this.config.inheritEnv, this.config.env);
|
|
575
437
|
if (this.config.isolation) {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
438
|
+
const launch = await this.prepareIsolation(ctx);
|
|
439
|
+
cwd = this.isolatedWorkspace as string;
|
|
440
|
+
args = [
|
|
441
|
+
...buildArgs({ ...this.config, addDirs: undefined }),
|
|
442
|
+
"--no-session-persistence",
|
|
443
|
+
"--strict-mcp-config",
|
|
444
|
+
"--setting-sources",
|
|
445
|
+
"",
|
|
446
|
+
"--settings",
|
|
447
|
+
JSON.stringify(launch.settings),
|
|
448
|
+
];
|
|
449
|
+
env = agentEnvironment(process.env, this.config.inheritEnv, launch.env);
|
|
583
450
|
}
|
|
584
451
|
this.collectDir = cwd;
|
|
585
452
|
this.artifactsDir = ctx.artifactsDir;
|
|
@@ -593,6 +460,7 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
593
460
|
args,
|
|
594
461
|
cwd,
|
|
595
462
|
env,
|
|
463
|
+
inheritEnv: false,
|
|
596
464
|
protocol: "custom",
|
|
597
465
|
parseEvent: makeParseEvent(this.config, cliVersion),
|
|
598
466
|
formatAnswer,
|
|
@@ -628,9 +496,7 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
628
496
|
|
|
629
497
|
async collect(): Promise<Artifact[]> {
|
|
630
498
|
if (!this.collectDir) return [];
|
|
631
|
-
//
|
|
632
|
-
// have exited. Quiesce the whole process tree before walking/copying files
|
|
633
|
-
// so it cannot swap a checked regular file into a symlink.
|
|
499
|
+
// Stop descendants before collecting: terminal events can precede their exit.
|
|
634
500
|
await this.quiesce();
|
|
635
501
|
const artifacts = await walkArtifacts(this.collectDir);
|
|
636
502
|
if (this.isolatedWorkspace && this.artifactsDir) {
|
|
@@ -648,19 +514,21 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
648
514
|
);
|
|
649
515
|
return artifacts;
|
|
650
516
|
}
|
|
651
|
-
//
|
|
652
|
-
// a walk of any other root must say what its paths are relative to, or
|
|
653
|
-
// the judge and file assertions silently miss every file.
|
|
517
|
+
// Assertions need an explicit root for artifacts collected outside artifactsDir.
|
|
654
518
|
if (this.collectDir === this.artifactsDir) return artifacts;
|
|
655
519
|
const root = this.collectDir;
|
|
656
|
-
//
|
|
657
|
-
// work product; only top-level files can collide with these names.
|
|
520
|
+
// Top-level bookkeeping in runDir is not agent work product.
|
|
658
521
|
const withRoot = artifacts.map((a) => ({ ...a, root }));
|
|
659
522
|
return root === this.runDir
|
|
660
523
|
? withRoot.filter((a) => !LUX_BOOKKEEPING_FILES.has(a.path))
|
|
661
524
|
: withRoot;
|
|
662
525
|
}
|
|
663
526
|
|
|
527
|
+
async verificationWorkspace(): Promise<string | undefined> {
|
|
528
|
+
await this.quiesce();
|
|
529
|
+
return this.isolatedWorkspace ?? undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
664
532
|
async quiesce(): Promise<void> {
|
|
665
533
|
if (this.config.interactionMode === "defer-resume") {
|
|
666
534
|
this.deferredAbort.abort();
|
|
@@ -683,16 +551,36 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
683
551
|
await this.inner?.close();
|
|
684
552
|
}
|
|
685
553
|
} finally {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
554
|
+
await Promise.all(
|
|
555
|
+
[this.isolatedWorkspace, this.isolationControlDir]
|
|
556
|
+
.filter((path) => path !== null)
|
|
557
|
+
.map((path) => rm(path, { recursive: true, force: true })),
|
|
558
|
+
);
|
|
559
|
+
this.isolatedWorkspace = null;
|
|
560
|
+
this.isolationControlDir = null;
|
|
690
561
|
}
|
|
691
562
|
}
|
|
692
563
|
|
|
564
|
+
private async prepareIsolation(
|
|
565
|
+
ctx: StartContext,
|
|
566
|
+
): Promise<Awaited<ReturnType<typeof isolatedLaunch>>> {
|
|
567
|
+
this.isolatedWorkspace = await realpath(
|
|
568
|
+
temporaryDirectory({ prefix: "lux-claude-workspace-" }),
|
|
569
|
+
);
|
|
570
|
+
this.isolationControlDir = await realpath(
|
|
571
|
+
temporaryDirectory({ prefix: "lux-claude-control-" }),
|
|
572
|
+
);
|
|
573
|
+
await cp(ctx.artifactsDir, this.isolatedWorkspace, { recursive: true });
|
|
574
|
+
return isolatedLaunch(this.config, ctx, this.isolatedWorkspace, this.isolationControlDir);
|
|
575
|
+
}
|
|
576
|
+
|
|
693
577
|
private async startDeferred(ctx: StartContext): Promise<ReadyState> {
|
|
694
|
-
|
|
578
|
+
let cwd =
|
|
695
579
|
this.config.cwd ?? (this.config.cwdMode === "artifacts" ? ctx.artifactsDir : ctx.runDir);
|
|
580
|
+
if (this.config.isolation) {
|
|
581
|
+
this.deferredIsolation = await this.prepareIsolation(ctx);
|
|
582
|
+
cwd = this.isolatedWorkspace as string;
|
|
583
|
+
}
|
|
696
584
|
this.collectDir = cwd;
|
|
697
585
|
this.artifactsDir = ctx.artifactsDir;
|
|
698
586
|
this.runDir = ctx.runDir;
|
|
@@ -743,15 +631,19 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
743
631
|
}
|
|
744
632
|
const terminal = await runDeferredProcess({
|
|
745
633
|
command: this.config.command,
|
|
746
|
-
args: buildDeferredArgs(
|
|
634
|
+
args: buildDeferredArgs(
|
|
635
|
+
this.config,
|
|
636
|
+
hookPath,
|
|
637
|
+
this.deferredSessionId ?? undefined,
|
|
638
|
+
this.deferredIsolation,
|
|
639
|
+
),
|
|
747
640
|
cwd,
|
|
748
|
-
env: {
|
|
749
|
-
...
|
|
641
|
+
env: agentEnvironment(process.env, this.config.inheritEnv, {
|
|
642
|
+
...(this.deferredIsolation?.env ?? this.config.env),
|
|
750
643
|
LUX_RUN_DIR: ctx.runDir,
|
|
751
644
|
LUX_ARTIFACTS_DIR: ctx.artifactsDir,
|
|
752
|
-
...this.config.env,
|
|
753
645
|
...(answer ? { LUX_CLAUDE_DEFERRED_ANSWER: JSON.stringify(answer) } : {}),
|
|
754
|
-
},
|
|
646
|
+
}),
|
|
755
647
|
...input,
|
|
756
648
|
idleTimeoutMs: durationMs(this.config.idleTimeout),
|
|
757
649
|
...(this.config.turnTimeout === undefined
|
|
@@ -901,7 +793,6 @@ export const claudeCodeDriver = defineDriver({
|
|
|
901
793
|
export {
|
|
902
794
|
buildArgs,
|
|
903
795
|
buildDeferredArgs,
|
|
904
|
-
carvedReadDenials,
|
|
905
796
|
copyRegularFileNoFollow,
|
|
906
797
|
formatAnswer,
|
|
907
798
|
formatPrompt,
|