@bivy/bivy 0.0.0 → 0.1.0-staging.2

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.
Files changed (146) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +265 -5
  3. package/bin/acp-shim.mjs +298 -0
  4. package/bin/agent-manifest.json +277 -0
  5. package/bin/bivy.mjs +4100 -0
  6. package/bin/codex-app-server-shim.mjs +447 -0
  7. package/bin/patch-pi-dependencies.mjs +44 -0
  8. package/bin/prune-sessions.mjs +52 -0
  9. package/bin/sessions-list.mjs +27 -0
  10. package/bin/shim-path.mjs +126 -0
  11. package/bin/uninstall-paths.mjs +48 -0
  12. package/dist/approval.js +87 -0
  13. package/dist/attach.js +248 -0
  14. package/dist/auth.js +258 -0
  15. package/dist/bivy-login.js +180 -0
  16. package/dist/browser-open.js +50 -0
  17. package/dist/control-plane-tasks.js +236 -0
  18. package/dist/data-dir.js +25 -0
  19. package/dist/device-registry.js +201 -0
  20. package/dist/e2e.js +70 -0
  21. package/dist/ephemeral-exec.js +109 -0
  22. package/dist/exec.js +209 -0
  23. package/dist/git-auth.js +155 -0
  24. package/dist/github-app-auth.js +107 -0
  25. package/dist/github-app-connect.js +235 -0
  26. package/dist/github-app-manifest.js +82 -0
  27. package/dist/github-app-sync-cli.js +93 -0
  28. package/dist/github-app-vault.js +106 -0
  29. package/dist/github-apps.js +121 -0
  30. package/dist/github-connect-repo.js +74 -0
  31. package/dist/github-device-auth.js +109 -0
  32. package/dist/github-tasks.js +650 -0
  33. package/dist/guard.js +109 -0
  34. package/dist/harness/cache-evict.js +88 -0
  35. package/dist/harness/checkpoint.js +0 -0
  36. package/dist/harness/cow-clone.js +84 -0
  37. package/dist/harness/dep-cache.js +78 -0
  38. package/dist/harness/disk-admission.js +46 -0
  39. package/dist/harness/egress.js +30 -0
  40. package/dist/harness/manager.js +97 -0
  41. package/dist/harness/mcp-config-formats.js +164 -0
  42. package/dist/harness/mcp-config.js +111 -0
  43. package/dist/harness/mcp-inject.js +134 -0
  44. package/dist/harness/mcp-proxy-cli.js +88 -0
  45. package/dist/harness/mcp-proxy.js +150 -0
  46. package/dist/harness/net-proxy.js +120 -0
  47. package/dist/harness/sandbox.js +96 -0
  48. package/dist/history-sync.js +26 -0
  49. package/dist/hosted-endpoints.d.mts +14 -0
  50. package/dist/hosted-endpoints.mjs +35 -0
  51. package/dist/identity.js +153 -0
  52. package/dist/integrations/index.js +4 -0
  53. package/dist/integrations/manager.js +279 -0
  54. package/dist/integrations/oauth.js +78 -0
  55. package/dist/integrations/registry.js +239 -0
  56. package/dist/integrations/store.js +54 -0
  57. package/dist/integrations/types.js +1 -0
  58. package/dist/linear-tasks.js +49 -0
  59. package/dist/metadata.js +226 -0
  60. package/dist/multiplexer.js +79 -0
  61. package/dist/native-pi.js +38 -0
  62. package/dist/node-stats.js +237 -0
  63. package/dist/pairing-crypto.js +105 -0
  64. package/dist/policy/conditions.js +103 -0
  65. package/dist/policy/policy-engine.js +20 -0
  66. package/dist/policy/risk.js +18 -0
  67. package/dist/policy/ruleset.js +113 -0
  68. package/dist/policy/run-policy.js +108 -0
  69. package/dist/policy/session-reroute.js +96 -0
  70. package/dist/pty-runner.py +95 -0
  71. package/dist/question.js +146 -0
  72. package/dist/redact.js +97 -0
  73. package/dist/relay-attach.js +345 -0
  74. package/dist/relay-chunk.js +73 -0
  75. package/dist/relay-cli-crypto.js +70 -0
  76. package/dist/relay-client.js +344 -0
  77. package/dist/relay-setup.js +262 -0
  78. package/dist/repo-workspace.js +208 -0
  79. package/dist/runtime/adoption.js +45 -0
  80. package/dist/runtime/agent-service-bin.js +149 -0
  81. package/dist/runtime/agent-service.js +439 -0
  82. package/dist/runtime/ansi.js +27 -0
  83. package/dist/runtime/anthropic-preflight.js +80 -0
  84. package/dist/runtime/claude-code.js +1364 -0
  85. package/dist/runtime/cli-parsers.js +647 -0
  86. package/dist/runtime/codex-auth.js +168 -0
  87. package/dist/runtime/codex-preflight.js +60 -0
  88. package/dist/runtime/codex-sessions.js +229 -0
  89. package/dist/runtime/control-plane-location.js +74 -0
  90. package/dist/runtime/credential-ingest.js +122 -0
  91. package/dist/runtime/credential-provisioning.js +79 -0
  92. package/dist/runtime/credential-store.js +435 -0
  93. package/dist/runtime/credentials.js +153 -0
  94. package/dist/runtime/host.js +153 -0
  95. package/dist/runtime/index.js +1548 -0
  96. package/dist/runtime/local-model-store.js +194 -0
  97. package/dist/runtime/location-registry.js +28 -0
  98. package/dist/runtime/model-catalog.js +97 -0
  99. package/dist/runtime/model-namer.js +85 -0
  100. package/dist/runtime/native-process-scan.js +102 -0
  101. package/dist/runtime/native-session-discovery.js +103 -0
  102. package/dist/runtime/normalize.js +75 -0
  103. package/dist/runtime/oauth/model-oauth-providers.js +75 -0
  104. package/dist/runtime/oauth/model-oauth.js +324 -0
  105. package/dist/runtime/opencode-preflight.js +55 -0
  106. package/dist/runtime/pi-auth.js +82 -0
  107. package/dist/runtime/pi-oauth.js +52 -0
  108. package/dist/runtime/pi-session-discovery.js +42 -0
  109. package/dist/runtime/pi.js +518 -0
  110. package/dist/runtime/process.js +499 -0
  111. package/dist/runtime/protocol.js +630 -0
  112. package/dist/runtime/remote.js +541 -0
  113. package/dist/runtime/rpc-protocol.js +56 -0
  114. package/dist/runtime/ruleset-store.js +117 -0
  115. package/dist/runtime/session-location.js +50 -0
  116. package/dist/runtime/types.js +17 -0
  117. package/dist/secrets-cli.js +134 -0
  118. package/dist/secrets.js +264 -0
  119. package/dist/server.js +9411 -0
  120. package/dist/session/bivy-session.js +1 -0
  121. package/dist/session/checkpoint-pack.js +133 -0
  122. package/dist/session/event-log.js +340 -0
  123. package/dist/session/fork-dirty.js +73 -0
  124. package/dist/session/fork-prereqs.js +61 -0
  125. package/dist/session/fork.js +57 -0
  126. package/dist/session/native-import.js +56 -0
  127. package/dist/session/reconnect.js +168 -0
  128. package/dist/session/replication-service.js +236 -0
  129. package/dist/session/replication.js +106 -0
  130. package/dist/session/replicator.js +140 -0
  131. package/dist/session/session-new-dedupe.js +42 -0
  132. package/dist/session/sibling-client.js +201 -0
  133. package/dist/session/transcript-merge.js +131 -0
  134. package/dist/session/transcript-normal.js +130 -0
  135. package/dist/session/workspace-context.js +1 -0
  136. package/dist/session-event-coalescer.js +50 -0
  137. package/dist/session-identity.js +34 -0
  138. package/dist/session-ref.js +65 -0
  139. package/dist/stt-cli.js +131 -0
  140. package/dist/stt.js +168 -0
  141. package/dist/terminal.js +409 -0
  142. package/dist/wire-format.js +67 -0
  143. package/dist/worktree-provision.js +118 -0
  144. package/dist/worktree.js +117 -0
  145. package/package.json +40 -6
  146. package/public/qr.js +464 -0
