@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
@@ -0,0 +1,228 @@
1
+ import { mkdir, readdir, realpath, stat, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve, sep } from "node:path";
4
+ import type { StartContext } from "../../core/index.js";
5
+ import type { ClaudeCodeDriverConfig } from "./index.js";
6
+
7
+ const isWithin = (parent: string, child: string): boolean =>
8
+ child === parent || child.startsWith(`${parent}${sep}`);
9
+
10
+ const permissionPath = (tool: "Edit" | "Read", path: string, directory: boolean): string => {
11
+ const absolute = resolve(path).replaceAll("\\", "/");
12
+ return `${tool}(//${absolute.replace(/^\/+/, "")}${directory ? "/**" : ""})`;
13
+ };
14
+
15
+ /**
16
+ * Permission rules have no "allow inside deny" override, so isolate a root by
17
+ * denying every sibling around explicitly allowed descendants.
18
+ */
19
+ const carvedPermissionDenials = async (
20
+ tool: "Edit" | "Read",
21
+ root: string,
22
+ allowed: readonly string[],
23
+ ): Promise<string[]> => {
24
+ const normalizedRoot = resolve(root);
25
+ const inside = allowed
26
+ .map((path) => resolve(path))
27
+ .filter((path) => isWithin(normalizedRoot, path));
28
+ if (inside.some((path) => path === normalizedRoot)) return [];
29
+ if (inside.length === 0) {
30
+ const metadata = await stat(normalizedRoot);
31
+ return [permissionPath(tool, normalizedRoot, metadata.isDirectory())];
32
+ }
33
+
34
+ const out: string[] = [];
35
+ const walk = async (dir: string): Promise<void> => {
36
+ const entries = await readdir(dir, { withFileTypes: true });
37
+ entries.sort((a, b) => a.name.localeCompare(b.name));
38
+ for (const entry of entries) {
39
+ const path = join(dir, entry.name);
40
+ if (inside.some((allowedPath) => allowedPath === path)) continue;
41
+ if (entry.isDirectory() && inside.some((allowedPath) => isWithin(path, allowedPath))) {
42
+ await walk(path);
43
+ } else {
44
+ out.push(permissionPath(tool, path, entry.isDirectory()));
45
+ }
46
+ }
47
+ };
48
+ await walk(normalizedRoot);
49
+ return out;
50
+ };
51
+
52
+ export const carvedReadDenials = (root: string, allowed: readonly string[]): Promise<string[]> =>
53
+ carvedPermissionDenials("Read", root, allowed);
54
+
55
+ const existingRealpaths = async (paths: readonly string[]): Promise<string[]> => {
56
+ const found = await Promise.all(
57
+ paths.map(async (path) => {
58
+ try {
59
+ return await realpath(resolve(path));
60
+ } catch {
61
+ return null;
62
+ }
63
+ }),
64
+ );
65
+ return [...new Set(found.filter((path): path is string => path !== null))];
66
+ };
67
+
68
+ const compactRoots = (paths: readonly string[]): string[] =>
69
+ [...new Set(paths)]
70
+ .sort((a, b) => a.length - b.length || a.localeCompare(b))
71
+ .filter((path, index, roots) => !roots.slice(0, index).some((root) => isWithin(root, path)));
72
+
73
+ export const isolatedLaunch = async (
74
+ config: ClaudeCodeDriverConfig,
75
+ ctx: StartContext,
76
+ workspace: string,
77
+ controlDir: string,
78
+ ): Promise<{ env: Record<string, string>; settings: object }> => {
79
+ const homeRoots = await existingRealpaths([
80
+ homedir(),
81
+ ...(config.env?.HOME ? [config.env.HOME] : []),
82
+ ]);
83
+ const sensitiveHomePaths = homeRoots.flatMap((home) => [
84
+ join(home, ".aws"),
85
+ join(home, ".claude.json"),
86
+ join(home, ".claude", ".credentials.json"),
87
+ join(home, ".config", "gcloud"),
88
+ join(home, ".docker", "config.json"),
89
+ join(home, ".git-credentials"),
90
+ join(home, ".gnupg"),
91
+ join(home, ".kube"),
92
+ join(home, ".netrc"),
93
+ join(home, ".npmrc"),
94
+ join(home, ".ssh"),
95
+ ...[
96
+ ".bash_profile",
97
+ ".bash_login",
98
+ ".bashrc",
99
+ ".bash_aliases",
100
+ ".profile",
101
+ ".zshenv",
102
+ ".zprofile",
103
+ ".zshrc",
104
+ ".zlogin",
105
+ ].map((name) => join(home, name)),
106
+ ]);
107
+ const stateDir = join(workspace, ".lux-claude");
108
+ const configDir = join(controlDir, "config");
109
+ await mkdir(configDir, { recursive: true });
110
+ // An empty override preserves the CLI's default, unhashed Keychain service.
111
+ const authConfigOverride =
112
+ config.env?.CLAUDE_SECURESTORAGE_CONFIG_DIR ??
113
+ process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR ??
114
+ config.env?.CLAUDE_CONFIG_DIR ??
115
+ process.env.CLAUDE_CONFIG_DIR ??
116
+ "";
117
+ const authConfigDir = authConfigOverride || join(config.env?.HOME ?? homedir(), ".claude");
118
+ const cleanShell = process.platform === "win32" ? undefined : join(controlDir, "clean-bash");
119
+ if (cleanShell)
120
+ await writeFile(
121
+ cleanShell,
122
+ '#!/bin/sh\nunset BASH_ENV ENV\nexec /bin/bash --noprofile --norc "$@"\n',
123
+ { mode: 0o700 },
124
+ );
125
+ const protectedRoots = await existingRealpaths([
126
+ ...(config.interactionMode === "defer-resume" ? [configDir, authConfigDir] : []),
127
+ ctx.runDir,
128
+ ...(ctx.runsRoot ? [ctx.runsRoot] : []),
129
+ ...(process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []),
130
+ ...(config.env?.CLAUDE_CONFIG_DIR ? [config.env.CLAUDE_CONFIG_DIR] : []),
131
+ ...(process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR
132
+ ? [process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR]
133
+ : []),
134
+ ...(config.env?.CLAUDE_SECURESTORAGE_CONFIG_DIR
135
+ ? [config.env.CLAUDE_SECURESTORAGE_CONFIG_DIR]
136
+ : []),
137
+ ...sensitiveHomePaths,
138
+ ...(config.isolation?.denyRead ?? []),
139
+ ...homeRoots.flatMap((home) => [
140
+ join(home, ".claude", "projects"),
141
+ join(home, ".claude", "memory"),
142
+ ]),
143
+ ]);
144
+ const denyRoots = compactRoots(protectedRoots);
145
+ const allowed = compactRoots(
146
+ await existingRealpaths([
147
+ ...(cleanShell ? [cleanShell] : []),
148
+ ...(config.pluginDirs ?? []),
149
+ ...(config.addDirs ?? []),
150
+ ...(config.isolation?.allowRead ?? []),
151
+ ]),
152
+ );
153
+ const overlapping = allowed.find(
154
+ (path) =>
155
+ (path !== workspace && isWithin(path, workspace)) ||
156
+ denyRoots.some((root) => isWithin(path, root)) ||
157
+ protectedRoots.some((root) => isWithin(root, path)),
158
+ );
159
+ if (overlapping) {
160
+ throw new Error(`isolation read carve-out overlaps a protected boundary: ${overlapping}`);
161
+ }
162
+ const readOnlyRoots = allowed.filter((path) => !isWithin(workspace, path));
163
+ const editDenyRoots = compactRoots([...denyRoots, ...readOnlyRoots, controlDir]);
164
+ const [readDenials, writeDenials] = await Promise.all([
165
+ Promise.all(denyRoots.map((root) => carvedPermissionDenials("Read", root, allowed))),
166
+ Promise.all(editDenyRoots.map((root) => carvedPermissionDenials("Edit", root, []))),
167
+ ]);
168
+ const denyRules = [...readDenials.flat(), ...writeDenials.flat()];
169
+ const sandboxSettings = {
170
+ disableAllHooks: true,
171
+ permissions: { deny: denyRules },
172
+ sandbox: {
173
+ enabled: true,
174
+ failIfUnavailable: true,
175
+ allowUnsandboxedCommands: false,
176
+ filesystem: {
177
+ denyRead: denyRoots,
178
+ denyWrite: [...denyRoots, controlDir],
179
+ allowRead: allowed,
180
+ },
181
+ // Filesystem isolation does not disable the live docs and package
182
+ // registry access that cases are intended to exercise.
183
+ network: { allowedDomains: ["*"] },
184
+ },
185
+ };
186
+ const tmpDir = join(stateDir, "tmp");
187
+ const cacheDir = join(stateDir, "cache");
188
+ const pluginCacheDir = join(stateDir, "plugins");
189
+ await Promise.all([
190
+ mkdir(tmpDir, { recursive: true }),
191
+ mkdir(cacheDir, { recursive: true }),
192
+ mkdir(pluginCacheDir, { recursive: true }),
193
+ ]);
194
+ return {
195
+ settings: sandboxSettings,
196
+ env: {
197
+ ...(config.env ?? {}),
198
+ ...(config.interactionMode === "defer-resume"
199
+ ? {
200
+ CLAUDE_CONFIG_DIR: configDir,
201
+ CLAUDE_SECURESTORAGE_CONFIG_DIR: authConfigOverride,
202
+ }
203
+ : {}),
204
+ ...(cleanShell ? { CLAUDE_CODE_SHELL: cleanShell } : {}),
205
+ // Hook shells launch outside the subject sandbox; clear their startup inputs too.
206
+ BASH_ENV: "",
207
+ ENV: "",
208
+ ZDOTDIR: controlDir,
209
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
210
+ CLAUDE_CODE_DISABLE_CLAUDE_MDS: "1",
211
+ // The CLI checks presence, not boolean parsing: "0" still disables resume.
212
+ CLAUDE_CODE_SKIP_PROMPT_HISTORY: config.interactionMode === "defer-resume" ? "" : "1",
213
+ ...(config.interactionMode === "defer-resume"
214
+ ? { CLAUDE_CODE_FORCE_SESSION_PERSISTENCE: "1" }
215
+ : {}),
216
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1",
217
+ CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
218
+ CLAUDE_CODE_AUTO_CONNECT_IDE: "false",
219
+ CLAUDE_CODE_PLUGIN_CACHE_DIR: pluginCacheDir,
220
+ CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: "1",
221
+ DISABLE_AUTOUPDATER: "1",
222
+ TMPDIR: tmpDir,
223
+ XDG_CACHE_HOME: cacheDir,
224
+ npm_config_cache: join(cacheDir, "npm"),
225
+ BUN_INSTALL_CACHE_DIR: join(cacheDir, "bun"),
226
+ },
227
+ };
228
+ };
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { InheritEnvironmentSchema } from "../../core/agent-environment.js";
2
3
  import { DurationSchema } from "../../core/duration.js";
