flowviant 0.38.0 → 0.40.0
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/bin/lib/claude.mjs +33 -2
- package/bin/lib/fleet.mjs +36 -2
- package/bin/lib/live.mjs +609 -22
- package/bin/lib/preflight.mjs +56 -15
- package/bin/lib/runtimes.mjs +473 -28
- package/package.json +2 -2
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -21,6 +21,15 @@
|
|
|
21
21
|
* Claude Code and Codex both do all four. Antigravity does 1, 3 and 4 and is
|
|
22
22
|
* declared below with the exact reason it cannot yet do 2.
|
|
23
23
|
*
|
|
24
|
+
* BUT ONLY A BUILD NEEDS ALL FOUR, and that qualifier was missing long enough to
|
|
25
|
+
* cost Antigravity every capability it has. Requirement 2 is the CONTROL PLANE —
|
|
26
|
+
* claiming, blockers, PRs, completing — and only a build uses it. A wiki turn
|
|
27
|
+
* writes markdown the daemon syncs afterwards; a consult answers a question in
|
|
28
|
+
* prose. Both already run with no MCP on every runtime, Claude included. So
|
|
29
|
+
* drivability is per PROFILE (`canRun`), not one verdict per runtime, and a CLI
|
|
30
|
+
* that cannot be handed a per-invocation MCP config is still a perfectly good
|
|
31
|
+
* cartographer.
|
|
32
|
+
*
|
|
24
33
|
* WHAT THIS MODULE DELIBERATELY DOES NOT DO: pick. Which runtime runs a task is
|
|
25
34
|
* decided in the app, by @mentioning it (CLAUDE.md: the @mention is the only
|
|
26
35
|
* dispatch), and arrives on the brief. Detection here answers "what does this
|
|
@@ -142,11 +151,90 @@ function parseCodexLine(line, cwd) {
|
|
|
142
151
|
activity: { kind: 'error', label: oneLine(ev.error?.message ?? 'turn failed') },
|
|
143
152
|
text: '',
|
|
144
153
|
};
|
|
154
|
+
// A bare `error` event — the shape an auth failure arrives in ("401
|
|
155
|
+
// Unauthorized: Missing bearer…", observed against 0.147.0 with no
|
|
156
|
+
// credentials). It used to fall through to `default` and be dropped, which
|
|
157
|
+
// meant a signed-out Codex produced an EMPTY turn: no sentinel, so the
|
|
158
|
+
// driver nudged twice and reported `stalled`, and the thread said the agent
|
|
159
|
+
// gave up rather than that the CLI is not signed in. The message goes into
|
|
160
|
+
// `text` so it reaches the operator's console AND the usage-limit
|
|
161
|
+
// classifier, which reads exactly this stream.
|
|
162
|
+
case 'error':
|
|
163
|
+
return {
|
|
164
|
+
activity: { kind: 'error', label: oneLine(ev.message ?? 'error') },
|
|
165
|
+
text: `${ev.message ?? ''}\n`,
|
|
166
|
+
};
|
|
145
167
|
default:
|
|
146
168
|
return null; // thread.started / turn.started / item.started / item.updated
|
|
147
169
|
}
|
|
148
170
|
}
|
|
149
171
|
|
|
172
|
+
// ── Antigravity ────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* `agy --output-format stream-json` emits `{event: init|step_update|result}`.
|
|
176
|
+
* Tool names read off a live 1.1.12 session rather than guessed.
|
|
177
|
+
*/
|
|
178
|
+
function humanizeAgyTool(name, p = {}, cwd = '') {
|
|
179
|
+
const path = p.TargetFile ?? p.AbsolutePath ?? p.DirectoryPath ?? p.File ?? '';
|
|
180
|
+
const tail = String(path).split('/').slice(-2).join('/');
|
|
181
|
+
switch (name) {
|
|
182
|
+
case 'view_file':
|
|
183
|
+
case 'read_resource':
|
|
184
|
+
return { kind: 'read', label: `read ${shortPath(path, cwd)}` };
|
|
185
|
+
case 'write_to_file':
|
|
186
|
+
case 'replace_file_content':
|
|
187
|
+
case 'multi_replace_file_content':
|
|
188
|
+
return {
|
|
189
|
+
kind: 'write',
|
|
190
|
+
path: String(path),
|
|
191
|
+
label: `${name === 'write_to_file' ? '+ page' : '~ page'} ${tail || 'file'}`,
|
|
192
|
+
};
|
|
193
|
+
case 'grep_search':
|
|
194
|
+
return { kind: 'search', label: `grep ${oneLine(p.Query ?? p.SearchTerm ?? '', 60)}` };
|
|
195
|
+
case 'find_by_name':
|
|
196
|
+
return { kind: 'glob', label: `find ${oneLine(p.Pattern ?? '', 60)}` };
|
|
197
|
+
case 'list_dir':
|
|
198
|
+
return { kind: 'list', label: `ls ${shortPath(path, cwd)}` };
|
|
199
|
+
case 'run_command':
|
|
200
|
+
return { kind: 'bash', label: `$ ${oneLine(p.CommandLine, 60)}` };
|
|
201
|
+
case 'call_mcp_tool':
|
|
202
|
+
return { kind: 'tool', label: `mcp.${p.ToolName ?? ''}` };
|
|
203
|
+
default:
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseAgyLine(line, cwd) {
|
|
209
|
+
let ev;
|
|
210
|
+
try {
|
|
211
|
+
ev = JSON.parse(line);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (ev.event === 'step_update') {
|
|
216
|
+
const su = ev.step_update ?? {};
|
|
217
|
+
const ti = su.tool_info;
|
|
218
|
+
if (!ti) return null;
|
|
219
|
+
const err = ti.error?.message;
|
|
220
|
+
if (err) return { activity: { kind: 'error', label: oneLine(err) }, text: '' };
|
|
221
|
+
// Each tool is reported twice — once ACTIVE, once DONE — so only the
|
|
222
|
+
// terminal state emits, otherwise every action appears in the thread twice.
|
|
223
|
+
if (su.state && su.state !== 'DONE') return null;
|
|
224
|
+
return { activity: humanizeAgyTool(ti.name, ti.parameters ?? {}, cwd), text: '' };
|
|
225
|
+
}
|
|
226
|
+
if (ev.event === 'result') {
|
|
227
|
+
const r = ev.result ?? {};
|
|
228
|
+
// The final answer is the ONLY sentinel-bearing text: agy has no incremental
|
|
229
|
+
// assistant-message event, so a turn's whole verdict arrives here at once.
|
|
230
|
+
return {
|
|
231
|
+
activity: r.error ? { kind: 'error', label: oneLine(r.error) } : null,
|
|
232
|
+
text: `${r.response ?? ''}${r.error ? `\n${r.error}` : ''}\n`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
150
238
|
// ── The registry ───────────────────────────────────────────────────────────
|
|
151
239
|
|
|
152
240
|
/**
|
|
@@ -209,17 +297,24 @@ export const RUNTIMES = {
|
|
|
209
297
|
* only — it is an Anthropic SDK, not a CLI contract. Everything else runs
|
|
210
298
|
* the subprocess path. */
|
|
211
299
|
live: true,
|
|
300
|
+
/**
|
|
301
|
+
* Every profile, because every profile is DEFINED in its vocabulary: the
|
|
302
|
+
* three `--allowedTools` lists in claude.mjs are what "build", "wiki" and
|
|
303
|
+
* "consult" currently mean. That is a statement about where the contract was
|
|
304
|
+
* written, not a claim that only Claude could ever satisfy it.
|
|
305
|
+
*/
|
|
306
|
+
profiles: ['build', 'wiki', 'consult'],
|
|
212
307
|
mcp: claudeMcp,
|
|
213
308
|
/**
|
|
214
309
|
* Claude takes the operating contract as a real system prompt, which is the
|
|
215
310
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
216
311
|
* above the conversation rather than inside it.
|
|
217
312
|
*/
|
|
218
|
-
args({ prompt, system, model, effort, resume, streamJson, perm, mcp = [] }) {
|
|
313
|
+
args({ prompt, system, model, effort, resume, streamJson, perm, mcp = [], resultSchemaArgs = [] }) {
|
|
219
314
|
const a = [];
|
|
220
315
|
if (resume) a.push('--continue');
|
|
221
316
|
a.push('-p', prompt, '--append-system-prompt', system);
|
|
222
|
-
a.push(...mcp);
|
|
317
|
+
a.push(...mcp, ...resultSchemaArgs);
|
|
223
318
|
a.push('--model', model || MODEL);
|
|
224
319
|
if (effort) a.push('--effort', effort);
|
|
225
320
|
if (streamJson) a.push('--output-format', 'stream-json', '--verbose');
|
|
@@ -237,6 +332,24 @@ export const RUNTIMES = {
|
|
|
237
332
|
install: 'npm i -g @openai/codex',
|
|
238
333
|
login: 'codex login',
|
|
239
334
|
live: false,
|
|
335
|
+
/**
|
|
336
|
+
* BUILD AND CONSULT. See `args()` below for how consult is expressed — the
|
|
337
|
+
* short version is that Codex keeps the promise at the kernel rather than in
|
|
338
|
+
* a verb allowlist, and for a consult's actual threat (exfiltration driven
|
|
339
|
+
* by an injected question) that is sufficient and arguably stronger.
|
|
340
|
+
*
|
|
341
|
+
* ALL THREE, and `wiki` is the one worth pausing on: Codex expresses it
|
|
342
|
+
* MORE strictly than Claude does. A cartographer must read the repo and
|
|
343
|
+
* write only the vault; Claude's WIKI_PERM cannot path-scope Write and says
|
|
344
|
+
* so, leaning on the worktree reset as a backstop. Codex's permission
|
|
345
|
+
* profiles enforce the same rule in the kernel — measured: repo readable,
|
|
346
|
+
* vault writable, repo writes refused, network off.
|
|
347
|
+
*
|
|
348
|
+
* Enforcement was verified on Linux (bubblewrap + seccomp). macOS Seatbelt
|
|
349
|
+
* and Windows are UNTESTED; if this daemon starts running there, re-verify
|
|
350
|
+
* before trusting the consult posture on those platforms.
|
|
351
|
+
*/
|
|
352
|
+
profiles: ['build', 'consult', 'wiki'],
|
|
240
353
|
mcp: codexMcp,
|
|
241
354
|
/**
|
|
242
355
|
* Codex has NO system-prompt flag. The contract therefore rides inside the
|
|
@@ -258,43 +371,182 @@ export const RUNTIMES = {
|
|
|
258
371
|
* placed before it. Appending them after the positional is the kind of argv
|
|
259
372
|
* that parses today and stops parsing on some future clap upgrade.
|
|
260
373
|
*/
|
|
261
|
-
args({ prompt, system, model, effort, resume,
|
|
374
|
+
args({ prompt, system, model, effort, resume, profile = 'build', vaultDir, mcp = [], resultSchemaArgs = [] }) {
|
|
262
375
|
const a = ['exec'];
|
|
263
376
|
if (resume) a.push('resume', '--last');
|
|
264
377
|
a.push('--json');
|
|
265
378
|
if (model) a.push('--model', model);
|
|
266
379
|
// Effort is a config value on Codex rather than a flag.
|
|
267
380
|
if (effort) a.push('-c', `model_reasoning_effort="${effort}"`);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
381
|
+
|
|
382
|
+
if (profile === 'consult') {
|
|
383
|
+
// A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — and it is a
|
|
384
|
+
// different shape from Claude's, which is the whole reason `profile` is
|
|
385
|
+
// a promise rather than a flag list.
|
|
386
|
+
//
|
|
387
|
+
// Claude gets a VERB allowlist: Read, Grep, Glob and a handful of
|
|
388
|
+
// read-only Bash forms, with nothing that reaches the network. Codex has
|
|
389
|
+
// no such thing — it has no file-read tool at all, so reading the repo
|
|
390
|
+
// IS command execution (`cat`, `rg`). Removing the shell leaves the
|
|
391
|
+
// model with exactly ["update_plan","request_user_input"], which cannot
|
|
392
|
+
// answer a question about a codebase. You get both capabilities or
|
|
393
|
+
// neither.
|
|
394
|
+
//
|
|
395
|
+
// So the promise is kept one layer down instead. `read-only` is
|
|
396
|
+
// kernel-enforced (bubblewrap + seccomp on Linux): writes fail, and a
|
|
397
|
+
// direct-IP connect fails with "Operation not permitted" — socket() is
|
|
398
|
+
// denied, not merely DNS. An injected command still RUNS and still
|
|
399
|
+
// cannot take the repository anywhere, which is the threat a consult
|
|
400
|
+
// actually has: its prompt is steered by a question any project editor
|
|
401
|
+
// can type. Arguably a stronger guarantee than the allowlist, being
|
|
402
|
+
// below the agent rather than inside it.
|
|
403
|
+
a.push('--sandbox', 'read-only');
|
|
404
|
+
|
|
405
|
+
// THE HOLE THE SANDBOX DOES NOT COVER. `web_search` ships in `codex
|
|
406
|
+
// exec`'s default tool list even without --search, and it executes
|
|
407
|
+
// SERVER-SIDE at OpenAI — no local sandbox touches it. An injected turn
|
|
408
|
+
// could pack repo contents into a query and egress them straight past
|
|
409
|
+
// everything above. Both spellings, because the two config systems
|
|
410
|
+
// disagree about which one is live.
|
|
411
|
+
a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
|
|
412
|
+
|
|
413
|
+
// Sub-agents would be a second turn whose posture nobody here chose.
|
|
414
|
+
a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false');
|
|
415
|
+
|
|
416
|
+
// HERMETIC. Without these a user's ~/.codex/config.toml, a project
|
|
417
|
+
// `.rules` execpolicy file, or their own MCP servers can widen a posture
|
|
418
|
+
// we are asserting on their behalf — silently, and on the one turn whose
|
|
419
|
+
// prompt comes from someone else's typing.
|
|
420
|
+
a.push('--ignore-user-config', '--ignore-rules');
|
|
421
|
+
} else if (profile === 'wiki' && vaultDir) {
|
|
422
|
+
// THE CARTOGRAPHER, AND THIS ONE IS STRICTER THAN CLAUDE'S.
|
|
423
|
+
//
|
|
424
|
+
// A wiki turn reads the whole repo and writes ONLY the vault. Claude
|
|
425
|
+
// cannot actually express that: WIKI_PERM hands it Write/Edit and its own
|
|
426
|
+
// comment admits "Write/Edit can't be path-scoped here; the worktree
|
|
427
|
+
// reset is the backstop" — i.e. the cartographer CAN scribble on the
|
|
428
|
+
// checkout and we clean up afterwards.
|
|
429
|
+
//
|
|
430
|
+
// Codex's permission profiles take a per-path filesystem map, so the
|
|
431
|
+
// rule is enforced by the kernel instead of apologised for. Measured:
|
|
432
|
+
// repo readable, vault writable, repo writes fail "Read-only file
|
|
433
|
+
// system", and curl returns 000 — the network is off, which is the half
|
|
434
|
+
// WIKI_PERM was really protecting (its comment: "Command execution is
|
|
435
|
+
// the line: it enables network exfil").
|
|
436
|
+
//
|
|
437
|
+
// The table is set WHOLE rather than as a dotted key, and that is not
|
|
438
|
+
// style: `-c permissions.wiki.filesystem."<path>"="write"` splits the
|
|
439
|
+
// dotted path on the dots INSIDE the path, and every real vault lives
|
|
440
|
+
// under `~/.flowviant/vaults/…`. It fails with "filesystem path must be
|
|
441
|
+
// absolute", which reads like a path problem and is a parsing one.
|
|
442
|
+
a.push('-c', 'permissions.flowviantwiki.extends=":read-only"');
|
|
443
|
+
a.push('-c', `permissions.flowviantwiki.filesystem={"${vaultDir}"="write"}`);
|
|
444
|
+
a.push('-P', 'flowviantwiki');
|
|
445
|
+
a.push('--ignore-user-config', '--ignore-rules');
|
|
446
|
+
} else {
|
|
447
|
+
// The daemon's build posture, mapped: SAFE keeps writes inside the
|
|
448
|
+
// workspace, the default lets the agent run its own tests and git
|
|
449
|
+
// commands. Neither asks a human — there is no human on this end of the
|
|
450
|
+
// pipe. Deliberately NOT hermetic: a build is work the user asked for by
|
|
451
|
+
// @mentioning this CLI, and their own config is theirs to apply.
|
|
452
|
+
a.push('--sandbox', SAFE ? 'workspace-write' : 'danger-full-access');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
a.push(...mcp, ...resultSchemaArgs);
|
|
273
456
|
a.push(`${system}\n\n---\n\n${prompt}`);
|
|
274
457
|
return a;
|
|
275
458
|
},
|
|
276
459
|
parse: parseCodexLine,
|
|
460
|
+
/** `codex exec --output-schema <file>` constrains the FINAL message to a
|
|
461
|
+
* JSON Schema. Only needed on the mediated path; the direct path reports
|
|
462
|
+
* through MCP tool calls, which are already structured. */
|
|
463
|
+
resultSchema: (path) => ['--output-schema', path],
|
|
277
464
|
},
|
|
278
465
|
|
|
279
466
|
/**
|
|
280
|
-
* DECLARED, NOT DRIVABLE — and
|
|
467
|
+
* DECLARED, NOT DRIVABLE — and this is now MEASURED rather than argued.
|
|
468
|
+
*
|
|
469
|
+
* The reason went wrong twice before it went right, so the evidence is written
|
|
470
|
+
* down here in full. First it cited upstream antigravity-cli#60, which is about
|
|
471
|
+
* `.antigravitycli/mcp_config.json` — the project-DISCOVERY folder — while
|
|
472
|
+
* claiming it was about `.agents/`, the workspace-CUSTOMIZATION folder. Then,
|
|
473
|
+
* on reading the docs (antigravity.google/docs/mcp describes `.agents/`, and
|
|
474
|
+
* Google's own codelab creates it), this comment swung the other way and said
|
|
475
|
+
* the blocker looked wrong. Both were reasoning from paperwork.
|
|
476
|
+
*
|
|
477
|
+
* THE TEST, run against agy 1.1.12, signed in, in a real git repo:
|
|
478
|
+
* the SAME minimal stdio MCP server, declared two ways.
|
|
479
|
+
* • workspace `<worktree>/.agents/mcp_config.json` → the server process is
|
|
480
|
+
* NEVER SPAWNED. Not connected-and-failed: never launched. The model
|
|
481
|
+
* answers "NO_MCP".
|
|
482
|
+
* • global `~/.gemini/config/mcp_config.json` → spawns immediately and
|
|
483
|
+
* handshakes: server/discover, initialize, notifications/initialized,
|
|
484
|
+
* tools/list.
|
|
485
|
+
* stdio deliberately, to remove every confound — no bearer token to reject,
|
|
486
|
+
* no network, no TLS. The difference is the config LOCATION and nothing else.
|
|
487
|
+
*
|
|
488
|
+
* AND IT SURVIVES THE TRUST VARIABLE, which was the obvious objection: agy
|
|
489
|
+
* keeps a `trustedWorkspaces` list in settings.json, a fresh per-task worktree
|
|
490
|
+
* is not on it, and an untrusted folder demonstrably changes behaviour (the
|
|
491
|
+
* model stops treating cwd as its workspace and works out of its own scratch
|
|
492
|
+
* dir). Adding the worktree to `trustedWorkspaces` and re-running changed
|
|
493
|
+
* nothing: the server still never spawned, the model still answered NO_MCP.
|
|
494
|
+
* So the workspace config is not trust-gated, it is simply not read.
|
|
495
|
+
*
|
|
496
|
+
* So Antigravity's MCP config is machine-wide IN PRACTICE, and the original
|
|
497
|
+
* blocker's conclusion stands even though its cited reason never did: every
|
|
498
|
+
* lane on the box would share one worker token, which is exactly the blast
|
|
499
|
+
* radius the per-lane token exists to prevent. That is why this stays
|
|
500
|
+
* undrivable, and it is a property of the CLI rather than something we can
|
|
501
|
+
* work around from here.
|
|
502
|
+
*
|
|
503
|
+
* A TRAP FOR WHOEVER RE-TESTS THIS: agy exposes MCP through a single generic
|
|
504
|
+
* `call_mcp_tool` dispatcher, so per-server tools NEVER appear in the `init`
|
|
505
|
+
* event's tools array even when a server is loaded correctly. Reading that
|
|
506
|
+
* array tells you nothing. Watch the server process instead, or ask the model.
|
|
507
|
+
*
|
|
508
|
+
* FOUR MORE FACTS from the same session, each an independent obstacle:
|
|
509
|
+
* 1. AUTH IS INTERACTIVE OAUTH (bubbletea TUI; needs a real /dev/tty), with
|
|
510
|
+
* no headless credential path. A signed-out `agy -p` prints a consent URL
|
|
511
|
+
* and then SITS until `--print-timeout` (default 5m) before erroring — so
|
|
512
|
+
* a lane on a signed-out agy burns five minutes per turn looking like a
|
|
513
|
+
* hang. Preflight cannot see it: `--version` succeeds while signed out.
|
|
514
|
+
* 2. `--sandbox` IS A BOOLEAN ("terminal restrictions enabled"), not a mode
|
|
515
|
+
* selector — but a CONSULT POSTURE IS STILL EXPRESSIBLE, and this was
|
|
516
|
+
* recorded backwards here for a while. It does not come from `--sandbox`
|
|
517
|
+
* at all; it comes from headless mode's default. Any tool needing a
|
|
518
|
+
* permission that cannot be prompted for is AUTO-DENIED:
|
|
519
|
+
* "User denied permission to run command: <cmd>"
|
|
520
|
+
* "a tool required the 'command' permission that headless mode cannot
|
|
521
|
+
* prompt for, so it was auto-denied."
|
|
522
|
+
* Observed repeatedly, including for an entirely benign `pwd && ls -la`,
|
|
523
|
+
* and a local listener confirmed no egress in any run. Reads (list_dir,
|
|
524
|
+
* file reads) work throughout. So: NOT passing
|
|
525
|
+
* `--dangerously-skip-permissions` IS the consult posture, and passing it
|
|
526
|
+
* is the build posture — both per invocation, both harness-enforced
|
|
527
|
+
* rather than model-instructed. `--mode plan` is a separate, WEAKER thing:
|
|
528
|
+
* it steers behaviour and blocks workspace writes, but it is not what
|
|
529
|
+
* stops command execution.
|
|
530
|
+
* RESIDUAL RISK, and it has no fix from here: `permissions.allow` in the
|
|
531
|
+
* machine-wide settings.json is inherited, so a user who has allowed
|
|
532
|
+
* `command(...)` widens every consult on that box. Codex has
|
|
533
|
+
* `--ignore-user-config` for exactly this; agy has no equivalent.
|
|
534
|
+
* 3. No `mcp` subcommand; `/mcp` is interactive-only.
|
|
535
|
+
* 4. It attempts to INSTALL PLAYWRIGHT at startup (observed failing 404
|
|
536
|
+
* against playwright.azureedge.net). A daemon runtime that downloads and
|
|
537
|
+
* runs a browser driver is worth knowing before it goes on a machine the
|
|
538
|
+
* project leaves running.
|
|
281
539
|
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* the
|
|
286
|
-
* workspace-local `.agents/mcp_config.json` is read-but-ignored (upstream
|
|
287
|
-
* antigravity-cli#60), and the HOME-level file cannot be made per-lane —
|
|
288
|
-
* pointing HOME elsewhere would take the cached credentials the headless mode
|
|
289
|
-
* signs in with along with it.
|
|
540
|
+
* There is no npm package. `antigravity-cli` (0.0.1, "placeholder") and `agy`
|
|
541
|
+
* (0.0.0, empty) on npm are SQUATS by unrelated accounts; the real channel is
|
|
542
|
+
* the install script at antigravity.google, which is why `install` below says
|
|
543
|
+
* to see the docs rather than naming an npm command.
|
|
290
544
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* or a flag appears, this becomes an `mcp` function and a `parse`, and nothing
|
|
297
|
-
* else in the daemon changes.
|
|
545
|
+
* WHAT WOULD UNBLOCK IT: a per-invocation MCP flag or env var, or workspace
|
|
546
|
+
* configs actually being honoured. Re-test with the two-location stdio probe
|
|
547
|
+
* above — it is five minutes and it answers the question outright. Pin any
|
|
548
|
+
* wiring to >= 1.1.10 (`--model`/`--effort` were ignored in headless before it;
|
|
549
|
+
* `--output-format` arrived in 1.1.8).
|
|
298
550
|
*/
|
|
299
551
|
antigravity: {
|
|
300
552
|
id: 'antigravity',
|
|
@@ -304,10 +556,97 @@ export const RUNTIMES = {
|
|
|
304
556
|
install: 'see antigravity.google/docs/cli',
|
|
305
557
|
login: 'agy',
|
|
306
558
|
live: false,
|
|
559
|
+
/** None, because it cannot reach the MCP server at all — see `blocked`. */
|
|
560
|
+
/**
|
|
561
|
+
* WIKI AND CONSULT, BUT NOT BUILD — and the split is the whole point of
|
|
562
|
+
* making drivability per-profile rather than one verdict.
|
|
563
|
+
*
|
|
564
|
+
* Only a BUILD needs the MCP control plane: claiming, reporting a blocker,
|
|
565
|
+
* attaching a PR, completing. A wiki turn writes markdown files that the
|
|
566
|
+
* daemon syncs afterwards, and a consult answers a question in prose; BOTH
|
|
567
|
+
* already run with no MCP at all on every runtime, Claude included. Gating
|
|
568
|
+
* them on an MCP capability they never use was a test of the wrong thing,
|
|
569
|
+
* and it is what kept Antigravity at zero for months.
|
|
570
|
+
*
|
|
571
|
+
* Build returns here when the mediated adapter lands (the daemon holds the
|
|
572
|
+
* MCP connection and the CLI just returns schema-enforced JSON via
|
|
573
|
+
* `--json-schema`), which needs no per-invocation MCP config from the vendor
|
|
574
|
+
* at all.
|
|
575
|
+
*/
|
|
576
|
+
profiles: ['build', 'wiki', 'consult'],
|
|
307
577
|
mcp: null,
|
|
308
|
-
args
|
|
309
|
-
|
|
310
|
-
|
|
578
|
+
args({ prompt, system, model, effort, resume, profile = 'build', vaultDir, resultSchemaArgs = [] }) {
|
|
579
|
+
const a = [];
|
|
580
|
+
if (resume) a.push('--continue');
|
|
581
|
+
// No system-prompt flag, same weakening as Codex: the contract rides in
|
|
582
|
+
// the prompt, fenced and first.
|
|
583
|
+
a.push('-p', `${system}\n\n---\n\n${prompt}`);
|
|
584
|
+
a.push('--output-format', 'stream-json');
|
|
585
|
+
if (model) a.push('--model', model);
|
|
586
|
+
if (effort) a.push('--effort', effort);
|
|
587
|
+
a.push(...resultSchemaArgs);
|
|
588
|
+
|
|
589
|
+
if (profile === 'wiki') {
|
|
590
|
+
// THE CARTOGRAPHER, at the same bar WIKI_PERM sets for Claude — writes
|
|
591
|
+
// allowed, network shut. Both halves measured on 1.1.12:
|
|
592
|
+
// • headless cannot prompt, so anything needing approval is
|
|
593
|
+
// auto-denied — including the file writes a wiki turn exists to
|
|
594
|
+
// make. `--dangerously-skip-permissions` is what grants them.
|
|
595
|
+
// • `--sandbox` blocks egress: with it, curl returned 400 and a local
|
|
596
|
+
// listener saw nothing; without it, 200 and the request arrived.
|
|
597
|
+
// Upstream #36 warns these two cancel out (skip-permissions
|
|
598
|
+
// auto-approving the sandbox-bypass prompt). It does NOT reproduce on
|
|
599
|
+
// 1.1.12 — tested together, the write landed and the network stayed
|
|
600
|
+
// shut. Re-check on upgrade: if it ever does cancel, this posture
|
|
601
|
+
// silently becomes "wiki turn with internet", which is the one line
|
|
602
|
+
// WIKI_PERM draws ("Command execution is the line: it enables network
|
|
603
|
+
// exfil").
|
|
604
|
+
if (vaultDir) a.push('--add-dir', vaultDir);
|
|
605
|
+
a.push('--sandbox', '--dangerously-skip-permissions');
|
|
606
|
+
// A sweep reads a whole repository; the 5m default would guillotine it.
|
|
607
|
+
a.push('--print-timeout', '60m');
|
|
608
|
+
} else if (profile === 'build') {
|
|
609
|
+
// A BUILD WRITES, and headless auto-denies anything needing approval —
|
|
610
|
+
// so without this every edit comes back "User denied permission" and the
|
|
611
|
+
// turn reports failure having touched nothing. Caught only because an
|
|
612
|
+
// end-to-end test had to add the flag by hand to work.
|
|
613
|
+
//
|
|
614
|
+
// NOT `--sandbox` here, unlike wiki: a build has to `git push` and run
|
|
615
|
+
// `gh pr create`, so it needs the network by definition. The containment
|
|
616
|
+
// is the worktree, as it is for every runtime.
|
|
617
|
+
//
|
|
618
|
+
// FLOWVIANT_SAFE HAS NO EXPRESSION ON THIS RUNTIME. Claude narrows to an
|
|
619
|
+
// allowlist and Codex to `workspace-write`; agy's only per-invocation
|
|
620
|
+
// control is this boolean, and its allow/deny engine is machine-wide.
|
|
621
|
+
// So a SAFE-mode operator gets an agy build that is not actually
|
|
622
|
+
// narrowed — which is why `mediatedSafeGap` is surfaced rather than
|
|
623
|
+
// quietly ignored.
|
|
624
|
+
a.push('--dangerously-skip-permissions');
|
|
625
|
+
a.push('--print-timeout', '60m');
|
|
626
|
+
} else if (profile === 'consult') {
|
|
627
|
+
// NOTHING GRANTED, deliberately. The headless default IS the consult
|
|
628
|
+
// posture: `run_command` comes back "User denied permission… headless
|
|
629
|
+
// mode cannot prompt for it, so it was auto-denied", observed even for a
|
|
630
|
+
// benign `pwd && ls -la`, with no egress in any run. Reads keep working,
|
|
631
|
+
// which is all a consult needs. Passing --dangerously-skip-permissions
|
|
632
|
+
// here would hand a question ANY project editor can type a shell.
|
|
633
|
+
a.push('--print-timeout', '10m');
|
|
634
|
+
}
|
|
635
|
+
return a;
|
|
636
|
+
},
|
|
637
|
+
parse: parseAgyLine,
|
|
638
|
+
/**
|
|
639
|
+
* `--json-schema` enforces the shape of the final result — verified against
|
|
640
|
+
* 1.1.12, which returned exactly the object asked for. This is what makes a
|
|
641
|
+
* BUILD possible on a runtime that cannot be handed an MCP config: the CLI
|
|
642
|
+
* stops needing to CALL anything and just returns a filled-in form, and the
|
|
643
|
+
* daemon makes the control-plane calls on its behalf with the lane's own
|
|
644
|
+
* per-lane token. Strictly better than parsing markers out of prose, which a
|
|
645
|
+
* model can wrap in a code fence, truncate or hallucinate.
|
|
646
|
+
*/
|
|
647
|
+
resultSchema: (path) => ['--json-schema', path],
|
|
648
|
+
blocked:
|
|
649
|
+
'its MCP config is machine-wide — a workspace .agents/mcp_config.json is never loaded (measured), so every lane would share one token',
|
|
311
650
|
},
|
|
312
651
|
};
|
|
313
652
|
|
|
@@ -316,6 +655,109 @@ export const DISPATCHABLE = Object.values(RUNTIMES).filter((r) => r.mcp && r.arg
|
|
|
316
655
|
|
|
317
656
|
export const runtimeById = (id) => RUNTIMES[id] ?? RUNTIMES.claude;
|
|
318
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Can the worker this daemon is running actually DRIVE this runtime right now?
|
|
660
|
+
*
|
|
661
|
+
* `dispatchable` on a detection row answers "is the CLI installed"; this answers
|
|
662
|
+
* "and can this process build with it", which is a different question and was
|
|
663
|
+
* briefly a narrower one.
|
|
664
|
+
*
|
|
665
|
+
* THE HISTORY MATTERS, because the answer moved twice. Live mode became the
|
|
666
|
+
* default and does not spawn a CLI at all — it drives the Anthropic Agent SDK
|
|
667
|
+
* in-process — so for one release this returned false for every non-live runtime
|
|
668
|
+
* under LIVE. That was honest rather than correct: a machine with Codex reported
|
|
669
|
+
* it could not drive Codex, which was true of the worker as it then existed, and
|
|
670
|
+
* `@codex` tasks visibly waited instead of being silently built by Claude.
|
|
671
|
+
*
|
|
672
|
+
* 0.40.0 made it wrong by making it unnecessary. `driveSubprocess` (live.mjs)
|
|
673
|
+
* gives live mode a subprocess path for non-live runtimes, sharing the same
|
|
674
|
+
* worktree prep, patch landing, checkpointing and teardown as the session path.
|
|
675
|
+
* So the restriction is gone and this is back to "installed, and this module
|
|
676
|
+
* knows how to spawn it" — which is what the registry's `live` flag always
|
|
677
|
+
* described: not which runtimes can run, but which get a session instead of a
|
|
678
|
+
* subprocess.
|
|
679
|
+
*
|
|
680
|
+
* LIVE is no longer read here. That is deliberate and load-bearing: this
|
|
681
|
+
* predicate feeds both the roster report AND the claim, and if the two ever
|
|
682
|
+
* disagree the daemon either claims work it cannot build or refuses work it can.
|
|
683
|
+
*/
|
|
684
|
+
/**
|
|
685
|
+
* WHICH PROFILES NEED THE MCP CONTROL PLANE. Only one does.
|
|
686
|
+
*
|
|
687
|
+
* A BUILD has to claim work, report a blocker, attach a PR and complete — that
|
|
688
|
+
* is the control plane, and a runtime that cannot reach it cannot participate.
|
|
689
|
+
* A WIKI turn writes markdown the daemon syncs afterwards. A CONSULT answers a
|
|
690
|
+
* question in prose. Neither passes an MCP config on ANY runtime today, Claude
|
|
691
|
+
* included — check the two call sites in fleet.mjs, they hand `runTurn` no
|
|
692
|
+
* `mcpArgs` at all.
|
|
693
|
+
*
|
|
694
|
+
* Conflating them cost Antigravity every capability it has: `mcp && args` was
|
|
695
|
+
* the single drivability test, so a machine-wide MCP config disqualified it from
|
|
696
|
+
* two jobs that never open an MCP connection.
|
|
697
|
+
*/
|
|
698
|
+
const PROFILE_NEEDS_MCP = { build: true, wiki: false, consult: false };
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* A build needs the control plane, but NOT necessarily an MCP config of its own.
|
|
702
|
+
* A runtime that can return schema-enforced output is driven MEDIATED: the
|
|
703
|
+
* daemon holds the MCP connection with the lane's own token and makes the calls,
|
|
704
|
+
* and the CLI just returns a filled-in form. So "can build" is "can reach the
|
|
705
|
+
* control plane, by either route".
|
|
706
|
+
*/
|
|
707
|
+
export const mediated = (rt) => Boolean(rt && !rt.mcp && rt.resultSchema);
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* A mediated runtime whose only permission control is all-or-nothing, so
|
|
711
|
+
* FLOWVIANT_SAFE cannot narrow its BUILD. Surfaced rather than swallowed: an
|
|
712
|
+
* operator who set SAFE asked for something we cannot give them here, and
|
|
713
|
+
* silently running unnarrowed would be the daemon deciding that on their behalf.
|
|
714
|
+
*/
|
|
715
|
+
export const mediatedSafeGap = (rt) => SAFE && mediated(rt);
|
|
716
|
+
|
|
717
|
+
/** Can this runtime do this job on this machine? */
|
|
718
|
+
export const canRun = (rt, profile) =>
|
|
719
|
+
Boolean(rt?.args) &&
|
|
720
|
+
(rt.profiles ?? []).includes(profile) &&
|
|
721
|
+
(!PROFILE_NEEDS_MCP[profile] || Boolean(rt.mcp) || mediated(rt));
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Reported on the roster poll and sent on every claim, so it answers the
|
|
725
|
+
* DISPATCH question specifically: can an @mention of this runtime result in a
|
|
726
|
+
* task being built? That is `build`, and build is the profile that needs MCP —
|
|
727
|
+
* which is why a runtime can be undispatchable and still run wiki and consult.
|
|
728
|
+
*/
|
|
729
|
+
export const drivableHere = (rt) => canRun(rt, 'build');
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* WHICH RUNTIME RUNS A JOB THAT NOBODY @MENTIONED.
|
|
733
|
+
*
|
|
734
|
+
* Building a task has an author: you @mentioned a CLI, and that is the only
|
|
735
|
+
* dispatch in this product. The other turns have none — the wiki sweep, the
|
|
736
|
+
* re-ground, the plan check, the quick edit and the consult are all started by
|
|
737
|
+
* the daemon or the server, and until now every one of them took `runTurn`'s
|
|
738
|
+
* default parameter value and ran Claude. That was not a decision; it was five
|
|
739
|
+
* call sites omitting an argument, and it only looked correct while Claude was
|
|
740
|
+
* the only runtime and preflight refused to start without it.
|
|
741
|
+
*
|
|
742
|
+
* A PROFILE is a promise about what is IMPOSSIBLE during the turn, not a flag
|
|
743
|
+
* list — flag lists are per-vendor, promises are not, and the goal is that every
|
|
744
|
+
* runtime behaves the same way predictably. A runtime declares the profiles it
|
|
745
|
+
* can actually express; one that cannot express a profile does not get that job,
|
|
746
|
+
* rather than getting it with weaker guarantees nobody wrote down.
|
|
747
|
+
*
|
|
748
|
+
* Claude first when it qualifies — not favouritism, and worth saying plainly:
|
|
749
|
+
* these prompts were written and tuned against it, so it is the known-good
|
|
750
|
+
* answer and anything else is a substitution. When it is absent, any runtime
|
|
751
|
+
* that can express the profile runs the job, which is the whole point.
|
|
752
|
+
*/
|
|
753
|
+
export function pickRuntimeFor(profile, { detected } = {}) {
|
|
754
|
+
const rows = detected ?? detectRuntimes();
|
|
755
|
+
const ok = (id) =>
|
|
756
|
+
canRun(RUNTIMES[id], profile) && Boolean(rows.find((d) => d.id === id)?.installed);
|
|
757
|
+
if (ok('claude')) return 'claude';
|
|
758
|
+
return Object.keys(RUNTIMES).find(ok) ?? null;
|
|
759
|
+
}
|
|
760
|
+
|
|
319
761
|
// ── Detection ──────────────────────────────────────────────────────────────
|
|
320
762
|
|
|
321
763
|
/**
|
|
@@ -355,7 +797,10 @@ export function detectRuntimes({ refresh = false } = {}) {
|
|
|
355
797
|
version,
|
|
356
798
|
// Installed and drivable are different questions, and conflating them is
|
|
357
799
|
// how a user ends up @mentioning something that silently never starts.
|
|
358
|
-
|
|
800
|
+
// THREE questions, in fact — see `drivableHere`: the CLI can be installed,
|
|
801
|
+
// and this module can know how to spawn it, and the worker this daemon is
|
|
802
|
+
// running can still be unable to drive it.
|
|
803
|
+
dispatchable: version !== null && drivableHere(rt),
|
|
359
804
|
blocked: rt.blocked ?? null,
|
|
360
805
|
};
|
|
361
806
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Run your own coding CLIs as headless build agents for Flowviant
|
|
3
|
+
"version": "0.40.0",
|
|
4
|
+
"description": "Run your own coding CLIs as headless build agents for Flowviant — Claude Code or Codex, on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"flowviant": "bin/cli.mjs"
|