@@ -0,0 +1,208 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { resolveSecret } from "./secrets.js";
8
+ import { cleanRemoteUrl, credConfigArgs, gitNonInteractiveEnv, configureRepoCredentialHelper } from "./git-auth.js";
9
+ const exec = promisify(execFile);
10
+ /** Parse "owner/repo" (also tolerates a full github.com URL or trailing .git). */
11
+ export function parseRepo(input) {
12
+ const cleaned = String(input)
13
+ .trim()
14
+ .replace(/^https?:\/\/github\.com\//i, "")
15
+ .replace(/^git@github\.com:/i, "")
16
+ .replace(/\.git$/i, "")
17
+ .replace(/\/+$/, "");
18
+ const m = cleaned.match(/^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/);
19
+ return m ? { owner: m[1], repo: m[2], slug: `${m[1]}/${m[2]}` } : undefined;
20
+ }
21
+ /** Parse a GitHub owner/repo slug from a git remote URL. */
22
+ export function parseGitHubRemote(input) {
23
+ const value = String(input).trim();
24
+ const https = value.match(/^https?:\/\/(?:[^/@]+(?::[^/@]*)?@)?github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
25
+ if (https)
26
+ return parseRepo(`${https[1]}/${https[2]}`);
27
+ const ssh = value.match(/^(?:ssh:\/\/)?git@github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
28
+ if (ssh)
29
+ return parseRepo(`${ssh[1]}/${ssh[2]}`);
30
+ return undefined;
31
+ }
32
+ /** Infer owner/repo from a workspace's origin remote, if it is a GitHub checkout. */
33
+ export async function inferGitHubRepoFromWorkspace(workspace) {
34
+ try {
35
+ const { stdout } = await exec("git", ["-C", workspace, "remote", "get-url", "origin"]);
36
+ return parseGitHubRemote(stdout);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /** A GitHub token from env or the local `gh` login, or undefined (public only). */
43
+ export async function resolveGitHubToken(env = process.env) {
44
+ const fromEnv = env.BIVY_GITHUB_TOKEN?.trim();
45
+ if (fromEnv) {
46
+ if (fromEnv.startsWith("secret://") || fromEnv.startsWith("op://") || fromEnv.startsWith("env://"))
47
+ return resolveSecret(fromEnv);
48
+ return fromEnv;
49
+ }
50
+ try {
51
+ const { stdout } = await exec("gh", ["auth", "token"]);
52
+ return stdout.trim() || undefined;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ /**
59
+ * Refresh the remote-tracking refs so a session branches off the CURRENT state
60
+ * of `origin`, not whatever the local checkout last saw. Best-effort: an offline
61
+ * node (or a checkout with no reachable origin) keeps its existing refs and the
62
+ * caller branches off those. `cloneOrUpdateRepo` already fetches on the clone
63
+ * path, so this is for the "workspace is an existing local checkout" path, which
64
+ * otherwise never pulls the latest origin before cutting a branch.
65
+ */
66
+ export async function fetchOrigin(repoDir) {
67
+ try {
68
+ await exec("git", ["-C", repoDir, "fetch", "origin", "--prune"], { timeout: 120_000 });
69
+ }
70
+ catch {
71
+ // offline / no origin / no rights — branch off the refs we already have.
72
+ }
73
+ }
74
+ /**
75
+ * Resolve the ref a new branch should be cut from: the remote's default branch
76
+ * (`origin/main`, `origin/master`, …). Falls back to `origin/main` when the
77
+ * remote HEAD isn't recorded. Used so a repo-backed session branches off the
78
+ * upstream default rather than whatever the shared checkout happens to be on.
79
+ */
80
+ export async function resolveDefaultBaseRef(repoDir) {
81
+ // origin/HEAD points at the remote's default branch when it's been recorded.
82
+ try {
83
+ const { stdout } = await exec("git", ["-C", repoDir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
84
+ const ref = stdout.trim();
85
+ if (ref)
86
+ return ref; // e.g. "origin/main"
87
+ }
88
+ catch {
89
+ // origin/HEAD is often unset on a fresh clone — record it, then retry.
90
+ }
91
+ try {
92
+ await exec("git", ["-C", repoDir, "remote", "set-head", "origin", "--auto"]);
93
+ const { stdout } = await exec("git", ["-C", repoDir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
94
+ const ref = stdout.trim();
95
+ if (ref)
96
+ return ref;
97
+ }
98
+ catch {
99
+ // best effort
100
+ }
101
+ return "origin/main";
102
+ }
103
+ /**
104
+ * Resolve the ref to branch a new session from when the caller (the composer's
105
+ * branch pill) requested a SPECIFIC remote branch instead of the repo's
106
+ * default. `cloneOrUpdateRepo`/`fetchOrigin` have already brought every remote
107
+ * branch's tracking ref down before this runs, so this only needs to verify
108
+ * `origin/<branch>` actually exists — surfacing a clear error instead of
109
+ * silently falling back to the default when the requested branch is missing
110
+ * (e.g. typo'd, deleted upstream since the picker's list was fetched).
111
+ */
112
+ export async function resolveBranchBaseRef(repoDir, branch) {
113
+ const ref = `origin/${branch}`;
114
+ try {
115
+ await exec("git", ["-C", repoDir, "rev-parse", "--verify", "--quiet", ref]);
116
+ return ref;
117
+ }
118
+ catch {
119
+ throw new Error(`Branch "${branch}" was not found on the remote.`);
120
+ }
121
+ }
122
+ /**
123
+ * Whether an existing Bivy-owned checkout at `dest` can be reused as-is, i.e. it
124
+ * has a `.git` entry AND `git rev-parse` accepts it as a real repository. A
125
+ * `.git` can survive an interrupted/corrupt clone, so presence alone is not
126
+ * enough — callers that reuse on presence alone hand a broken directory to the
127
+ * worktree flow, which then fails with "Not a git repository". Exported for
128
+ * tests.
129
+ */
130
+ export async function isReusableCheckout(dest) {
131
+ if (!fs.existsSync(path.join(dest, ".git")))
132
+ return false;
133
+ try {
134
+ await exec("git", ["-C", dest, "rev-parse", "--show-toplevel"]);
135
+ return true;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ /**
142
+ * Ensure a working clone of the repo exists under `root` and return its path.
143
+ * Clones on first use; on later use fetches the latest. One checkout per repo.
144
+ */
145
+ // The remote URL is now token-free — auth goes through the daemon credential
146
+ // helper (see src/git-auth.ts), so no token is written into `.git/config`.
147
+ // Still tighten `.git/config` to 0600 (git writes it 0644 by default): it holds
148
+ // the credential-helper config and other local settings, and a least-readable
149
+ // posture is cheap insurance. Best-effort.
150
+ function hardenGitConfigPerms(dest) {
151
+ try {
152
+ fs.chmodSync(path.join(dest, ".git", "config"), 0o600);
153
+ }
154
+ catch {
155
+ // no .git/config yet, or a filesystem without POSIX modes — ignore
156
+ }
157
+ }
158
+ export async function cloneOrUpdateRepo(opts) {
159
+ const dest = path.join(opts.root, `${opts.owner}__${opts.repo}`);
160
+ // Keep the remote URL token-free; auth flows through the daemon credential
161
+ // helper (see src/git-auth.ts), which fetches a fresh token on demand — so no
162
+ // token is written into this clone's `.git/config`, where an agent (or
163
+ // `git remote -v` / a log / a screenshot) could read it.
164
+ const url = cleanRemoteUrl(opts.owner, opts.repo);
165
+ const env = gitNonInteractiveEnv();
166
+ const cc = credConfigArgs();
167
+ // Reuse the existing checkout only if it is a REAL git repository. A `.git`
168
+ // entry can survive a clone that was interrupted (network drop, killed
169
+ // process) or otherwise left corrupt — the directory exists and has a `.git`,
170
+ // but `git rev-parse` rejects it. Trusting `.git`'s mere presence took the
171
+ // fetch path and returned that broken directory, so every later "new session
172
+ // on this repo" failed downstream in createWorktree with "Not a git
173
+ // repository" — permanently, because the wipe-and-reclone repair below only
174
+ // ran when `.git` was entirely absent. Validate first; if it's broken, fall
175
+ // through and rebuild it.
176
+ if (await isReusableCheckout(dest)) {
177
+ try {
178
+ // Rewrite origin to the clean URL. This also MIGRATES any pre-existing
179
+ // clone whose remote still carries an embedded token from before this fix.
180
+ await exec("git", ["-C", dest, "remote", "set-url", "origin", url]);
181
+ await configureRepoCredentialHelper((a) => exec("git", a), dest);
182
+ await exec("git", ["-C", dest, ...cc, "fetch", "--all", "--prune"], { timeout: 120_000, env });
183
+ // Re-point origin/HEAD at the remote's CURRENT default branch. Without
184
+ // this a cached checkout keeps whatever default it recorded at first
185
+ // clone, so if the repo's default later changed (e.g. main → master) a new
186
+ // session would branch off the stale remote default. --auto is best-effort
187
+ // and only rewrites the local origin/HEAD pointer, never a branch.
188
+ await exec("git", ["-C", dest, "remote", "set-head", "origin", "--auto"]);
189
+ }
190
+ catch {
191
+ // offline / fetch failed — reuse the existing checkout as-is
192
+ }
193
+ hardenGitConfigPerms(dest);
194
+ return dest;
195
+ }
196
+ fs.mkdirSync(opts.root, { recursive: true });
197
+ // A failed/interrupted/corrupt clone can leave a non-repository (or broken
198
+ // repository) directory behind; because this path is Bivy-owned
199
+ // (<root>/<owner>__<repo>), repair it instead of letting every later "new repo
200
+ // session" fail with "destination exists" or "Not a git repository".
201
+ if (fs.existsSync(dest))
202
+ fs.rmSync(dest, { recursive: true, force: true });
203
+ await exec("git", [...cc, "clone", url, dest], { timeout: 600_000, env });
204
+ // Persist the helper config so agent-run git in this clone authenticates too.
205
+ await configureRepoCredentialHelper((a) => exec("git", a), dest);
206
+ hardenGitConfigPerms(dest);
207
+ return dest;
208
+ }
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ /**
4
+ * The agent service's `attach` op replies "No detached session to attach: <id>"
5
+ * (see agent-service.ts handleStart) when it is reachable but has no such live
6
+ * session — the only DEFINITIVE "gone" signal. Every other failure (connection
7
+ * refused / reset / closed-during-start / timeout) is transient: the service may
8
+ * be down or restarting, so the child may well still exist.
9
+ */
10
+ export function classifyAttachFailure(err) {
11
+ const message = err instanceof Error ? err.message : String(err);
12
+ return /no detached session to attach/i.test(message) ? "gone" : "transient";
13
+ }
14
+ /**
15
+ * Attempt to re-attach to each adoptable session concurrently, applying the
16
+ * failure policy. Never throws — a single session's failure only affects its own
17
+ * classification. Callers should PRE-RECORD every row's address into the durable
18
+ * location registry BEFORE calling this, so a racing `advertiseSessions()` (the
19
+ * replace-all-per-node POST) preserves the addresses even while attaches are still
20
+ * in flight — otherwise the first advert would wipe the very column adoption reads.
21
+ */
22
+ export async function attachAdoptedSessions(rows, deps) {
23
+ const adopted = [];
24
+ const kept = [];
25
+ const forgotten = [];
26
+ await Promise.all(rows.map(async (location) => {
27
+ try {
28
+ await deps.attach(location);
29
+ adopted.push(location.sessionId);
30
+ deps.log?.(`adopted ${location.sessionId} at ${location.agentServiceAddress}`);
31
+ }
32
+ catch (error) {
33
+ if (classifyAttachFailure(error) === "gone") {
34
+ forgotten.push(location.sessionId);
35
+ await deps.forget(location.sessionId).catch(() => { });
36
+ deps.log?.(`forgot definitively-gone session ${location.sessionId}`);
37
+ }
38
+ else {
39
+ kept.push(location.sessionId);
40
+ deps.log?.(`kept unreachable session ${location.sessionId} (${location.agentServiceAddress}) — will retry`);
41
+ }
42
+ }
43
+ }));
44
+ return { adopted, kept, forgotten };
45
+ }
@@ -0,0 +1,149 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ /**
4
+ * Agent service entry point — runs the real agent runtimes in a process
5
+ * SEPARATE from the node daemon and exposes them over the RPC protocol
6
+ * (src/runtime/rpc-protocol.ts). Launch it beside a daemon and point the daemon
7
+ * at it with BIVY_REMOTE_RUNTIME=1 + BIVY_REMOTE_RUNTIME_ADDR (see
8
+ * docs/agent-runtime-rpc.md).
9
+ *
10
+ * BIVY_AGENT_SERVICE_LISTEN where to listen: "unix:/path.sock" | "PORT" | "host:PORT"
11
+ * BIVY_DATA_DIR data dir (defaults to <cwd>/.bivy), same as the daemon
12
+ * BIVY_REMOTE_RUNTIME_DETACH_REAP_MS optional: reap a detached, idle session after this many ms (off by default)
13
+ *
14
+ * Dev: tsx src/runtime/agent-service-bin.ts
15
+ * Prod: node dist/runtime/agent-service-bin.js
16
+ *
17
+ * The service must have the same agent binaries on PATH and credential access
18
+ * the in-process daemon would (the Claude SDK execs the `claude` CLI internally),
19
+ * because the child now runs HERE, not in the daemon.
20
+ */
21
+ import net from "node:net";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+ import { pathToFileURL } from "node:url";
25
+ import { AgentService } from "./agent-service.js";
26
+ import { encodeFrame, FrameDecoder } from "./rpc-protocol.js";
27
+ import { parseRemoteAddress } from "./remote.js";
28
+ import { makeRuntime } from "./index.js";
29
+ import { defaultDataDir } from "../data-dir.js";
30
+ /** Adapt a raw socket to the transport-agnostic ServiceConnection. */
31
+ export function socketConnection(socket) {
32
+ const decoder = new FrameDecoder();
33
+ let onMessage;
34
+ let onClose;
35
+ socket.on("data", (chunk) => {
36
+ let messages;
37
+ try {
38
+ messages = decoder.push(chunk);
39
+ }
40
+ catch {
41
+ socket.destroy();
42
+ return;
43
+ }
44
+ if (onMessage)
45
+ for (const message of messages)
46
+ onMessage(message);
47
+ });
48
+ socket.on("error", () => {
49
+ // A 'close' event always follows; teardown happens there.
50
+ });
51
+ socket.on("close", () => onClose?.());
52
+ return {
53
+ send(message) {
54
+ try {
55
+ socket.write(encodeFrame(message));
56
+ }
57
+ catch {
58
+ // peer went away mid-write; 'close' will reap
59
+ }
60
+ },
61
+ onMessage(handler) {
62
+ onMessage = handler;
63
+ },
64
+ onClose(handler) {
65
+ onClose = handler;
66
+ },
67
+ close() {
68
+ socket.destroy();
69
+ },
70
+ };
71
+ }
72
+ /** Start a socket server that hands each connection to the AgentService. */
73
+ export function startAgentServiceServer(options) {
74
+ const server = net.createServer((socket) => options.service.accept(socketConnection(socket)));
75
+ return new Promise((resolve, reject) => {
76
+ server.once("error", reject);
77
+ server.listen(parseRemoteAddress(options.listen), () => {
78
+ server.removeListener("error", reject);
79
+ resolve(server);
80
+ });
81
+ });
82
+ }
83
+ /** Build the default runtime provider backed by the real runtime registry. */
84
+ export function defaultRuntimeProvider() {
85
+ const appDir = defaultDataDir();
86
+ const piDir = path.join(appDir, "pi");
87
+ const sessionsDir = path.join(piDir, "sessions");
88
+ // The shared, agent-neutral credential vault (not inside any agent's dir).
89
+ const credsDir = path.join(appDir, "credentials");
90
+ // Build the runtime the daemon asked for directly via makeRuntime — NOT
91
+ // through RuntimeHost.resolveRuntimeId, whose picker/availability gate is the
92
+ // daemon's UI concern. The daemon already chose the id; the service just hosts
93
+ // it (makeRuntime still throws if the agent binary/SDK is missing here).
94
+ // Node-hosted tools (Bivy integrations / MCP) reach the runtime here via the
95
+ // per-session ToolProvider the daemon supplies over RPC (agent-agnostic), so
96
+ // the service needs no runtime-specific tool wiring of its own.
97
+ const factory = { credsDir, piDir, sessionsDir };
98
+ const cache = new Map();
99
+ return (runtimeId, sandbox) => {
100
+ const key = sandbox ? `${runtimeId}::sandbox=${sandbox}` : runtimeId;
101
+ let rt = cache.get(key);
102
+ if (!rt) {
103
+ rt = makeRuntime({ ...factory, runtime: runtimeId, sandbox: sandbox });
104
+ cache.set(key, rt);
105
+ }
106
+ return rt;
107
+ };
108
+ }
109
+ async function main() {
110
+ const listen = process.env.BIVY_AGENT_SERVICE_LISTEN?.trim();
111
+ if (!listen) {
112
+ console.error("[agent-service] BIVY_AGENT_SERVICE_LISTEN is required (e.g. unix:/tmp/bivy-agent.sock or 4711)");
113
+ process.exit(2);
114
+ }
115
+ // Idle-reaper for DETACHED sessions (Stage 3). Off by default; set
116
+ // BIVY_REMOTE_RUNTIME_DETACH_REAP_MS to bound how long a session the daemon
117
+ // evicted/lost persists here with no bound connection.
118
+ const detachReapMs = Number(process.env.BIVY_REMOTE_RUNTIME_DETACH_REAP_MS) || 0;
119
+ const service = new AgentService({ runtimeProvider: defaultRuntimeProvider(), log: (message) => console.error(`[agent-service] ${message}`), detachReapMs });
120
+ if (detachReapMs > 0)
121
+ console.error(`[agent-service] detached-session idle reaper: ${detachReapMs}ms`);
122
+ const server = await startAgentServiceServer({ service, listen });
123
+ console.error(`[agent-service] listening on ${listen} (pid ${process.pid}, host ${os.hostname()})`);
124
+ let shuttingDown = false;
125
+ const shutdown = (signal) => {
126
+ if (shuttingDown)
127
+ return;
128
+ shuttingDown = true;
129
+ console.error(`[agent-service] ${signal} — reaping ${service.sessionCount} session(s)`);
130
+ service.disposeAll(); // reap every child so the service leaves no orphans
131
+ server.close(() => process.exit(0));
132
+ setTimeout(() => process.exit(0), 3000).unref();
133
+ };
134
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
135
+ process.on("SIGINT", () => shutdown("SIGINT"));
136
+ // Crash net: this service process hosts detached agent sessions, so a stray
137
+ // unhandled rejection/exception must not tear all of them down. Log and keep
138
+ // running; a real supervisor restart remains the fallback.
139
+ process.on("unhandledRejection", (reason) => {
140
+ console.error("[bivy agent-service] unhandledRejection (kept running):", reason);
141
+ });
142
+ process.on("uncaughtException", (error) => {
143
+ console.error("[bivy agent-service] uncaughtException (kept running):", error);
144
+ });
145
+ }
146
+ // Run only when executed directly (not when imported by a test).
147
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
148
+ void main();
149
+ }