@bivy/bivy 0.6.0 → 0.7.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/README.md +15 -6
- package/bin/agent-manifest.json +38 -0
- package/bin/bivy.mjs +99 -13
- package/bin/patch-pi-dependencies.mjs +22 -16
- package/dist/automation-checks.js +68 -0
- package/dist/bivy-login.js +13 -0
- package/dist/control-plane-tasks.js +39 -8
- package/dist/diagnostics.js +75 -0
- package/dist/github-tasks.js +27 -7
- package/dist/guard.js +51 -8
- package/dist/harness/egress.js +64 -1
- package/dist/harness/net-proxy.js +28 -0
- package/dist/repo-workspace.js +19 -0
- package/dist/runtime/anthropic-preflight.js +41 -0
- package/dist/runtime/codex-sessions.js +10 -1
- package/dist/runtime/credential-store.js +35 -6
- package/dist/runtime/index.js +83 -3
- package/dist/runtime/oauth/model-oauth.js +5 -4
- package/dist/runtime/process.js +33 -9
- package/dist/runtime/protocol.js +49 -1
- package/dist/runtime/slash-commands.js +246 -0
- package/dist/server.js +587 -93
- package/dist/session/attachment-store.js +99 -11
- package/dist/session/event-log.js +75 -11
- package/dist/session/fork-dirty.js +41 -3
- package/dist/session/revert-file.js +44 -0
- package/dist/session/turn-watchdog.js +19 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -199,24 +199,33 @@ Every environment variable, config file, and precedence rule:
|
|
|
199
199
|
## Approvals and sandboxing
|
|
200
200
|
|
|
201
201
|
The default approval mode is **`autonomous`**: agents act without per-action
|
|
202
|
-
prompts.
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
prompts. The actual protection depends on the selected runtime. Native-sandbox
|
|
203
|
+
agents enforce the chosen access tier; structured runtimes also pass tool calls
|
|
204
|
+
through Bivy's policy and approval layer. Process agents that Bivy cannot
|
|
205
|
+
intercept run with your OS user permissions. The picker shows this distinction
|
|
206
|
+
and requires confirmation before selecting that limited path.
|
|
207
|
+
|
|
208
|
+
Where Bivy receives structured shell/file calls, a heuristic floor blocks known
|
|
209
|
+
catastrophic commands and structured writes outside the workspace, and a
|
|
210
|
+
backstop set (force-push, publish, deploy, sudo) pauses for a human. This catches
|
|
211
|
+
accidents; it is not an adversarial isolation boundary.
|
|
205
212
|
|
|
206
213
|
If you want to be asked about more, set the mode explicitly:
|
|
207
214
|
|
|
208
215
|
```bash
|
|
209
216
|
BIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits
|
|
210
217
|
BIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits
|
|
211
|
-
BIVY_APPROVAL_MODE=never # no prompts
|
|
218
|
+
BIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available
|
|
212
219
|
```
|
|
213
220
|
|
|
214
221
|
Approve from the terminal, browser, or phone.
|
|
215
222
|
|
|
216
223
|
Sandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced
|
|
217
224
|
natively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.
|
|
218
|
-
Agents without a native sandbox
|
|
219
|
-
|
|
225
|
+
Agents without a native sandbox may expose structured tool or MCP controls, but
|
|
226
|
+
those controls do not cover activity the agent performs outside those channels;
|
|
227
|
+
some process adapters run entirely with your user permissions. Check the
|
|
228
|
+
picker's Protection label. **Bivy does not currently ship its own OS-level jail.**
|
|
220
229
|
|
|
221
230
|
## Credentials
|
|
222
231
|
|
package/bin/agent-manifest.json
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
"label": "Codex",
|
|
6
6
|
"command": "codex",
|
|
7
7
|
"hidden": true,
|
|
8
|
+
"supportTier": "beta",
|
|
9
|
+
"certification": "adapter-tested",
|
|
8
10
|
"headlessFlags": [
|
|
9
11
|
"exec"
|
|
10
12
|
],
|
|
@@ -18,6 +20,8 @@
|
|
|
18
20
|
"label": "OpenCode",
|
|
19
21
|
"command": "opencode",
|
|
20
22
|
"hidden": false,
|
|
23
|
+
"supportTier": "beta",
|
|
24
|
+
"certification": "adapter-tested",
|
|
21
25
|
"headlessFlags": [
|
|
22
26
|
"run",
|
|
23
27
|
"-s"
|
|
@@ -32,6 +36,8 @@
|
|
|
32
36
|
"label": "Aider",
|
|
33
37
|
"command": "aider",
|
|
34
38
|
"hidden": false,
|
|
39
|
+
"supportTier": "beta",
|
|
40
|
+
"certification": "adapter-tested",
|
|
35
41
|
"headlessFlags": [
|
|
36
42
|
"--yes-always",
|
|
37
43
|
"--message"
|
|
@@ -46,6 +52,8 @@
|
|
|
46
52
|
"label": "Hermes",
|
|
47
53
|
"command": "hermes",
|
|
48
54
|
"hidden": true,
|
|
55
|
+
"supportTier": "beta",
|
|
56
|
+
"certification": "adapter-tested",
|
|
49
57
|
"headlessFlags": [],
|
|
50
58
|
"install": {
|
|
51
59
|
"kind": "npm",
|
|
@@ -57,6 +65,8 @@
|
|
|
57
65
|
"label": "Goose",
|
|
58
66
|
"command": "goose",
|
|
59
67
|
"hidden": false,
|
|
68
|
+
"supportTier": "beta",
|
|
69
|
+
"certification": "adapter-tested",
|
|
60
70
|
"headlessFlags": [
|
|
61
71
|
"run",
|
|
62
72
|
"-t",
|
|
@@ -76,6 +86,8 @@
|
|
|
76
86
|
"label": "Gemini CLI",
|
|
77
87
|
"command": "gemini",
|
|
78
88
|
"hidden": false,
|
|
89
|
+
"supportTier": "beta",
|
|
90
|
+
"certification": "adapter-tested",
|
|
79
91
|
"headlessFlags": [
|
|
80
92
|
"-p",
|
|
81
93
|
"-o",
|
|
@@ -92,6 +104,8 @@
|
|
|
92
104
|
"label": "Qwen Code",
|
|
93
105
|
"command": "qwen",
|
|
94
106
|
"hidden": false,
|
|
107
|
+
"supportTier": "beta",
|
|
108
|
+
"certification": "adapter-tested",
|
|
95
109
|
"headlessFlags": [
|
|
96
110
|
"-p",
|
|
97
111
|
"--output-format",
|
|
@@ -108,6 +122,8 @@
|
|
|
108
122
|
"label": "Cline",
|
|
109
123
|
"command": "cline",
|
|
110
124
|
"hidden": false,
|
|
125
|
+
"supportTier": "beta",
|
|
126
|
+
"certification": "adapter-tested",
|
|
111
127
|
"headlessFlags": [
|
|
112
128
|
"-y",
|
|
113
129
|
"--id"
|
|
@@ -122,6 +138,8 @@
|
|
|
122
138
|
"label": "Crush",
|
|
123
139
|
"command": "crush",
|
|
124
140
|
"hidden": false,
|
|
141
|
+
"supportTier": "beta",
|
|
142
|
+
"certification": "adapter-tested",
|
|
125
143
|
"headlessFlags": [
|
|
126
144
|
"run",
|
|
127
145
|
"-q"
|
|
@@ -136,6 +154,8 @@
|
|
|
136
154
|
"label": "Cursor",
|
|
137
155
|
"command": "cursor-agent",
|
|
138
156
|
"hidden": false,
|
|
157
|
+
"supportTier": "beta",
|
|
158
|
+
"certification": "adapter-tested",
|
|
139
159
|
"headlessFlags": [
|
|
140
160
|
"--force",
|
|
141
161
|
"-p"
|
|
@@ -151,6 +171,8 @@
|
|
|
151
171
|
"label": "GitHub Copilot",
|
|
152
172
|
"command": "copilot",
|
|
153
173
|
"hidden": false,
|
|
174
|
+
"supportTier": "beta",
|
|
175
|
+
"certification": "adapter-tested",
|
|
154
176
|
"headlessFlags": [
|
|
155
177
|
"--allow-all-tools",
|
|
156
178
|
"-p"
|
|
@@ -165,6 +187,8 @@
|
|
|
165
187
|
"label": "Grok",
|
|
166
188
|
"command": "grok",
|
|
167
189
|
"hidden": false,
|
|
190
|
+
"supportTier": "beta",
|
|
191
|
+
"certification": "adapter-tested",
|
|
168
192
|
"headlessFlags": [
|
|
169
193
|
"-p"
|
|
170
194
|
],
|
|
@@ -178,6 +202,8 @@
|
|
|
178
202
|
"label": "Amp",
|
|
179
203
|
"command": "amp",
|
|
180
204
|
"hidden": false,
|
|
205
|
+
"supportTier": "beta",
|
|
206
|
+
"certification": "adapter-tested",
|
|
181
207
|
"headlessFlags": [
|
|
182
208
|
"-x",
|
|
183
209
|
"threads",
|
|
@@ -193,6 +219,8 @@
|
|
|
193
219
|
"label": "Auggie",
|
|
194
220
|
"command": "auggie",
|
|
195
221
|
"hidden": false,
|
|
222
|
+
"supportTier": "beta",
|
|
223
|
+
"certification": "adapter-tested",
|
|
196
224
|
"headlessFlags": [
|
|
197
225
|
"--quiet",
|
|
198
226
|
"--print"
|
|
@@ -207,6 +235,8 @@
|
|
|
207
235
|
"label": "Droid",
|
|
208
236
|
"command": "droid",
|
|
209
237
|
"hidden": false,
|
|
238
|
+
"supportTier": "beta",
|
|
239
|
+
"certification": "adapter-tested",
|
|
210
240
|
"headlessFlags": [
|
|
211
241
|
"exec",
|
|
212
242
|
"--auto",
|
|
@@ -223,6 +253,8 @@
|
|
|
223
253
|
"label": "Continue",
|
|
224
254
|
"command": "cn",
|
|
225
255
|
"hidden": false,
|
|
256
|
+
"supportTier": "beta",
|
|
257
|
+
"certification": "adapter-tested",
|
|
226
258
|
"headlessFlags": [
|
|
227
259
|
"--auto",
|
|
228
260
|
"-p"
|
|
@@ -237,6 +269,8 @@
|
|
|
237
269
|
"label": "Kilo Code",
|
|
238
270
|
"command": "kilo",
|
|
239
271
|
"hidden": false,
|
|
272
|
+
"supportTier": "beta",
|
|
273
|
+
"certification": "adapter-tested",
|
|
240
274
|
"headlessFlags": [
|
|
241
275
|
"run",
|
|
242
276
|
"--auto",
|
|
@@ -252,6 +286,8 @@
|
|
|
252
286
|
"label": "Rovo Dev",
|
|
253
287
|
"command": "acli",
|
|
254
288
|
"hidden": false,
|
|
289
|
+
"supportTier": "beta",
|
|
290
|
+
"certification": "adapter-tested",
|
|
255
291
|
"headlessFlags": [
|
|
256
292
|
"rovodev",
|
|
257
293
|
"run",
|
|
@@ -265,6 +301,8 @@
|
|
|
265
301
|
"label": "Codebuff",
|
|
266
302
|
"command": "codebuff",
|
|
267
303
|
"hidden": true,
|
|
304
|
+
"supportTier": "experimental",
|
|
305
|
+
"certification": "unverified",
|
|
268
306
|
"headlessFlags": [
|
|
269
307
|
"--continue"
|
|
270
308
|
],
|
package/bin/bivy.mjs
CHANGED
|
@@ -425,6 +425,25 @@ function run(cmd, args, opts = {}) {
|
|
|
425
425
|
});
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/** Fixed executable + fixed entry point for setup's inline model-auth stage.
|
|
429
|
+
* Keep this separate from the generic CLI forwarding helper: no user-provided
|
|
430
|
+
* command or argv value reaches this process boundary. */
|
|
431
|
+
function runSetupModelLogin(config) {
|
|
432
|
+
return new Promise((resolve) => {
|
|
433
|
+
const child = spawn(process.execPath, nodeScriptArgs(bivyLoginEntry), {
|
|
434
|
+
stdio: "inherit",
|
|
435
|
+
cwd: repoRoot,
|
|
436
|
+
env: startEnv(config),
|
|
437
|
+
shell: false,
|
|
438
|
+
});
|
|
439
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
440
|
+
child.on("error", (error) => {
|
|
441
|
+
console.error(c.red(`Failed to start model login: ${error.message}`));
|
|
442
|
+
resolve(1);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
428
447
|
function runQuiet(cmd, args, opts = {}) {
|
|
429
448
|
const res = spawnSync(cmd, args, { encoding: "utf8", ...opts });
|
|
430
449
|
return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
@@ -1530,7 +1549,7 @@ function cmdCompletions(args = []) {
|
|
|
1530
1549
|
const shell = (args[0] || "").toLowerCase();
|
|
1531
1550
|
const commands = [
|
|
1532
1551
|
"run", "sessions", "ls", "resume", "promote", "rename", "nodes", "agents", "agents:install", "shim", "takeover", "token", "exec",
|
|
1533
|
-
"send", "attach", "kill", "setup", "start", "stop", "restart", "status", "doctor", "logs", "login",
|
|
1552
|
+
"send", "attach", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "logs", "login",
|
|
1534
1553
|
"update", "update:log", "open", "service", "secrets", "voice", "link", "relay:setup",
|
|
1535
1554
|
"github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
|
|
1536
1555
|
];
|
|
@@ -3194,9 +3213,10 @@ async function cmdSetup(args = []) {
|
|
|
3194
3213
|
config.env = { ...config.env, BIVY_RUNTIME: setupAgent.runtimeId };
|
|
3195
3214
|
saveConfig(config);
|
|
3196
3215
|
}
|
|
3216
|
+
let agentReady = true;
|
|
3197
3217
|
if (setupAgent && setupAgent.runtimeId !== "pi") {
|
|
3198
|
-
|
|
3199
|
-
if (!
|
|
3218
|
+
agentReady = await ensureSetupAgent(setupAgent);
|
|
3219
|
+
if (!agentReady) console.log(c.yellow(`${setupAgent.label} was not fully installed. Install it later from the app or with 'bivy agents:install'.`));
|
|
3200
3220
|
}
|
|
3201
3221
|
console.log(c.dim(`Default agent: ${setupAgent?.label || "Pi"} (change in Settings; sign into your model from the agent's CLI/TUI or Settings → Keys & OAuth)`));
|
|
3202
3222
|
|
|
@@ -3211,13 +3231,17 @@ async function cmdSetup(args = []) {
|
|
|
3211
3231
|
console.log(c.bold("\n Remote access\n"));
|
|
3212
3232
|
|
|
3213
3233
|
console.log("Bivy uses remote access to make agent sessions visible and steerable from your other devices.");
|
|
3234
|
+
// If self-host endpoints are already provided via the environment, default to
|
|
3235
|
+
// self-hosted so a scripted or self-hosted install doesn't have to re-pick it
|
|
3236
|
+
// (BIVY_CONTROL_PLANE_URL / BIVY_RELAY_URL then pre-fill the URL prompts below).
|
|
3237
|
+
const selfHostEnv = Boolean((process.env.BIVY_CONTROL_PLANE_URL || "").trim() || (process.env.BIVY_RELAY_URL || "").trim());
|
|
3214
3238
|
const syncChoice = await askChoice(
|
|
3215
3239
|
"Remote access",
|
|
3216
3240
|
[
|
|
3217
3241
|
{ key: "h", label: "hosted (recommended — one node is free)" },
|
|
3218
3242
|
{ key: "s", label: "self-hosted (your own control plane + relay)" },
|
|
3219
3243
|
],
|
|
3220
|
-
"h",
|
|
3244
|
+
selfHostEnv ? "s" : "h",
|
|
3221
3245
|
);
|
|
3222
3246
|
const relayArgs = [];
|
|
3223
3247
|
if (syncChoice === "s") {
|
|
@@ -3278,6 +3302,22 @@ async function cmdSetup(args = []) {
|
|
|
3278
3302
|
// `bivy github:app-create` / `github:app-connect`. One app covers every repo,
|
|
3279
3303
|
// and the node mints its own tokens, so there's no per-repo token to set up here.
|
|
3280
3304
|
|
|
3305
|
+
// Model access is part of activation, not a post-success footnote. Pi/Aider use
|
|
3306
|
+
// Bivy's provider login; offer it inline so setup cannot imply the first task
|
|
3307
|
+
// is ready while the required credential is still absent. Agent-native auth is
|
|
3308
|
+
// explained in the readiness checklist below because those CLIs own the flow.
|
|
3309
|
+
if (setupAgent?.needsBivyModel && !hasModelConfig(config)) {
|
|
3310
|
+
const signInNow = await askYesNo("Sign in to a model now so your first task can run?", true);
|
|
3311
|
+
if (signInNow) {
|
|
3312
|
+
rl.pause();
|
|
3313
|
+
const loginCode = await runSetupModelLogin(config);
|
|
3314
|
+
rl.resume();
|
|
3315
|
+
if (loginCode !== 0 || !hasModelConfig(loadConfig())) {
|
|
3316
|
+
console.log(c.yellow("Model sign-in did not complete. The node can start, but an agent reply still requires 'bivy login'."));
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3281
3321
|
// 4. Background service — always installed so the node keeps running (and stays
|
|
3282
3322
|
// reachable remotely) after you close this terminal. No prompt.
|
|
3283
3323
|
let started = false;
|
|
@@ -3296,9 +3336,17 @@ async function cmdSetup(args = []) {
|
|
|
3296
3336
|
return;
|
|
3297
3337
|
}
|
|
3298
3338
|
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3339
|
+
const finalConfig = loadConfig();
|
|
3340
|
+
const modelReady = !setupAgent?.needsBivyModel || hasModelConfig(finalConfig);
|
|
3341
|
+
console.log(c.bold(c.green("\n ✓ Node running. Check first-task readiness below.\n")));
|
|
3342
|
+
console.log(` ${c.green("✓")} node reachable at ${url(finalConfig)}`);
|
|
3343
|
+
console.log(` ${agentReady ? c.green("✓") : c.yellow("!")} runtime ${agentReady ? `${setupAgent?.label || "Pi"} available` : "not installed — run 'bivy agents:install'"}`);
|
|
3344
|
+
console.log(` ${modelReady ? (setupAgent?.needsBivyModel ? c.green("✓") : c.dim("○")) : c.yellow("!")} model ${modelReady ? (setupAgent?.needsBivyModel ? "credential configured" : "agent-managed — verified by the first task") : "not configured — run 'bivy login'"}`);
|
|
3345
|
+
console.log(` ${c.dim("○")} repository chosen from the directory where you start Bivy`);
|
|
3346
|
+
console.log(` ${agentReady && modelReady ? c.green("✓") : c.yellow("!")} first task ${agentReady && modelReady ? "ready to try" : "blocked by the stage above"}`);
|
|
3347
|
+
console.log(` ${fs.existsSync(relayConfigPath) ? c.green("✓") : c.yellow("!")} remote ${fs.existsSync(relayConfigPath) ? "configured" : "not configured — run 'bivy relay:setup'"}\n`);
|
|
3348
|
+
printFirstRunSteps(modelReady);
|
|
3349
|
+
await finishSetupRemote(finalConfig, setupSession);
|
|
3302
3350
|
}
|
|
3303
3351
|
|
|
3304
3352
|
// Read and delete the one-time account-session handoff written by relay:setup
|
|
@@ -3385,11 +3433,11 @@ async function openRemoteApp(config, { setupSession = null, open = true } = {})
|
|
|
3385
3433
|
return { relay, remoteBase, accountUrl, pairedUrl, openUrl };
|
|
3386
3434
|
}
|
|
3387
3435
|
|
|
3388
|
-
function printFirstRunSteps() {
|
|
3436
|
+
function printFirstRunSteps(modelReady = false) {
|
|
3389
3437
|
console.log(" Run your first task:");
|
|
3390
|
-
console.log(` 1. Model access:
|
|
3391
|
-
console.log(` 2. Start chatting: ${c.cyan("bivy")}`);
|
|
3392
|
-
console.log(`
|
|
3438
|
+
if (!modelReady) console.log(` 1. Model access: ${c.cyan("bivy login")} ${c.dim("(for Pi; other agents use their own login)")}`);
|
|
3439
|
+
console.log(` ${modelReady ? "1" : "2"}. Start chatting: ${c.cyan("bivy")}`);
|
|
3440
|
+
console.log(` Starter task: ${c.cyan('bivy exec "explain this repository and identify one low-risk improvement"')}\n`);
|
|
3393
3441
|
}
|
|
3394
3442
|
|
|
3395
3443
|
async function finishSetupRemote(config, setupSession = null) {
|
|
@@ -3506,7 +3554,7 @@ async function cmdStatus(args = []) {
|
|
|
3506
3554
|
console.log(` sessions: ${status.sessions?.open ?? 0} open, ${status.sessions?.indexed ?? 0} indexed${status.sessions?.active ? `, active ${status.sessions.active}` : ""}`);
|
|
3507
3555
|
console.log(` devices: ${status.devices?.paired ?? 0} paired remote, ${status.devices?.localTokens ?? 0} local token(s)`);
|
|
3508
3556
|
console.log(` approvals: ${status.approvals?.pending ?? 0} pending`);
|
|
3509
|
-
console.log(` guard: ${status.approvalMode || "autonomous"}
|
|
3557
|
+
console.log(` guard: ${status.approvalMode || "autonomous"} · ${status.guardrails?.protection || (status.guardrails?.workspaceBoundary ? "structured workspace controls" : "runs with user permissions")}`);
|
|
3510
3558
|
if (status.updatedAt) {
|
|
3511
3559
|
const when = new Date(status.updatedAt);
|
|
3512
3560
|
console.log(` updated: ${Number.isNaN(when.getTime()) ? status.updatedAt : when.toLocaleString()}`);
|
|
@@ -3517,9 +3565,34 @@ async function cmdStatus(args = []) {
|
|
|
3517
3565
|
|
|
3518
3566
|
// `bivy doctor` — one health screen: runtime deps, node reachability, model auth,
|
|
3519
3567
|
// remote/relay, and agents on PATH.
|
|
3568
|
+
// `bivy diagnostics [--out <file>]` — fetch the node's redacted diagnostics
|
|
3569
|
+
// bundle (versions, health counters, whitelisted config, activation record — no
|
|
3570
|
+
// secrets/prompts/transcripts) and print it, or write it to a file to attach to a
|
|
3571
|
+
// support request. See src/diagnostics.ts for exactly what is (and isn't) included.
|
|
3572
|
+
async function cmdDiagnostics(args = []) {
|
|
3573
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
3574
|
+
console.log('Usage: bivy diagnostics [--out <file>]\n\nPrint a redacted, shareable diagnostics bundle (no secrets, prompts, transcripts, or repo content). --out writes it to a file instead of stdout.');
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
const config = loadConfig();
|
|
3578
|
+
if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
|
|
3579
|
+
let report;
|
|
3580
|
+
try { report = await localApi(config, "/api/diagnostics"); }
|
|
3581
|
+
catch (error) { console.error(c.red(`Could not fetch diagnostics: ${error?.message || String(error)}`)); process.exit(1); return; }
|
|
3582
|
+
const json = JSON.stringify(report, null, 2);
|
|
3583
|
+
const outIdx = args.indexOf("--out");
|
|
3584
|
+
const out = outIdx >= 0 && outIdx + 1 < args.length ? args[outIdx + 1] : undefined;
|
|
3585
|
+
if (out) {
|
|
3586
|
+
fs.writeFileSync(out, json + "\n");
|
|
3587
|
+
console.log(c.green(`Wrote redacted diagnostics to ${out}`));
|
|
3588
|
+
} else {
|
|
3589
|
+
console.log(json);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3520
3593
|
async function cmdDoctor(args = []) {
|
|
3521
3594
|
if (args.includes("-h") || args.includes("--help")) {
|
|
3522
|
-
console.log("Usage: bivy doctor\n\nHealth check: runtime deps, node reachability, model auth, remote/relay, and agents on PATH. Exits non-zero if Node is unsupported or the node is unreachable, so it can gate CI/monitoring.");
|
|
3595
|
+
console.log("Usage: bivy doctor\n\nHealth check: runtime deps, node reachability, model auth, remote/relay, and agents on PATH. Exits non-zero if Node is unsupported or the node is unreachable, so it can gate CI/monitoring. See also 'bivy diagnostics' for a shareable redacted bundle.");
|
|
3523
3596
|
return;
|
|
3524
3597
|
}
|
|
3525
3598
|
if (!(await ensureDeps())) process.exit(1);
|
|
@@ -3565,6 +3638,16 @@ async function cmdDoctor(args = []) {
|
|
|
3565
3638
|
const agentCommands = [...BUILTIN_TERMINAL_AGENTS.values()].filter((a) => a.type === "command").map((a) => a.command);
|
|
3566
3639
|
const agents = agentCommands.filter((a) => commandExists(a));
|
|
3567
3640
|
console.log(` ${mark(agents.length > 0, true)} agents on PATH: ${agents.length ? c.cyan(agents.join(", ")) : c.dim("none (built-in Pi still works; 'bivy agents:install')")}`);
|
|
3641
|
+
if (status?.eventLog) {
|
|
3642
|
+
const healthy = status.eventLog.ok !== false;
|
|
3643
|
+
const mib = Number(status.eventLog.bytes || 0) / (1024 * 1024);
|
|
3644
|
+
console.log(` ${mark(healthy, true)} event log ${healthy ? "writable" : `${status.eventLog.affectedSessions ?? 0} session(s) need attention`} · ${mib.toFixed(1)} MiB`);
|
|
3645
|
+
}
|
|
3646
|
+
if (status?.attachments) {
|
|
3647
|
+
const mib = Number(status.attachments.bytes || 0) / (1024 * 1024);
|
|
3648
|
+
const over = Number(status.attachments.overCapBytes || 0);
|
|
3649
|
+
console.log(` ${over > 0 ? warn : ok} attachments ${status.attachments.blobs ?? 0} blob(s), ${mib.toFixed(1)} MiB${over > 0 ? c.dim(" (over cap; referenced history retained)") : ""}`);
|
|
3650
|
+
}
|
|
3568
3651
|
console.log("");
|
|
3569
3652
|
|
|
3570
3653
|
// Fail the command when a hard check is red (unsupported Node or an
|
|
@@ -4234,6 +4317,9 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
4234
4317
|
case "doctor":
|
|
4235
4318
|
await cmdDoctor(args);
|
|
4236
4319
|
break;
|
|
4320
|
+
case "diagnostics":
|
|
4321
|
+
await cmdDiagnostics(args);
|
|
4322
|
+
break;
|
|
4237
4323
|
case "logs":
|
|
4238
4324
|
await cmdLogs(args);
|
|
4239
4325
|
break;
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
3
3
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
4
4
|
/**
|
|
5
|
-
* pi-coding-agent 0.82.1 publishes an npm-shrinkwrap that pins
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* installation. Replace
|
|
9
|
-
*
|
|
5
|
+
* pi-coding-agent 0.82.1 publishes an npm-shrinkwrap that pins vulnerable
|
|
6
|
+
* transitive packages below its own node_modules. npm overrides update the outer
|
|
7
|
+
* lockfile/audit result but do not replace those shrinkwrapped files during
|
|
8
|
+
* installation. Replace the affected nested packages with direct, exact patched
|
|
9
|
+
* dependencies until pi publishes a corrected shrinkwrap.
|
|
10
10
|
*/
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
@@ -27,18 +27,24 @@ function findDependency(relativePath) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const source = findDependency("brace-expansion");
|
|
31
30
|
const piPackage = findDependency(path.join("@earendil-works", "pi-coding-agent"));
|
|
32
|
-
|
|
31
|
+
if (!piPackage) process.exit(0);
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const patches = [
|
|
34
|
+
{ name: "brace-expansion", version: "5.0.9" },
|
|
35
|
+
{ name: "undici", version: "8.9.0" },
|
|
36
|
+
];
|
|
36
37
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
for (const patch of patches) {
|
|
39
|
+
const source = findDependency(patch.name);
|
|
40
|
+
const target = path.join(piPackage, "node_modules", patch.name);
|
|
41
|
+
if (!fs.existsSync(target)) continue;
|
|
42
|
+
if (!source) throw new Error(`Security patch source ${patch.name}@${patch.version} is missing`);
|
|
43
|
+
const sourcePackage = JSON.parse(fs.readFileSync(path.join(source, "package.json"), "utf8"));
|
|
44
|
+
if (sourcePackage.version !== patch.version) {
|
|
45
|
+
throw new Error(`Refusing dependency patch from unexpected ${patch.name} ${sourcePackage.version}`);
|
|
46
|
+
}
|
|
47
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
48
|
+
fs.cpSync(source, target, { recursive: true });
|
|
49
|
+
console.log(`Patched pi-coding-agent's nested ${patch.name} to ${patch.version}`);
|
|
40
50
|
}
|
|
41
|
-
|
|
42
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
43
|
-
fs.cpSync(source, target, { recursive: true });
|
|
44
|
-
console.log("Patched pi-coding-agent's nested brace-expansion to 5.0.9");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
const DEFAULT_SCRIPT_NAMES = ["test", "lint", "typecheck"];
|
|
8
|
+
const DEFAULT_CHECK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
9
|
+
const MAX_CHECK_TIMEOUT_MS = 30 * 60 * 1000;
|
|
10
|
+
function configuredScriptNames(env) {
|
|
11
|
+
const raw = env.BIVY_AUTOMATION_CHECKS?.trim();
|
|
12
|
+
if (!raw)
|
|
13
|
+
return DEFAULT_SCRIPT_NAMES;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
if (Array.isArray(parsed))
|
|
17
|
+
return [...new Set(parsed.map(String).map((s) => s.trim()).filter((s) => /^[\w:.-]+$/.test(s)))].slice(0, 10);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Fall through to a comma list; malformed entries are discarded rather than
|
|
21
|
+
// interpreted as shell. Only package-script names are accepted.
|
|
22
|
+
}
|
|
23
|
+
return [...new Set(raw.split(",").map((s) => s.trim()).filter((s) => /^[\w:.-]+$/.test(s)))].slice(0, 10);
|
|
24
|
+
}
|
|
25
|
+
function checkTimeoutMs(env) {
|
|
26
|
+
const parsed = Number(env.BIVY_AUTOMATION_CHECK_TIMEOUT_MS);
|
|
27
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
28
|
+
return DEFAULT_CHECK_TIMEOUT_MS;
|
|
29
|
+
return Math.min(MAX_CHECK_TIMEOUT_MS, Math.max(1_000, Math.floor(parsed)));
|
|
30
|
+
}
|
|
31
|
+
function packageManager(cwd) {
|
|
32
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml")))
|
|
33
|
+
return { command: "pnpm", args: (script) => ["run", script] };
|
|
34
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock")))
|
|
35
|
+
return { command: "yarn", args: (script) => ["run", script] };
|
|
36
|
+
return { command: "npm", args: (script) => ["run", script] };
|
|
37
|
+
}
|
|
38
|
+
/** Discover and run only declared package scripts. Command text and output stay
|
|
39
|
+
* on the node; hosted evidence receives a name, hash, pass/fail, and exit code. */
|
|
40
|
+
export function runRequiredAutomationChecks(cwd, env = process.env, run = spawnSync) {
|
|
41
|
+
let pkg;
|
|
42
|
+
try {
|
|
43
|
+
pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const manager = packageManager(cwd);
|
|
49
|
+
const timeout = checkTimeoutMs(env);
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const name of configuredScriptNames(env)) {
|
|
52
|
+
if (typeof pkg.scripts?.[name] !== "string")
|
|
53
|
+
continue;
|
|
54
|
+
const args = manager.args(name);
|
|
55
|
+
const commandHash = `sha256:${createHash("sha256").update(JSON.stringify([manager.command, ...args])).digest("hex")}`;
|
|
56
|
+
const startedAt = Date.now();
|
|
57
|
+
const result = run(manager.command, args, {
|
|
58
|
+
cwd,
|
|
59
|
+
env,
|
|
60
|
+
stdio: "ignore",
|
|
61
|
+
timeout,
|
|
62
|
+
killSignal: "SIGTERM",
|
|
63
|
+
});
|
|
64
|
+
const exitCode = typeof result.status === "number" ? result.status : 1;
|
|
65
|
+
results.push({ name, commandHash, status: exitCode === 0 ? "passed" : "failed", exitCode, durationMs: Math.max(0, Date.now() - startedAt) });
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
package/dist/bivy-login.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { createCredentialVault } from "./runtime/credential-store.js";
|
|
8
|
+
import { probeAnthropicAccess } from "./runtime/anthropic-preflight.js";
|
|
8
9
|
import { listPiProviders } from "./runtime/pi-oauth.js";
|
|
9
10
|
import { loginModelOAuth } from "./runtime/oauth/model-oauth.js";
|
|
10
11
|
import { openBrowser } from "./browser-open.js";
|
|
@@ -95,6 +96,18 @@ async function loginApiKey(provider) {
|
|
|
95
96
|
throw new Error("API key cannot be empty.");
|
|
96
97
|
await createCredentialVault(credsDir).setApiKey(provider.id, apiKey);
|
|
97
98
|
console.log(`Saved API key for ${provider.name} to Bivy's credential vault.`);
|
|
99
|
+
// B1: validate real access, not just that a key was typed, where a safe probe
|
|
100
|
+
// exists. A rejected key is reported now instead of surfacing later as an
|
|
101
|
+
// opaque 401 on the user's first task.
|
|
102
|
+
if (provider.id === "anthropic") {
|
|
103
|
+
const probe = await probeAnthropicAccess(apiKey);
|
|
104
|
+
if (probe.probed && !probe.ok) {
|
|
105
|
+
console.log(`⚠ ${probe.reason || "The key was saved but Anthropic rejected it."} Double-check the key; re-run 'bivy login' to replace it.`);
|
|
106
|
+
}
|
|
107
|
+
else if (probe.probed) {
|
|
108
|
+
console.log("✓ Verified: the key can reach the Anthropic API.");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
98
111
|
}
|
|
99
112
|
/** Bridge Pi's AuthInteraction to the terminal (prompt/notify). */
|
|
100
113
|
function terminalInteraction(provider, signal) {
|
|
@@ -53,7 +53,19 @@ async function cp(cfg, method, path) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
async function transitionWork(cfg, id, action) {
|
|
56
|
-
|
|
56
|
+
// Best-effort — a dropped transition never loses the run itself — but NOT
|
|
57
|
+
// silent: a swallowed `complete`/`fail`/`needs-attention` leaves the control
|
|
58
|
+
// plane's view of the item stale (stuck "running", or re-dispatched), so the
|
|
59
|
+
// failure must be visible in node logs/diagnostics rather than discarded (A4).
|
|
60
|
+
try {
|
|
61
|
+
const res = await cp(cfg, "POST", `/node/work/${encodeURIComponent(id)}/${action}`);
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
console.warn(`[control-plane-tasks] work ${id} "${action}" rejected by control plane (${res.status}); its status may be stale`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`[control-plane-tasks] work ${id} "${action}" could not reach control plane:`, error instanceof Error ? error.message : error);
|
|
68
|
+
}
|
|
57
69
|
}
|
|
58
70
|
export async function fetchPendingWork(cfg) {
|
|
59
71
|
const res = await cp(cfg, "GET", `/node/work?labels=${encodeURIComponent(cfg.labels.join(","))}`);
|
|
@@ -67,6 +79,10 @@ export async function claimWork(cfg, id) {
|
|
|
67
79
|
const res = await cp(cfg, "POST", `/node/work/${encodeURIComponent(id)}/claim`);
|
|
68
80
|
return res.ok;
|
|
69
81
|
}
|
|
82
|
+
export async function renewWorkLease(cfg, id) {
|
|
83
|
+
const res = await cp(cfg, "POST", `/node/work/${encodeURIComponent(id)}/heartbeat`);
|
|
84
|
+
return res.ok;
|
|
85
|
+
}
|
|
70
86
|
export async function completeWork(cfg, id) {
|
|
71
87
|
await transitionWork(cfg, id, "complete");
|
|
72
88
|
}
|
|
@@ -80,13 +96,22 @@ export async function needsAttentionWork(cfg, id) {
|
|
|
80
96
|
/** Report privacy-safe run evidence (issue #153) — routing reason, output refs
|
|
81
97
|
* (branch/PR/checkpoint/commit/...), check results, and new timeline events.
|
|
82
98
|
* Best-effort: a dropped report loses one evidence update, never the run
|
|
83
|
-
* itself
|
|
99
|
+
* itself. It is not throwing, but the failure is logged (A4) so a persistently
|
|
100
|
+
* failing evidence channel is visible in diagnostics instead of silent. */
|
|
84
101
|
export async function reportEvidence(cfg, id, patch) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(`${cfg.controlPlaneUrl}/node/work/${encodeURIComponent(id)}/evidence`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { authorization: `Bearer ${cfg.enrollmentToken}`, "content-type": "application/json" },
|
|
106
|
+
body: JSON.stringify(patch),
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
console.warn(`[control-plane-tasks] work ${id} evidence report rejected (${res.status})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.warn(`[control-plane-tasks] work ${id} evidence report could not reach control plane:`, error instanceof Error ? error.message : error);
|
|
114
|
+
}
|
|
90
115
|
}
|
|
91
116
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
92
117
|
export class ControlPlaneTaskPoller {
|
|
@@ -174,11 +199,15 @@ export class ControlPlaneTaskPoller {
|
|
|
174
199
|
await Promise.all(running);
|
|
175
200
|
}
|
|
176
201
|
async runOne(item) {
|
|
202
|
+
let leaseHeartbeat;
|
|
177
203
|
try {
|
|
178
204
|
// Claim first so only one node runs it; skip if another node won (no
|
|
179
|
-
// claim → not ours → don't run or complete it).
|
|
205
|
+
// claim → not ours → don't run or complete it). A heartbeat keeps the
|
|
206
|
+
// finite lease alive; process death stops it and makes the item reclaimable.
|
|
180
207
|
if (!(await claimWork(this.cfg, item.id)))
|
|
181
208
|
return;
|
|
209
|
+
leaseHeartbeat = setInterval(() => void renewWorkLease(this.cfg, item.id), 30_000);
|
|
210
|
+
leaseHeartbeat.unref?.();
|
|
182
211
|
const report = (patch) => reportEvidence(this.cfg, item.id, patch);
|
|
183
212
|
await transitionWork(this.cfg, item.id, "running");
|
|
184
213
|
console.log(`[control-plane-tasks] running ${item.source} item ${item.id}: ${item.title}`);
|
|
@@ -190,6 +219,8 @@ export class ControlPlaneTaskPoller {
|
|
|
190
219
|
await this.runWithPolicy(item, report);
|
|
191
220
|
}
|
|
192
221
|
finally {
|
|
222
|
+
if (leaseHeartbeat)
|
|
223
|
+
clearInterval(leaseHeartbeat);
|
|
193
224
|
this.inFlight.delete(item.id);
|
|
194
225
|
}
|
|
195
226
|
}
|