@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 CHANGED
@@ -1,28 +1,59 @@
1
1
  # Bivy
2
2
 
3
- **Run agents where your environment lives. Reach, automate, and govern them from
4
- anywhere.**
5
-
6
- Bivy makes coding agents on your infrastructure available from anywhere. Use
7
- Claude Code or Codex with the repositories, tools, services, and compute already
8
- in your development environment; continue the same Session from a phone, leave
9
- work running, and return to checked changes or a pull request with a clear
10
- Receipt.
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. The relay cannot decrypt it. Explicitly enabled hosted
14
- provisioning has a different boundary: the control plane may hold encrypted
15
- cloud, repository, or key-escrow material that the service can technically
16
- access. Slack and generic webhook instructions also reach the control plane in
17
- plaintext. See [Why Bivy](docs/why-bivy.md) and the
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) — start with the [quickstart](docs/quickstart.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
- if (this.runtimeOptions.attachToChat) {
831
- const attachServer = buildAttachMcpServer(sdk, this.id, this.runtimeOptions.attachToChat);
832
- if (attachServer)
833
- options.mcpServers = { [BIVY_ATTACH_MCP_SERVER_NAME]: attachServer };
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
- if (req.params.name !== "attach_to_chat") {
93
- return { isError: true, content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }] };
94
- }
95
- const result = await runAttachToChat(endpoint, sessionId, (req.params.arguments ?? {}), fetchImpl, token);
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
- constructor(appDir, registry = BUILT_IN_INTEGRATIONS, attachToChat) {
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
- const integrations = new IntegrationManager(appDir, undefined, attachToChatForSession);
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 request = item.source === "schedule"
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.299",
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
  }