@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.
Files changed (152) hide show
  1. package/lib/answerers/persona/index.d.ts +6 -0
  2. package/lib/answerers/persona/index.d.ts.map +1 -1
  3. package/lib/answerers/persona/index.js +5 -0
  4. package/lib/answerers/persona/index.js.map +1 -1
  5. package/lib/assertions/core/command-exits-zero.d.ts +2 -0
  6. package/lib/assertions/core/command-exits-zero.d.ts.map +1 -1
  7. package/lib/assertions/core/command-exits-zero.js +46 -4
  8. package/lib/assertions/core/command-exits-zero.js.map +1 -1
  9. package/lib/assertions/rubric/index.d.ts +16 -0
  10. package/lib/assertions/rubric/index.d.ts.map +1 -1
  11. package/lib/assertions/rubric/index.js +34 -3
  12. package/lib/assertions/rubric/index.js.map +1 -1
  13. package/lib/assertions/rubric/internal.d.ts +14 -0
  14. package/lib/assertions/rubric/internal.d.ts.map +1 -1
  15. package/lib/assertions/rubric/internal.js +122 -12
  16. package/lib/assertions/rubric/internal.js.map +1 -1
  17. package/lib/core/agent-environment.d.ts +5 -0
  18. package/lib/core/agent-environment.d.ts.map +1 -0
  19. package/lib/core/agent-environment.js +47 -0
  20. package/lib/core/agent-environment.js.map +1 -0
  21. package/lib/core/command-verification.d.ts +10 -0
  22. package/lib/core/command-verification.d.ts.map +1 -0
  23. package/lib/core/command-verification.js +66 -0
  24. package/lib/core/command-verification.js.map +1 -0
  25. package/lib/core/driver.d.ts +7 -0
  26. package/lib/core/driver.d.ts.map +1 -1
  27. package/lib/core/driver.js +2 -0
  28. package/lib/core/driver.js.map +1 -1
  29. package/lib/core/index.d.ts +2 -0
  30. package/lib/core/index.d.ts.map +1 -1
  31. package/lib/core/index.js +2 -0
  32. package/lib/core/index.js.map +1 -1
  33. package/lib/core/model-cli.d.ts +5 -0
  34. package/lib/core/model-cli.d.ts.map +1 -1
  35. package/lib/core/model-cli.js +8 -2
  36. package/lib/core/model-cli.js.map +1 -1
  37. package/lib/core/run.d.ts +5 -0
  38. package/lib/core/run.d.ts.map +1 -1
  39. package/lib/core/secure-copy.d.ts.map +1 -0
  40. package/lib/core/secure-copy.js.map +1 -0
  41. package/lib/drivers/antigravity/config.d.ts +3 -0
  42. package/lib/drivers/antigravity/config.d.ts.map +1 -1
  43. package/lib/drivers/antigravity/config.js +2 -0
  44. package/lib/drivers/antigravity/config.js.map +1 -1
  45. package/lib/drivers/antigravity/index.d.ts +4 -0
  46. package/lib/drivers/antigravity/index.d.ts.map +1 -1
  47. package/lib/drivers/antigravity/runtime.d.ts.map +1 -1
  48. package/lib/drivers/antigravity/runtime.js +7 -2
  49. package/lib/drivers/antigravity/runtime.js.map +1 -1
  50. package/lib/drivers/claude-code/index.d.ts +18 -4
  51. package/lib/drivers/claude-code/index.d.ts.map +1 -1
  52. package/lib/drivers/claude-code/index.js +97 -200
  53. package/lib/drivers/claude-code/index.js.map +1 -1
  54. package/lib/drivers/claude-code/isolation.d.ts +8 -0
  55. package/lib/drivers/claude-code/isolation.d.ts.map +1 -0
  56. package/lib/drivers/claude-code/isolation.js +199 -0
  57. package/lib/drivers/claude-code/isolation.js.map +1 -0
  58. package/lib/drivers/codex/config.d.ts +3 -0
  59. package/lib/drivers/codex/config.d.ts.map +1 -1
  60. package/lib/drivers/codex/config.js +2 -0
  61. package/lib/drivers/codex/config.js.map +1 -1
  62. package/lib/drivers/codex/index.d.ts +4 -0
  63. package/lib/drivers/codex/index.d.ts.map +1 -1
  64. package/lib/drivers/codex/index.js +52 -24
  65. package/lib/drivers/codex/index.js.map +1 -1
  66. package/lib/drivers/copilot/config.d.ts +3 -0
  67. package/lib/drivers/copilot/config.d.ts.map +1 -1
  68. package/lib/drivers/copilot/config.js +2 -0
  69. package/lib/drivers/copilot/config.js.map +1 -1
  70. package/lib/drivers/copilot/index.d.ts +4 -0
  71. package/lib/drivers/copilot/index.d.ts.map +1 -1
  72. package/lib/drivers/copilot/index.js +2 -1
  73. package/lib/drivers/copilot/index.js.map +1 -1
  74. package/lib/drivers/cursor/config.d.ts +1 -0
  75. package/lib/drivers/cursor/config.d.ts.map +1 -1
  76. package/lib/drivers/cursor/config.js +2 -0
  77. package/lib/drivers/cursor/config.js.map +1 -1
  78. package/lib/drivers/cursor/index.d.ts +2 -0
  79. package/lib/drivers/cursor/index.d.ts.map +1 -1
  80. package/lib/drivers/cursor/index.js +2 -1
  81. package/lib/drivers/cursor/index.js.map +1 -1
  82. package/lib/drivers/grok-build/config.d.ts +3 -0
  83. package/lib/drivers/grok-build/config.d.ts.map +1 -1
  84. package/lib/drivers/grok-build/config.js +2 -0
  85. package/lib/drivers/grok-build/config.js.map +1 -1
  86. package/lib/drivers/grok-build/index.d.ts +4 -0
  87. package/lib/drivers/grok-build/index.d.ts.map +1 -1
  88. package/lib/drivers/grok-build/index.js +2 -1
  89. package/lib/drivers/grok-build/index.js.map +1 -1
  90. package/lib/drivers/subprocess/index.d.ts +3 -3
  91. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  92. package/lib/drivers/subprocess/index.js +5 -2
  93. package/lib/drivers/subprocess/index.js.map +1 -1
  94. package/lib/orchestrator/artifact-origin.d.ts +3 -0
  95. package/lib/orchestrator/artifact-origin.d.ts.map +1 -0
  96. package/lib/orchestrator/artifact-origin.js +40 -0
  97. package/lib/orchestrator/artifact-origin.js.map +1 -0
  98. package/lib/orchestrator/config.d.ts +6 -0
  99. package/lib/orchestrator/config.d.ts.map +1 -1
  100. package/lib/orchestrator/experiment-context.d.ts +2 -0
  101. package/lib/orchestrator/experiment-context.d.ts.map +1 -1
  102. package/lib/orchestrator/experiment-corpus.d.ts.map +1 -1
  103. package/lib/orchestrator/experiment-corpus.js +2 -2
  104. package/lib/orchestrator/experiment-corpus.js.map +1 -1
  105. package/lib/orchestrator/experiment-identity.d.ts.map +1 -1
  106. package/lib/orchestrator/experiment-identity.js +4 -4
  107. package/lib/orchestrator/experiment-identity.js.map +1 -1
  108. package/lib/orchestrator/fixture-identity.d.ts +1 -0
  109. package/lib/orchestrator/fixture-identity.d.ts.map +1 -1
  110. package/lib/orchestrator/fixture-identity.js +6 -1
  111. package/lib/orchestrator/fixture-identity.js.map +1 -1
  112. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  113. package/lib/orchestrator/orchestrator.js +2 -2
  114. package/lib/orchestrator/orchestrator.js.map +1 -1
  115. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  116. package/lib/orchestrator/run-execution.js +14 -3
  117. package/lib/orchestrator/run-execution.js.map +1 -1
  118. package/package.json +1 -1
  119. package/skills/lux-answerer/SKILL.md +1 -1
  120. package/src/answerers/persona/index.ts +5 -0
  121. package/src/assertions/core/command-exits-zero.ts +57 -4
  122. package/src/assertions/rubric/index.ts +38 -3
  123. package/src/assertions/rubric/internal.ts +172 -11
  124. package/src/core/agent-environment.ts +64 -0
  125. package/src/core/command-verification.ts +74 -0
  126. package/src/core/driver.ts +4 -0
  127. package/src/core/index.ts +2 -0
  128. package/src/core/model-cli.ts +14 -2
  129. package/src/drivers/antigravity/config.ts +2 -0
  130. package/src/drivers/antigravity/runtime.ts +8 -2
  131. package/src/drivers/claude-code/index.ts +112 -221
  132. package/src/drivers/claude-code/isolation.ts +228 -0
  133. package/src/drivers/codex/config.ts +2 -0
  134. package/src/drivers/codex/index.ts +71 -31
  135. package/src/drivers/copilot/config.ts +2 -0
  136. package/src/drivers/copilot/index.ts +2 -1
  137. package/src/drivers/cursor/config.ts +2 -0
  138. package/src/drivers/cursor/index.ts +2 -1
  139. package/src/drivers/grok-build/config.ts +2 -0
  140. package/src/drivers/grok-build/index.ts +2 -1
  141. package/src/drivers/subprocess/index.ts +5 -2
  142. package/src/orchestrator/artifact-origin.ts +50 -0
  143. package/src/orchestrator/experiment-corpus.ts +10 -7
  144. package/src/orchestrator/experiment-identity.ts +16 -13
  145. package/src/orchestrator/fixture-identity.ts +9 -1
  146. package/src/orchestrator/orchestrator.ts +12 -6
  147. package/src/orchestrator/run-execution.ts +21 -4
  148. package/lib/drivers/shared/secure-copy.d.ts.map +0 -1
  149. package/lib/drivers/shared/secure-copy.js.map +0 -1
  150. /package/lib/{drivers/shared → core}/secure-copy.d.ts +0 -0
  151. /package/lib/{drivers/shared → core}/secure-copy.js +0 -0
  152. /package/src/{drivers/shared → core}/secure-copy.ts +0 -0
