@prismatic-io/lux 0.0.2-preview.27 → 0.0.2-preview.28

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 (99) hide show
  1. package/lib/authoring.d.ts +1 -0
  2. package/lib/authoring.d.ts.map +1 -1
  3. package/lib/authoring.js +1 -0
  4. package/lib/authoring.js.map +1 -1
  5. package/lib/container/driver.d.ts +27 -0
  6. package/lib/container/driver.d.ts.map +1 -0
  7. package/lib/container/driver.js +197 -0
  8. package/lib/container/driver.js.map +1 -0
  9. package/lib/container/protocol.d.ts +12 -0
  10. package/lib/container/protocol.d.ts.map +1 -0
  11. package/lib/container/protocol.js +38 -0
  12. package/lib/container/protocol.js.map +1 -0
  13. package/lib/container/resources.d.ts +12 -0
  14. package/lib/container/resources.d.ts.map +1 -0
  15. package/lib/container/resources.js +133 -0
  16. package/lib/container/resources.js.map +1 -0
  17. package/lib/container/worker.d.ts +2 -0
  18. package/lib/container/worker.d.ts.map +1 -0
  19. package/lib/container/worker.js +85 -0
  20. package/lib/container/worker.js.map +1 -0
  21. package/lib/core/case.d.ts +80 -0
  22. package/lib/core/case.d.ts.map +1 -1
  23. package/lib/core/case.js +8 -1
  24. package/lib/core/case.js.map +1 -1
  25. package/lib/core/driver-execution.d.ts +83 -0
  26. package/lib/core/driver-execution.d.ts.map +1 -0
  27. package/lib/core/driver-execution.js +72 -0
  28. package/lib/core/driver-execution.js.map +1 -0
  29. package/lib/core/driver.d.ts +5 -1
  30. package/lib/core/driver.d.ts.map +1 -1
  31. package/lib/core/driver.js +5 -1
  32. package/lib/core/driver.js.map +1 -1
  33. package/lib/core/harness-matrix.d.ts +84 -0
  34. package/lib/core/harness-matrix.d.ts.map +1 -1
  35. package/lib/core/harness-matrix.js +11 -1
  36. package/lib/core/harness-matrix.js.map +1 -1
  37. package/lib/core/run.d.ts +40 -0
  38. package/lib/core/run.d.ts.map +1 -1
  39. package/lib/drivers/antigravity/artifacts.d.ts +2 -0
  40. package/lib/drivers/antigravity/artifacts.d.ts.map +1 -0
  41. package/lib/drivers/antigravity/artifacts.js +33 -0
  42. package/lib/drivers/antigravity/artifacts.js.map +1 -0
  43. package/lib/drivers/antigravity/config.d.ts +27 -0
  44. package/lib/drivers/antigravity/config.d.ts.map +1 -1
  45. package/lib/drivers/antigravity/config.js +35 -0
  46. package/lib/drivers/antigravity/config.js.map +1 -1
  47. package/lib/drivers/antigravity/index.d.ts +35 -0
  48. package/lib/drivers/antigravity/index.d.ts.map +1 -1
  49. package/lib/drivers/antigravity/index.js +42 -20
  50. package/lib/drivers/antigravity/index.js.map +1 -1
  51. package/lib/drivers/antigravity/runtime.d.ts +7 -0
  52. package/lib/drivers/antigravity/runtime.d.ts.map +1 -0
  53. package/lib/drivers/antigravity/runtime.js +78 -0
  54. package/lib/drivers/antigravity/runtime.js.map +1 -0
  55. package/lib/index.d.ts +1 -0
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js +1 -0
  58. package/lib/index.js.map +1 -1
  59. package/lib/orchestrator/config.d.ts +122 -1
  60. package/lib/orchestrator/config.d.ts.map +1 -1
  61. package/lib/orchestrator/config.js +2 -1
  62. package/lib/orchestrator/config.js.map +1 -1
  63. package/lib/orchestrator/doctor.d.ts.map +1 -1
  64. package/lib/orchestrator/doctor.js +3 -1
  65. package/lib/orchestrator/doctor.js.map +1 -1
  66. package/lib/orchestrator/driver-capabilities.d.ts +2 -1
  67. package/lib/orchestrator/driver-capabilities.d.ts.map +1 -1
  68. package/lib/orchestrator/driver-capabilities.js +5 -2
  69. package/lib/orchestrator/driver-capabilities.js.map +1 -1
  70. package/lib/orchestrator/driver-identity.d.ts.map +1 -1
  71. package/lib/orchestrator/driver-identity.js +11 -1
  72. package/lib/orchestrator/driver-identity.js.map +1 -1
  73. package/lib/orchestrator/experiment-context.d.ts +34 -0
  74. package/lib/orchestrator/experiment-context.d.ts.map +1 -1
  75. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  76. package/lib/orchestrator/orchestrator.js +16 -3
  77. package/lib/orchestrator/orchestrator.js.map +1 -1
  78. package/package.json +1 -1
  79. package/skills/lux-answerer/SKILL.md +1 -1
  80. package/src/authoring.ts +6 -0
  81. package/src/container/driver.ts +211 -0
  82. package/src/container/protocol.ts +48 -0
  83. package/src/container/resources.ts +153 -0
  84. package/src/container/worker.ts +107 -0
  85. package/src/core/case.ts +8 -1
  86. package/src/core/driver-execution.ts +84 -0
  87. package/src/core/driver.ts +9 -1
  88. package/src/core/harness-matrix.ts +19 -1
  89. package/src/drivers/antigravity/README.md +108 -10
  90. package/src/drivers/antigravity/artifacts.ts +37 -0
  91. package/src/drivers/antigravity/config.ts +35 -0
  92. package/src/drivers/antigravity/index.ts +41 -22
  93. package/src/drivers/antigravity/runtime.ts +88 -0
  94. package/src/index.ts +6 -0
  95. package/src/orchestrator/config.ts +4 -1
  96. package/src/orchestrator/doctor.ts +3 -1
  97. package/src/orchestrator/driver-capabilities.ts +6 -1
  98. package/src/orchestrator/driver-identity.ts +11 -1
  99. package/src/orchestrator/orchestrator.ts +19 -3
