@prismatic-io/lux 0.0.2-preview.27 → 0.0.2-preview.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/authoring.d.ts +1 -0
- package/lib/authoring.d.ts.map +1 -1
- package/lib/authoring.js +1 -0
- package/lib/authoring.js.map +1 -1
- package/lib/container/driver.d.ts +27 -0
- package/lib/container/driver.d.ts.map +1 -0
- package/lib/container/driver.js +197 -0
- package/lib/container/driver.js.map +1 -0
- package/lib/container/protocol.d.ts +12 -0
- package/lib/container/protocol.d.ts.map +1 -0
- package/lib/container/protocol.js +38 -0
- package/lib/container/protocol.js.map +1 -0
- package/lib/container/resources.d.ts +12 -0
- package/lib/container/resources.d.ts.map +1 -0
- package/lib/container/resources.js +133 -0
- package/lib/container/resources.js.map +1 -0
- package/lib/container/worker.d.ts +2 -0
- package/lib/container/worker.d.ts.map +1 -0
- package/lib/container/worker.js +85 -0
- package/lib/container/worker.js.map +1 -0
- package/lib/core/case.d.ts +80 -0
- package/lib/core/case.d.ts.map +1 -1
- package/lib/core/case.js +8 -1
- package/lib/core/case.js.map +1 -1
- package/lib/core/driver-execution.d.ts +83 -0
- package/lib/core/driver-execution.d.ts.map +1 -0
- package/lib/core/driver-execution.js +72 -0
- package/lib/core/driver-execution.js.map +1 -0
- package/lib/core/driver.d.ts +5 -1
- package/lib/core/driver.d.ts.map +1 -1
- package/lib/core/driver.js +5 -1
- package/lib/core/driver.js.map +1 -1
- package/lib/core/harness-catalogs/cursor.js +1 -1
- package/lib/core/harness-catalogs/cursor.js.map +1 -1
- package/lib/core/harness-matrix.d.ts +84 -0
- package/lib/core/harness-matrix.d.ts.map +1 -1
- package/lib/core/harness-matrix.js +11 -1
- package/lib/core/harness-matrix.js.map +1 -1
- package/lib/core/run.d.ts +40 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/drivers/antigravity/artifacts.d.ts +2 -0
- package/lib/drivers/antigravity/artifacts.d.ts.map +1 -0
- package/lib/drivers/antigravity/artifacts.js +33 -0
- package/lib/drivers/antigravity/artifacts.js.map +1 -0
- package/lib/drivers/antigravity/config.d.ts +27 -0
- package/lib/drivers/antigravity/config.d.ts.map +1 -1
- package/lib/drivers/antigravity/config.js +35 -0
- package/lib/drivers/antigravity/config.js.map +1 -1
- package/lib/drivers/antigravity/index.d.ts +35 -0
- package/lib/drivers/antigravity/index.d.ts.map +1 -1
- package/lib/drivers/antigravity/index.js +42 -20
- package/lib/drivers/antigravity/index.js.map +1 -1
- package/lib/drivers/antigravity/runtime.d.ts +7 -0
- package/lib/drivers/antigravity/runtime.d.ts.map +1 -0
- package/lib/drivers/antigravity/runtime.js +78 -0
- package/lib/drivers/antigravity/runtime.js.map +1 -0
- package/lib/drivers/copilot/config.d.ts +3 -3
- package/lib/drivers/copilot/config.js +5 -5
- package/lib/drivers/copilot/config.js.map +1 -1
- package/lib/drivers/copilot/index.d.ts +4 -4
- package/lib/drivers/copilot/index.d.ts.map +1 -1
- package/lib/drivers/copilot/index.js +13 -3
- package/lib/drivers/copilot/index.js.map +1 -1
- package/lib/drivers/cursor/config.js +1 -1
- package/lib/drivers/cursor/config.js.map +1 -1
- package/lib/drivers/cursor/index.d.ts.map +1 -1
- package/lib/drivers/cursor/index.js +8 -3
- package/lib/drivers/cursor/index.js.map +1 -1
- package/lib/drivers/shared/acp-session.d.ts +4 -0
- package/lib/drivers/shared/acp-session.d.ts.map +1 -0
- package/lib/drivers/shared/acp-session.js +23 -0
- package/lib/drivers/shared/acp-session.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/config.d.ts +122 -1
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +2 -1
- package/lib/orchestrator/config.js.map +1 -1
- package/lib/orchestrator/doctor.d.ts.map +1 -1
- package/lib/orchestrator/doctor.js +3 -1
- package/lib/orchestrator/doctor.js.map +1 -1
- package/lib/orchestrator/driver-capabilities.d.ts +2 -1
- package/lib/orchestrator/driver-capabilities.d.ts.map +1 -1
- package/lib/orchestrator/driver-capabilities.js +5 -2
- package/lib/orchestrator/driver-capabilities.js.map +1 -1
- package/lib/orchestrator/driver-identity.d.ts.map +1 -1
- package/lib/orchestrator/driver-identity.js +11 -1
- package/lib/orchestrator/driver-identity.js.map +1 -1
- package/lib/orchestrator/experiment-context.d.ts +34 -0
- package/lib/orchestrator/experiment-context.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +16 -3
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/package.json +1 -1
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/authoring.ts +6 -0
- package/src/container/driver.ts +211 -0
- package/src/container/protocol.ts +48 -0
- package/src/container/resources.ts +153 -0
- package/src/container/worker.ts +107 -0
- package/src/core/case.ts +8 -1
- package/src/core/driver-execution.ts +84 -0
- package/src/core/driver.ts +9 -1
- package/src/core/harness-catalogs/cursor.ts +1 -1
- package/src/core/harness-matrix.ts +19 -1
- package/src/drivers/antigravity/README.md +108 -10
- package/src/drivers/antigravity/artifacts.ts +37 -0
- package/src/drivers/antigravity/config.ts +35 -0
- package/src/drivers/antigravity/index.ts +41 -22
- package/src/drivers/antigravity/runtime.ts +88 -0
- package/src/drivers/copilot/README.md +19 -11
- package/src/drivers/copilot/config.ts +5 -5
- package/src/drivers/copilot/index.ts +20 -4
- package/src/drivers/cursor/README.md +14 -8
- package/src/drivers/cursor/config.ts +1 -1
- package/src/drivers/cursor/index.ts +11 -7
- package/src/drivers/shared/acp-session.ts +40 -0
- package/src/index.ts +6 -0
- package/src/orchestrator/config.ts +4 -1
- package/src/orchestrator/doctor.ts +3 -1
- package/src/orchestrator/driver-capabilities.ts +6 -1
- package/src/orchestrator/driver-identity.ts +11 -1
- package/src/orchestrator/orchestrator.ts +19 -3
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
64
|
-
|
|
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.
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
@@ -22,27 +22,35 @@ and silently falls back on the rest, so the harness catalog in
|
|
|
22
22
|
`core/harness-catalogs/copilot.ts` records the levels per model id and Lux
|
|
23
23
|
rejects a level the model does not honor. Models whose levels are not published
|
|
24
24
|
(Gemini, Grok, Kimi, MAI) are listed with no effort. When no effort is set, a
|
|
25
|
-
model with levels runs at `low`.
|
|
26
|
-
|
|
25
|
+
model with levels runs at `low`. When ACP advertises a model selector, Lux
|
|
26
|
+
selects and verifies the requested model; otherwise it records the model as
|
|
27
|
+
requested rather than confirmed. `lux doctor` compares the catalog's model ids with
|
|
27
28
|
`copilot help config` and warns when they differ.
|
|
28
29
|
|
|
29
30
|
Tool permission requests arrive as ACP `session/request_permission` calls and
|
|
30
31
|
become `approve` interrupts. Lux answers with the one-operation option only and
|
|
31
32
|
never selects a remembered grant. `allowTools` and `denyTools` pass Copilot
|
|
32
33
|
permission patterns; `allowAllTools` approves everything, which turns the
|
|
33
|
-
interactive capability off. The `ask_user` tool is disabled
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Copilot loads
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
interactive capability off. The `ask_user` tool is disabled because Copilot has
|
|
35
|
+
no ACP wire for it. `askUser: true` is rejected before launching the CLI;
|
|
36
|
+
permission interrupts remain supported through the normal Lux answerer.
|
|
37
|
+
|
|
38
|
+
Copilot loads its native workspace instructions and bundled GitHub MCP server
|
|
39
|
+
by default, matching ordinary harness execution. Set `customInstructions: false`
|
|
40
|
+
or `builtinMcps: false` to disable them for a specific evaluation. Auto-update is
|
|
41
|
+
always off. The driver provides no ambient-configuration isolation: Copilot still
|
|
40
42
|
reads `~/.copilot` settings, MCP configuration, and credentials. Usage and
|
|
41
43
|
cost are recorded as unknown; `--usage-output-file` cannot be combined with
|
|
42
44
|
`--acp`.
|
|
43
45
|
|
|
44
|
-
The adapter
|
|
45
|
-
|
|
46
|
+
The adapter has synthetic protocol and lifecycle tests plus an opt-in live
|
|
47
|
+
smoke task: `mise run smoke:harnesses -- copilot` from the repository root.
|
|
48
|
+
The live task uses your installed CLI, existing login, and model quota. CLI
|
|
49
|
+
`1.0.83` passed the authenticated file-copy smoke on 2026-09-30 after the account
|
|
50
|
+
received a Copilot seat and CLI access. A seat alone does not enable the CLI:
|
|
51
|
+
check the organization's Copilot CLI policy when GitHub returns a feature-access
|
|
52
|
+
403. Lux also recognizes the CLI's native authorization diagnostic as an error
|
|
53
|
+
even when Copilot ends the turn normally.
|
|
46
54
|
|
|
47
55
|
Protocol references:
|
|
48
56
|
|
|
@@ -14,12 +14,12 @@ export const CopilotDriverConfigSchema = z
|
|
|
14
14
|
denyTools: z.array(z.string().min(1)).optional(),
|
|
15
15
|
/** Approve every tool without interrupts; permissions stop being evidence. */
|
|
16
16
|
allowAllTools: z.boolean().default(false),
|
|
17
|
-
/**
|
|
18
|
-
askUser: z.
|
|
17
|
+
/** ACP cannot answer ask_user requests; reject configurations that would strand the run. */
|
|
18
|
+
askUser: z.literal(false).default(false),
|
|
19
19
|
/** Load AGENTS.md and related instruction files from the workspace. */
|
|
20
|
-
customInstructions: z.boolean().default(
|
|
20
|
+
customInstructions: z.boolean().default(true),
|
|
21
21
|
/** Enable Copilot's bundled GitHub MCP server. */
|
|
22
|
-
builtinMcps: z.boolean().default(
|
|
22
|
+
builtinMcps: z.boolean().default(true),
|
|
23
23
|
addDirs: z.array(z.string()).optional(),
|
|
24
24
|
extraArgs: z.array(z.string()).optional(),
|
|
25
25
|
cwd: z.string().optional(),
|
|
@@ -42,7 +42,7 @@ export const buildCopilotArgs = (config: CopilotDriverConfig): string[] => [
|
|
|
42
42
|
"--model",
|
|
43
43
|
config.model,
|
|
44
44
|
...(config.reasoningEffort ? ["--effort", config.reasoningEffort] : []),
|
|
45
|
-
|
|
45
|
+
"--no-ask-user",
|
|
46
46
|
...(config.customInstructions ? [] : ["--no-custom-instructions"]),
|
|
47
47
|
...(config.builtinMcps ? [] : ["--disable-builtin-mcps"]),
|
|
48
48
|
...(config.allowAllTools ? ["--allow-all-tools"] : []),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../../core/index.js";
|
|
15
15
|
import { AcpEvents } from "../shared/acp-events.js";
|
|
16
16
|
import { permissionAnswer, permissionRequestText, records } from "../shared/acp-interaction.js";
|
|
17
|
+
import { acpModel, selectAcpModel } from "../shared/acp-session.js";
|
|
17
18
|
import { AcpTransport, asRecord, type RpcId, type RpcRecord } from "../shared/acp-transport.js";
|
|
18
19
|
import { walkArtifacts } from "../shared/artifacts.js";
|
|
19
20
|
import {
|
|
@@ -35,6 +36,7 @@ class CopilotDriver implements AgentDriver {
|
|
|
35
36
|
private terminal = false;
|
|
36
37
|
private idleTimer: NodeJS.Timeout | undefined;
|
|
37
38
|
private interruptCount = 0;
|
|
39
|
+
private responseText = "";
|
|
38
40
|
private readonly interrupts = new Map<string, { id: RpcId; params: RpcRecord }>();
|
|
39
41
|
|
|
40
42
|
private readonly config: CopilotDriverConfig;
|
|
@@ -91,9 +93,12 @@ class CopilotDriver implements AgentDriver {
|
|
|
91
93
|
"Expected Copilot ACP with copilot-login authentication; check the configured command points to GitHub Copilot CLI",
|
|
92
94
|
);
|
|
93
95
|
}
|
|
94
|
-
|
|
96
|
+
let session = await this.request("session/new", { cwd: this.cwd, mcpServers: [] });
|
|
95
97
|
if (typeof session.sessionId !== "string") throw new Error("Copilot ACP omitted sessionId");
|
|
96
98
|
this.sessionId = session.sessionId;
|
|
99
|
+
session = await selectAcpModel("Copilot", session, this.config.model, (method, params) =>
|
|
100
|
+
this.request(method, params),
|
|
101
|
+
);
|
|
97
102
|
const modeId = COPILOT_MODE_IDS[this.config.mode];
|
|
98
103
|
const currentModeId = asRecord(session.modes)?.currentModeId;
|
|
99
104
|
if (currentModeId !== modeId) {
|
|
@@ -117,7 +122,7 @@ class CopilotDriver implements AgentDriver {
|
|
|
117
122
|
...(cliVersion ? { cliVersion } : {}),
|
|
118
123
|
mode: this.config.mode,
|
|
119
124
|
reasoningEffort: this.config.reasoningEffort,
|
|
120
|
-
modelResolution: "requested",
|
|
125
|
+
modelResolution: typeof acpModel(session) === "string" ? "reported" : "requested",
|
|
121
126
|
nativeConfig: session.configOptions ?? [],
|
|
122
127
|
usageAvailability: "unknown",
|
|
123
128
|
isolation: "none",
|
|
@@ -143,6 +148,13 @@ class CopilotDriver implements AgentDriver {
|
|
|
143
148
|
if (this.terminal) return;
|
|
144
149
|
if (result?.stopReason !== "end_turn")
|
|
145
150
|
throw new Error(`Copilot stopped with ${String(result?.stopReason ?? "unknown reason")}`);
|
|
151
|
+
// Copilot 1.0.83 emits this native auth failure as text followed by end_turn.
|
|
152
|
+
if (
|
|
153
|
+
/^Error: Authorization error\. Your credentials may be expired or invalid\.(?: \(Request ID: [^)]+\))?\s*$/.test(
|
|
154
|
+
this.responseText,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
throw new Error("Copilot authorization failed; run 'copilot login' and retry");
|
|
146
158
|
this.terminal = true;
|
|
147
159
|
clearTimeout(this.idleTimer);
|
|
148
160
|
await this.emitter.emit("done", {
|
|
@@ -174,7 +186,8 @@ class CopilotDriver implements AgentDriver {
|
|
|
174
186
|
}
|
|
175
187
|
if (++this.interruptCount > this.config.maxInterrupts) {
|
|
176
188
|
this.transport?.write({ id, result: { outcome: { outcome: "cancelled" } } });
|
|
177
|
-
|
|
189
|
+
this.fail(new Error("Copilot exceeded maxInterrupts"), "max-interactions");
|
|
190
|
+
return;
|
|
178
191
|
}
|
|
179
192
|
const interruptId = `copilot-${id}`;
|
|
180
193
|
if (this.interrupts.has(interruptId)) throw new Error("Copilot reused a pending request ID");
|
|
@@ -191,6 +204,9 @@ class CopilotDriver implements AgentDriver {
|
|
|
191
204
|
if (method === "session/update") {
|
|
192
205
|
if (params.sessionId !== this.sessionId) return;
|
|
193
206
|
const update = asRecord(params.update);
|
|
207
|
+
const content = asRecord(update?.content);
|
|
208
|
+
if (update?.sessionUpdate === "agent_message_chunk" && typeof content?.text === "string")
|
|
209
|
+
this.responseText = (this.responseText + content.text).slice(0, 8_192);
|
|
194
210
|
if (update)
|
|
195
211
|
for (const event of this.events.update(update)) await this.emitter.emit("progress", event);
|
|
196
212
|
}
|
|
@@ -217,7 +233,7 @@ class CopilotDriver implements AgentDriver {
|
|
|
217
233
|
);
|
|
218
234
|
}
|
|
219
235
|
|
|
220
|
-
private fail(error: unknown, exitReason: "error"
|
|
236
|
+
private fail(error: unknown, exitReason: DriverEventMap["error"]["exitReason"] = "error"): void {
|
|
221
237
|
if (this.terminal) return;
|
|
222
238
|
this.terminal = true;
|
|
223
239
|
clearTimeout(this.idleTimer);
|