@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 +36 -1
- package/dist/audit/index.js +69 -0
- package/dist/server.js +28 -1
- package/package.json +1 -1
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
|
-
|
|
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