package/src/core/case.ts CHANGED
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import type { AssertionPlugin } from "./assertion.js";
3
3
  import { collectEvalCaseExtensions, setEvalCaseExtensions } from "./case-authoring-state.js";
4
4
  import { DriverCapabilitySchema } from "./driver.js";
5
+ import { DriverExecutionSchema } from "./driver-execution.js";
5
6
  import { EvaluationMetadataSchema } from "./evaluation-clusters.js";
6
7
  import { HarnessMatrixSchema } from "./harness-matrix.js";
7
8
  import type { LifecycleHooksPlugin } from "./lifecycle-hooks.js";
@@ -20,7 +21,13 @@ export type FixturesRef = z.infer<typeof FixturesRefSchema>;
20
21
  // `defineEvalCase` discriminate the config type on `name`.
21
22
  export const DriverRefSchema = z.union([
22
23
  z.string(),
23
- z.object({ name: z.string(), config: z.unknown().optional() }).strict(),
24
+ z
25
+ .object({
26
+ name: z.string(),
27
+ config: z.unknown().optional(),
28
+ execution: DriverExecutionSchema.optional(),
29
+ })
30
+ .strict(),
24
31
  ]);
25
32
  export type DriverRef = z.infer<typeof DriverRefSchema>;
26
33
 
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+
3
+ const homePath = z
4
+ .string()
5
+ .min(1)
6
+ .refine(
7
+ (value) =>
8
+ !value.startsWith("/") &&
9
+ !value.includes("\\") &&
10
+ value.split("/").every((part) => part !== ".." && part !== "." && part !== ""),
11
+ "must be a relative path within the container home",
12
+ );
13
+
14
+ export const ContainerExecutionSchema = z
15
+ .object({
16
+ kind: z.literal("container"),
17
+ /** A locally available image containing Lux and the selected Linux harness. */
18
+ image: z.string().min(1),
19
+ command: z.string().min(1).optional(),
20
+ /** The worker shipped with the same Lux build as the host. */
21
+ worker: z.string().min(1).optional(),
22
+ /** Optional module in the image exporting a custom DriverPlugin as default. */
23
+ driverModule: z.string().min(1).optional(),
24
+ mounts: z
25
+ .array(
26
+ z
27
+ .object({
28
+ source: z.string().min(1),
29
+ target: z.string().min(1).optional(),
30
+ readOnly: z.boolean().optional(),
31
+ })
32
+ .strict(),
33
+ )
34
+ .optional(),
35
+ /** Only these host environment variables enter the container. */
36
+ passEnv: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).optional(),
37
+ env: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string()).optional(),
38
+ /** Private copies, never host-home mounts; deleted after the run. */
39
+ homeFiles: z
40
+ .array(
41
+ z
42
+ .object({
43
+ destination: homePath,
44
+ source: z.discriminatedUnion("kind", [
45
+ z.object({ kind: z.literal("file"), path: z.string().min(1) }).strict(),
46
+ z
47
+ .object({
48
+ kind: z.literal("keychain"),
49
+ service: z.string().min(1),
50
+ account: z.string().min(1),
51
+ encoding: z.enum(["utf8", "go-keyring-base64"]).optional(),
52
+ })
53
+ .strict(),
54
+ ]),
55
+ })
56
+ .strict(),
57
+ )
58
+ .max(32)
59
+ .optional(),
60
+ network: z.enum(["bridge", "none"]).optional(),
61
+ user: z.string().min(1).optional(),
62
+ memory: z
63
+ .string()
64
+ .regex(/^[1-9][0-9]*[bkmgt]?$/i)
65
+ .optional(),
66
+ cpus: z.number().positive().optional(),
67
+ pidsLimit: z.number().int().positive().optional(),
68
+ startupTimeoutMs: z.number().int().positive().optional(),
69
+ })
70
+ .strict();
71
+
72
+ export const DriverExecutionSchema = z.discriminatedUnion("kind", [
73
+ z.object({ kind: z.literal("host") }).strict(),
74
+ ContainerExecutionSchema,
75
+ ]);
76
+ export type DriverExecution = z.infer<typeof DriverExecutionSchema>;
77
+ export type ContainerExecution = z.infer<typeof ContainerExecutionSchema>;
78
+
79
+ export const driverExecutionOf = (reference: unknown): DriverExecution | undefined => {
80
+ if (!reference || typeof reference !== "object" || !("execution" in reference)) return undefined;
81
+ return reference.execution === undefined
82
+ ? undefined
83
+ : DriverExecutionSchema.parse(reference.execution);
84
+ };
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import type { DriverExecution } from "./driver-execution.js";
2
3
  import type { EventHandler } from "./emitter.js";