@@ -1,8 +1,8 @@
1
- import { cp, mkdir, readdir, realpath, rm, writeFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
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
- ["--input-format", "--no-session-persistence", "--resume", "-r", "--settings"].some(
130
- (name) => arg === name || arg.startsWith(`${name}=`),
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: ["interactionMode"],
154
- message: "defer-resume requires session persistence and is incompatible with isolation",
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
- const out = ["--print", "--output-format", "stream-json", "--verbose"];
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("--settings", JSON.stringify(deferredHookSettings(config, hookPath)));
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
- // The attach closure captures its (event, handler) pair with narrow types,
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 workspace = await realpath(temporaryDirectory({ prefix: "lux-claude-workspace-" }));
577
- await cp(ctx.artifactsDir, workspace, { recursive: true });
578
- this.isolatedWorkspace = workspace;
579
- cwd = workspace;
580
- const launch = await isolatedLaunch(this.config, ctx, workspace);
581
- args = launch.args;
582
- env = launch.env;
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
- // A terminal protocol event can arrive before the CLI and its descendants
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
- // Consumers resolve artifact paths against <runDir>/artifacts by default;
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
- // Walking the run dir itself would list Lux's own bookkeeping as agent
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
- if (this.isolatedWorkspace) {
687
- await rm(this.isolatedWorkspace, { recursive: true, force: true });
688
- this.isolatedWorkspace = null;
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
- const cwd =
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(this.config, hookPath, this.deferredSessionId ?? undefined),
634
+ args: buildDeferredArgs(
635
+ this.config,
636
+ hookPath,
637
+ this.deferredSessionId ?? undefined,
638
+ this.deferredIsolation,
639
+ ),
747
640
  cwd,
748
- env: {
749
- ...process.env,
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,