@cavi-ai/antigravity 0.2.3 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ - Make the guided-discovery **Reconnect** verification tool-free by installing
6
+ a bundled, isolated `agy` custom agent for setup probes. Normal provider
7
+ turns retain the configured Antigravity mode and tool behavior.
8
+ - Declare the native `agy` executable as the CLI backend runtime artifact so
9
+ current OpenClaw hosts can bind the setup probe to a durable owner.
10
+ - Extend the installed-host integration check through Reconnect activation and
11
+ isolate its temporary `agy` plugin config from the user's existing plugins.
12
+ - Require Antigravity CLI 1.2.1 or newer for hard tool-free setup probes.
13
+
3
14
  ## 0.2.3
4
15
 
5
16
  - Use OpenClaw's provider-owned guided discovery for Control UI reconnect, the
package/README.md CHANGED
@@ -18,7 +18,7 @@ ingests and serves at [cavi-ai.xyz/docs/antigravity](https://cavi-ai.xyz/docs/an
18
18
 
19
19
  - OpenClaw 2026.7 or newer
20
20
  - Node.js 20 or newer
21
- - Antigravity CLI installed and signed in
21
+ - Antigravity CLI 1.2.1 or newer, installed and signed in
22
22
 
23
23
  Confirm that Antigravity is ready:
24
24
 
@@ -40,7 +40,10 @@ Restart the OpenClaw Gateway after installation.
40
40
  In the Control UI, open **Models → Providers** and choose **Reconnect** on the
41
41
  Antigravity CLI card. The plugin validates the existing `agy` session and
42
42
  refreshes the provider connection without creating a credential or changing the
43
- default model.
43
+ default model. Reconnect installs or refreshes the bundled
44
+ `openclaw-antigravity-tool-free` custom agent, which OpenClaw uses only for its
45
+ connection probe. Normal provider turns keep the configured `agy` mode and tool
46
+ behavior.
44
47
 
45
48
  ## Use
46
49
 
@@ -124,11 +127,13 @@ npm run check:host-integration # live agy + isolated installed-host Reconnect ch
124
127
 
125
128
  The host-integration check requires a signed-in `agy` session and a compatible
126
129
  installed `openclaw` command. It runs `agy models`, starts the working-tree
127
- plugin in a temporary loopback-only OpenClaw state, verifies that the host
128
- projects **Reconnect** for Antigravity, and verifies that credential-only
129
- **Connect** is absent. The temporary gateway and state are removed afterward;
130
- the installed gateway, OpenClaw config, credentials, and default model are not
131
- changed. Set `AGY_BIN` or `OPENCLAW_BIN` to use a non-default executable.
130
+ plugin in a temporary loopback-only OpenClaw state, activates the projected
131
+ **Reconnect** action, and verifies that credential-only **Connect** is absent.
132
+ The check links the live `agy` session into a temporary home while keeping its
133
+ plugin configuration isolated. The temporary gateway, home, and state are
134
+ removed afterward; the installed gateway, OpenClaw config, credentials,
135
+ default model, and pre-existing `agy` plugins are not changed. Set `AGY_BIN` or
136
+ `OPENCLAW_BIN` to use a non-default executable.
132
137
 
133
138
  Releasing docs is automated: publish a GitHub Release `vX.Y.Z` (matching
134
139
  `package.json`) and the `Publish release documentation` workflow builds the
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: openclaw-antigravity-setup
3
+ description: Tool-free OpenClaw Antigravity connection probe
4
+ mainAgent: true
5
+ excludeDefaultComponents: true
6
+ inheritCustomizations: false
7
+ ---
8
+
9
+ # Connection probe
10
+
11
+ Reply directly to the user's message. You have no tools.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "openclaw-antigravity-tool-free",
3
+ "version": "1.0.0",
4
+ "description": "Tool-free connection probe for the OpenClaw Antigravity provider",
5
+ "author": {
6
+ "name": "CAVI"
7
+ },
8
+ "license": "MIT"
9
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "antigravity",
3
3
  "name": "Antigravity CLI",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "description": "Runs Google's Antigravity CLI (agy) as a subscription-backed model provider.",
6
6
  "enabledByDefault": false,
7
7
  "activation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavi-ai/antigravity",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Runs Google's Antigravity CLI (agy) as a subscription-backed OpenClaw model provider.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "files": [
31
31
  "src",
32
+ "agy-plugin",
32
33
  "scripts/check-host-integration.mjs",
33
34
  "setup-api.js",
34
35
  "doctor-contract-api.js",
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { execFileSync, spawn } from "node:child_process";
4
4
  import { randomBytes } from "node:crypto";
5
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
6
6
  import { createServer } from "node:net";
7
- import { tmpdir } from "node:os";
7
+ import { homedir, tmpdir } from "node:os";
8
8
  import { dirname, join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { parseAntigravityModelIds } from "../src/provider.js";
@@ -90,8 +90,8 @@ function parseJsonOutput(command, args, options) {
90
90
  }
91
91
  }
92
92
 
93
- export function buildGatewayCallArgs(method, gatewayUrl, token, timeout = 10_000) {
94
- return [
93
+ export function buildGatewayCallArgs(method, gatewayUrl, token, timeout = 10_000, params) {
94
+ const args = [
95
95
  "gateway",
96
96
  "call",
97
97
  method,
@@ -103,6 +103,47 @@ export function buildGatewayCallArgs(method, gatewayUrl, token, timeout = 10_000
103
103
  "--timeout",
104
104
  String(timeout),
105
105
  ];
106
+ if (params !== undefined) {
107
+ args.push("--params", JSON.stringify(params));
108
+ }
109
+ return args;
110
+ }
111
+
112
+ function findReconnectCandidate(detection) {
113
+ const candidate = detection?.candidates?.find(
114
+ (entry) =>
115
+ entry?.kind === `provider-auto:${CHOICE_ID}` &&
116
+ typeof entry?.modelRef === "string" &&
117
+ entry.modelRef.startsWith(`${PROVIDER_ID}/`),
118
+ );
119
+ if (!candidate) {
120
+ fail("guided discovery did not return an Antigravity Reconnect candidate");
121
+ }
122
+ return candidate;
123
+ }
124
+
125
+ function assertReconnectActivation(result, candidate) {
126
+ if (result?.ok !== true || result?.modelRef !== candidate.modelRef) {
127
+ fail(`Reconnect activation failed: ${result?.error ?? "unexpected host response"}`);
128
+ }
129
+ return { modelRef: result.modelRef, latencyMs: result.latencyMs };
130
+ }
131
+
132
+ export async function createIsolatedAgyHome(tempRoot, sourceHome = homedir()) {
133
+ const sourceGemini = join(sourceHome, ".gemini");
134
+ const isolatedHome = join(tempRoot, "home");
135
+ const isolatedGemini = join(isolatedHome, ".gemini");
136
+ await mkdir(isolatedGemini, { recursive: true });
137
+ for (const entry of await readdir(sourceGemini, { withFileTypes: true })) {
138
+ if (entry.name === "config") continue;
139
+ await symlink(
140
+ join(sourceGemini, entry.name),
141
+ join(isolatedGemini, entry.name),
142
+ entry.isDirectory() ? "dir" : "file",
143
+ );
144
+ }
145
+ await mkdir(join(isolatedGemini, "config"), { recursive: true });
146
+ return isolatedHome;
106
147
  }
107
148
 
108
149
  async function reserveLoopbackPort() {
@@ -177,77 +218,100 @@ export async function runHostIntegrationCheck({
177
218
 
178
219
  const modelIds = assertAgySession(commandOutput(agy, ["models"]));
179
220
  return await withTemporaryRoot(async (tempRoot) => {
180
- const stateDir = join(tempRoot, "state");
181
- const configPath = join(tempRoot, "openclaw.json");
182
- const port = await reserveLoopbackPort();
183
- const gatewayUrl = `ws://127.0.0.1:${port}`;
184
- const gatewayToken = randomBytes(24).toString("hex");
185
- const env = {
186
- ...process.env,
187
- OPENCLAW_CONFIG_PATH: configPath,
188
- OPENCLAW_CONFIG_READONLY: "1",
189
- OPENCLAW_STATE_DIR: stateDir,
190
- };
191
- const gatewayOutput = [];
192
- let gateway;
221
+ const isolatedHome = await createIsolatedAgyHome(tempRoot);
222
+ const stateDir = join(tempRoot, "state");
223
+ const configPath = join(tempRoot, "openclaw.json");
224
+ const port = await reserveLoopbackPort();
225
+ const gatewayUrl = `ws://127.0.0.1:${port}`;
226
+ const gatewayToken = randomBytes(24).toString("hex");
227
+ const env = {
228
+ ...process.env,
229
+ HOME: isolatedHome,
230
+ USERPROFILE: isolatedHome,
231
+ OPENCLAW_CONFIG_PATH: configPath,
232
+ OPENCLAW_STATE_DIR: stateDir,
233
+ };
234
+ const gatewayOutput = [];
235
+ let gateway;
193
236
 
194
- try {
195
- await mkdir(stateDir, { recursive: true });
196
- await writeFile(
197
- configPath,
198
- `${JSON.stringify(
199
- {
200
- gateway: { mode: "local", bind: "loopback", auth: { mode: "token" } },
201
- plugins: {
202
- allow: ["antigravity"],
203
- load: { paths: [pluginRoot] },
204
- entries: { antigravity: { enabled: true, config: { command: agy } } },
237
+ try {
238
+ await mkdir(stateDir, { recursive: true });
239
+ await writeFile(
240
+ configPath,
241
+ `${JSON.stringify(
242
+ {
243
+ gateway: { mode: "local", bind: "loopback", auth: { mode: "token" } },
244
+ plugins: {
245
+ allow: ["antigravity"],
246
+ load: { paths: [pluginRoot] },
247
+ entries: { antigravity: { enabled: true, config: { command: agy } } },
248
+ },
205
249
  },
206
- },
207
- null,
208
- 2,
209
- )}\n`,
210
- { mode: 0o600 },
211
- );
250
+ null,
251
+ 2,
252
+ )}\n`,
253
+ { mode: 0o600 },
254
+ );
212
255
 
213
- gateway = spawn(
214
- openclaw,
215
- [
216
- "gateway",
217
- "run",
218
- "--port",
219
- String(port),
220
- "--bind",
221
- "loopback",
222
- "--auth",
223
- "token",
224
- "--token",
225
- gatewayToken,
226
- "--allow-unconfigured",
227
- ],
228
- { env, stdio: ["ignore", "pipe", "pipe"] },
229
- );
230
- gateway.stdout.on("data", (chunk) => gatewayOutput.push(String(chunk)));
231
- gateway.stderr.on("data", (chunk) => gatewayOutput.push(String(chunk)));
232
- const diagnostics = () => boundedDiagnostics(gatewayOutput);
256
+ gateway = spawn(
257
+ openclaw,
258
+ [
259
+ "gateway",
260
+ "run",
261
+ "--port",
262
+ String(port),
263
+ "--bind",
264
+ "loopback",
265
+ "--auth",
266
+ "token",
267
+ "--token",
268
+ gatewayToken,
269
+ "--allow-unconfigured",
270
+ ],
271
+ { env, stdio: ["ignore", "pipe", "pipe"] },
272
+ );
273
+ gateway.stdout.on("data", (chunk) => gatewayOutput.push(String(chunk)));
274
+ gateway.stderr.on("data", (chunk) => gatewayOutput.push(String(chunk)));
275
+ const diagnostics = () => boundedDiagnostics(gatewayOutput);
233
276
 
234
- await waitForGateway(openclaw, gatewayUrl, gatewayToken, env, gateway, diagnostics);
235
- const status = parseJsonOutput(
236
- openclaw,
237
- buildGatewayCallArgs("models.authStatus", gatewayUrl, gatewayToken),
238
- { env, timeout: 15_000 },
239
- );
240
- const action = assertReconnectProjection(status);
241
- return {
242
- pluginVersion: packageJson.version,
243
- hostVersion: hostVersion.join("."),
244
- agyModelCount: modelIds.length,
245
- action,
246
- };
247
- } finally {
248
- if (gateway) await stopGateway(gateway);
249
- }
250
- });
277
+ await waitForGateway(openclaw, gatewayUrl, gatewayToken, env, gateway, diagnostics);
278
+ const status = parseJsonOutput(
279
+ openclaw,
280
+ buildGatewayCallArgs("models.authStatus", gatewayUrl, gatewayToken),
281
+ { env, timeout: 15_000 },
282
+ );
283
+ const action = assertReconnectProjection(status);
284
+ const detection = parseJsonOutput(
285
+ openclaw,
286
+ buildGatewayCallArgs("openclaw.setup.detect", gatewayUrl, gatewayToken, 30_000),
287
+ { env, timeout: 35_000 },
288
+ );
289
+ const candidate = findReconnectCandidate(detection);
290
+ const activation = assertReconnectActivation(
291
+ parseJsonOutput(
292
+ openclaw,
293
+ buildGatewayCallArgs(
294
+ "openclaw.setup.activate",
295
+ gatewayUrl,
296
+ gatewayToken,
297
+ 120_000,
298
+ { kind: candidate.kind, modelRef: candidate.modelRef },
299
+ ),
300
+ { env, timeout: 125_000 },
301
+ ),
302
+ candidate,
303
+ );
304
+ return {
305
+ pluginVersion: packageJson.version,
306
+ hostVersion: hostVersion.join("."),
307
+ agyModelCount: modelIds.length,
308
+ action,
309
+ activation,
310
+ };
311
+ } finally {
312
+ if (gateway) await stopGateway(gateway);
313
+ }
314
+ });
251
315
  }
252
316
 
253
317
  const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
@@ -260,6 +324,7 @@ if (isMain) {
260
324
  `OpenClaw ${result.hostVersion}`,
261
325
  `live agy models: ${result.agyModelCount}`,
262
326
  `host action: ${result.action.actionLabel}`,
327
+ `Reconnect activation: ${result.activation.modelRef}`,
263
328
  "credential-only Connect: absent",
264
329
  ].join("\n") + "\n",
265
330
  );
@@ -15,6 +15,7 @@
15
15
  import { ANTIGRAVITY_MODEL_ALIASES } from "./models.js";
16
16
 
17
17
  export const ANTIGRAVITY_BACKEND_ID = "antigravity-cli";
18
+ export const TOOL_FREE_SETUP_AGENT_ID = "openclaw-antigravity-setup";
18
19
 
19
20
  /** agy print mode cannot prompt a human, so tool calls need a non-review mode. */
20
21
  export const DEFAULT_MODE = "accept-edits";
@@ -44,6 +45,17 @@ export function buildAntigravityCliBackend(options = {}) {
44
45
  const base = buildBaseArgs(options);
45
46
  return {
46
47
  id: ANTIGRAVITY_BACKEND_ID,
48
+ runtimeArtifact: {
49
+ kind: "bundled-package-tree",
50
+ packageName: "agy",
51
+ entrypoint: "command",
52
+ nativeExecutableNames: ["agy", "agy.exe"],
53
+ },
54
+ sideQuestionToolMode: "disabled",
55
+ resolveExecutionArgs: ({ baseArgs, executionMode }) =>
56
+ executionMode === "side-question"
57
+ ? [...baseArgs, "--agent", TOOL_FREE_SETUP_AGENT_ID]
58
+ : baseArgs,
47
59
  // Standalone backend: agy's catalog (Gemini + Claude + GPT-OSS behind one
48
60
  // subscription) belongs to no existing provider, so it owns direct
49
61
  // `antigravity-cli/<model>` refs rather than aliasing a canonical provider.
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ export const PLUGIN_ID = "antigravity";
7
7
  export const plugin = {
8
8
  id: PLUGIN_ID,
9
9
  name: "Antigravity CLI",
10
- version: "0.2.3",
10
+ version: "0.2.4",
11
11
  description: "Runs Google's Antigravity CLI (agy) as a subscription-backed model provider.",
12
12
  register: registerAntigravity,
13
13
  };
package/src/provider.js CHANGED
@@ -4,6 +4,8 @@
4
4
  // validates that CLI-owned session and records the provider's non-secret
5
5
  // connection (models, endpoint) in config; OpenClaw stores no key for this provider.
6
6
  import { execFile } from "node:child_process";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
7
9
  import { promisify } from "node:util";
8
10
  import { ANTIGRAVITY_BACKEND_ID } from "./cli-backend.js";
9
11
  import {
@@ -18,6 +20,12 @@ export const ANTIGRAVITY_PROVIDER_ID = ANTIGRAVITY_BACKEND_ID;
18
20
  const execFileAsync = promisify(execFile);
19
21
  const COMMAND_TIMEOUT_MS = 15_000;
20
22
  const COMMAND_MAX_BUFFER_BYTES = 1_048_576;
23
+ const MINIMUM_TOOL_FREE_AGY_VERSION = [1, 2, 1];
24
+ const TOOL_FREE_SETUP_PLUGIN_ROOT = join(
25
+ dirname(fileURLToPath(import.meta.url)),
26
+ "..",
27
+ "agy-plugin",
28
+ );
21
29
 
22
30
  const MISSING_AUTH_MESSAGE =
23
31
  "Antigravity CLI is not ready. Install Google's Antigravity CLI (`agy`), sign in with your Google subscription, then confirm with `agy models`. This provider stores no API key in OpenClaw.";
@@ -42,6 +50,20 @@ function isAbortError(error, signal) {
42
50
  return signal?.aborted === true || error?.name === "AbortError" || error?.code === "ABORT_ERR";
43
51
  }
44
52
 
53
+ function supportsToolFreeSetupAgent(output) {
54
+ const match = /(\d+)\.(\d+)\.(\d+)/u.exec(String(output ?? ""));
55
+ if (!match) {
56
+ return false;
57
+ }
58
+ const version = match.slice(1).map(Number);
59
+ for (let index = 0; index < version.length; index += 1) {
60
+ if (version[index] !== MINIMUM_TOOL_FREE_AGY_VERSION[index]) {
61
+ return version[index] > MINIMUM_TOOL_FREE_AGY_VERSION[index];
62
+ }
63
+ }
64
+ return true;
65
+ }
66
+
45
67
  /** Extracts model ids from the human-readable `agy models` table. */
46
68
  export function parseAntigravityModelIds(output) {
47
69
  const ids = [];
@@ -77,6 +99,16 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
77
99
  return parseAntigravityModelIds(output);
78
100
  };
79
101
 
102
+ const installToolFreeSetupAgent = async (context) => {
103
+ throwIfAborted(context.signal);
104
+ const version = await execute(command, ["--version"], context);
105
+ if (!supportsToolFreeSetupAgent(version)) {
106
+ throw new Error("Antigravity CLI 1.2.1 or newer is required for tool-free setup checks.");
107
+ }
108
+ await execute(command, ["plugin", "install", TOOL_FREE_SETUP_PLUGIN_ROOT], context);
109
+ throwIfAborted(context.signal);
110
+ };
111
+
80
112
  const detect = async (context) => {
81
113
  try {
82
114
  const modelId = chooseDetectedModel(await listModels(context));
@@ -138,9 +170,9 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
138
170
  detectAvailability: async (context) => {
139
171
  try {
140
172
  throwIfAborted(context.signal);
141
- await execute(command, ["--version"], context);
173
+ const version = await execute(command, ["--version"], context);
142
174
  throwIfAborted(context.signal);
143
- return true;
175
+ return supportsToolFreeSetupAgent(version);
144
176
  } catch (error) {
145
177
  if (isAbortError(error, context.signal)) {
146
178
  throw error;
@@ -156,6 +188,7 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
156
188
  }
157
189
  const modelId = context.modelRef.slice(prefix.length);
158
190
  try {
191
+ await installToolFreeSetupAgent(context);
159
192
  const available = await listModels(context);
160
193
  return available.includes(modelId)
161
194
  ? validatedResult(context.modelRef, context.config)
@@ -169,6 +202,14 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
169
202
  },
170
203
  },
171
204
  run: async (context) => {
205
+ try {
206
+ await installToolFreeSetupAgent(context);
207
+ } catch (error) {
208
+ if (isAbortError(error, context.signal)) {
209
+ throw error;
210
+ }
211
+ throw new Error(RECONNECT_ERROR_MESSAGE);
212
+ }
172
213
  const detected = await detect(context);
173
214
  if (!detected) {
174
215
  throw new Error(RECONNECT_ERROR_MESSAGE);