@bivy/bivy 0.10.1-staging.209 → 0.10.1-staging.211

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/bin/bivy.mjs CHANGED
@@ -1681,7 +1681,7 @@ function cmdCompletions(args = []) {
1681
1681
  const commands = [
1682
1682
  "run", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
1683
1683
  "send", "attach", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "logs", "login",
1684
- "update", "update:log", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
1684
+ "update", "update:log", "audit", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
1685
1685
  "github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
1686
1686
  ];
1687
1687
  const agents = [...AGENT_INTEGRATIONS.keys()];
@@ -4088,6 +4088,38 @@ async function runUpdate(args = []) {
4088
4088
  console.log(c.dim(`=== bivy update finished ${new Date().toISOString()} ===`));
4089
4089
  }
4090
4090
 
4091
+ // Show the node's governance audit trail (moat #1): the append-only record of
4092
+ // tool-call decisions (and, as it grows, network/approval events) the node made,
4093
+ // attributed per session + agent. Reads the JSONL directly — the daemon need not
4094
+ // be running. Decisions + metadata only; never tool payloads.
4095
+ function cmdAudit(args = []) {
4096
+ if (args.includes("-h") || args.includes("--help")) {
4097
+ console.log("Usage: bivy audit [--session <id>] [--kind <kind>] [--limit <n>] [--json]\n\nShow the node's governance audit trail (tool-call decisions, …).");
4098
+ return;
4099
+ }
4100
+ const file = path.join(appDir, "audit", "audit.jsonl");
4101
+ let raw;
4102
+ try { raw = fs.readFileSync(file, "utf8"); }
4103
+ catch { console.log("No audit events recorded yet."); return; }
4104
+ const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
4105
+ const session = flag("--session");
4106
+ const kind = flag("--kind");
4107
+ const limit = Number(flag("--limit")) || 0;
4108
+ let events = raw.split("\n").map((l) => l.trim()).filter(Boolean)
4109
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
4110
+ if (session) events = events.filter((e) => e.session === session);
4111
+ if (kind) events = events.filter((e) => e.kind === kind);
4112
+ if (limit > 0) events = events.slice(-limit);
4113
+ if (args.includes("--json")) { console.log(JSON.stringify(events, null, 2)); return; }
4114
+ if (!events.length) { console.log("No matching audit events."); return; }
4115
+ for (const e of events) {
4116
+ const when = new Date(e.ts).toISOString();
4117
+ const cols = [when, e.kind, e.agent || "-", e.session ? String(e.session).slice(0, 8) : "-", e.tool || e.host || "", e.decision || ""].filter((p) => p !== "");
4118
+ console.log(cols.join(" "));
4119
+ if (e.reason) console.log(` reason: ${e.reason}`);
4120
+ }
4121
+ }
4122
+
4091
4123
  // Print the update log (default: the tail; `-f`/`--follow` streams new output).
4092
4124
  // After a `bivy update` detaches and this terminal reconnects, this is how you
4093
4125
  // confirm the background update succeeded.
@@ -4607,6 +4639,9 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4607
4639
  case "update:log":
4608
4640
  cmdUpdateLog(args);
4609
4641
  break;
4642
+ case "audit":
4643
+ cmdAudit(args);
4644
+ break;
4610
4645
  case "agents:install":
4611
4646
  case "runtimes:install":
4612
4647
  if (args.includes("-h") || args.includes("--help")) {
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ //
4
+ // Node audit trail — the governance record (moat work #1). One append-only
5
+ // JSONL stream of GOVERNANCE events (tool-call decisions, network attempts,
6
+ // approvals, …) that Bivy already intercepts at the substrate, attributed per
7
+ // session + agent, uniformly across every agent. This is distinct from the
8
+ // per-session transcript (`session/event-log.ts`, which is UI history): the
9
+ // audit trail answers "what did agents do on this node, and did Bivy allow it",
10
+ // and is queryable/exportable via `bivy audit`.
11
+ //
12
+ // REDACTION IS THE CALLER'S CONTRACT: this writer records exactly the fields it
13
+ // is handed, so callers pass DECISIONS + bounded METADATA (tool name, allow/deny,
14
+ // host:port) and NEVER payloads (tool args/results, request bodies, source).
15
+ // Payload capture stays opt-in and separate (see runtime/tool-trace.ts).
16
+ //
17
+ // Pure fs leaf: imports nothing from the kernel; the daemon constructs one
18
+ // AuditLog and hands `record` to the guardian / egress / approval seams.
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ /** Build the node's audit log under `<dir>/audit.jsonl` (dir created lazily). */
22
+ export function createAuditLog(dir) {
23
+ const file = path.join(dir, "audit.jsonl");
24
+ let ensured = false;
25
+ return {
26
+ file,
27
+ record(event) {
28
+ try {
29
+ if (!ensured) {
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ ensured = true;
32
+ }
33
+ fs.appendFileSync(file, `${JSON.stringify({ ts: Date.now(), ...event })}\n`);
34
+ }
35
+ catch {
36
+ /* best-effort: audit failure must never break a session */
37
+ }
38
+ },
39
+ };
40
+ }
41
+ /** Read + filter the audit trail. Malformed lines are skipped, not thrown. */
42
+ export function readAuditEvents(file, opts = {}) {
43
+ let raw;
44
+ try {
45
+ raw = fs.readFileSync(file, "utf8");
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ const out = [];
51
+ for (const line of raw.split("\n")) {
52
+ const trimmed = line.trim();
53
+ if (!trimmed)
54
+ continue;
55
+ let event;
56
+ try {
57
+ event = JSON.parse(trimmed);
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ if (opts.session && event.session !== opts.session)
63
+ continue;
64
+ if (opts.kind && event.kind !== opts.kind)
65
+ continue;
66
+ out.push(event);
67
+ }
68
+ return opts.limit && opts.limit > 0 ? out.slice(-opts.limit) : out;
69
+ }
package/dist/server.js CHANGED
@@ -15,6 +15,7 @@ import { DEFAULT_BACKOFF } from "./policy/ruleset.js";
15
15
  import { SessionRerouteController } from "./policy/session-reroute.js";
16
16
  import { activeRulesetFor } from "./runtime/ruleset-store.js";
17
17
  import { createRulesetController } from "./controllers/rulesets.js";
18
+ import { createAuditLog } from "./audit/index.js";
18
19
  import { createWorkspaceController } from "./controllers/workspaces.js";
19
20
  import { createModelController } from "./controllers/models.js";
20
21
  import { Type } from "typebox";
@@ -319,6 +320,11 @@ function publishBootstrapSecret() {
319
320
  }
320
321
  }
321
322
  const approvals = new ApprovalManager();
323
+ // Node audit trail (moat #1): one append-only, redaction-aware record of the
324
+ // governance events Bivy already intercepts — tool-call decisions today,
325
+ // network/approval events next — attributed per session + agent, queryable via
326
+ // `bivy audit`. Distinct from the per-session transcript (session/event-log.ts).
327
+ const auditLog = createAuditLog(path.join(appDir, "audit"));
322
328
  // Bivy owns the AskUserQuestion → question-card feature at the guardian layer,
323
329
  // runtime-agnostically (see src/question.ts). The manager holds every pending
324
330
  // clarifying question across sessions; the guardian interceptor below raises
@@ -5297,14 +5303,35 @@ const guardianInterceptorImpl = async ({ sessionId, toolName, input, signal }) =
5297
5303
  // boundary (policy + approvals + questions), so if any of it throws — a bug in
5298
5304
  // rule evaluation, a rejected approval promise, an aborted signal — the tool
5299
5305
  // must be BLOCKED, never allowed to fall through to the SDK's default outcome.
5306
+ // Record the guardian's decision for every governed tool call — the core of the
5307
+ // node audit trail. AskUserQuestion is a Bivy interaction (handled), not a
5308
+ // governed side effect, so it is skipped. Records the tool NAME + allow/deny
5309
+ // decision only — never the tool payload (redaction contract; see src/audit).
5310
+ function recordToolCallAudit(params, outcome) {
5311
+ const o = outcome;
5312
+ if (o?.handled)
5313
+ return;
5314
+ const agent = openSessions.get(params.sessionId)?.runtimeId;
5315
+ auditLog.record({
5316
+ kind: "tool.call",
5317
+ session: params.sessionId,
5318
+ ...(agent ? { agent } : {}),
5319
+ tool: params.toolName,
5320
+ decision: o?.block ? "blocked" : "allowed",
5321
+ ...(o?.block && typeof o.reason === "string" ? { reason: o.reason } : {}),
5322
+ });
5323
+ }
5300
5324
  const guardianInterceptor = async (params) => {
5301
5325
  try {
5302
- return await guardianInterceptorImpl(params);
5326
+ const outcome = await guardianInterceptorImpl(params);
5327
+ recordToolCallAudit(params, outcome);
5328
+ return outcome;
5303
5329
  }
5304
5330
  catch (error) {
5305
5331
  broadcast({ type: "tool.blocked", sessionId: params.sessionId, toolName: params.toolName, reason: "internal approval error" });
5306
5332
  if (process.env.BIVY_DEBUG)
5307
5333
  console.error("guardianInterceptor error:", error);
5334
+ recordToolCallAudit(params, { block: true, reason: "internal approval error" });
5308
5335
  return { block: true, reason: "internal approval error" };
5309
5336
  }
5310
5337
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.209",
3
+ "version": "0.10.1-staging.211",
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.",