3
4
  import type { Answer, Done, ErrorEvent, Interrupt, Progress, ReadyState } from "./events.js";
4
5
  import type { ReasoningEffort } from "./harness-catalog.js";
@@ -28,6 +29,8 @@ export type ResolvedFixtures = z.infer<typeof ResolvedFixturesSchema>;
28
29
  export type StartContext = {
29
30
  /** Stable selected case id, available to drivers that multiplex scenarios. */
30
31
  caseId?: string;
32
+ /** Private home owned by an outer execution boundary; never the caller’s home. */
33
+ isolatedHome?: string;
31
34
  prompt: string;
32
35
  fixtures?: ResolvedFixtures | undefined;
33
36
  /** Authored fixture values selected for this case. */
@@ -139,4 +142,9 @@ export const defineDriver = <S extends z.ZodType>(
139
142
  export const driverRef = <S extends z.ZodType>(
140
143
  plugin: { name: string; configSchema: S },
141
144
  config: z.input<S>,
142
- ): { name: string; config: z.input<S> } => ({ name: plugin.name, config });
145
+ execution?: DriverExecution,
146
+ ): { name: string; config: z.input<S>; execution?: DriverExecution } => ({
147
+ name: plugin.name,
148
+ config,
149
+ ...(execution ? { execution } : {}),
150
+ });
@@ -1,6 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import type { DriverRef } from "./case.js";
3
3
  import type { DriverPlugin } from "./driver.js";
4
+ import {
5
+ type DriverExecution,
6
+ DriverExecutionSchema,
7
+ driverExecutionOf,
8
+ } from "./driver-execution.js";
4
9
  import {
5
10
  type CatalogModels,
6
11
  catalogEfforts,
@@ -24,6 +29,7 @@ type CatalogEntry<C extends HarnessCatalog> = {
24
29
  models: C["models"] extends CatalogModels ? (keyof C["models"] & string)[] : string[];
25
30
  efforts?: C["efforts"][number][];
26
31
  config?: unknown;
32
+ execution?: DriverExecution;
27
33
  };
28
34
 
29
35
  export type BuiltInHarnessMatrixEntry = {
@@ -37,6 +43,7 @@ export type CustomHarnessMatrixEntry = {
37
43
  models?: string[];
38
44
  efforts?: ReasoningEffort[];
39
45
  config?: unknown;
46
+ execution?: DriverExecution;
40
47
  };
41
48
 
42
49
  export type HarnessMatrixEntry = BuiltInHarnessMatrixEntry | CustomHarnessMatrixEntry;
@@ -54,6 +61,7 @@ const catalogEntrySchema = (catalog: HarnessCatalog) => {
54
61
  models: z.array(models).min(1),
55
62
  efforts: z.array(nonEmptyEnum(catalog.efforts)).min(1).optional(),
56
63
  config: z.unknown().optional(),
64
+ execution: DriverExecutionSchema.optional(),
57
65
  })
58
66
  .strict()
59
67
  .superRefine((entry, ctx) => {
@@ -86,6 +94,7 @@ const CustomHarnessMatrixEntrySchema = z
86
94
  models: z.array(z.string().min(1)).min(1).optional(),
87
95
  efforts: z.array(ReasoningEffortSchema).min(1).optional(),
88
96
  config: z.unknown().optional(),
97
+ execution: DriverExecutionSchema.optional(),
89
98
  })
90
99
  .strict();
91
100
 
@@ -109,6 +118,7 @@ export type HarnessPermutation = {
109
118
  model?: string | undefined;
110
119
  effort?: ReasoningEffort | undefined;
111
120
  config?: unknown;
121
+ execution?: DriverExecution;
112
122
  };
113
123
 
114
124
  export const permutationId = (permutation: HarnessPermutation): string =>
@@ -125,6 +135,7 @@ const entryPermutation = (
125
135
  ...(model !== undefined ? { model } : {}),
126
136
  ...(effort !== undefined ? { effort } : {}),
127
137
  ...(entry.config !== undefined ? { config: entry.config } : {}),
138
+ ...(entry.execution !== undefined ? { execution: entry.execution } : {}),
128
139
  });
129
140
 
130
141
  const expandEntry = (entry: HarnessMatrixEntry): HarnessPermutation[] => {
@@ -294,5 +305,12 @@ export const permutationDriverRef = (
294
305
  }
295
306
  config = plugin.selectHarness(config, { model: selected.model, effort: selected.effort });
296
307
  }
297
- return config === undefined ? selected.driver : { name: selected.driver, config };
308
+ const execution =
309
+ selected.execution ??
310
+ (fallback && driverNameOf(fallback) === selected.driver
311
+ ? driverExecutionOf(fallback)
312
+ : undefined);
313
+ return config === undefined && execution === undefined
314
+ ? selected.driver
315
+ : { name: selected.driver, config, ...(execution ? { execution } : {}) };
298
316
  };
