@bivy/bivy 0.10.1-staging.299 → 0.10.1-staging.301
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 +48 -17
- package/dist/agents/claude-code/runtime.js +42 -8
- package/dist/harness/mcp-serve-cli.js +40 -4
- package/dist/integrations/manager.js +12 -1
- package/dist/run-tools.js +181 -0
- package/dist/runtime/tool-call-map.js +1 -1
- package/dist/server.js +101 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
# Bivy
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
**Give coding agents your real environment, then reach them from anywhere.**
|
|
4
|
+
|
|
5
|
+
A provider sandbox starts from an approximation. A Bivy Machine can use the
|
|
6
|
+
repository, local services and databases, private networks, existing tools and
|
|
7
|
+
caches, and GPUs or local inference already available on your workstation or
|
|
8
|
+
server. Bivy turns that local capability into Sessions you can continue remotely
|
|
9
|
+
and Runs you can leave working unattended.
|
|
10
|
+
|
|
11
|
+
## What Bivy lets you do
|
|
12
|
+
|
|
13
|
+
- **Work where the full environment lives.** Run Claude Code, Codex, Pi, or
|
|
14
|
+
another agent beside real repos, dev servers, databases, internal APIs,
|
|
15
|
+
toolchains, package caches, and specialized compute.
|
|
16
|
+
- **Continue from a phone, browser, or terminal.** Reconnect to the same Session,
|
|
17
|
+
steer or stop it, answer questions, and approve supported tool calls. The PWA
|
|
18
|
+
supports voice input and read-aloud, phone-to-agent file/image uploads, and
|
|
19
|
+
agent-to-phone attachments. Native terminal Sessions can stay remote-visible;
|
|
20
|
+
supported runtimes can hand work between terminal and structured chat.
|
|
21
|
+
- **Move work without starting over.** Import existing Claude Code and Codex
|
|
22
|
+
Sessions, or fork/copy/move a Bivy Session to another agent, model, or Machine.
|
|
23
|
+
Fidelity is runtime-dependent: a fork may use a native transcript, replayed
|
|
24
|
+
history, or a bounded seeded continuation, and cross-Machine moves require the
|
|
25
|
+
destination's repo access, agent, and credentials.
|
|
26
|
+
- **Operate more than one Machine.** Keep a workstation, private-network server,
|
|
27
|
+
and GPU box on one account; select the environment a Session or Run needs.
|
|
28
|
+
Optional warm Session replication is Beta, off by default, and manually
|
|
29
|
+
promoted rather than automatic failover.
|
|
30
|
+
- **Let events start checked work.** Create Runs manually or from failed CI,
|
|
31
|
+
GitHub/Linear issues, Slack, schedules, and signed webhooks. Automations can
|
|
32
|
+
pin a Machine, agent, model, sandbox, approval mode, and attempt ceiling, then
|
|
33
|
+
report bounded check and outcome evidence in a Receipt.
|
|
34
|
+
- **Bring your own stack.** Use provider subscriptions through supported native
|
|
35
|
+
agent logins, API keys in Bivy's vault, or local/OpenAI-compatible inference.
|
|
36
|
+
Add an existing ACP or headless process agent with `bivy agent add`; declarative
|
|
37
|
+
agent plugins are Experimental (`v1alpha1`) and run out of process.
|
|
38
|
+
|
|
39
|
+
Start with the [five-minute quickstart](docs/quickstart.md), then try the
|
|
40
|
+
[capability recipes](docs/capability-recipes.md) or check the exact
|
|
41
|
+
[runtime support matrix](docs/runtime-support-matrix.md).
|
|
11
42
|
|
|
12
43
|
Interactive Session traffic is end-to-end encrypted between a Machine and its
|
|
13
|
-
paired devices
|
|
14
|
-
|
|
15
|
-
cloud, repository, or key-escrow material
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
[security model](docs/security-model.md) for the complete trust model.
|
|
44
|
+
paired devices, so the relay cannot decrypt it. The seatbelt has a precise edge:
|
|
45
|
+
explicitly enabled hosted provisioning may give the control plane technical
|
|
46
|
+
access to encrypted cloud, repository, or key-escrow material, while Slack and
|
|
47
|
+
generic webhook instructions reach it in plaintext. See [Why Bivy](docs/why-bivy.md)
|
|
48
|
+
and the [security model](docs/security-model.md) for the complete trust model.
|
|
19
49
|
|
|
20
50
|
- **Website:** [bivy.sh](https://bivy.sh)
|
|
21
|
-
- **Documentation:** [`docs/`](docs/README.md)
|
|
51
|
+
- **Documentation:** [`docs/`](docs/README.md)
|
|
22
52
|
- **License:** [AGPL-3.0-only](LICENSE)
|
|
23
53
|
|
|
24
|
-
> Bivy is 0.x software. The core loop is solid and used daily, but interfaces
|
|
25
|
-
> and behaviour can change between releases.
|
|
54
|
+
> Bivy is 0.x software. The core loop is solid and used daily, but interfaces,
|
|
55
|
+
> cross-runtime fidelity, and behaviour can change between releases. Check the
|
|
56
|
+
> support matrix before depending on a particular agent capability.
|
|
26
57
|
|
|
27
58
|
## Install
|
|
28
59
|
|
|
@@ -136,6 +136,36 @@ function buildAttachMcpServer(sdk, sessionId, attachToChat) {
|
|
|
136
136
|
});
|
|
137
137
|
return sdk.createSdkMcpServer({ name: BIVY_ATTACH_MCP_SERVER_NAME, tools: [attachTool] });
|
|
138
138
|
}
|
|
139
|
+
/** Adapt Bivy's runtime-agnostic ToolProvider to Claude SDK's in-process MCP
|
|
140
|
+
* builder. Credentials and execution remain on the daemon. */
|
|
141
|
+
function buildToolProviderMcpServer(sdk, provider) {
|
|
142
|
+
if (typeof sdk?.tool !== "function" || typeof sdk?.createSdkMcpServer !== "function")
|
|
143
|
+
return undefined;
|
|
144
|
+
const toShape = (schema) => {
|
|
145
|
+
const object = schema && typeof schema === "object" ? schema : {};
|
|
146
|
+
const properties = object.properties && typeof object.properties === "object" ? object.properties : {};
|
|
147
|
+
const required = new Set(Array.isArray(object.required) ? object.required.filter((v) => typeof v === "string") : []);
|
|
148
|
+
const shape = {};
|
|
149
|
+
for (const [name, raw] of Object.entries(properties)) {
|
|
150
|
+
const field = raw && typeof raw === "object" ? raw : {};
|
|
151
|
+
let value = field.type === "string" ? z.string()
|
|
152
|
+
: field.type === "integer" ? z.number().int()
|
|
153
|
+
: field.type === "number" ? z.number()
|
|
154
|
+
: field.type === "boolean" ? z.boolean()
|
|
155
|
+
: field.type === "object" ? z.object(toShape(field)).passthrough()
|
|
156
|
+
: z.unknown();
|
|
157
|
+
if (Array.isArray(field.enum) && field.enum.every((v) => typeof v === "string") && field.enum.length > 0)
|
|
158
|
+
value = z.enum(field.enum);
|
|
159
|
+
shape[name] = required.has(name) ? value : value.optional();
|
|
160
|
+
}
|
|
161
|
+
return shape;
|
|
162
|
+
};
|
|
163
|
+
const tools = provider.list().map((spec) => sdk.tool(spec.name, spec.description, toShape(spec.parameters), async (args) => {
|
|
164
|
+
const result = await provider.invoke(spec.name, `claude-${randomUUID()}`, args);
|
|
165
|
+
return { content: result.content, ...(result.isError ? { isError: true } : {}) };
|
|
166
|
+
}));
|
|
167
|
+
return sdk.createSdkMcpServer({ name: BIVY_ATTACH_MCP_SERVER_NAME, tools });
|
|
168
|
+
}
|
|
139
169
|
export function claudeRuntimeFromEnv() {
|
|
140
170
|
return {
|
|
141
171
|
defaultModel: process.env.BIVY_CLAUDE_MODEL?.trim() || undefined,
|
|
@@ -632,6 +662,7 @@ class ClaudeSession {
|
|
|
632
662
|
runtimeOptions;
|
|
633
663
|
cwd;
|
|
634
664
|
toolInterceptor;
|
|
665
|
+
toolProvider;
|
|
635
666
|
query;
|
|
636
667
|
// Re-created on every spawnQuery: the previous SDK query owns the old queue, so
|
|
637
668
|
// a re-spawn (credential reload) must not share it. Not readonly for that reason.
|
|
@@ -687,10 +718,11 @@ class ClaudeSession {
|
|
|
687
718
|
* init arrives; getCommands() exposes them and a `runtime.commands` event lets
|
|
688
719
|
* the daemon re-advertise once they're known. */
|
|
689
720
|
commands = [];
|
|
690
|
-
constructor(runtimeOptions, cwd, toolInterceptor, resumeId) {
|
|
721
|
+
constructor(runtimeOptions, cwd, toolInterceptor, toolProvider, resumeId) {
|
|
691
722
|
this.runtimeOptions = runtimeOptions;
|
|
692
723
|
this.cwd = cwd;
|
|
693
724
|
this.toolInterceptor = toolInterceptor;
|
|
725
|
+
this.toolProvider = toolProvider;
|
|
694
726
|
this.resumeId = resumeId;
|
|
695
727
|
this.id = resumeId ?? randomUUID();
|
|
696
728
|
this.desiredModel = runtimeOptions.defaultModel;
|
|
@@ -827,11 +859,13 @@ class ClaudeSession {
|
|
|
827
859
|
// callback (see ClaudeCodeRuntimeOptions.attachToChat); absent in a few
|
|
828
860
|
// deliberately minimal test harnesses, and gracefully degrades to the prompt
|
|
829
861
|
// hint alone if this SDK build lacks the MCP builder helpers.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
862
|
+
const bivyServer = this.toolProvider
|
|
863
|
+
? buildToolProviderMcpServer(sdk, this.toolProvider)
|
|
864
|
+
: this.runtimeOptions.attachToChat
|
|
865
|
+
? buildAttachMcpServer(sdk, this.id, this.runtimeOptions.attachToChat)
|
|
866
|
+
: undefined;
|
|
867
|
+
if (bivyServer)
|
|
868
|
+
options.mcpServers = { [BIVY_ATTACH_MCP_SERVER_NAME]: bivyServer };
|
|
835
869
|
const q = sdk.query({ prompt: this.input, options });
|
|
836
870
|
this.query = q;
|
|
837
871
|
void this.consume(q);
|
|
@@ -1369,12 +1403,12 @@ export class ClaudeCodeRuntime {
|
|
|
1369
1403
|
return [{ id: "anthropic", name: "Anthropic", oauth: true, models: FALLBACK_MODELS }];
|
|
1370
1404
|
}
|
|
1371
1405
|
async createSession(options) {
|
|
1372
|
-
const session = new ClaudeSession(this.options, options.workspace, options.toolInterceptor);
|
|
1406
|
+
const session = new ClaudeSession(this.options, options.workspace, options.toolInterceptor, options.toolProvider);
|
|
1373
1407
|
this.sessions.push(session);
|
|
1374
1408
|
return { session };
|
|
1375
1409
|
}
|
|
1376
1410
|
async openSession(options) {
|
|
1377
|
-
const session = new ClaudeSession(this.options, options.workspace, options.toolInterceptor, options.sessionFile);
|
|
1411
|
+
const session = new ClaudeSession(this.options, options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
|
|
1378
1412
|
this.sessions.push(session);
|
|
1379
1413
|
return {
|
|
1380
1414
|
session,
|
|
@@ -44,6 +44,21 @@ export const BIVY_MCP_TOOLS = [
|
|
|
44
44
|
additionalProperties: false,
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
|
+
{
|
|
48
|
+
name: "start_run",
|
|
49
|
+
description: "Delegate one bounded task to another governed Bivy Run or Machine. Returns lifecycle and safe references only, never the child transcript, raw tool output, secrets, or file contents.",
|
|
50
|
+
inputSchema: { type: "object", properties: { instructions: { type: "string", maxLength: 16000 }, repo: { type: "string" }, machine: { type: "string" }, agent: { type: "string" }, model: { type: "string" }, safety: { type: "object", properties: { approval: { type: "string", enum: ["never", "risky", "always", "autonomous"] }, sandbox: { type: "string", enum: ["read-only", "workspace-write", "danger-full-access"] }, maxAttempts: { type: "integer", minimum: 1, maximum: 10 } }, additionalProperties: false }, idempotencyKey: { type: "string", maxLength: 128 } }, required: ["instructions"], additionalProperties: false },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "get_run_status",
|
|
54
|
+
description: "Inspect a child Run started by this Session. Returns bounded lifecycle, check states, and safe output references only.",
|
|
55
|
+
inputSchema: { type: "object", properties: { runId: { type: "string" } }, required: ["runId"], additionalProperties: false },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "wait_for_run",
|
|
59
|
+
description: "Wait up to 300 seconds for a child Run. A wait timeout is reported honestly and does not cancel the child.",
|
|
60
|
+
inputSchema: { type: "object", properties: { runId: { type: "string" }, timeoutSeconds: { type: "number", minimum: 1, maximum: 300 } }, required: ["runId", "timeoutSeconds"], additionalProperties: false },
|
|
61
|
+
},
|
|
47
62
|
];
|
|
48
63
|
/**
|
|
49
64
|
* Perform an `attach_to_chat` call by POSTing to the node's attach endpoint,
|
|
@@ -80,6 +95,25 @@ export async function runAttachToChat(endpoint, sessionId, args, fetchImpl, toke
|
|
|
80
95
|
const name = body?.name || filePath;
|
|
81
96
|
return { isError: false, text: `Attached ${name} to the chat as ${body?.kind === "image" ? "an inline image" : "a downloadable file"}. The user can see it now.` };
|
|
82
97
|
}
|
|
98
|
+
export async function runBivyRunTool(endpoint, sessionId, name, args, fetchImpl, token) {
|
|
99
|
+
if (!sessionId)
|
|
100
|
+
return { isError: true, text: "No active Bivy Session (BIVY_SESSION_ID is not set)." };
|
|
101
|
+
const suffix = name === "start_run" ? "delegated-runs" : name === "get_run_status" ? `delegated-runs/${encodeURIComponent(String(args.runId ?? ""))}` : `delegated-runs/${encodeURIComponent(String(args.runId ?? ""))}/wait`;
|
|
102
|
+
const headers = { "content-type": "application/json" };
|
|
103
|
+
if (token)
|
|
104
|
+
headers.authorization = `Bearer ${token}`;
|
|
105
|
+
try {
|
|
106
|
+
const method = name === "get_run_status" ? "GET" : "POST";
|
|
107
|
+
const res = await fetchImpl(`${endpoint.replace(/\/+$/, "")}/api/session/${encodeURIComponent(sessionId)}/${suffix}`, { method, headers, ...(method === "POST" ? { body: JSON.stringify(args) } : {}) });
|
|
108
|
+
const body = await res.json().catch(() => ({}));
|
|
109
|
+
if (!res.ok)
|
|
110
|
+
return { isError: true, text: typeof body.error === "string" ? body.error : `Bivy node returned ${res.status}` };
|
|
111
|
+
return { isError: false, text: JSON.stringify(body) };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return { isError: true, text: `Could not reach the Bivy node: ${error instanceof Error ? error.message : String(error)}` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
83
117
|
/** Build the Bivy MCP `Server` with tools/list + tools/call handlers wired. */
|
|
84
118
|
export function createBivyMcpServer(deps = {}) {
|
|
85
119
|
const endpoint = deps.endpoint ?? process.env.BIVY_MCP_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
@@ -89,10 +123,12 @@ export function createBivyMcpServer(deps = {}) {
|
|
|
89
123
|
const server = new Server({ name: "bivy", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
90
124
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: BIVY_MCP_TOOLS }));
|
|
91
125
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
const args = (req.params.arguments ?? {});
|
|
127
|
+
const result = req.params.name === "attach_to_chat"
|
|
128
|
+
? await runAttachToChat(endpoint, sessionId, args, fetchImpl, token)
|
|
129
|
+
: ["start_run", "get_run_status", "wait_for_run"].includes(req.params.name)
|
|
130
|
+
? await runBivyRunTool(endpoint, sessionId, req.params.name, args, fetchImpl, token)
|
|
131
|
+
: { isError: true, text: `Unknown tool: ${req.params.name}` };
|
|
96
132
|
return { isError: result.isError, content: [{ type: "text", text: result.text }] };
|
|
97
133
|
});
|
|
98
134
|
return server;
|
|
@@ -26,12 +26,16 @@ export class IntegrationManager {
|
|
|
26
26
|
/** Backs the native `attach_to_chat` tool (see toolProvider); undefined = the
|
|
27
27
|
* tool isn't offered (e.g. a test harness that never wired one). */
|
|
28
28
|
attachToChat;
|
|
29
|
-
|
|
29
|
+
/** Additional Bivy-owned tools (for example governed child Runs), bound to
|
|
30
|
+
* the same lazy Session id as attach_to_chat. */
|
|
31
|
+
builtInToolProvider;
|
|
32
|
+
constructor(appDir, registry = BUILT_IN_INTEGRATIONS, attachToChat, builtInToolProvider) {
|
|
30
33
|
this.store = new IntegrationStore(appDir);
|
|
31
34
|
this.secrets = new SecretVault(appDir);
|
|
32
35
|
this.registry = registry;
|
|
33
36
|
this.riskyTools = new Set(registry.flatMap((d) => d.tools.filter((t) => t.risky).map((t) => t.name)));
|
|
34
37
|
this.attachToChat = attachToChat;
|
|
38
|
+
this.builtInToolProvider = builtInToolProvider;
|
|
35
39
|
}
|
|
36
40
|
// --- helpers ------------------------------------------------------------
|
|
37
41
|
def(id) {
|
|
@@ -280,6 +284,13 @@ export class IntegrationManager {
|
|
|
280
284
|
});
|
|
281
285
|
}
|
|
282
286
|
}
|
|
287
|
+
if (this.builtInToolProvider && sessionIdRef) {
|
|
288
|
+
const provider = this.builtInToolProvider(sessionIdRef);
|
|
289
|
+
for (const spec of provider.list()) {
|
|
290
|
+
specs.push(spec);
|
|
291
|
+
executors.set(spec.name, (params, signal) => provider.invoke(spec.name, `bivy-${randomUUID()}`, params, signal));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
283
294
|
if (this.attachToChat && sessionIdRef) {
|
|
284
295
|
const attachToChat = this.attachToChat;
|
|
285
296
|
specs.push({
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
export const RUN_TOOL_LIMITS = {
|
|
5
|
+
maxInstructions: 16_000,
|
|
6
|
+
maxDepth: 3,
|
|
7
|
+
maxConcurrentChildren: 3,
|
|
8
|
+
maxWaitSeconds: 300,
|
|
9
|
+
minPollMs: 1_000,
|
|
10
|
+
maxIdempotencyKey: 128,
|
|
11
|
+
};
|
|
12
|
+
const TERMINAL = new Set(["succeeded", "failed", "cancelled"]);
|
|
13
|
+
const STATUSES = new Set(["pending", "claimed", "running", "waiting", "needs_attention", "succeeded", "failed", "cancelled"]);
|
|
14
|
+
const SOURCE_PREFIX = "agent-delegation:v1:";
|
|
15
|
+
const bounded = (v, n) => typeof v === "string" && v ? v.slice(0, n) : undefined;
|
|
16
|
+
function idempotencyDigest(key) { return key ? createHash("sha256").update(key).digest("base64url").slice(0, 22) : undefined; }
|
|
17
|
+
export function delegationSource(parentSessionId, parentRunId, depth, idempotencyKey) {
|
|
18
|
+
const digest = idempotencyDigest(idempotencyKey);
|
|
19
|
+
return `${SOURCE_PREFIX}${depth}:${Buffer.from(parentSessionId).toString("base64url")}:${parentRunId ? Buffer.from(parentRunId).toString("base64url") : "-"}${digest ? `:${digest}` : ""}`;
|
|
20
|
+
}
|
|
21
|
+
export function parseDelegationSource(source) {
|
|
22
|
+
if (typeof source !== "string" || !source.startsWith(SOURCE_PREFIX))
|
|
23
|
+
return undefined;
|
|
24
|
+
const [depthRaw, sessionRaw, runRaw] = source.slice(SOURCE_PREFIX.length).split(":");
|
|
25
|
+
const depth = Number(depthRaw);
|
|
26
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > RUN_TOOL_LIMITS.maxDepth || !sessionRaw)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
const parentSessionId = Buffer.from(sessionRaw, "base64url").toString("utf8");
|
|
30
|
+
const parentRunId = runRaw && runRaw !== "-" ? Buffer.from(runRaw, "base64url").toString("utf8") : undefined;
|
|
31
|
+
if (!parentSessionId || parentSessionId.length > 256 || (parentRunId?.length ?? 0) > 256)
|
|
32
|
+
return undefined;
|
|
33
|
+
return { parentSessionId, ...(parentRunId ? { parentRunId } : {}), depth };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function safeRun(raw, provenance) {
|
|
40
|
+
const status = STATUSES.has(raw.status) ? raw.status : "pending";
|
|
41
|
+
const output = raw.output && typeof raw.output === "object" ? raw.output : {};
|
|
42
|
+
const references = {
|
|
43
|
+
sessionId: bounded(output.sessionId, 256), branch: bounded(output.branch, 256),
|
|
44
|
+
prUrl: bounded(output.prUrl, 1_024), artifactUrl: bounded(output.artifactUrl, 1_024), failure: bounded(output.failure, 500),
|
|
45
|
+
};
|
|
46
|
+
const checks = Array.isArray(raw.checks) ? raw.checks.slice(0, 50).flatMap((item) => {
|
|
47
|
+
if (!item || typeof item !== "object")
|
|
48
|
+
return [];
|
|
49
|
+
const c = item;
|
|
50
|
+
const name = bounded(c.name, 120);
|
|
51
|
+
const state = bounded(c.status, 40);
|
|
52
|
+
return name && state ? [{ name, status: state, ...(Number.isInteger(c.exitCode) ? { exitCode: Number(c.exitCode) } : {}) }] : [];
|
|
53
|
+
}) : undefined;
|
|
54
|
+
return {
|
|
55
|
+
runId: bounded(raw.id, 256) ?? "unknown", status,
|
|
56
|
+
createdAt: bounded(raw.createdAt, 64), startedAt: bounded(raw.startedAt, 64), completedAt: bounded(raw.completedAt, 64),
|
|
57
|
+
attempt: Number.isInteger(raw.attempt) ? Number(raw.attempt) : undefined,
|
|
58
|
+
maxAttempts: Number.isInteger(raw.maxAttempts) ? Number(raw.maxAttempts) : undefined,
|
|
59
|
+
provenance,
|
|
60
|
+
...(Object.values(references).some(Boolean) ? { references } : {}),
|
|
61
|
+
...(checks?.length ? { checks } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function validateStart(input) {
|
|
65
|
+
if (!input || typeof input !== "object")
|
|
66
|
+
throw new Error("input is required");
|
|
67
|
+
if (typeof input.instructions !== "string" || !input.instructions.trim())
|
|
68
|
+
throw new Error("instructions are required");
|
|
69
|
+
if (input.instructions.length > RUN_TOOL_LIMITS.maxInstructions)
|
|
70
|
+
throw new Error(`instructions must be at most ${RUN_TOOL_LIMITS.maxInstructions} characters`);
|
|
71
|
+
for (const [key, value, max] of [["repo", input.repo, 200], ["machine", input.machine, 120], ["agent", input.agent, 120], ["model", input.model, 200], ["idempotencyKey", input.idempotencyKey, RUN_TOOL_LIMITS.maxIdempotencyKey]]) {
|
|
72
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim() || value.length > max))
|
|
73
|
+
throw new Error(`${key} must be a non-empty string of at most ${max} characters`);
|
|
74
|
+
}
|
|
75
|
+
if (input.safety?.maxAttempts !== undefined && (!Number.isInteger(input.safety.maxAttempts) || input.safety.maxAttempts < 1 || input.safety.maxAttempts > 10))
|
|
76
|
+
throw new Error("safety.maxAttempts must be an integer from 1 to 10");
|
|
77
|
+
}
|
|
78
|
+
export class RunDelegationService {
|
|
79
|
+
backend;
|
|
80
|
+
now;
|
|
81
|
+
sleep;
|
|
82
|
+
cache = new Map();
|
|
83
|
+
startLocks = new Map();
|
|
84
|
+
constructor(backend, now = () => Date.now(), sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
85
|
+
const timer = setTimeout(resolve, ms);
|
|
86
|
+
signal?.addEventListener("abort", () => { clearTimeout(timer); reject(signal.reason ?? new Error("wait cancelled; the child Run may still be running")); }, { once: true });
|
|
87
|
+
})) {
|
|
88
|
+
this.backend = backend;
|
|
89
|
+
this.now = now;
|
|
90
|
+
this.sleep = sleep;
|
|
91
|
+
}
|
|
92
|
+
context(sessionId) {
|
|
93
|
+
const parent = this.backend.parentContext(sessionId);
|
|
94
|
+
if (!parent)
|
|
95
|
+
throw new Error("calling Session is not available");
|
|
96
|
+
const depth = (parent.depth ?? 0) + 1;
|
|
97
|
+
if (depth > RUN_TOOL_LIMITS.maxDepth)
|
|
98
|
+
throw new Error(`delegation depth limit (${RUN_TOOL_LIMITS.maxDepth}) reached`);
|
|
99
|
+
return { parentSessionId: sessionId, ...(parent.parentRunId ? { parentRunId: parent.parentRunId } : {}), depth };
|
|
100
|
+
}
|
|
101
|
+
async authorized(sessionId, runId, force = false) {
|
|
102
|
+
const cached = this.cache.get(`${sessionId}:${runId}`);
|
|
103
|
+
if (!force && cached && this.now() - cached.at < RUN_TOOL_LIMITS.minPollMs)
|
|
104
|
+
return cached.value;
|
|
105
|
+
const raw = await this.backend.get(runId);
|
|
106
|
+
const provenance = raw ? parseDelegationSource(raw.source) : undefined;
|
|
107
|
+
if (!raw || provenance?.parentSessionId !== sessionId)
|
|
108
|
+
throw new Error("delegated Run not found");
|
|
109
|
+
const value = safeRun(raw, provenance);
|
|
110
|
+
this.cache.set(`${sessionId}:${runId}`, { at: this.now(), value });
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
async startRun(sessionId, input) {
|
|
114
|
+
validateStart(input);
|
|
115
|
+
const previous = this.startLocks.get(sessionId) ?? Promise.resolve();
|
|
116
|
+
let release;
|
|
117
|
+
const current = new Promise((resolve) => { release = resolve; });
|
|
118
|
+
this.startLocks.set(sessionId, current);
|
|
119
|
+
await previous;
|
|
120
|
+
try {
|
|
121
|
+
const provenance = this.context(sessionId);
|
|
122
|
+
const recent = await this.backend.listRecent();
|
|
123
|
+
const expectedSource = input.idempotencyKey ? delegationSource(sessionId, provenance.parentRunId, provenance.depth, input.idempotencyKey) : undefined;
|
|
124
|
+
const existing = expectedSource ? recent.find((r) => r.source === expectedSource) : undefined;
|
|
125
|
+
if (existing)
|
|
126
|
+
return safeRun(existing, provenance);
|
|
127
|
+
const active = recent.filter((r) => parseDelegationSource(r.source)?.parentSessionId === sessionId && !TERMINAL.has(r.status)).length;
|
|
128
|
+
if (active >= RUN_TOOL_LIMITS.maxConcurrentChildren)
|
|
129
|
+
throw new Error(`concurrent child Run limit (${RUN_TOOL_LIMITS.maxConcurrentChildren}) reached`);
|
|
130
|
+
const raw = await this.backend.start(sessionId, input, provenance);
|
|
131
|
+
const value = safeRun(raw, provenance);
|
|
132
|
+
this.cache.set(`${sessionId}:${value.runId}`, { at: this.now(), value });
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
release();
|
|
137
|
+
if (this.startLocks.get(sessionId) === current)
|
|
138
|
+
this.startLocks.delete(sessionId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
getRunStatus(sessionId, runId) { return this.authorized(sessionId, runId); }
|
|
142
|
+
async waitForRun(sessionId, runId, timeoutSeconds, signal) {
|
|
143
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > RUN_TOOL_LIMITS.maxWaitSeconds)
|
|
144
|
+
throw new Error(`timeoutSeconds must be from 1 to ${RUN_TOOL_LIMITS.maxWaitSeconds}`);
|
|
145
|
+
const deadline = this.now() + timeoutSeconds * 1_000;
|
|
146
|
+
while (true) {
|
|
147
|
+
if (signal?.aborted)
|
|
148
|
+
throw signal.reason ?? new Error("wait cancelled; the child Run may still be running");
|
|
149
|
+
const result = await this.authorized(sessionId, runId, true);
|
|
150
|
+
if (TERMINAL.has(result.status))
|
|
151
|
+
return result;
|
|
152
|
+
if (this.now() >= deadline)
|
|
153
|
+
return { ...result, timedOut: true, wait: { timedOut: true, childContinues: true } };
|
|
154
|
+
await this.sleep(Math.min(RUN_TOOL_LIMITS.minPollMs, Math.max(1, deadline - this.now())), signal);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const START_SPEC = { name: "start_run", label: "Delegate Run", description: "Delegate one bounded task to another governed Bivy Run or Machine. Returns references only; never the child transcript or file contents.", promptSnippet: "Use start_run for a focused independent review, test, GPU, or implementation task; then use wait_for_run or get_run_status.", parameters: { type: "object", properties: { instructions: { type: "string", maxLength: RUN_TOOL_LIMITS.maxInstructions }, repo: { type: "string" }, machine: { type: "string" }, agent: { type: "string" }, model: { type: "string" }, safety: { type: "object", properties: { approval: { type: "string", enum: ["never", "risky", "always", "autonomous"] }, sandbox: { type: "string", enum: ["read-only", "workspace-write", "danger-full-access"] }, maxAttempts: { type: "integer", minimum: 1, maximum: 10 } }, additionalProperties: false }, idempotencyKey: { type: "string", maxLength: RUN_TOOL_LIMITS.maxIdempotencyKey } }, required: ["instructions"], additionalProperties: false } };
|
|
159
|
+
const STATUS_SPEC = { name: "get_run_status", label: "Inspect delegated Run", description: "Inspect a child Run started by this Session. Returns bounded lifecycle, check states, and safe references only.", parameters: { type: "object", properties: { runId: { type: "string" } }, required: ["runId"], additionalProperties: false } };
|
|
160
|
+
const WAIT_SPEC = { name: "wait_for_run", label: "Wait for delegated Run", description: `Wait for a child Run for at most ${RUN_TOOL_LIMITS.maxWaitSeconds} seconds. A wait timeout does not cancel the child.`, parameters: { type: "object", properties: { runId: { type: "string" }, timeoutSeconds: { type: "number", minimum: 1, maximum: RUN_TOOL_LIMITS.maxWaitSeconds } }, required: ["runId", "timeoutSeconds"], additionalProperties: false } };
|
|
161
|
+
export const RUN_TOOL_SPECS = [START_SPEC, STATUS_SPEC, WAIT_SPEC];
|
|
162
|
+
export function runToolProvider(service, sessionId) {
|
|
163
|
+
const text = (value, field) => { if (typeof value !== "string" || !value.trim())
|
|
164
|
+
throw new Error(`${field} is required`); return value.trim(); };
|
|
165
|
+
return { list: () => [...RUN_TOOL_SPECS], invoke: async (name, _id, params, signal) => {
|
|
166
|
+
try {
|
|
167
|
+
const sid = sessionId();
|
|
168
|
+
if (!sid)
|
|
169
|
+
throw new Error("Session is not ready yet — try again in a moment");
|
|
170
|
+
const p = (params ?? {});
|
|
171
|
+
const result = name === "start_run" ? await service.startRun(sid, p)
|
|
172
|
+
: name === "get_run_status" ? await service.getRunStatus(sid, text(p.runId, "runId"))
|
|
173
|
+
: name === "wait_for_run" ? await service.waitForRun(sid, text(p.runId, "runId"), Number(p.timeoutSeconds), signal)
|
|
174
|
+
: (() => { throw new Error(`Unknown Bivy Run tool: ${name}`); })();
|
|
175
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { run: result } };
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return { content: [{ type: "text", text: (error instanceof Error ? error.message : String(error)).slice(0, 500) }], details: {}, isError: true };
|
|
179
|
+
}
|
|
180
|
+
} };
|
|
181
|
+
}
|
|
@@ -83,7 +83,7 @@ const PLAN = new Set(["plan", "updateplan", "setplan", "todo", "todowrite", "exi
|
|
|
83
83
|
// still falls through to the opaque default, never a false "Delegated".
|
|
84
84
|
const DELEGATE = new Set([
|
|
85
85
|
"task", "agent", "subagent", "runagent", "runsubagent", "spawnagent",
|
|
86
|
-
"dispatchagent", "delegate", "delegatetask", "agenttask", "startagent",
|
|
86
|
+
"dispatchagent", "delegate", "delegatetask", "agenttask", "startagent", "startrun",
|
|
87
87
|
]);
|
|
88
88
|
const PATH_KEYS = ["path", "file_path", "filePath", "filename", "fileName", "file", "target_file", "targetFile"];
|
|
89
89
|
/**
|
package/dist/server.js
CHANGED
|
@@ -111,6 +111,7 @@ import { redactSecrets } from "./redact.js";
|
|
|
111
111
|
import { getSttConfig, isSttProvider, removeSttKey, setSttKey, setSttProvider, transcribeAudio, MAX_AUDIO_BYTES, } from "./stt.js";
|
|
112
112
|
import { synthesizeOpenAiSpeech } from "./tts.js";
|
|
113
113
|
import { seal, open } from "./e2e.js";
|
|
114
|
+
import { RunDelegationService, parseDelegationSource, runToolProvider } from "./run-tools.js";
|
|
114
115
|
import { ControlPlaneTaskPoller, resolveControlPlaneTaskConfig, } from "./control-plane-tasks.js";
|
|
115
116
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
116
117
|
const repoRoot = path.resolve(__dirname, "..");
|
|
@@ -436,7 +437,66 @@ function resolveApproval(id, approved) {
|
|
|
436
437
|
}
|
|
437
438
|
return ok;
|
|
438
439
|
}
|
|
439
|
-
|
|
440
|
+
async function delegatedRunRequest(pathname, init = {}) {
|
|
441
|
+
if (!sessionAdvertiseTarget)
|
|
442
|
+
throw new Error("Hosted Runs are not configured. Run bivy setup first.");
|
|
443
|
+
const headers = new Headers(init.headers);
|
|
444
|
+
headers.set("authorization", `Bearer ${sessionAdvertiseTarget.enrollmentToken}`);
|
|
445
|
+
if (init.body)
|
|
446
|
+
headers.set("content-type", "application/json");
|
|
447
|
+
const response = await fetch(`${sessionAdvertiseTarget.controlPlaneUrl.replace(/\/$/, "")}${pathname}`, { ...init, headers, signal: init.signal ?? AbortSignal.timeout(15_000) });
|
|
448
|
+
const body = await response.json().catch(() => ({}));
|
|
449
|
+
if (!response.ok)
|
|
450
|
+
throw new Error(typeof body.error === "string" ? body.error : `control plane returned ${response.status}`);
|
|
451
|
+
return body;
|
|
452
|
+
}
|
|
453
|
+
const runDelegation = new RunDelegationService({
|
|
454
|
+
parentContext: (sessionId) => {
|
|
455
|
+
const record = openSessions.get(sessionId);
|
|
456
|
+
return record ? { parentRunId: record.automationRunId, depth: record.delegationDepth } : undefined;
|
|
457
|
+
},
|
|
458
|
+
listRecent: async () => {
|
|
459
|
+
const body = await delegatedRunRequest("/node/automation-runs?limit=100");
|
|
460
|
+
return Array.isArray(body.runs) ? body.runs : [];
|
|
461
|
+
},
|
|
462
|
+
get: async (runId) => {
|
|
463
|
+
try {
|
|
464
|
+
return await delegatedRunRequest(`/node/automation-runs/${encodeURIComponent(runId)}`);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
if (error instanceof Error && /not found/i.test(error.message))
|
|
468
|
+
return undefined;
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
start: async (_sessionId, input, provenance) => {
|
|
473
|
+
const machine = input.machine?.trim() || identity.name;
|
|
474
|
+
const nodes = await delegatedRunRequest("/nodes");
|
|
475
|
+
const target = Array.isArray(nodes) ? nodes.find((node) => node && typeof node === "object" && node.name === machine) : undefined;
|
|
476
|
+
const targetId = typeof target?.id === "string" ? target.id : machine === identity.name ? identity.nodeId : undefined;
|
|
477
|
+
if (!targetId)
|
|
478
|
+
throw new Error(`Machine not found on this account: ${machine}`);
|
|
479
|
+
return delegatedRunRequest("/node/automation-runs", {
|
|
480
|
+
method: "POST",
|
|
481
|
+
body: JSON.stringify({
|
|
482
|
+
title: "Delegated Run",
|
|
483
|
+
body: `bivy-room-v1:${targetId}:${seal(pairingStore.roomKey(), input.instructions)}`,
|
|
484
|
+
repo: input.repo,
|
|
485
|
+
node: machine,
|
|
486
|
+
runtimeId: input.agent,
|
|
487
|
+
model: input.model,
|
|
488
|
+
approvalMode: input.safety?.approval,
|
|
489
|
+
sandbox: input.safety?.sandbox,
|
|
490
|
+
maxAttempts: input.safety?.maxAttempts ?? 2,
|
|
491
|
+
idempotencyKey: input.idempotencyKey,
|
|
492
|
+
parentSessionId: provenance.parentSessionId,
|
|
493
|
+
parentRunId: provenance.parentRunId,
|
|
494
|
+
delegationDepth: provenance.depth,
|
|
495
|
+
}),
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
const integrations = new IntegrationManager(appDir, undefined, attachToChatForSession, (ref) => runToolProvider(runDelegation, () => ref.current));
|
|
440
500
|
const terminals = new TerminalManager();
|
|
441
501
|
// Per-session agents: a node holds one AgentRuntime instance *per agent id*,
|
|
442
502
|
// built lazily and cached, instead of a single global runtime. `defaultRuntimeId`
|
|
@@ -4070,8 +4130,12 @@ function withIssueLock(key, fn) {
|
|
|
4070
4130
|
return run;
|
|
4071
4131
|
}
|
|
4072
4132
|
function recordRunAuditCorrelation(record, correlation) {
|
|
4073
|
-
if (correlation)
|
|
4133
|
+
if (correlation) {
|
|
4134
|
+
record.automationRunId = correlation.runId;
|
|
4135
|
+
record.delegationDepth ??= 0;
|
|
4136
|
+
persistSessionMetadata(record);
|
|
4074
4137
|
auditLog.record({ kind: "run.correlation", session: record.id, agent: record.runtimeId, ...correlation });
|
|
4138
|
+
}
|
|
4075
4139
|
}
|
|
4076
4140
|
async function runIssueTask(cfg, issue, overrides = {}) {
|
|
4077
4141
|
const source = `issue:${cfg.owner}/${cfg.repo}#${issue.number}`;
|
|
@@ -4988,7 +5052,8 @@ async function runWorkItem(item, report, signal) {
|
|
|
4988
5052
|
// definition name is metadata, not part of the instructions, so don't fold it
|
|
4989
5053
|
// into the prompt. A scheduled chat message must arrive verbatim; the name is
|
|
4990
5054
|
// only the run/session label.
|
|
4991
|
-
const
|
|
5055
|
+
const delegatedProvenance = parseDelegationSource(item.source);
|
|
5056
|
+
const request = item.source === "schedule" || delegatedProvenance
|
|
4992
5057
|
? (item.body || item.title)
|
|
4993
5058
|
: item.body ? `${item.title}\n\n${item.body}` : item.title;
|
|
4994
5059
|
// Plain chat messages (scheduled "message me later" reminders) skip the
|
|
@@ -5028,7 +5093,10 @@ async function runWorkItem(item, report, signal) {
|
|
|
5028
5093
|
// `repo:owner/repo`, which is required by branch push/PR detection.
|
|
5029
5094
|
if (!parsedRepo && !record.worktree)
|
|
5030
5095
|
record.source = `queue:${item.source}`;
|
|
5096
|
+
record.automationRunId = item.id;
|
|
5097
|
+
record.delegationDepth = delegatedProvenance?.depth ?? 0;
|
|
5031
5098
|
record.approvalMode = safety.approval;
|
|
5099
|
+
persistSessionMetadata(record);
|
|
5032
5100
|
if (item.model) {
|
|
5033
5101
|
try {
|
|
5034
5102
|
await record.session.setModel("", item.model);
|
|
@@ -6143,6 +6211,8 @@ function persistSessionMetadata(record, status = sessionStatus(record)) {
|
|
|
6143
6211
|
workspace: record.workspace,
|
|
6144
6212
|
source: record.source ?? "manual",
|
|
6145
6213
|
forkedFrom: record.forkedFrom,
|
|
6214
|
+
automationRunId: record.automationRunId,
|
|
6215
|
+
delegationDepth: record.delegationDepth,
|
|
6146
6216
|
runtimeId: record.runtimeId,
|
|
6147
6217
|
sandbox: record.sandbox,
|
|
6148
6218
|
agentName: getRuntime(record.runtimeId).displayName,
|
|
@@ -7843,7 +7913,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
7843
7913
|
// Rehydrating (rather than leaving this undefined for a resumed session)
|
|
7844
7914
|
// also avoids a persistSessionMetadata call later silently clobbering the
|
|
7845
7915
|
// stored contract with undefined via its `{...prev, ...input}` merge.
|
|
7846
|
-
const record = { id: sessionId, session, runtimeId: rt.id, sandbox: sessionSandbox, approvalMode: sessionSafety.approval, workspace: sessionWorkspace, sessionFile: session.sessionFile, agentServiceAddress: attachedAddress ?? rt.agentServiceAddress, worktree, source, prUrl: storedMeta?.prUrl, prs: storedMeta?.prs, contract: storedMeta?.contract, lastTouchedAt: resumedLastActive ?? Date.now(), warning: modelFallbackMessage, ephemeral: opts.ephemeral, workspaceState: runGit(["status", "--porcelain", "--untracked-files=normal"], sessionWorkspace) ? "dirty" : "clean" };
|
|
7916
|
+
const record = { id: sessionId, session, runtimeId: rt.id, sandbox: sessionSandbox, approvalMode: sessionSafety.approval, automationRunId: storedMeta?.automationRunId, delegationDepth: storedMeta?.delegationDepth, workspace: sessionWorkspace, sessionFile: session.sessionFile, agentServiceAddress: attachedAddress ?? rt.agentServiceAddress, worktree, source, prUrl: storedMeta?.prUrl, prs: storedMeta?.prs, contract: storedMeta?.contract, lastTouchedAt: resumedLastActive ?? Date.now(), warning: modelFallbackMessage, ephemeral: opts.ephemeral, workspaceState: runGit(["status", "--porcelain", "--untracked-files=normal"], sessionWorkspace) ? "dirty" : "clean" };
|
|
7847
7917
|
// Migration: a session resumed/reopened from before this feature (or from a
|
|
7848
7918
|
// node that predates it) has no stored contract. Stamp an honest one now
|
|
7849
7919
|
// from currently-observed facts rather than leaving it blank forever or
|
|
@@ -10408,6 +10478,33 @@ app.post("/api/session/:id/attach", (req, res) => {
|
|
|
10408
10478
|
const { hash, name, mimeType, size, kind } = result.ref;
|
|
10409
10479
|
res.json({ ok: true, hash, name, mimeType, size, kind });
|
|
10410
10480
|
});
|
|
10481
|
+
// Bivy-owned child Run tools for MCP agents. These use the exact same service as
|
|
10482
|
+
// Pi's native ToolProvider and remain behind /api authentication. The service
|
|
10483
|
+
// authorizes every status lookup against this parent Session's provenance.
|
|
10484
|
+
app.post("/api/session/:id/delegated-runs", async (req, res) => {
|
|
10485
|
+
try {
|
|
10486
|
+
res.status(201).json(await runDelegation.startRun(String(req.params.id), req.body));
|
|
10487
|
+
}
|
|
10488
|
+
catch (error) {
|
|
10489
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
10490
|
+
}
|
|
10491
|
+
});
|
|
10492
|
+
app.get("/api/session/:id/delegated-runs/:runId", async (req, res) => {
|
|
10493
|
+
try {
|
|
10494
|
+
res.json(await runDelegation.getRunStatus(String(req.params.id), String(req.params.runId)));
|
|
10495
|
+
}
|
|
10496
|
+
catch (error) {
|
|
10497
|
+
res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
|
|
10498
|
+
}
|
|
10499
|
+
});
|
|
10500
|
+
app.post("/api/session/:id/delegated-runs/:runId/wait", async (req, res) => {
|
|
10501
|
+
try {
|
|
10502
|
+
res.json(await runDelegation.waitForRun(String(req.params.id), String(req.params.runId), Number(req.body?.timeoutSeconds), AbortSignal.timeout(305_000)));
|
|
10503
|
+
}
|
|
10504
|
+
catch (error) {
|
|
10505
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
10506
|
+
}
|
|
10507
|
+
});
|
|
10411
10508
|
app.post("/api/session/prompt", async (req, res, next) => {
|
|
10412
10509
|
try {
|
|
10413
10510
|
const text = String(req.body?.text ?? "").trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bivy/bivy",
|
|
3
|
-
"version": "0.10.1-staging.
|
|
3
|
+
"version": "0.10.1-staging.301",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",
|
|
@@ -63,6 +63,6 @@
|
|
|
63
63
|
"brace-expansion": "5.0.9",
|
|
64
64
|
"undici": "8.9.0"
|
|
65
65
|
},
|
|
66
|
-
"readme": "# Bivy\n\n**Run agents where your environment lives. Reach, automate, and govern them from\nanywhere.**\n\nBivy makes coding agents on your infrastructure available from anywhere. Use\nClaude Code or Codex with the repositories, tools, services, and compute already\nin your development environment; continue the same Session from a phone, leave\nwork running, and return to checked changes or a pull request with a clear\nReceipt.\n\nInteractive Session traffic is end-to-end encrypted between a Machine and its\npaired devices. The relay cannot decrypt it. Explicitly enabled hosted\nprovisioning has a different boundary: the control plane may hold encrypted\ncloud, repository, or key-escrow material that the service can technically\naccess. Slack and generic webhook instructions also reach the control plane in\nplaintext. See [Why Bivy](docs/why-bivy.md) and the\n[security model](docs/security-model.md) for the complete trust model.\n\n- **Website:** [bivy.sh](https://bivy.sh)\n- **Documentation:** [`docs/`](docs/README.md) — start with the [quickstart](docs/quickstart.md)\n- **License:** [AGPL-3.0-only](LICENSE)\n\n> Bivy is 0.x software. The core loop is solid and used daily, but interfaces\n> and behaviour can change between releases.\n\n## Install\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nmacOS and Linux. Requires Node.js 22.19 or newer; the installer installs it for\nyou on Debian/Ubuntu and otherwise points you at nodejs.org. It installs the\n[`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) package from npm, puts\nthe `bivy` command on your `PATH`, then runs the guided `bivy setup` wizard —\nagent choice, relay/control-plane sign-in, and an auto-start background service\n(launchd on macOS, systemd on Linux). Re-running it on a machine that already\nhas Bivy just applies the latest build and restarts the service.\n\nAlready have Node.js 22.19+? The installer is optional:\n\n```bash\nnpm install -g @bivy/bivy\nbivy setup\n```\n\nOr try it once without installing anything (`npx` always fetches the latest):\n\n```bash\nnpx @bivy/bivy setup\n```\n\nReleases are published from CI with provenance attestations; verify a build's\norigin with `npm audit signatures`. See [`docs/releasing.md`](docs/releasing.md).\n\n### Install options\n\nEnvironment variables passed to the one-line installer change what it does:\n\n| Goal | Variable |\n|---|---|\n| Track the dev channel (new build on every merge to `main`) | `BIVY_CHANNEL=staging` |\n| Pin an exact version | `BIVY_VERSION=0.1.0` |\n| Install without sudo, into a user-owned prefix | `BIVY_NPM_PREFIX=~/.local` |\n| Preinstall every known upstream agent | `BIVY_INSTALL_ALL_AGENTS=1` |\n\nFor example: `BIVY_CHANNEL=staging curl -fsSL https://bivy.sh/install.sh | bash`.\n\nWorking from a checkout of this repository instead:\n\n```bash\npnpm install\npnpm run setup\n```\n\nSee [`docs/install.md`](docs/install.md) for where data lives, service\nmanagement, and uninstall.\n\n## Updating\n\n```bash\nbivy update\n```\n\n`bivy update` detects how Bivy was installed and does the right thing, then\nwaits for any active session to finish its current turn and restarts the\nbackground service so the node reconnects on the new build:\n\n| Install kind | What `bivy update` does |\n|---|---|\n| npm global (`npm i -g`) | `npm install -g @bivy/bivy@<channel>`, then restart the service |\n| installer / packaged | re-runs `install.sh` (migrating to npm if needed), then restart |\n| git checkout | `git pull --ff-only` + `pnpm install --frozen-lockfile`, then restart |\n| `npx` run | nothing to update — each run already fetches the latest |\n\nUpdates follow the release **channel** recorded at install time — `latest`\n(production) by default, or `staging` if you installed with\n`BIVY_CHANNEL=staging`. Switch channels (the choice is remembered for next\ntime), or skip the wait for a busy session:\n\n```bash\nbivy update --staging # move to the dev channel\nbivy update --stable # move back to production (latest)\nbivy update --force # don't wait for an in-flight turn to finish\n```\n\nThe daemon also checks the registry periodically and posts an in-session notice\nwhen a newer build is available.\n\n## Architecture\n\nBivy has three parts. Only the first one holds your data.\n\n```text\n your machine hosted or self-hosted\n\n ┌──────────────┐ ┌─────────┐ ┌───────────────┐\n │ node daemon │ ──dials──▶ │ relay │ ◀────▶ │ control plane │\n │ agents, keys │ outbound │ opaque │ │ accounts, web │\n │ repo, tools │ │ frames │ │ app, metadata │\n └──────────────┘ └─────────┘ └───────────────┘\n ▲ ▲\n └────────── end-to-end encrypted session ───────────┘\n phone · browser · another terminal\n```\n\n- **Node** — a daemon on your machine. Owns the workspace, credentials, and agent\n processes. Serves an API and WebSocket on `http://localhost:4317` plus a\n `/healthz` probe. **It hosts no web UI.**\n- **Relay** — forwards encrypted frames between your node and your devices. Your\n node dials out, so no inbound port is opened. The relay cannot read the frames.\n- **Control plane** — holds your account, node registry, and session index, and\n serves the web/PWA client. Use the hosted one or run your own.\n\nBecause the node serves no UI, a browser or phone needs a control plane — hosted\nat `app.bivy.sh`, or one you deploy yourself. The terminal CLI needs neither.\n\nSee [`docs/remote-access.md`](docs/remote-access.md) and\n[`docs/security-model.md`](docs/security-model.md).\n\n## Supported agents\n\n**Claude Code and Codex are the recommended, release-certified paths.** The\nbroader catalog remains available under **More agents** for users who need it;\ncapabilities and fidelity vary by runtime:\n\n| Agent | Command | Notes |\n|---|---|---|\n| Pi | `bivy run pi` | Uses the operator-installed `pi` command and Pi auth/config |\n| Claude Code | `bivy run claude` | Uses the operator-installed `claude` command through an SDK bridge |\n| Codex | `bivy run codex` | Installs `@openai/codex` |\n| OpenCode | `bivy run opencode` | Installs `opencode-ai` |\n| Gemini CLI | `bivy run gemini` | Installs `@google/gemini-cli` |\n| Qwen Code | `bivy run qwen` | Installs `@qwen-code/qwen-code` |\n| Goose | `bivy run goose` | Requires `goose` on PATH |\n| Aider | `bivy run aider` | No session resume (upstream gap) |\n| Cline | `bivy run cline` | Installs `cline` |\n| Crush | `bivy run crush` | No session resume (upstream gap) |\n| Cursor | `bivy run cursor` | ACP-capable |\n| GitHub Copilot | `bivy run copilot` | ACP-capable |\n| Grok | `bivy run grok` | Model selection |\n| Amp | `bivy run amp` | Native thread resume |\n| Auggie | `bivy run auggie` | Headless CLI |\n| Droid | `bivy run droid` | Model selection |\n| Continue | `bivy run continue` | Headless CLI |\n| Kilo Code | `bivy run kilocode` | ACP-capable |\n| Rovo Dev | `bivy run rovodev` | Installed out of band |\n\nAny other command works via `bivy run -- ./your-agent --flags`. ACP-capable\nagents can be promoted to Bivy's governed protocol path for per-tool approvals\nand native resume. To add a reusable process or ACP agent to both the CLI and web\npicker without changing Bivy, run `bivy agent add`, or scaffold and install a\ndeclarative [plugin manifest](docs/plugins.md) with `bivy plugin init`. Both use\nthe same schema/store; the plugin SDK, diagnostics, and ACP conformance fixtures\nsupport distributable or custom protocol bridges.\n\n[`docs/runtime-support-matrix.md`](docs/runtime-support-matrix.md) lists exactly\nwhat each agent supports — resume, model selection, approvals, sandboxing.\n\n## Common commands\n\n```bash\nbivy # show the command overview\nbivy run pi # launch Pi as a durable session\nbivy run claude # run a different agent\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires relay setup)\nbivy automation init # create .bivy/automations.yaml\nbivy agent add # connect an existing ACP or process agent\nbivy plugin list # installed declarative integration packages\nbivy status # config summary and node reachability\nbivy doctor # health check\nbivy logs -f # tail node logs\nbivy update # update Bivy and restart the service\n```\n\nFull command list, flags, and examples: [`docs/cli-reference.md`](docs/cli-reference.md).\n\n## Configuration\n\nThe common knobs:\n\n```bash\nBIVY_WORKSPACE=/path/to/repo # default workspace\nBIVY_SANDBOX=read-only # read-only | workspace-write (default) | danger-full-access\nBIVY_APPROVAL_MODE=risky # never | risky | always | autonomous (default)\n```\n\nCreate and inspect the typed node configuration, or add repository-owned\nsafety/check/retry policy:\n\n```bash\nbivy config init\nbivy config set defaults.agent codex\nbivy config explain defaults.sandbox\nbivy config init --project # .bivy/policy.yaml\n```\n\nSee [`docs/config-as-code.md`](docs/config-as-code.md). Every environment\nvariable and precedence rule remains in\n[`docs/configuration.md`](docs/configuration.md).\n\n## Approvals and sandboxing\n\nThe default approval mode is **`autonomous`**: agents act without per-action\nprompts. The actual protection depends on the selected runtime. Native-sandbox\nagents enforce the chosen access tier; structured runtimes also pass tool calls\nthrough Bivy's policy and approval layer. Process agents that Bivy cannot\nintercept run with your OS user permissions. The picker shows this distinction\nand requires confirmation before selecting that limited path.\n\nWhere Bivy receives structured shell/file calls, a heuristic floor blocks known\ncatastrophic commands and structured writes outside the workspace, and a\nbackstop set (force-push, publish, deploy, sudo) pauses for a human. This catches\naccidents; it is not an adversarial isolation boundary.\n\nIf you want to be asked about more, set the mode explicitly:\n\n```bash\nBIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits\nBIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits\nBIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available\n```\n\nApprove from the terminal, browser, or phone.\n\nSandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced\nnatively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.\nAgents without a native sandbox may expose structured tool or MCP controls, but\nthose controls do not cover activity the agent performs outside those channels;\nsome process adapters run entirely with your user permissions. Check the\npicker's Protection label. **Bivy does not currently ship its own OS-level jail.**\n\n## Credentials\n\nInteractive prompts, transcripts, and workspace files stay encrypted across the\nrelay. Credentials can remain on a Machine or in a vault you control. If you\nexplicitly enable hosted unattended provisioning, Bivy Cloud may instead store\nencrypted cloud, repository, model, or key-escrow material that the service can\ntechnically access. Treat this as an explicit hosted-custody mode, not as relay\nblindness. See the\n[security model](docs/security-model.md#what-the-control-plane-sees).\n\n```bash\nbivy secrets list\nbivy secrets set github.repo-token\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\nbivy secrets doctor\n```\n\n`secret://`, `env://`, and `op://` (1Password) references are resolved on demand\nwhen the daemon provisions an agent run, so the raw values never sit in your\nconfig. See [`docs/key-management.md`](docs/key-management.md).\n\n## Automations as code\n\nDefine governed jobs in `.bivy/automations.yaml`, validate them, and simulate\ntrigger events locally before applying anything:\n\n```bash\nbivy automation init\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml\nbivy automation apply\n```\n\nInstructions are encrypted on the applying node before upload. Safety policy\nlives beside the job—sandbox, approval mode, and a hard attempt ceiling that\nretry/fallback rules cannot exceed. See\n[`docs/automations-as-code.md`](docs/automations-as-code.md).\n\n## GitHub Runs\n\nLabel an issue `bivy` (or `bivy/<machine>` to target a Machine), or mention the\nBivy GitHub App in a comment. Bivy creates a Run on the selected Machine, uses an\nisolated worktree, executes configured checks, and reports an explicit outcome.\n\nAvailable on every plan. On Bivy Cloud, the free trial shows the first 25\ndistinct sessions in the hosted app and includes 10 unattended automations per\nrolling 7-day window across GitHub, Slack, webhooks, and schedules. Sessions keep\nrunning on your machines after the hosted trial is exhausted, but new ones are\nhidden from the hosted app until you subscribe. Pro removes both limits.\nSelf-hosted stacks are unlimited.\n\nA private GitHub App only installs on the account that owns it, so connect one\napp per GitHub account — one for your personal repos, one per organization\n(`bivy github:app-create --org <org>`). A node can serve several at once, each\nwith its own key and `@`-mention handle.\n\nSee [`docs/github-work-queue.md`](docs/github-work-queue.md).\n\n## Linear Runs\n\nApply `bivy` or `bivy/<machine>` to a Linear issue to create a Run on the selected\nMachine. The Machine fetches issue content directly from Linear, works in an\nisolated GitHub worktree, and asks the agent to open a pull request. See\n[`docs/linear-work-queue.md`](docs/linear-work-queue.md).\n\n## Development\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server (proxies /api and /ws to the node)\n```\n\nChecks — all of these run in CI:\n\n```bash\npnpm run typecheck\npnpm run typecheck:web\npnpm run lint\npnpm run test:unit\npnpm run test:core\npnpm run check:licenses\npnpm run check:secrets\n```\n\nRepository layout:\n\n- `src/` — node daemon, runtime adapters, approvals, secrets, sessions\n- `bin/` — the `bivy` CLI\n- `packages/core` — shared protocol, pairing, wire format\n- `packages/web` — the React/Vite PWA client (`@bivy/web`)\n- `services/relay` — self-hostable relay\n- `services/control-plane` — self-hostable control plane\n- `deploy/` — self-host deployment examples\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md).\n\n## Self-hosting\n\nNode, relay, and control plane are all in this repository. Point a node at your\nown deployment by passing URLs to `bivy relay:setup` — re-running it switches an\nexisting node over to the new endpoints:\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nEach URL has a flag and an environment-variable equivalent (the flag wins):\n\n| Flag | Environment variable | Points at | Default |\n|---|---|---|---|\n| `--control-plane <url>` | `BIVY_CONTROL_PLANE_URL` | accounts, node registry, and the web-app API | hosted (`app.bivy.sh`) |\n| `--relay <wss-url>` | `BIVY_RELAY_URL` | the encrypted-frame relay your node dials out to | hosted |\n| `--client <url>` | `BIVY_CLIENT_BASE_URL` | base URL used when building app/PWA links | the `--control-plane` URL |\n\nSign-in defaults to GitHub device login (`--github`); pass\n`--email you@example.com` for an email magic-link, or `--session-token <token>`\nto skip interactive sign-in. `relay:setup` checks the control plane is reachable, enrolls\nthis node, and writes the endpoints to `.bivy/relay.json`, so `bivy open`,\n`bivy link`, and `bivy update` all keep using your deployment afterwards.\n\n**Self-hosting is unsupported** — no SLA, community best-effort via GitHub\nissues. You own TLS, backups, upgrades, and hardening. See\n[`docs/self-host.md`](docs/self-host.md).\n\n## Security\n\nReport vulnerabilities through [GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new).\nPlease do not open a public issue. See [`SECURITY.md`](SECURITY.md) for scope,\nresponse times, and safe harbour, and [`docs/security-model.md`](docs/security-model.md)\nfor the trust model and known limitations.\n\n## License\n\nBivy Core is free and open-source software licensed under the GNU Affero General\nPublic License, version 3.0 only (AGPL-3.0-only). You may use, study, modify, and\nself-host it under that license. If you modify Bivy and let users interact with\nit over a network, section 13 requires you to offer those users the corresponding\nsource code.\n\nSee [`LICENSE`](LICENSE), [`CORE.md`](CORE.md), and [`CLOUD.md`](CLOUD.md).\n",
|
|
66
|
+
"readme": "# Bivy\n\n**Give coding agents your real environment, then reach them from anywhere.**\n\nA provider sandbox starts from an approximation. A Bivy Machine can use the\nrepository, local services and databases, private networks, existing tools and\ncaches, and GPUs or local inference already available on your workstation or\nserver. Bivy turns that local capability into Sessions you can continue remotely\nand Runs you can leave working unattended.\n\n## What Bivy lets you do\n\n- **Work where the full environment lives.** Run Claude Code, Codex, Pi, or\n another agent beside real repos, dev servers, databases, internal APIs,\n toolchains, package caches, and specialized compute.\n- **Continue from a phone, browser, or terminal.** Reconnect to the same Session,\n steer or stop it, answer questions, and approve supported tool calls. The PWA\n supports voice input and read-aloud, phone-to-agent file/image uploads, and\n agent-to-phone attachments. Native terminal Sessions can stay remote-visible;\n supported runtimes can hand work between terminal and structured chat.\n- **Move work without starting over.** Import existing Claude Code and Codex\n Sessions, or fork/copy/move a Bivy Session to another agent, model, or Machine.\n Fidelity is runtime-dependent: a fork may use a native transcript, replayed\n history, or a bounded seeded continuation, and cross-Machine moves require the\n destination's repo access, agent, and credentials.\n- **Operate more than one Machine.** Keep a workstation, private-network server,\n and GPU box on one account; select the environment a Session or Run needs.\n Optional warm Session replication is Beta, off by default, and manually\n promoted rather than automatic failover.\n- **Let events start checked work.** Create Runs manually or from failed CI,\n GitHub/Linear issues, Slack, schedules, and signed webhooks. Automations can\n pin a Machine, agent, model, sandbox, approval mode, and attempt ceiling, then\n report bounded check and outcome evidence in a Receipt.\n- **Bring your own stack.** Use provider subscriptions through supported native\n agent logins, API keys in Bivy's vault, or local/OpenAI-compatible inference.\n Add an existing ACP or headless process agent with `bivy agent add`; declarative\n agent plugins are Experimental (`v1alpha1`) and run out of process.\n\nStart with the [five-minute quickstart](docs/quickstart.md), then try the\n[capability recipes](docs/capability-recipes.md) or check the exact\n[runtime support matrix](docs/runtime-support-matrix.md).\n\nInteractive Session traffic is end-to-end encrypted between a Machine and its\npaired devices, so the relay cannot decrypt it. The seatbelt has a precise edge:\nexplicitly enabled hosted provisioning may give the control plane technical\naccess to encrypted cloud, repository, or key-escrow material, while Slack and\ngeneric webhook instructions reach it in plaintext. See [Why Bivy](docs/why-bivy.md)\nand the [security model](docs/security-model.md) for the complete trust model.\n\n- **Website:** [bivy.sh](https://bivy.sh)\n- **Documentation:** [`docs/`](docs/README.md)\n- **License:** [AGPL-3.0-only](LICENSE)\n\n> Bivy is 0.x software. The core loop is solid and used daily, but interfaces,\n> cross-runtime fidelity, and behaviour can change between releases. Check the\n> support matrix before depending on a particular agent capability.\n\n## Install\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nmacOS and Linux. Requires Node.js 22.19 or newer; the installer installs it for\nyou on Debian/Ubuntu and otherwise points you at nodejs.org. It installs the\n[`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) package from npm, puts\nthe `bivy` command on your `PATH`, then runs the guided `bivy setup` wizard —\nagent choice, relay/control-plane sign-in, and an auto-start background service\n(launchd on macOS, systemd on Linux). Re-running it on a machine that already\nhas Bivy just applies the latest build and restarts the service.\n\nAlready have Node.js 22.19+? The installer is optional:\n\n```bash\nnpm install -g @bivy/bivy\nbivy setup\n```\n\nOr try it once without installing anything (`npx` always fetches the latest):\n\n```bash\nnpx @bivy/bivy setup\n```\n\nReleases are published from CI with provenance attestations; verify a build's\norigin with `npm audit signatures`. See [`docs/releasing.md`](docs/releasing.md).\n\n### Install options\n\nEnvironment variables passed to the one-line installer change what it does:\n\n| Goal | Variable |\n|---|---|\n| Track the dev channel (new build on every merge to `main`) | `BIVY_CHANNEL=staging` |\n| Pin an exact version | `BIVY_VERSION=0.1.0` |\n| Install without sudo, into a user-owned prefix | `BIVY_NPM_PREFIX=~/.local` |\n| Preinstall every known upstream agent | `BIVY_INSTALL_ALL_AGENTS=1` |\n\nFor example: `BIVY_CHANNEL=staging curl -fsSL https://bivy.sh/install.sh | bash`.\n\nWorking from a checkout of this repository instead:\n\n```bash\npnpm install\npnpm run setup\n```\n\nSee [`docs/install.md`](docs/install.md) for where data lives, service\nmanagement, and uninstall.\n\n## Updating\n\n```bash\nbivy update\n```\n\n`bivy update` detects how Bivy was installed and does the right thing, then\nwaits for any active session to finish its current turn and restarts the\nbackground service so the node reconnects on the new build:\n\n| Install kind | What `bivy update` does |\n|---|---|\n| npm global (`npm i -g`) | `npm install -g @bivy/bivy@<channel>`, then restart the service |\n| installer / packaged | re-runs `install.sh` (migrating to npm if needed), then restart |\n| git checkout | `git pull --ff-only` + `pnpm install --frozen-lockfile`, then restart |\n| `npx` run | nothing to update — each run already fetches the latest |\n\nUpdates follow the release **channel** recorded at install time — `latest`\n(production) by default, or `staging` if you installed with\n`BIVY_CHANNEL=staging`. Switch channels (the choice is remembered for next\ntime), or skip the wait for a busy session:\n\n```bash\nbivy update --staging # move to the dev channel\nbivy update --stable # move back to production (latest)\nbivy update --force # don't wait for an in-flight turn to finish\n```\n\nThe daemon also checks the registry periodically and posts an in-session notice\nwhen a newer build is available.\n\n## Architecture\n\nBivy has three parts. Only the first one holds your data.\n\n```text\n your machine hosted or self-hosted\n\n ┌──────────────┐ ┌─────────┐ ┌───────────────┐\n │ node daemon │ ──dials──▶ │ relay │ ◀────▶ │ control plane │\n │ agents, keys │ outbound │ opaque │ │ accounts, web │\n │ repo, tools │ │ frames │ │ app, metadata │\n └──────────────┘ └─────────┘ └───────────────┘\n ▲ ▲\n └────────── end-to-end encrypted session ───────────┘\n phone · browser · another terminal\n```\n\n- **Node** — a daemon on your machine. Owns the workspace, credentials, and agent\n processes. Serves an API and WebSocket on `http://localhost:4317` plus a\n `/healthz` probe. **It hosts no web UI.**\n- **Relay** — forwards encrypted frames between your node and your devices. Your\n node dials out, so no inbound port is opened. The relay cannot read the frames.\n- **Control plane** — holds your account, node registry, and session index, and\n serves the web/PWA client. Use the hosted one or run your own.\n\nBecause the node serves no UI, a browser or phone needs a control plane — hosted\nat `app.bivy.sh`, or one you deploy yourself. The terminal CLI needs neither.\n\nSee [`docs/remote-access.md`](docs/remote-access.md) and\n[`docs/security-model.md`](docs/security-model.md).\n\n## Supported agents\n\n**Claude Code and Codex are the recommended, release-certified paths.** The\nbroader catalog remains available under **More agents** for users who need it;\ncapabilities and fidelity vary by runtime:\n\n| Agent | Command | Notes |\n|---|---|---|\n| Pi | `bivy run pi` | Uses the operator-installed `pi` command and Pi auth/config |\n| Claude Code | `bivy run claude` | Uses the operator-installed `claude` command through an SDK bridge |\n| Codex | `bivy run codex` | Installs `@openai/codex` |\n| OpenCode | `bivy run opencode` | Installs `opencode-ai` |\n| Gemini CLI | `bivy run gemini` | Installs `@google/gemini-cli` |\n| Qwen Code | `bivy run qwen` | Installs `@qwen-code/qwen-code` |\n| Goose | `bivy run goose` | Requires `goose` on PATH |\n| Aider | `bivy run aider` | No session resume (upstream gap) |\n| Cline | `bivy run cline` | Installs `cline` |\n| Crush | `bivy run crush` | No session resume (upstream gap) |\n| Cursor | `bivy run cursor` | ACP-capable |\n| GitHub Copilot | `bivy run copilot` | ACP-capable |\n| Grok | `bivy run grok` | Model selection |\n| Amp | `bivy run amp` | Native thread resume |\n| Auggie | `bivy run auggie` | Headless CLI |\n| Droid | `bivy run droid` | Model selection |\n| Continue | `bivy run continue` | Headless CLI |\n| Kilo Code | `bivy run kilocode` | ACP-capable |\n| Rovo Dev | `bivy run rovodev` | Installed out of band |\n\nAny other command works via `bivy run -- ./your-agent --flags`. ACP-capable\nagents can be promoted to Bivy's governed protocol path for per-tool approvals\nand native resume. To add a reusable process or ACP agent to both the CLI and web\npicker without changing Bivy, run `bivy agent add`, or scaffold and install a\ndeclarative [plugin manifest](docs/plugins.md) with `bivy plugin init`. Both use\nthe same schema/store; the plugin SDK, diagnostics, and ACP conformance fixtures\nsupport distributable or custom protocol bridges.\n\n[`docs/runtime-support-matrix.md`](docs/runtime-support-matrix.md) lists exactly\nwhat each agent supports — resume, model selection, approvals, sandboxing.\n\n## Common commands\n\n```bash\nbivy # show the command overview\nbivy run pi # launch Pi as a durable session\nbivy run claude # run a different agent\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires relay setup)\nbivy automation init # create .bivy/automations.yaml\nbivy agent add # connect an existing ACP or process agent\nbivy plugin list # installed declarative integration packages\nbivy status # config summary and node reachability\nbivy doctor # health check\nbivy logs -f # tail node logs\nbivy update # update Bivy and restart the service\n```\n\nFull command list, flags, and examples: [`docs/cli-reference.md`](docs/cli-reference.md).\n\n## Configuration\n\nThe common knobs:\n\n```bash\nBIVY_WORKSPACE=/path/to/repo # default workspace\nBIVY_SANDBOX=read-only # read-only | workspace-write (default) | danger-full-access\nBIVY_APPROVAL_MODE=risky # never | risky | always | autonomous (default)\n```\n\nCreate and inspect the typed node configuration, or add repository-owned\nsafety/check/retry policy:\n\n```bash\nbivy config init\nbivy config set defaults.agent codex\nbivy config explain defaults.sandbox\nbivy config init --project # .bivy/policy.yaml\n```\n\nSee [`docs/config-as-code.md`](docs/config-as-code.md). Every environment\nvariable and precedence rule remains in\n[`docs/configuration.md`](docs/configuration.md).\n\n## Approvals and sandboxing\n\nThe default approval mode is **`autonomous`**: agents act without per-action\nprompts. The actual protection depends on the selected runtime. Native-sandbox\nagents enforce the chosen access tier; structured runtimes also pass tool calls\nthrough Bivy's policy and approval layer. Process agents that Bivy cannot\nintercept run with your OS user permissions. The picker shows this distinction\nand requires confirmation before selecting that limited path.\n\nWhere Bivy receives structured shell/file calls, a heuristic floor blocks known\ncatastrophic commands and structured writes outside the workspace, and a\nbackstop set (force-push, publish, deploy, sudo) pauses for a human. This catches\naccidents; it is not an adversarial isolation boundary.\n\nIf you want to be asked about more, set the mode explicitly:\n\n```bash\nBIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits\nBIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits\nBIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available\n```\n\nApprove from the terminal, browser, or phone.\n\nSandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced\nnatively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.\nAgents without a native sandbox may expose structured tool or MCP controls, but\nthose controls do not cover activity the agent performs outside those channels;\nsome process adapters run entirely with your user permissions. Check the\npicker's Protection label. **Bivy does not currently ship its own OS-level jail.**\n\n## Credentials\n\nInteractive prompts, transcripts, and workspace files stay encrypted across the\nrelay. Credentials can remain on a Machine or in a vault you control. If you\nexplicitly enable hosted unattended provisioning, Bivy Cloud may instead store\nencrypted cloud, repository, model, or key-escrow material that the service can\ntechnically access. Treat this as an explicit hosted-custody mode, not as relay\nblindness. See the\n[security model](docs/security-model.md#what-the-control-plane-sees).\n\n```bash\nbivy secrets list\nbivy secrets set github.repo-token\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\nbivy secrets doctor\n```\n\n`secret://`, `env://`, and `op://` (1Password) references are resolved on demand\nwhen the daemon provisions an agent run, so the raw values never sit in your\nconfig. See [`docs/key-management.md`](docs/key-management.md).\n\n## Automations as code\n\nDefine governed jobs in `.bivy/automations.yaml`, validate them, and simulate\ntrigger events locally before applying anything:\n\n```bash\nbivy automation init\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml\nbivy automation apply\n```\n\nInstructions are encrypted on the applying node before upload. Safety policy\nlives beside the job—sandbox, approval mode, and a hard attempt ceiling that\nretry/fallback rules cannot exceed. See\n[`docs/automations-as-code.md`](docs/automations-as-code.md).\n\n## GitHub Runs\n\nLabel an issue `bivy` (or `bivy/<machine>` to target a Machine), or mention the\nBivy GitHub App in a comment. Bivy creates a Run on the selected Machine, uses an\nisolated worktree, executes configured checks, and reports an explicit outcome.\n\nAvailable on every plan. On Bivy Cloud, the free trial shows the first 25\ndistinct sessions in the hosted app and includes 10 unattended automations per\nrolling 7-day window across GitHub, Slack, webhooks, and schedules. Sessions keep\nrunning on your machines after the hosted trial is exhausted, but new ones are\nhidden from the hosted app until you subscribe. Pro removes both limits.\nSelf-hosted stacks are unlimited.\n\nA private GitHub App only installs on the account that owns it, so connect one\napp per GitHub account — one for your personal repos, one per organization\n(`bivy github:app-create --org <org>`). A node can serve several at once, each\nwith its own key and `@`-mention handle.\n\nSee [`docs/github-work-queue.md`](docs/github-work-queue.md).\n\n## Linear Runs\n\nApply `bivy` or `bivy/<machine>` to a Linear issue to create a Run on the selected\nMachine. The Machine fetches issue content directly from Linear, works in an\nisolated GitHub worktree, and asks the agent to open a pull request. See\n[`docs/linear-work-queue.md`](docs/linear-work-queue.md).\n\n## Development\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server (proxies /api and /ws to the node)\n```\n\nChecks — all of these run in CI:\n\n```bash\npnpm run typecheck\npnpm run typecheck:web\npnpm run lint\npnpm run test:unit\npnpm run test:core\npnpm run check:licenses\npnpm run check:secrets\n```\n\nRepository layout:\n\n- `src/` — node daemon, runtime adapters, approvals, secrets, sessions\n- `bin/` — the `bivy` CLI\n- `packages/core` — shared protocol, pairing, wire format\n- `packages/web` — the React/Vite PWA client (`@bivy/web`)\n- `services/relay` — self-hostable relay\n- `services/control-plane` — self-hostable control plane\n- `deploy/` — self-host deployment examples\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md).\n\n## Self-hosting\n\nNode, relay, and control plane are all in this repository. Point a node at your\nown deployment by passing URLs to `bivy relay:setup` — re-running it switches an\nexisting node over to the new endpoints:\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nEach URL has a flag and an environment-variable equivalent (the flag wins):\n\n| Flag | Environment variable | Points at | Default |\n|---|---|---|---|\n| `--control-plane <url>` | `BIVY_CONTROL_PLANE_URL` | accounts, node registry, and the web-app API | hosted (`app.bivy.sh`) |\n| `--relay <wss-url>` | `BIVY_RELAY_URL` | the encrypted-frame relay your node dials out to | hosted |\n| `--client <url>` | `BIVY_CLIENT_BASE_URL` | base URL used when building app/PWA links | the `--control-plane` URL |\n\nSign-in defaults to GitHub device login (`--github`); pass\n`--email you@example.com` for an email magic-link, or `--session-token <token>`\nto skip interactive sign-in. `relay:setup` checks the control plane is reachable, enrolls\nthis node, and writes the endpoints to `.bivy/relay.json`, so `bivy open`,\n`bivy link`, and `bivy update` all keep using your deployment afterwards.\n\n**Self-hosting is unsupported** — no SLA, community best-effort via GitHub\nissues. You own TLS, backups, upgrades, and hardening. See\n[`docs/self-host.md`](docs/self-host.md).\n\n## Security\n\nReport vulnerabilities through [GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new).\nPlease do not open a public issue. See [`SECURITY.md`](SECURITY.md) for scope,\nresponse times, and safe harbour, and [`docs/security-model.md`](docs/security-model.md)\nfor the trust model and known limitations.\n\n## License\n\nBivy Core is free and open-source software licensed under the GNU Affero General\nPublic License, version 3.0 only (AGPL-3.0-only). You may use, study, modify, and\nself-host it under that license. If you modify Bivy and let users interact with\nit over a network, section 13 requires you to offer those users the corresponding\nsource code.\n\nSee [`LICENSE`](LICENSE), [`CORE.md`](CORE.md), and [`CLOUD.md`](CLOUD.md).\n",
|
|
67
67
|
"readmeFilename": "README.md"
|
|
68
68
|
}
|