3
4
  import { applyCatalogEffort, codexCatalog, DEFAULT_CODEX_MODEL } from "../../core/index.js";
4
5
 
@@ -36,6 +37,7 @@ export const CodexDriverConfigSchema = z
36
37
  cwdMode: z.enum(["artifacts", "runDir"]).default("artifacts"),
37
38
  cwd: z.string().optional(),
38
39
  env: z.record(z.string(), z.string()).optional(),
40
+ inheritEnv: InheritEnvironmentSchema.optional(),
39
41
  addDirs: z.array(z.string()).optional(),
40
42
  profile: z.string().optional(),
41
43
  config: z.record(z.string(), CodexConfigValueSchema).optional(),
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { createInterface } from "node:readline";
5
5
  import { temporaryDirectory } from "tempy";
6
+ import { agentEnvironment } from "../../core/agent-environment.js";
6
7
  import { durationMs } from "../../core/duration.js";
7
8
  import {
8
9
  type AgentDriver,
@@ -18,10 +19,11 @@ import {
18
19
  type TokenUsage,
19
20
  TypedEmitter,
20
21
  } from "../../core/index.js";
22
+ import { resolveExecutable } from "../../core/platform-process.js";
23
+ import { copyRegularFileNoFollow } from "../../core/secure-copy.js";
21
24
  import { walkArtifacts } from "../shared/artifacts.js";
22
25
  import { configuredClaudeSourceHashes } from "../shared/experiment-behavior.js";
23
26
  import { prepareCodexExperimentConfig } from "../shared/experiment-policy.js";
24
- import { copyRegularFileNoFollow } from "../shared/secure-copy.js";
25
27
  import { subprocessDriver } from "../subprocess/index.js";
26
28
  import {
27
29
  type AppTurnFacets,
@@ -59,6 +61,7 @@ class CodexAppServerDriverImpl implements AgentDriver {
59
61
  private runDir: string | null = null;
60
62
  private isolatedWorkspace: string | null = null;
61
63
  private isolatedStateDir: string | null = null;
64
+ private isolationConfig: Record<string, CodexConfigValue> = {};
62
65
  private threadId: string | null = null;
63
66
  private cliVersion: string | undefined;
64
67
  private nextRequestId = 1;
@@ -96,6 +99,7 @@ class CodexAppServerDriverImpl implements AgentDriver {
96
99
  let cwd =
97
100
  this.config.cwd ?? (this.config.cwdMode === "artifacts" ? ctx.artifactsDir : ctx.runDir);
98
101
  let env = this.config.env;
102
+ let launchCommand = this.config.command;
99
103
  if (this.config.isolation) {
100
104
  const workspace = await realpath(temporaryDirectory({ prefix: "lux-codex-workspace-" }));
101
105
  await cp(ctx.artifactsDir, workspace, { recursive: true });
@@ -123,6 +127,45 @@ class CodexAppServerDriverImpl implements AgentDriver {
123
127
  CODEX_CONFIG_HOME: stateDir,
124
128
  };
125
129
  }
130
+ if (this.config.isolation) {
131
+ const reads = await Promise.all(
132
+ this.config.isolation.allowRead.map((path) => realpath(path)),
133
+ );
134
+ const executable = resolveExecutable(
135
+ this.config.command,
136
+ { ...agentEnvironment(process.env, this.config.inheritEnv), ...env },
137
+ undefined,
138
+ cwd,
139
+ );
140
+ if (executable) {
141
+ launchCommand = await realpath(executable);
142
+ reads.push(executable, launchCommand);
143
+ }
144
+ // Native filesystem tools relaunch the CLI inside the subject sandbox.
145
+ // Workspace requirements reload process configuration during a turn.
146
+ // A thread-only profile is accepted at startup but fails that reload.
147
+ this.isolationConfig = {
148
+ allow_login_shell: false,
149
+ project_doc_max_bytes: 0,
150
+ projects: { [cwd]: { trust_level: "untrusted" } },
151
+ default_permissions: "lux-isolated",
152
+ permissions: {
153
+ "lux-isolated": {
154
+ filesystem: {
155
+ ":minimal": "read",
156
+ [cwd]: "write",
157
+ ...Object.fromEntries(reads.map((path) => [path, "read"])),
158
+ },
159
+ network: { enabled: true },
160
+ },
161
+ },
162
+ features: {
163
+ ...((this.config.config?.features as Record<string, CodexConfigValue>) ?? {}),
164
+ shell_snapshot: false,
165
+ shell_snapshot_v2: false,
166
+ },
167
+ };
168
+ }
126
169
  this.collectDir = cwd;
127
170
  this.artifactsDir = ctx.artifactsDir;
128
171
  this.runDir = ctx.runDir;
@@ -133,16 +176,23 @@ class CodexAppServerDriverImpl implements AgentDriver {
133
176
  commandName(this.config.command) === "codex"
134
177
  ? await discoverCliVersion(this.config.command)
135
178
  : undefined;
136
- const managed = spawnManagedProcess(this.config.command, buildAppServerArgs(this.config), {
137
- cwd,
138
- env: {
139
- ...process.env,
140
- LUX_RUN_DIR: ctx.runDir,
141
- LUX_ARTIFACTS_DIR: ctx.artifactsDir,
142
- ...env,
179
+ const managed = spawnManagedProcess(
180
+ launchCommand,
181
+ buildAppServerArgs({
182
+ ...this.config,
183
+ config: { ...this.config.config, ...this.isolationConfig },
184
+ }),
185
+ {
186
+ cwd,
187
+ env: {
188
+ ...agentEnvironment(process.env, this.config.inheritEnv),
189
+ LUX_RUN_DIR: ctx.runDir,
190
+ LUX_ARTIFACTS_DIR: ctx.artifactsDir,
191
+ ...env,
192
+ },
193
+ signal: ctx.abortSignal,
143
194
  },
144
- signal: ctx.abortSignal,
145
- });
195
+ );
146
196
  this.process = managed;
147
197
  managed.child.stderr.setEncoding("utf8");
148
198
  let stderrTail = "";
@@ -187,35 +237,19 @@ class CodexAppServerDriverImpl implements AgentDriver {
187
237
  capabilities: { experimentalApi: true, requestAttestation: false },
188
238
  });
189
239
  this.notify("initialized");
190
- const isolationReads = this.config.isolation
191
- ? await Promise.all(this.config.isolation.allowRead.map((path) => realpath(path)))
192
- : [];
193
240
  const threadConfig: Record<string, CodexConfigValue> = {
194
241
  ...codexConfigOverrides(this.config),
195
242
  ...(this.config.reasoningEffort
196
243
  ? { model_reasoning_effort: this.config.reasoningEffort }
197
244
  : {}),
198
- ...(this.config.isolation
199
- ? {
200
- project_doc_max_bytes: 0,
201
- projects: { [cwd]: { trust_level: "untrusted" } },
202
- permissions: {
203
- "lux-isolated": {
204
- filesystem: {
205
- ":minimal": "read",
206
- ":workspace_roots": { ".": "write" },
207
- ...Object.fromEntries(isolationReads.map((path) => [path, "read"])),
208
- },
209
- network: { enabled: true },
210
- },
211
- },
212
- }
213
- : {}),
245
+ ...this.isolationConfig,
214
246
  };
215
247
  const started = await this.request("thread/start", {
216
248
  model: this.config.model,
217
249
  cwd,
218
- runtimeWorkspaceRoots: this.config.addDirs ?? [],
250
+ runtimeWorkspaceRoots: this.config.isolation
251
+ ? [cwd, ...(this.config.addDirs ?? [])]
252
+ : (this.config.addDirs ?? []),
219
253
  approvalPolicy: this.config.approvalPolicy,
220
254
  approvalsReviewer: this.config.approvalsReviewer,
221
255
  ...(this.config.isolation
@@ -348,6 +382,11 @@ class CodexAppServerDriverImpl implements AgentDriver {
348
382
  : withRoot;
349
383
  }
350
384
 
385
+ async verificationWorkspace(): Promise<string | undefined> {
386
+ await this.quiesce();
387
+ return this.isolatedWorkspace ?? undefined;
388
+ }
389
+
351
390
  async quiesce(): Promise<void> {
352
391
  this.clearIdleTimer();
353
392
  await this.process?.terminate();
@@ -714,7 +753,8 @@ class CodexExecDriverImpl implements AgentDriver {
714
753
  command: this.config.command,
715
754
  args: buildArgs(this.config),
716
755
  cwd,
717
- env: this.config.env,
756
+ env: agentEnvironment(process.env, this.config.inheritEnv, this.config.env),
757
+ inheritEnv: false,
718
758
  protocol: "custom",
719
759
  parseEvent: makeParseEvent(this.config, cliVersion),
720
760
  formatPrompt: (prompt) => prompt,
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { InheritEnvironmentSchema } from "../../core/agent-environment.js";
2
3
  import { DurationSchema } from "../../core/duration.js";
3
4
  import { applyCatalogEffort, copilotCatalog } from "../../core/index.js";
4
5
 
@@ -24,6 +25,7 @@ export const CopilotDriverConfigSchema = z
24
25
  extraArgs: z.array(z.string()).optional(),
25
26
  cwd: z.string().optional(),
26
27
  env: z.record(z.string(), z.string()).optional(),
28
+ inheritEnv: InheritEnvironmentSchema.optional(),
27
29
  startupTimeout: DurationSchema.default("30 seconds"),
28
30
  idleTimeout: DurationSchema.default("10 minutes"),
29
31
  maxInterrupts: z.number().int().positive().default(20),
@@ -1,4 +1,5 @@
1
1
  import { realpath } from "node:fs/promises";
2
+ import { agentEnvironment } from "../../core/agent-environment.js";
2
3
  import { durationMs } from "../../core/duration.js";
3
4
  import {
4
5
  type AgentDriver,
@@ -70,7 +71,7 @@ class CopilotDriver implements AgentDriver {
70
71
  command: this.config.command,
71
72
  args: buildCopilotArgs(this.config),
72
73
  cwd: this.cwd,
73
- env: { ...process.env, ...this.config.env },
74
+ env: agentEnvironment(process.env, this.config.inheritEnv, this.config.env),
74
75
  signal: ctx.abortSignal,
75
76
  maxLineBytes: this.config.maxLineBytes,
76
77
  maxQueuedBytes: this.config.maxQueuedBytes,
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { InheritEnvironmentSchema } from "../../core/agent-environment.js";
2
3
  import { DurationSchema } from "../../core/duration.js";
3
4
  import { cursorCatalog } from "../../core/index.js";
4
5
 
@@ -10,6 +11,7 @@ export const CursorDriverConfigSchema = z
10
11
  mode: z.enum(["agent", "plan", "ask"]).default("agent"),
11
12
  cwd: z.string().optional(),
12
13
  env: z.record(z.string(), z.string()).optional(),
14
+ inheritEnv: InheritEnvironmentSchema.optional(),
13
15
  startupTimeout: DurationSchema.default("30 seconds"),
14
16
  idleTimeout: DurationSchema.default("10 minutes"),
15
17
  maxInterrupts: z.number().int().positive().default(20),
@@ -1,4 +1,5 @@
1
1
  import { realpath } from "node:fs/promises";
2
+ import { agentEnvironment } from "../../core/agent-environment.js";
2
3
  import { durationMs } from "../../core/duration.js";
3
4
  import {
4
5
  type AgentDriver,
@@ -65,7 +66,7 @@ class CursorDriver implements AgentDriver {
65
66
  command: this.config.command,
66
67
  args: buildCursorArgs(this.config),
67
68
  cwd: this.cwd,
68
- env: { ...process.env, ...this.config.env },
69
+ env: agentEnvironment(process.env, this.config.inheritEnv, this.config.env),
69
70
  signal: ctx.abortSignal,
70
71
  maxLineBytes: this.config.maxLineBytes,
71
72
  maxQueuedBytes: this.config.maxQueuedBytes,
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { InheritEnvironmentSchema } from "../../core/agent-environment.js";
2
3
  import { DurationSchema } from "../../core/duration.js";
3
4
  import { applyCatalogEffort, grokBuildCatalog } from "../../core/index.js";
4
5
 
@@ -12,6 +13,7 @@ export const GrokBuildDriverConfigSchema = z
12
13
  sandbox: z.enum(["workspace", "read-only", "strict"]).default("workspace"),
13
14
  cwd: z.string().optional(),
14
15
  env: z.record(z.string(), z.string()).optional(),
16
+ inheritEnv: InheritEnvironmentSchema.optional(),
15
17
  startupTimeout: DurationSchema.default("30 seconds"),
16
18
  idleTimeout: DurationSchema.default("10 minutes"),
17
19
  permissionTimeout: DurationSchema.default("150 seconds"),
@@ -1,4 +1,5 @@
1
1
  import { realpath } from "node:fs/promises";
2
+ import { agentEnvironment } from "../../core/agent-environment.js";
2
3
  import { durationMs } from "../../core/duration.js";
3
4
  import {
4
5
  type AgentDriver,
@@ -70,7 +71,7 @@ class GrokBuildDriver implements AgentDriver {
70
71
  command: this.config.command,
71
72
  args: buildGrokBuildArgs(this.config),
72
73
  cwd: this.cwd,
73
- env: { ...process.env, ...this.config.env },
74
+ env: agentEnvironment(process.env, this.config.inheritEnv, this.config.env),
74
75
  signal: ctx.abortSignal,
75
76
  maxLineBytes: this.config.maxLineBytes,
76
77
  maxQueuedBytes: this.config.maxQueuedBytes,
@@ -2,6 +2,7 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { z } from "zod";
5
+ import { agentEnvironment, InheritEnvironmentSchema } from "../../core/agent-environment.js";
5
6
  import { DurationSchema, durationMs } from "../../core/duration.js";
6
7
  import {
7
8
  type AgentDriver,
@@ -35,7 +36,7 @@ export const SubprocessDriverConfigSchema = z
35
36
  cwd: z.string().optional(),
36
37
  env: z.record(z.string(), z.string()).optional(),
37
38
  /** Inherit the host environment. Experiment mode always forces this off. */
38
- inheritEnv: z.boolean().optional(),
39
+ inheritEnv: z.union([InheritEnvironmentSchema, z.literal(false)]).optional(),
39
40
  protocol: z.enum(["lux-jsonl", "custom"]).default("lux-jsonl"),
40
41
  idleTimeout: DurationSchema.default("2 minutes"),
41
42
  maxInterrupts: z.number().positive().default(20),
@@ -151,7 +152,9 @@ class SubprocessDriverImpl implements AgentDriver {
151
152
  const managed = spawnManagedProcess(this.config.command, this.config.args ?? [], {
152
153
  cwd: this.config.cwd ?? ctx.artifactsDir,
153
154
  env: {
154
- ...(this.config.inheritEnv === false ? {} : process.env),
155
+ ...(this.config.inheritEnv === false
156
+ ? {}
157
+ : agentEnvironment(process.env, this.config.inheritEnv)),
155
158
  ...this.config.env,
156
159
  ...(ctx.caseId ? { LUX_CASE_ID: ctx.caseId } : {}),
157
160
  LUX_RUN_DIR: ctx.runDir,
@@ -0,0 +1,50 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import {
3
+ type Artifact,
4
+ contentHash,
5
+ inspectRunArtifact,
6
+ readRunArtifactBytes,
7
+ } from "../core/index.js";
8
+
9
+ const MAX_ORIGIN_BYTES = 10 * 1024 * 1024;
10
+
11
+ export const identifyArtifactOrigins = async (
12
+ runDir: string,
13
+ artifacts: readonly Artifact[],
14
+ fixtureHashes: ReadonlyMap<string, string>,
15
+ ): Promise<Artifact[]> => {
16
+ const run = { runDir, artifacts };
17
+ const result: Artifact[] = [];
18
+ for (const artifact of artifacts) {
19
+ const { origin: _claimedOrigin, ...captured } = artifact;
20
+ const inspected = await inspectRunArtifact(run, artifact.path);
21
+ if (!inspected.stats?.isFile()) {
22
+ result.push(captured);
23
+ continue;
24
+ }
25
+ const path = relative(resolve(runDir, "artifacts"), inspected.target);
26
+ const expected =
27
+ !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`)
28
+ ? fixtureHashes.get(path)
29
+ : undefined;
30
+ if (expected === undefined) {
31
+ result.push({ ...captured, origin: "generated" });
32
+ continue;
33
+ }
34
+ if (inspected.stats.size > MAX_ORIGIN_BYTES) {
35
+ result.push(captured);
36
+ continue;
37
+ }
38
+ try {
39
+ const bytes = await readRunArtifactBytes(run, artifact.path, { maxBytes: MAX_ORIGIN_BYTES });
40
+ result.push({
41
+ ...captured,
42
+ origin: contentHash(bytes) === expected ? "fixture" : "modified",
43
+ });
44
+ } catch {
45
+ // Advisory provenance must not discard the rest of a safely archived index.
46
+ result.push(captured);
47
+ }
48
+ }
49
+ return result;
50
+ };
@@ -130,14 +130,17 @@ export const agentCorpusHash = async (
130
130
  execution.loaded.campaign.profiles.map(async (profileId) => {
131
131
  const profile = profileOrThrow(execution.config, profileId);
132
132
  const harness = profile.harness ?? execution.config.harness;
133
- const driver = resolveDriverReference(
134
- bound,
135
- profile.driver ?? execution.config.defaultDriver,
133
+ const driver = bindSubjectRoot(
134
+ resolveDriverReference(bound, profile.driver ?? execution.config.defaultDriver),
135
+ execution.loaded.subjectRoot,
136
136
  );
137
- const answerer = resolveAnswererReference(
138
- bound,
139
- profile.answerer ?? execution.config.defaultAnswerer,
140
- harness,
137
+ const answerer = bindSubjectRoot(
138
+ resolveAnswererReference(
139
+ bound,
140
+ profile.answerer ?? execution.config.defaultAnswerer,
141
+ harness,
142
+ ),
143
+ execution.loaded.subjectRoot,
141
144
  );
142
145
  const answererCli = referencedAnswererCli(answerer);
143
146
  return {
@@ -131,9 +131,9 @@ const trustedExecutableEntrypoints = async (
131
131
  );
132
132
  for (const profileId of execution.loaded.campaign.profiles) {
133
133
  const profile = profileOrThrow(execution.config, profileId);
134
- const driver = resolveDriverReference(
135
- authored,
136
- profile.driver ?? execution.config.defaultDriver,
134
+ const driver = bindSubjectRoot(
135
+ resolveDriverReference(authored, profile.driver ?? execution.config.defaultDriver),
136
+ execution.loaded.subjectRoot,
137
137
  );
138
138
  for (const command of referencedDriverRuntime(driver, execution.registry).commands)
139
139
  add(command);
@@ -197,14 +197,17 @@ export const runtimeCliReferences = async (
197
197
  for (const profileId of execution.loaded.campaign.profiles) {
198
198
  const profile = profileOrThrow(execution.config, profileId);
199
199
  const harness = profile.harness ?? execution.config.harness;
200
- const driver = resolveDriverReference(
201
- authored,
202
- profile.driver ?? execution.config.defaultDriver,
200
+ const driver = bindSubjectRoot(
201
+ resolveDriverReference(authored, profile.driver ?? execution.config.defaultDriver),
202
+ execution.loaded.subjectRoot,
203
203
  );
204
- const answerer = resolveAnswererReference(
205
- authored,
206
- profile.answerer ?? execution.config.defaultAnswerer,
207
- harness,
204
+ const answerer = bindSubjectRoot(
205
+ resolveAnswererReference(
206
+ authored,
207
+ profile.answerer ?? execution.config.defaultAnswerer,
208
+ harness,
209
+ ),
210
+ execution.loaded.subjectRoot,
208
211
  );
209
212
  for (const command of referencedDriverRuntime(driver, execution.registry).commands)
210
213
  add(command, execution.config.rootDir);
@@ -271,9 +274,9 @@ export const validateAgentCaseProfileDrivers = async (
271
274
  );
272
275
  for (const profileId of execution.loaded.campaign.profiles) {
273
276
  const profile = profileOrThrow(execution.config, profileId);
274
- const reference = resolveDriverReference(
275
- authored,
276
- profile.driver ?? execution.config.defaultDriver,
277
+ const reference = bindSubjectRoot(
278
+ resolveDriverReference(authored, profile.driver ?? execution.config.defaultDriver),
279
+ execution.loaded.subjectRoot,
277
280
  );
278
281
  const name = typeof reference === "string" ? reference : reference.name;
279
282
  const rawConfig = typeof reference === "object" ? reference.config : undefined;