@bivy/bivy 0.5.1-staging.71 → 0.5.1-staging.73

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.
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ /**
4
+ * Keep terminal semantics opt-in for short-lived commands. Native interactive
5
+ * commands must set requiresTty; ordinary commands stay on direct pipes and do
6
+ * not pay for a Python PTY relay.
7
+ */
8
+ export function commandLaunch(command, args, requiresTty, pythonCommand, ptyRunnerScript) {
9
+ return requiresTty
10
+ ? { command: pythonCommand, args: [ptyRunnerScript, command, ...args], usesPty: true }
11
+ : { command, args: [...args], usesPty: false };
12
+ }
@@ -80,6 +80,7 @@ function claudeCodeInfo() {
80
80
  const installed = claudeSdkInstalled();
81
81
  return {
82
82
  id: "claude-code-sdk",
83
+ executionMode: "protocol",
83
84
  displayName: "Claude Code SDK",
84
85
  description: "Anthropic's Claude Agent SDK driven as a Bivy runtime: streaming turns, model picker, and tool approvals via the SDK permission callback.",
85
86
  status: installed ? "available" : "planned",
@@ -117,6 +118,7 @@ function genericCliInfo() {
117
118
  const resume = Boolean(options?.resumeArgs);
118
119
  return {
119
120
  id: "generic-cli",
121
+ executionMode: "pipe",
120
122
  displayName: process.env.BIVY_AGENT_NAME?.trim() || "Generic CLI Agent",
121
123
  description: "Run any local agent CLI underneath Bivy by spawning a configured process and streaming stdout/stderr.",
122
124
  status: configured ? "available" : "planned",
@@ -899,10 +901,23 @@ function cliAgentInfo(id) {
899
901
  let resume = id === "codex" || Boolean(cliResumeTemplate(id));
900
902
  let modelSelection = Boolean(cliModelConfig(id));
901
903
  const usageReporting = cliUsageReporting(id);
904
+ const structuredPref = process.env.BIVY_AGENT_STRUCTURED;
905
+ const structuredAvailable = Boolean(process.env.BIVY_AGENT_PARSER || spec.parserId) && (!spec.parserUnverified || structuredPref === "1") && structuredPref !== "0";
902
906
  // When the agent is promoted to ACP (spec.acp + BIVY_<ID>_ACP / BIVY_PREFER_ACP),
903
907
  // it runs through the governed ProtocolRuntime — so it honestly gains per-tool
904
908
  // approvals and resume. Reflect that in the catalog the picker reads.
905
909
  const acpActive = prefersAcp(id);
910
+ // Catalog discovery must remain usable even when an operator has configured
911
+ // an invalid mode. The actual launch path resolves strictly and reports the
912
+ // actionable error; the picker falls back to the honest default here.
913
+ let executionMode;
914
+ try {
915
+ executionMode = resolveCliExecutionMode({ requested: requestedCliExecutionMode(id), protocolAvailable: Boolean(spec.acp), structuredAvailable, protocolPreferred: acpActive });
916
+ }
917
+ catch {
918
+ executionMode = structuredAvailable ? "structured-pipe" : "pipe";
919
+ }
920
+ const structured = executionMode === "structured-pipe";
906
921
  if (acpActive)
907
922
  resume = true;
908
923
  // Opt-in self-healing: if the installed binary's --help doesn't evidence a
@@ -918,6 +933,7 @@ function cliAgentInfo(id) {
918
933
  }
919
934
  return {
920
935
  id,
936
+ executionMode,
921
937
  displayName: spec.displayName,
922
938
  description: spec.blurb ?? `Run the local ${spec.displayName} CLI underneath Bivy in the session workspace.`,
923
939
  status: installed ? "available" : "external",
@@ -950,6 +966,7 @@ function codexApprovalsInfo() {
950
966
  const installed = commandAvailable("codex");
951
967
  return {
952
968
  id: "codex-approvals",
969
+ executionMode: "protocol",
953
970
  displayName: "Codex",
954
971
  description: "Codex driven through its app-server: every shell command or file change it proposes is gated through Bivy's Approve/Deny before it runs (not just the exec jail), and sessions resume with full history.",
955
972
  status: installed ? "available" : "external",
@@ -1144,10 +1161,45 @@ function prefersAcp(id) {
1144
1161
  return false;
1145
1162
  return process.env.BIVY_PREFER_ACP === "1" || process.env[`BIVY_${id.toUpperCase()}_ACP`] === "1";
1146
1163
  }
1164
+ /**
1165
+ * Resolve the communication mode for a CLI agent. This is deliberately pure so
1166
+ * it can be tested without starting a process. PTY is a terminal-launch mode,
1167
+ * not a governed ProcessRuntime mode; callers must handle it explicitly.
1168
+ */
1169
+ export function resolveCliExecutionMode(input) {
1170
+ const raw = input.requested?.trim().toLowerCase() || "auto";
1171
+ const requested = (raw === "structured" ? "structured-pipe" : raw);
1172
+ if (!["auto", "protocol", "structured-pipe", "pipe", "pty"].includes(requested)) {
1173
+ throw new Error(`Invalid agent execution mode "${input.requested}". Use auto, protocol, structured-pipe, pipe, or pty.`);
1174
+ }
1175
+ if (requested === "pty")
1176
+ return "pty";
1177
+ if (requested === "protocol") {
1178
+ if (!input.protocolAvailable)
1179
+ throw new Error("Protocol execution was requested, but this agent has no configured protocol adapter.");
1180
+ return "protocol";
1181
+ }
1182
+ if (requested === "structured-pipe") {
1183
+ if (!input.structuredAvailable)
1184
+ throw new Error("Structured pipe execution was requested, but this agent has no available structured parser.");
1185
+ return "structured-pipe";
1186
+ }
1187
+ if (requested === "pipe")
1188
+ return "pipe";
1189
+ if (input.protocolAvailable && input.protocolPreferred)
1190
+ return "protocol";
1191
+ if (input.structuredAvailable)
1192
+ return "structured-pipe";
1193
+ return "pipe";
1194
+ }
1195
+ function requestedCliExecutionMode(id) {
1196
+ return process.env[`BIVY_${id.toUpperCase()}_MODE`] ?? process.env.BIVY_AGENT_MODE;
1197
+ }
1147
1198
  function acpInfo() {
1148
1199
  const configured = Boolean(process.env.BIVY_ACP_COMMAND?.trim());
1149
1200
  return {
1150
1201
  id: "acp",
1202
+ executionMode: "protocol",
1151
1203
  displayName: process.env.BIVY_ACP_NAME?.trim() || "ACP Agent",
1152
1204
  description: "Any Agent Client Protocol (ACP) agent, driven through Bivy's shim for per-tool approvals, streaming, and resume.",
1153
1205
  status: configured ? "available" : "planned",
@@ -1202,6 +1254,7 @@ function openClawInfo() {
1202
1254
  const installCommand = `npm install --global --prefix ${npmPrefix} openclaw`;
1203
1255
  return {
1204
1256
  id: "openclaw",
1257
+ executionMode: "pipe",
1205
1258
  displayName: options.displayName,
1206
1259
  description: "Run the local OpenClaw CLI underneath Bivy in the session workspace.",
1207
1260
  status: installed ? "available" : "external",
@@ -1228,6 +1281,7 @@ function protocolInfo() {
1228
1281
  const commands = protocolCommandsFromEnv();
1229
1282
  return {
1230
1283
  id: "bivy-agent-protocol",
1284
+ executionMode: "protocol",
1231
1285
  displayName: process.env.BIVY_PROTOCOL_NAME?.trim() || "Bivy Protocol",
1232
1286
  description: "JSON-lines process protocol for any agent to expose structured events, tool calls, approvals, models, and sessions without a bespoke Bivy adapter.",
1233
1287
  status: configured ? "available" : "planned",
@@ -1244,6 +1298,7 @@ function protocolInfo() {
1244
1298
  export const RUNTIME_CATALOG = [
1245
1299
  {
1246
1300
  id: "pi",
1301
+ executionMode: "protocol",
1247
1302
  displayName: "Pi",
1248
1303
  description: "Native Bivy/Pi coding agent runtime with packages, approvals, and model picker.",
1249
1304
  status: "available",
@@ -1476,23 +1531,26 @@ function makeCliRuntime(id, options) {
1476
1531
  const spec = CLI_AGENT_SPECS[id];
1477
1532
  if (!commandAvailable(spec.command))
1478
1533
  throw new Error(`${spec.displayName} command not found on PATH: ${spec.command}`);
1479
- // ACP promotion: when the agent declares an `acp` mode and it's preferred
1480
- // (BIVY_<ID>_ACP=1 / BIVY_PREFER_ACP=1), drive it through the governed
1481
- // ProtocolRuntime (per-tool approvals + streaming + resume) instead of the
1482
- // one-shot pipe below — the high-capability path, selected as data.
1483
- if (spec.acp && prefersAcp(id)) {
1534
+ // Resolve the mode before launching anything. ACP and structured parsing
1535
+ // remain data-driven; explicit mode overrides are fail-closed rather than
1536
+ // silently degrading to a less capable path.
1537
+ const structuredPref = process.env.BIVY_AGENT_STRUCTURED;
1538
+ const structuredAvailable = Boolean(process.env.BIVY_AGENT_PARSER || spec.parserId) && (!spec.parserUnverified || structuredPref === "1") && structuredPref !== "0";
1539
+ const executionMode = resolveCliExecutionMode({
1540
+ requested: requestedCliExecutionMode(id),
1541
+ protocolAvailable: Boolean(spec.acp),
1542
+ structuredAvailable,
1543
+ protocolPreferred: prefersAcp(id),
1544
+ });
1545
+ if (executionMode === "pty") {
1546
+ throw new Error(`PTY mode is for interactive terminal launches. Use 'bivy run ${spec.command}' instead of a governed chat session.`);
1547
+ }
1548
+ if (executionMode === "protocol") {
1549
+ if (!spec.acp)
1550
+ throw new Error(`${spec.displayName} does not declare an ACP/protocol launch mode.`);
1484
1551
  return new ProtocolRuntime(acpRuntimeOptions({ id, displayName: spec.displayName, command: spec.command, agentArgs: spec.acp.args, credsDir: options.credsDir }));
1485
1552
  }
1486
- // Phase 4 — structured mode ON by default when the agent has a VALIDATED JSON
1487
- // parser: launch with its native JSON flags and parse stdout into normalized
1488
- // events. BIVY_AGENT_STRUCTURED=0 forces the dumb-pipe fallback everywhere;
1489
- // BIVY_AGENT_STRUCTURED=1 opts INTO structured mode for agents whose parser
1490
- // is still unverified (spec.parserUnverified — safe default is dumb pipe so a
1491
- // wrong flag can't regress a working agent). BIVY_AGENT_PARSER overrides the
1492
- // parser id (e.g. to "bivy-protocol").
1493
- const structuredPref = process.env.BIVY_AGENT_STRUCTURED;
1494
- const parserReady = Boolean(spec.parserId) && (!spec.parserUnverified || structuredPref === "1");
1495
- const structured = parserReady && structuredPref !== "0";
1553
+ const structured = executionMode === "structured-pipe";
1496
1554
  const parserId = process.env.BIVY_AGENT_PARSER || (structured ? spec.parserId : undefined);
1497
1555
  const tier = sandboxTier(options.sandbox);
1498
1556
  // BIVY_<ID>_ARGS overrides the launch flags for a CLI version we haven't
package/dist/server.js CHANGED
@@ -46,6 +46,7 @@ import { buildSessionSnapshot, applySessionSnapshot } from "./session/snapshot.j
46
46
  import { createCheckpointBundle, applyCheckpointBundle, materializeCheckpoint } from "./session/checkpoint-pack.js";
47
47
  import { PolicyEngine } from "./policy/policy-engine.js";
48
48
  import { TerminalManager } from "./terminal.js";
49
+ import { commandLaunch } from "./command-launch.js";
49
50
  import { listMultiplexerSessions, attachCommand } from "./multiplexer.js";
50
51
  import { createWorktree, removeWorktree, branchSlug, gitRepoRoot } from "./worktree.js";
51
52
  import { HarnessManager } from "./harness/manager.js";
@@ -1415,12 +1416,12 @@ const commands = [
1415
1416
  { name: "/help", description: "Show quick chat help.", kind: "server", run: () => ({
1416
1417
  text: "Use /commands to open the command list. Press Cmd/Ctrl+Enter to send a prompt. Attach files/images with the + button or by dragging them into the message box.",
1417
1418
  }) },
1418
- { name: "/login", description: "Connect a model provider in Terminal.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "/login"] } },
1419
- { name: "/model", description: "Open the searchable model selector.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "/model"] } },
1420
- { name: "/terminal", description: "Start the terminal agent in this workspace and stream its output.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix] } },
1421
- { name: "/config", description: "Show agent configuration.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "config"] } },
1422
- { name: "/list", description: "List installed agent packages.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "list"] } },
1423
- { name: "/update", description: "Update agent packages.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "update"] } },
1419
+ { name: "/login", description: "Connect a model provider in Terminal.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "/login"], requiresTty: true } },
1420
+ { name: "/model", description: "Open the searchable model selector.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "/model"], requiresTty: true } },
1421
+ { name: "/terminal", description: "Start the terminal agent in this workspace and stream its output.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix], requiresTty: true } },
1422
+ { name: "/config", description: "Show agent configuration.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "config"], requiresTty: true } },
1423
+ { name: "/list", description: "List installed agent packages.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "list"], requiresTty: true } },
1424
+ { name: "/update", description: "Update agent packages.", kind: "native", spawn: { command: process.execPath, args: [...agentPrefix, "update"], requiresTty: true } },
1424
1425
  ];
1425
1426
  // A local client whose send buffer has grown past this is behind on reads (slow
1426
1427
  // network, background tab); used by both broadcast paths below.
@@ -5309,9 +5310,6 @@ function stripAnsi(text) {
5309
5310
  .replace(/\r\n/g, "\n")
5310
5311
  .replace(/\r/g, "\n");
5311
5312
  }
5312
- function wrapWithSystemPty(spawnCommand, args) {
5313
- return { command: pythonCommand, args: [ptyRunnerScript, spawnCommand, ...args] };
5314
- }
5315
5313
  function shellQuote(value) {
5316
5314
  return `'${value.replace(/'/g, `'\\''`)}'`;
5317
5315
  }
@@ -5433,18 +5431,20 @@ function runNativeCommand(command) {
5433
5431
  throw new Error(`Command ${command.name} is not executable`);
5434
5432
  const runId = `cmd-${Date.now()}`;
5435
5433
  broadcast({ type: "command.started", runId, command: command.name });
5436
- const wrapped = wrapWithSystemPty(command.spawn.command, command.spawn.args);
5437
- const child = spawn(wrapped.command, wrapped.args, {
5438
- cwd: repoRoot,
5439
- env: {
5440
- ...process.env,
5441
- PI_CODING_AGENT_DIR: piDir,
5442
- BIVY_WORKSPACE: active?.workspace ?? defaultWorkspace,
5443
- TERM: "xterm-256color",
5444
- FORCE_COLOR: "0",
5445
- NO_COLOR: "1",
5446
- },
5447
- });
5434
+ const env = {
5435
+ ...process.env,
5436
+ PI_CODING_AGENT_DIR: piDir,
5437
+ BIVY_WORKSPACE: active?.workspace ?? defaultWorkspace,
5438
+ TERM: "xterm-256color",
5439
+ FORCE_COLOR: "0",
5440
+ NO_COLOR: "1",
5441
+ };
5442
+ // Native commands (login/model/config/etc.) need a TTY for prompts and the
5443
+ // terminal UI. Everything else uses ordinary pipes: this avoids an extra
5444
+ // Python process and PTY relay for non-interactive commands while preserving
5445
+ // the old behavior for commands that genuinely require terminal semantics.
5446
+ const launch = commandLaunch(command.spawn.command, command.spawn.args, command.spawn.requiresTty, pythonCommand, ptyRunnerScript);
5447
+ const child = spawn(launch.command, launch.args, { cwd: repoRoot, env });
5448
5448
  commandProcesses.set(runId, child);
5449
5449
  child.stdout.on("data", (data) => {
5450
5450
  broadcast({ type: "command.output", runId, command: command.name, stream: "stdout", text: stripAnsi(String(data)) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.5.1-staging.71",
3
+ "version": "0.5.1-staging.73",
4
4
  "type": "module",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "description": "Run coding agents on machines you own. Source-available, self-hostable agent workspace.",