@@ -32,7 +32,7 @@ response is also retained as message evidence when it was not already streamed.
32
32
  Both `DONE` and `ERROR` tool steps close the corresponding tool call; failed
33
33
  steps retain their error message. A result with `denied_actions` is an error even
34
34
  when Antigravity reports `SUCCESS`. `WAITING` and other non-success statuses are
35
- also errors. Configure the required scoped permissions in Antigravity before a
35
+ also errors. Configure the required scoped permissions for the
36
36
  headless evaluation; Lux does not retry a denial with broader permissions.
37
37
 
38
38
  Lux explicitly selects `mode: "accept-edits"` for coding tasks. Set `mode: "plan"`
@@ -41,14 +41,61 @@ workspaces read-only even when native file tools can edit them and scoped write
41
41
  grants exist. `accept-edits` permits workspace writes within the terminal sandbox;
42
42
  it does not disable that sandbox or authorize outside-sandbox execution.
43
43
 
44
- For sandboxed headless coding, set `sandbox: true` and configure the CLI with
45
- `enableTerminalSandbox: true` and `toolPermission: "proceed-in-sandbox"`.
44
+ ## Run-scoped configuration
45
+
46
+ Lux can own the CLI settings for a run:
47
+
48
+ ```ts
49
+ {
50
+ name: "antigravity",
51
+ config: {
52
+ model: "gemini-3.1-pro",
53
+ reasoningEffort: "low",
54
+ sandbox: true,
55
+ permissionMode: "proceed-in-sandbox",
56
+ permissions: { allow: ["read_url(registry.npmjs.org)"] },
57
+ },
58
+ }
59
+ ```
60
+
61
+ This mode gives only the child process a private home directory. Lux writes
62
+ `enableTerminalSandbox: true`, `toolPermission: "proceed-in-sandbox"`, and the
63
+ configured `permissions` there. It does not edit the user's settings, copy their
64
+ history/plugins, or reuse their project records. Concurrent runs get distinct
65
+ homes. `permissionMode: "inherit"` is the default and preserves the previous
66
+ behavior. Scoped mode requires `sandbox: true`, rejects `skipPermissions: true`
67
+ and a host `project`, and supports scoped `allow`, `deny`, and `ask` lists.
68
+
69
+ Authentication must work in that private home. An inherited `GEMINI_API_KEY` is
70
+ passed to the CLI. For file-based OAuth, Lux copies only the CLI's
71
+ `antigravity-oauth-token` into the private runtime, using the normal CLI token
72
+ path by default or an explicit `authTokenFile`. The copy has mode 0600 inside
73
+ a mode-0700 runtime directory; token refresh cannot modify the source file.
74
+ On macOS, `reuseKeychain: true` explicitly opts into reading Antigravity’s own
75
+ Keychain entry (service `gemini`, account `antigravity`) when the default token
76
+ file is absent. Lux writes that credential only to the private runtime and
77
+ removes it on close. This option defaults to false. Do not put secret values in
78
+ committed Lux configuration.
79
+
80
+ Before deleting a private runtime, Lux snapshots native deliverables from its
81
+ `brain` directory under `antigravity-artifacts/` in the task artifacts. This keeps
82
+ handoff documents available to graders even when the final response only links
83
+ to them. Hidden CLI logs, revision copies, metadata, and symlinks are excluded;
84
+ collection fails explicitly above 500 files or 32 MiB. Existing user-home brain
85
+ history is never scanned in inherited mode.
86
+
87
+ The runtime is outside task artifacts and removed on driver close, collection,
88
+ and startup failure. A force-killed Lux process cannot run cleanup; private
89
+ runtime directories have the `lux-antigravity-home-` prefix in the OS temp root.
90
+ This is configuration separation, not a complete OS security boundary; the
91
+ driver continues declaring `isolation: false`.
92
+
46
93
  Grant scoped file and documentation access as needed, including read access to
47
- language runtimes installed outside system directories. In 1.2.10, `command(...)`
48
- allow rules also authorize outside-sandbox execution, and `unsandboxed(...)`
49
- rules are ignored. Keep command allow rules absent when outside-sandbox
50
- execution must remain unapproved. The terminal sandbox is not full driver
51
- isolation; native file tools still use the CLI's file permissions.
94
+ language runtimes installed outside system directories. `command(...)` allow
95
+ rules can authorize outside-sandbox execution too. Keep them absent when that
96
+ execution must remain unapproved; `unsandboxed(...)` rules were ignored in the
97
+ observed macOS CLI. The terminal sandbox and native file-tool permissions are
98
+ separate. Extra CLI arguments remain caller-controlled.
52
99
 
53
100
  Each run creates a fresh Antigravity project and passes its canonical working
54
101
  directory through `--add-dir` and an explicit task-workspace prompt preamble.
@@ -60,14 +107,65 @@ permissions: Antigravity 1.2.10 can still deny workspace writes in headless mode
60
107
  bypass tool permission checks.
61
108
 
62
109
  The headless protocol carries no user questions or permission prompts, so the
63
- driver is not interactive and declares no isolation: Antigravity still reads
64
- its user settings and conversation history. Controlled experiments are not
110
+ driver is not interactive. In inherited mode, Antigravity still reads user
111
+ settings and conversation history. Controlled experiments are not
65
112
  declared.
66
113
 
67
114
  Regression tests cover the `ERROR` tool step followed by `SUCCESS` with denied
68
115
  actions observed in an authenticated Antigravity 1.2.10 session.
69
116
 
117
+ ## Live readiness checks
118
+
119
+ From a Lux checkout, after installing locked dependencies and authenticating `agy`:
120
+
121
+ ```sh
122
+ LUX_ANTIGRAVITY_MODEL=gemini-3.1-pro mise run smoke:antigravity
123
+ ```
124
+
125
+ This opt-in smoke test consumes model usage. It runs four bounded checks in
126
+ fresh artifact workspaces: a tool-free protocol response, a Node write/read
127
+ check, native handoff-document capture, and a local HTTP server/client check. It stops on the first failure and
128
+ retains events, fixtures, and results under `.lux-runs/antigravity-smoke/`.
129
+ After building, select one stage with
130
+ `LUX_ANTIGRAVITY_MODEL=gemini-3.1-pro node scripts/smoke-antigravity.mjs protocol`
131
+ (or `workspace` / `artifact` / `loopback`); `--list` makes no model calls.
132
+
133
+ The script uses `permissionMode: "proceed-in-sandbox"` with scoped read access to
134
+ the running Node installation, plus localhost grants for the loopback stage.
135
+ It never changes global settings. Authenticate
136
+ with an inherited `GEMINI_API_KEY`, a normal CLI OAuth token file, or
137
+ `LUX_ANTIGRAVITY_AUTH_TOKEN_FILE` pointing to such a file. To opt into macOS
138
+ Keychain reuse, set `LUX_ANTIGRAVITY_REUSE_KEYCHAIN=1`. On macOS with CLI 1.2.12,
139
+ private-runtime authentication and sandboxed file writes passed on 2026-09-29.
140
+ The loopback check failed with `connect EPERM 127.0.0.1`; its missing fixture
141
+ evidence correctly failed the smoke test despite normal agent completion.
142
+
143
+ `--sandbox` alone does not select an execution policy: a native 1.2.12 probe on
144
+ macOS with only `enableTerminalSandbox: true` denied the command, then returned
145
+ `SUCCESS` with empty text and `denied_actions`. Lux correctly classifies that
146
+ result as an error. A separate tool-free live probe passed in inherited mode;
147
+ it does not establish private-runtime authentication.
148
+
149
+ A passing protocol check establishes authentication and streaming only.
150
+ Workspace and loopback checks require fixture-produced evidence; a completed
151
+ agent response alone cannot pass them. These checks do not establish skill
152
+ quality, package-install access, or complete sandbox isolation. Scoped access
153
+ to a runtime outside system directories may also be necessary.
154
+
155
+ On macOS, native 1.2.11 probes could write/build in editing mode but could
156
+ not connect to their own loopback HTTP server: direct connections returned
157
+ `EPERM`, and proxy requests rejected literal IPs or nonstandard HTTP ports.
158
+ That failure occurred without Lux. Recheck the installed CLI with `loopback`
159
+ before running suites that depend on local HTTP fixtures. Domain grants alone
160
+ have not been shown to resolve this restriction. Keep a native reproduction
161
+ separate from driver traces when diagnosing it; do not treat it as skill failure
162
+ or bypass the sandbox to obtain a passing score.
163
+
70
164
  Protocol references:
71
165
 
72
166
  - [Antigravity CLI headless mode](https://antigravity.google/docs/cli/headless/)
73
167
  - [Antigravity CLI](https://github.com/google-antigravity/antigravity-cli)
168
+
169
+ - [Antigravity terminal sandbox](https://antigravity.google/docs/sandbox?tab=cli)
170
+ - [CLI settings reference](https://antigravity.google/docs/cli/reference/)
171
+ - [Native artifacts](https://antigravity.google/docs/cli/artifacts/)
@@ -0,0 +1,37 @@
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import { join, sep } from "node:path";
3
+ import { writeArtifactSnapshot } from "../../core/artifact-snapshot.js";
4
+ import { walkArtifacts } from "../shared/artifacts.js";
5
+
6
+ export const captureAntigravityArtifacts = async (
7
+ home: string,
8
+ destination: string,
9
+ ): Promise<void> => {
10
+ const brain = join(home, ".gemini/antigravity-cli/brain");
11
+ let root: string;
12
+ try {
13
+ root = await realpath(brain);
14
+ } catch (error) {
15
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
16
+ throw error;
17
+ }
18
+ if (!root.startsWith(`${await realpath(home)}${sep}`))
19
+ throw new Error("Antigravity artifact directory escaped its private runtime");
20
+ // Native revisions and CLI bookkeeping are not authored deliverables.
21
+ const artifacts = (await walkArtifacts(brain)).filter(
22
+ ({ path }) => !/\.(?:metadata\.json|resolved(?:\.\d+)?)$/.test(path),
23
+ );
24
+ if (!artifacts.length) return;
25
+ const maxBytes = 32 * 1024 * 1024;
26
+ if (artifacts.reduce((total, artifact) => total + (artifact.size ?? 0), 0) > maxBytes)
27
+ throw new Error("Antigravity native artifacts exceed the 32 MiB collection limit");
28
+ const files = Object.fromEntries(
29
+ await Promise.all(artifacts.map(async ({ path }) => [path, await readFile(join(brain, path))])),
30
+ );
31
+ await writeArtifactSnapshot({
32
+ artifactsDir: destination,
33
+ name: "antigravity-artifacts",
34
+ files,
35
+ maxBytes,
36
+ });
37
+ };
@@ -21,6 +21,20 @@ export const AntigravityDriverConfigSchema = z
21
21
  skipPermissions: z.boolean().default(false),
22
22
  /** Enable Antigravity's terminal sandbox restrictions. */
23
23
  sandbox: z.boolean().default(false),
24
+ /** Own the CLI settings in a private runtime home instead of changing user settings. */
25
+ permissionMode: z.enum(["inherit", "proceed-in-sandbox"]).default("inherit"),
26
+ permissions: z
27
+ .object({
28
+ allow: z.array(z.string().min(1)).default([]),
29
+ deny: z.array(z.string().min(1)).default([]),
30
+ ask: z.array(z.string().min(1)).default([]),
31
+ })
32
+ .strict()
33
+ .optional(),
34
+ /** Optional CLI-format OAuth token file for the private runtime; never logged. */
35
+ authTokenFile: z.string().min(1).optional(),
36
+ /** Explicitly authorize copying this application's macOS Keychain credential. */
37
+ reuseKeychain: z.boolean().default(false),
24
38
  cwd: z.string().optional(),
25
39
  env: z.record(z.string(), z.string()).optional(),
26
40
  extraArgs: z.array(z.string()).optional(),
@@ -28,6 +42,27 @@ export const AntigravityDriverConfigSchema = z
28
42
  maxLineBytes: z.number().positive().default(10_000_000),
29
43
  })
30
44
  .strict()
45
+ .superRefine((config, ctx) => {
46
+ if (config.permissionMode === "proceed-in-sandbox") {
47
+ if (!config.sandbox || config.skipPermissions)
48
+ ctx.addIssue({
49
+ code: "custom",
50
+ message: "proceed-in-sandbox requires sandbox: true and skipPermissions: false",
51
+ });
52
+ if (config.project)
53
+ ctx.addIssue({
54
+ code: "custom",
55
+ path: ["project"],
56
+ message: "A private Antigravity runtime cannot reuse a host project",
57
+ });
58
+ } else if (config.permissions || config.authTokenFile || config.reuseKeychain) {
59
+ ctx.addIssue({
60
+ code: "custom",
61
+ message:
62
+ "permissions, authTokenFile, and reuseKeychain require permissionMode: proceed-in-sandbox",
63
+ });
64
+ }
65
+ })
31
66
  .transform(applyCatalogEffort(antigravityCatalog));
32
67
 
33
68
  export type AntigravityDriverConfig = z.infer<typeof AntigravityDriverConfigSchema>;
@@ -19,12 +19,15 @@ import {
19
19
  formatAntigravityPrompt,
20
20
  } from "./config.js";
21
21
  import { makeParseEvent } from "./events.js";
22
+ import { prepareAntigravityRuntime } from "./runtime.js";
22
23
 
23
24
  class AntigravityDriver implements AgentDriver {
24
25
  private inner: AgentDriver | null = null;
25
26
  private pendingAttach: Array<(inner: AgentDriver) => void> = [];
26
27
  private cwd: string | null = null;
27
28
  private artifactsDir: string | null = null;
29
+ private runtime: Awaited<ReturnType<typeof prepareAntigravityRuntime>> | null = null;
30
+ private started = false;
28
31
  private readonly config: AntigravityDriverConfig;
29
32
 
30
33
  constructor(config: AntigravityDriverConfig) {
@@ -42,30 +45,37 @@ class AntigravityDriver implements AgentDriver {
42
45
  }
43
46
 
44
47
  async start(ctx: StartContext): Promise<ReadyState> {
45
- if (this.inner) throw new Error("Antigravity driver cannot be started twice");
48
+ if (this.started) throw new Error("Antigravity driver cannot be started twice");
49
+ this.started = true;
46
50
  this.cwd = await realpath(this.config.cwd ?? ctx.artifactsDir);
47
51
  this.artifactsDir = ctx.artifactsDir;
48
52
  const cliVersion = await discoverCliVersion(this.config.command);
49
- const inner = await subprocessDriver.create({
50
- command: this.config.command,
51
- args: buildAntigravityArgs(this.config, this.cwd),
52
- cwd: this.cwd,
53
- env: this.config.env,
54
- protocol: "custom",
55
- parseEvent: makeParseEvent(this.config, cliVersion),
56
- formatPrompt: (prompt) =>
57
- formatAntigravityPrompt(
58
- `The task workspace is ${JSON.stringify(this.cwd)}. Treat that directory as the current directory for this task and create task artifacts there.\n\n${prompt}`,
59
- ),
60
- closeStdinAfterPrompt: true,
61
- idleTimeout: this.config.idleTimeout,
62
- maxLineBytes: this.config.maxLineBytes,
63
- maxInterrupts: 1,
64
- });
65
- for (const attach of this.pendingAttach) attach(inner);
66
- this.pendingAttach = [];
67
- this.inner = inner;
68
- return inner.start(ctx);
53
+ this.runtime = await prepareAntigravityRuntime(this.config, ctx.isolatedHome);
54
+ try {
55
+ const inner = await subprocessDriver.create({
56
+ command: this.config.command,
57
+ args: buildAntigravityArgs(this.config, this.cwd),
58
+ cwd: this.cwd,
59
+ env: this.runtime.env,
60
+ protocol: "custom",
61
+ parseEvent: makeParseEvent(this.config, cliVersion),
62
+ formatPrompt: (prompt) =>
63
+ formatAntigravityPrompt(
64
+ `The task workspace is ${JSON.stringify(this.cwd)}. Treat that directory as the current directory for this task and create task artifacts there.\n\n${prompt}`,
65
+ ),
66
+ closeStdinAfterPrompt: true,
67
+ idleTimeout: this.config.idleTimeout,
68
+ maxLineBytes: this.config.maxLineBytes,
69
+ maxInterrupts: 1,
70
+ });
71
+ for (const attach of this.pendingAttach) attach(inner);
72
+ this.pendingAttach = [];
73
+ this.inner = inner;
74
+ return await inner.start(ctx);
75
+ } catch (error) {
76
+ await this.close();
77
+ throw error;
78
+ }
69
79
  }
70
80
 
71
81
  async respond(_answer: Answer): Promise<void> {
@@ -79,6 +89,11 @@ class AntigravityDriver implements AgentDriver {
79
89
  async collect(): Promise<Artifact[]> {
80
90
  await this.quiesce();
81
91
  if (!this.cwd) return [];
92
+ try {
93
+ await this.runtime?.collect(this.cwd);
94
+ } finally {
95
+ await this.runtime?.close();
96
+ }
82
97
  const artifacts = await walkArtifacts(this.cwd);
83
98
  return this.cwd === this.artifactsDir
84
99
  ? artifacts
@@ -86,7 +101,11 @@ class AntigravityDriver implements AgentDriver {
86
101
  }
87
102
 
88
103
  async close(): Promise<void> {
89
- await this.inner?.close();
104
+ try {
105
+ await this.inner?.close();
106
+ } finally {
107
+ await this.runtime?.close();
108
+ }
90
109
  }
91
110
  }
92
111
 
@@ -0,0 +1,88 @@
1
+ import { chmod, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir, platform } from "node:os";
3
+ import { join } from "node:path";
4
+ import { temporaryDirectory } from "tempy";
5
+ import { exec } from "../../core/exec.js";
6
+ import { captureAntigravityArtifacts } from "./artifacts.js";
7
+ import type { AntigravityDriverConfig } from "./config.js";
8
+
9
+ export const prepareAntigravityRuntime = async (
10
+ config: AntigravityDriverConfig,
11
+ isolatedHome?: string,
12
+ ): Promise<{
13
+ env: Record<string, string> | undefined;
14
+ collect: (destination: string) => Promise<void>;
15
+ close: () => Promise<void>;
16
+ }> => {
17
+ if (config.permissionMode === "inherit")
18
+ return {
19
+ env: config.env,
20
+ collect: async (destination) => {
21
+ if (isolatedHome) await captureAntigravityArtifacts(isolatedHome, destination);
22
+ },
23
+ close: async () => {},
24
+ };
25
+
26
+ const runtimeHome = temporaryDirectory({ prefix: "lux-antigravity-home-" });
27
+ const close = () => rm(runtimeHome, { recursive: true, force: true });
28
+ try {
29
+ await chmod(runtimeHome, 0o700);
30
+ const cliRoot = join(runtimeHome, ".gemini", "antigravity-cli");
31
+ await mkdir(cliRoot, { recursive: true, mode: 0o700 });
32
+ await writeFile(
33
+ join(cliRoot, "settings.json"),
34
+ JSON.stringify({
35
+ enableTerminalSandbox: true,
36
+ toolPermission: "proceed-in-sandbox",
37
+ permissions: config.permissions,
38
+ }),
39
+ { mode: 0o600 },
40
+ );
41
+ const source =
42
+ config.authTokenFile ??
43
+ join(config.env?.HOME ?? homedir(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
44
+ try {
45
+ // Copy only the CLI's own token, never settings, conversations, or plugins.
46
+ // A private copy prevents token refresh from mutating the caller's login.
47
+ const target = join(cliRoot, "antigravity-oauth-token");
48
+ await copyFile(source, target);
49
+ await chmod(target, 0o600);
50
+ // Reject empty files early without including credential contents in diagnostics.
51
+ if ((await readFile(target)).length === 0) throw new Error("Antigravity token file is empty");
52
+ } catch (error) {
53
+ if (config.authTokenFile || (error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
54
+ if (config.reuseKeychain) {
55
+ if (platform() !== "darwin") throw new Error("Antigravity Keychain reuse requires macOS");
56
+ const credential = await exec(
57
+ "/usr/bin/security",
58
+ ["find-generic-password", "-s", "gemini", "-a", "antigravity", "-w"],
59
+ { timeoutMs: 10_000, maxBuffer: 1_000_000 },
60
+ );
61
+ if (credential.exitCode !== 0 || credential.stdoutTruncated)
62
+ throw new Error("Could not read the authorized Antigravity Keychain credential");
63
+ let token = credential.stdout.trim();
64
+ if (token.startsWith("go-keyring-base64:"))
65
+ token = Buffer.from(token.slice("go-keyring-base64:".length), "base64").toString("utf8");
66
+ try {
67
+ const parsed = JSON.parse(token);
68
+ if (!parsed?.token?.refresh_token || !parsed.auth_method) throw new Error();
69
+ } catch {
70
+ throw new Error("Antigravity Keychain credential has an unsupported format");
71
+ }
72
+ await writeFile(join(cliRoot, "antigravity-oauth-token"), token, { mode: 0o600 });
73
+ }
74
+ }
75
+ return {
76
+ env: {
77
+ ...config.env,
78
+ HOME: runtimeHome,
79
+ ...(process.platform === "win32" ? { USERPROFILE: runtimeHome } : {}),
80
+ },
81
+ collect: (destination) => captureAntigravityArtifacts(runtimeHome, destination),
82
+ close,
83
+ };
84
+ } catch (error) {
85
+ await close();
86
+ throw error;
87
+ }
88
+ };
package/src/index.ts CHANGED
@@ -265,6 +265,12 @@ export {
265
265
  type StartContext,
266
266
  selectModelAndReasoningEffort,
267
267
  } from "./core/driver.js";
268
+ export {
269
+ type ContainerExecution,
270
+ ContainerExecutionSchema,
271
+ type DriverExecution,
272
+ DriverExecutionSchema,
273
+ } from "./core/driver-execution.js";
268
274
  export {
269
275
  type DriverMetric,
270
276
  type DriverMetricDescriptor,
@@ -74,7 +74,7 @@ const LifecycleHooksPluginSchema = z.custom<LifecycleHooksPlugin>().superRefine(
74
74
  if (issue) ctx.addIssue({ code: "custom", message: issue });
75
75
  });
76
76
 
77
- const LuxConfigSchema = z
77
+ export const LuxConfigSchema = z
78
78
  .object({
79
79
  environment: EnvironmentDeclarationSchema.optional(),
80
80
  casesRoot: z.string().optional(),
@@ -194,6 +194,9 @@ const builtinDrivers: DriverPlugin<never>[] = [
194
194
  subprocessDriver,
195
195
  mcpProbeDriver,
196
196
  ];
197
+ export const isBuiltinDriver = (plugin: DriverPlugin): boolean =>
198
+ builtinDrivers.some((builtin) => builtin === plugin);
199
+
197
200
  const builtinAnswerers: AnswererPlugin<never>[] = [
198
201
  personaAnswerer,
199
202
  claudeCodeAnswerer,