@agentvault/claude-bridge 0.6.5 → 0.7.0

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.
@@ -25,4 +25,13 @@ export declare function drainApprovals(dataDir: string): DrainedApproval[];
25
25
  * Usage: `av-bridge approve-arm <request-id> <room-id>`. Both are shown to the
26
26
  * operator in the bridge's arm-request log line and the web arm sheet. */
27
27
  export declare function maybeRunApproveArmSubcommand(argv: string[], dataDir: string): boolean;
28
+ /**
29
+ * #20 — `av-bridge trust-host [--yes]` / `av-bridge untrust-host [--all]`.
30
+ *
31
+ * Headless like `approve-arm` (no channel, no network) so it works over a bare
32
+ * SSH session with no daemon running. The device-id is ALWAYS read from the
33
+ * persisted identity, never taken from argv — an operator-typed id is exactly
34
+ * the confused-deputy input F1 closed for approve-arm.
35
+ */
36
+ export declare function maybeRunHostTrustSubcommand(argv: string[], dataDir: string, root?: string): boolean;
28
37
  //# sourceMappingURL=approve-cli.d.ts.map
package/dist/bridge.d.ts CHANGED
@@ -61,7 +61,17 @@ export interface RoomChannel {
61
61
  sendWorkerHeartbeat?(h: {
62
62
  workerCapable: boolean;
63
63
  armedRooms: string[];
64
+ hostTrusted: boolean;
65
+ gatesEffective: boolean;
64
66
  }): void;
67
+ /**
68
+ * #20: this worker's own device-id, checked against the host-trust directory
69
+ * to decide whether a web-granted "Remove all gates" switch actually takes
70
+ * effect (see bsafe-arming.ts). Optional so legacy/test fakes without it
71
+ * don't break wiring — `decideArming` treats a missing id as untrusted
72
+ * (fail-closed), matching `channel.deviceId ?? ""`.
73
+ */
74
+ deviceId?: string | null;
65
75
  }
66
76
  /**
67
77
  * #392 cooperative quiet: tracks per-room hush windows so the native agent holds
@@ -207,5 +217,14 @@ export declare function wireBridge(channel: RoomChannel, session: RoomSession, t
207
217
  * worker session's gate.
208
218
  */
209
219
  onGatesRemoved?: (getter: () => boolean) => void;
220
+ /**
221
+ * #20 test-only injection point: overrides where `isTrusted` (via
222
+ * `decideArming`) looks for host-trust markers, mirroring `host-trust.ts`'s
223
+ * own `root?` parameter. Production callers never pass this, so
224
+ * `decideArming` receives `undefined` and `isTrusted` falls back to its
225
+ * default `~/.agentvault` — unchanged from today. Tests pass a `mkdtemp`
226
+ * dir so they can `grant(...)` into it without touching the real host.
227
+ */
228
+ trustRoot?: string;
210
229
  }): ArmingState;
211
230
  //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1,30 @@
1
+ export type ArmingMode = "authoritative-under-grant" | "authoritative-bsafe" | "c2-disarm-only" | "authoritative-bpure";
2
+ export interface ArmingDecision {
3
+ /** The grant actually applied — web intent AND host trust. */
4
+ gatesRemoved: boolean;
5
+ mode: ArmingMode;
6
+ /**
7
+ * True when rooms armed from THIS snapshot must be remembered as revocable:
8
+ * the web armed them on an OS-isolated worker under an authority that can
9
+ * lapse. BOTH authorities qualify — the full-access grant AND host trust on
10
+ * its own — because both lapse into `c2-disarm-only`, which is the only place
11
+ * the record is ever consumed.
12
+ *
13
+ * This was `recordUnderGrant`, true for the grant alone, and that was the
14
+ * defect the 2026-07-27 whole-branch review found: rooms armed under
15
+ * `authoritative-bsafe` went unrecorded, so `av-bridge untrust-host` left them
16
+ * armed — and an armed room on an OS-isolated worker means Bash is allowed.
17
+ *
18
+ * `authoritative-bpure` stays false: a non-isolated worker never reaches
19
+ * `c2-disarm-only`, so the record would never be read.
20
+ */
21
+ recordWebArmed: boolean;
22
+ hostTrusted: boolean;
23
+ }
24
+ export declare function decideArming(opts: {
25
+ snapshotGatesRemoved: boolean;
26
+ osIsolated: boolean;
27
+ deviceId: string;
28
+ trustRoot?: string;
29
+ }): ArmingDecision;
30
+ //# sourceMappingURL=bsafe-arming.d.ts.map
@@ -0,0 +1,31 @@
1
+ export declare class HostTrustError extends Error {
2
+ }
3
+ export declare function trustDir(root?: string): string;
4
+ /**
5
+ * Fail-closed in every direction: any doubt at all means untrusted.
6
+ *
7
+ * `lstatSync`, NOT `statSync`: a symlink must never count as a marker. This
8
+ * used `statSync`, which FOLLOWS symlinks, so `ln -s <any regular file>
9
+ * ~/.agentvault/host-trust/<device-id>` read as a grant. That was not
10
+ * exploitable under today's threat model — the stated adversary is web-only and
11
+ * cannot create files on the host, and an agent with Bash could write the real
12
+ * marker anyway — but it is precisely the bypass that would defeat the deferred
13
+ * Task 10 root-owned marker, whose whole claim is that an agent running as the
14
+ * user cannot forge one. The trust DIRECTORY is deliberately user-owned so that
15
+ * revocation needs no privilege, so an agent with a shell can plant a symlink
16
+ * there; pointed at any root-owned, non-group/other-writable file (`/etc/passwd`
17
+ * is uid 0, mode 644 on both macOS and Linux) it would satisfy every clause of
18
+ * that check against the TARGET. Rejecting symlinks here closes it in advance.
19
+ */
20
+ export declare function isTrusted(deviceId: string, root?: string): boolean;
21
+ export declare function grant(deviceId: string, root?: string): void;
22
+ /** Returns true only if a marker was actually removed. */
23
+ export declare function revoke(deviceId: string, root?: string): boolean;
24
+ export declare function listTrusted(root?: string): string[];
25
+ /**
26
+ * Resolve this agent's device-id from its persisted identity. `trust-host` runs
27
+ * headless with no channel and possibly no running bridge, so it cannot ask the
28
+ * backend who it is — and it must never accept an operator-typed id (F1).
29
+ */
30
+ export declare function readDeviceId(dataDir: string): string;
31
+ //# sourceMappingURL=host-trust.d.ts.map
package/dist/index.js CHANGED
@@ -155,7 +155,7 @@ var init_launcher = __esm({
155
155
  });
156
156
 
157
157
  // src/service/spec.ts
158
- import { join as join7 } from "node:path";
158
+ import { join as join8 } from "node:path";
159
159
  function buildServiceSpec(cfg, opts) {
160
160
  const env = {
161
161
  AV_AGENT_NAME: cfg.agentName,
@@ -168,7 +168,7 @@ function buildServiceSpec(cfg, opts) {
168
168
  if (cfg.armRoom) env.AV_ARM_ROOM = "1";
169
169
  if (cfg.model) env.AV_CLAUDE_MODEL = cfg.model;
170
170
  if (cfg.systemPrompt) env.AV_SYSTEM_PROMPT = cfg.systemPrompt;
171
- const launcherPath = join7(cfg.dataDir, "launch-bridge.sh");
171
+ const launcherPath = join8(cfg.dataDir, "launch-bridge.sh");
172
172
  return {
173
173
  label: `dev.agentvault.bridge-${slugify2(cfg.agentName)}`,
174
174
  programArgs: [launcherPath],
@@ -176,8 +176,8 @@ function buildServiceSpec(cfg, opts) {
176
176
  launcherScript: renderLauncher({ installPath: opts.path, entrypoint: opts.entrypoint }),
177
177
  env,
178
178
  workingDir: cfg.workspaceDir ? cfg.workspaceDir : opts.home,
179
- logOut: join7(cfg.dataDir, "logs", "bridge.log"),
180
- logErr: join7(cfg.dataDir, "logs", "bridge.error.log"),
179
+ logOut: join8(cfg.dataDir, "logs", "bridge.log"),
180
+ logErr: join8(cfg.dataDir, "logs", "bridge.error.log"),
181
181
  keepAliveUnlessCleanExit: true,
182
182
  runAtLoad: true
183
183
  };
@@ -191,7 +191,7 @@ var init_spec = __esm({
191
191
  });
192
192
 
193
193
  // src/service/launchd.ts
194
- import { dirname as dirname4, join as join8 } from "node:path";
194
+ import { dirname as dirname4, join as join9 } from "node:path";
195
195
  function esc3(s10) {
196
196
  return s10.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
197
197
  }
@@ -238,7 +238,7 @@ var init_launchd = __esm({
238
238
  }
239
239
  deps;
240
240
  plistPath(label) {
241
- return join8(this.deps.home, "Library", "LaunchAgents", `${label}.plist`);
241
+ return join9(this.deps.home, "Library", "LaunchAgents", `${label}.plist`);
242
242
  }
243
243
  domain() {
244
244
  return `gui/${this.deps.uid}`;
@@ -281,7 +281,7 @@ var init_launchd = __esm({
281
281
  });
282
282
 
283
283
  // src/service/systemd.ts
284
- import { dirname as dirname5, join as join9 } from "node:path";
284
+ import { dirname as dirname5, join as join10 } from "node:path";
285
285
  function q5(s10) {
286
286
  const esc4 = s10.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/%/g, "%%");
287
287
  return `"${esc4}"`;
@@ -318,7 +318,7 @@ var init_systemd = __esm({
318
318
  }
319
319
  deps;
320
320
  unitPath(label) {
321
- return join9(this.deps.home, ".config", "systemd", "user", `${label}.service`);
321
+ return join10(this.deps.home, ".config", "systemd", "user", `${label}.service`);
322
322
  }
323
323
  install(spec) {
324
324
  const path2 = this.unitPath(spec.label);
@@ -349,7 +349,7 @@ var init_systemd = __esm({
349
349
 
350
350
  // src/service/backend.ts
351
351
  import { spawnSync } from "node:child_process";
352
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, rmSync as rmSync3, existsSync as existsSync4, chmodSync as chmodSync2 } from "node:fs";
352
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync4, existsSync as existsSync5, chmodSync as chmodSync2 } from "node:fs";
353
353
  import { userInfo } from "node:os";
354
354
  function defaultDeps() {
355
355
  return {
@@ -360,11 +360,11 @@ function defaultDeps() {
360
360
  const r7 = spawnSync(cmd, args, { encoding: "utf8" });
361
361
  return { code: r7.status ?? 1, stdout: r7.stdout ?? "" };
362
362
  },
363
- writeFile: (p2, d10) => writeFileSync2(p2, d10),
364
- mkdir: (p2) => mkdirSync4(p2, { recursive: true }),
363
+ writeFile: (p2, d10) => writeFileSync3(p2, d10),
364
+ mkdir: (p2) => mkdirSync5(p2, { recursive: true }),
365
365
  chmod: (p2, m6) => chmodSync2(p2, m6),
366
- rm: (p2) => rmSync3(p2, { force: true }),
367
- exists: (p2) => existsSync4(p2)
366
+ rm: (p2) => rmSync4(p2, { force: true }),
367
+ exists: (p2) => existsSync5(p2)
368
368
  };
369
369
  }
370
370
  function selectBackend(platform, deps) {
@@ -447,8 +447,8 @@ var init_subcommand = __esm({
447
447
  });
448
448
 
449
449
  // src/index.ts
450
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "node:fs";
451
- import { join as join10 } from "node:path";
450
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
451
+ import { join as join11 } from "node:path";
452
452
 
453
453
  // ../plugin/dist/index.js
454
454
  import * as nc from "node:crypto";
@@ -65559,7 +65559,7 @@ var init_channel = __esm2({
65559
65559
  */
65560
65560
  sendActivitySpan(spanData) {
65561
65561
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65562
- const pluginVersion = true ? "0.23.10" : "0.0.0-dev";
65562
+ const pluginVersion = true ? "0.23.11" : "0.0.0-dev";
65563
65563
  const agentName = this.config.agentName ?? "Agent";
65564
65564
  const resource = {
65565
65565
  "service.name": "agentvault-agent",
@@ -65593,7 +65593,16 @@ var init_channel = __esm2({
65593
65593
  this._ws.send(
65594
65594
  JSON.stringify({
65595
65595
  event: "worker_heartbeat",
65596
- data: { worker_capable: h22.workerCapable, armed_rooms: h22.armedRooms }
65596
+ data: {
65597
+ worker_capable: h22.workerCapable,
65598
+ armed_rooms: h22.armedRooms,
65599
+ // #20: OPTIONAL on the way in so an older bridge against a newer
65600
+ // plugin still emits a shape the backend can read. `=== true` means
65601
+ // absent/malformed becomes false — the fail-closed direction, and it
65602
+ // never emits `undefined` (which would drop the key entirely).
65603
+ host_trusted: h22.hostTrusted === true,
65604
+ gates_effective: h22.gatesEffective === true
65605
+ }
65597
65606
  })
65598
65607
  );
65599
65608
  }
@@ -67289,7 +67298,7 @@ var init_channel = __esm2({
67289
67298
  agentVersion: this.config.agentVersion ?? "0.0.0",
67290
67299
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67291
67300
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67292
- pluginVersion: true ? "0.23.10" : "0.0.0-dev"
67301
+ pluginVersion: true ? "0.23.11" : "0.0.0-dev"
67293
67302
  });
67294
67303
  this._telemetryReporter.startAutoFlush(3e4);
67295
67304
  }
@@ -67607,7 +67616,7 @@ var init_channel = __esm2({
67607
67616
  agentVersion: this.config.agentVersion ?? "0.0.0",
67608
67617
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67609
67618
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67610
- pluginVersion: true ? "0.23.10" : "0.0.0-dev"
67619
+ pluginVersion: true ? "0.23.11" : "0.0.0-dev"
67611
67620
  });
67612
67621
  this._telemetryReporter.startAutoFlush(3e4);
67613
67622
  }
@@ -68481,13 +68490,13 @@ ${messageText}`;
68481
68490
  * Looks for OpenClaw workspace config, falls back to default path.
68482
68491
  */
68483
68492
  _resolveWorkspaceDir() {
68484
- const homedir = osHomedir();
68493
+ const homedir2 = osHomedir();
68485
68494
  const agentName = this.config.agentName;
68486
68495
  if (this._persisted?.agentRole === "lead") {
68487
- return join4(homedir, ".openclaw", "workspace");
68496
+ return join4(homedir2, ".openclaw", "workspace");
68488
68497
  }
68489
68498
  try {
68490
- const configPath = join4(homedir, ".openclaw", "openclaw.json");
68499
+ const configPath = join4(homedir2, ".openclaw", "openclaw.json");
68491
68500
  const raw = readFileSync(configPath, "utf-8");
68492
68501
  const config22 = JSON.parse(raw);
68493
68502
  const agents = config22?.agents?.list;
@@ -68501,9 +68510,9 @@ ${messageText}`;
68501
68510
  } catch {
68502
68511
  }
68503
68512
  if (agentName && agentName !== "CLI Agent" && agentName !== "OpenClaw Agent") {
68504
- return join4(homedir, ".openclaw", `workspace-${agentName}`);
68513
+ return join4(homedir2, ".openclaw", `workspace-${agentName}`);
68505
68514
  }
68506
- return join4(homedir, ".openclaw", "workspace");
68515
+ return join4(homedir2, ".openclaw", "workspace");
68507
68516
  }
68508
68517
  /**
68509
68518
  * Send a structured JSON reply to a specific conversation.
@@ -68702,20 +68711,20 @@ ${messageText}`;
68702
68711
  try {
68703
68712
  const payload = JSON.parse(plaintext);
68704
68713
  if (messageType === "credential_grant") {
68705
- const grant = payload;
68706
- if (grant.nonce && !this._credentialStore.checkNonce(roomId, grant.nonce)) {
68714
+ const grant2 = payload;
68715
+ if (grant2.nonce && !this._credentialStore.checkNonce(roomId, grant2.nonce)) {
68707
68716
  console.warn(`[SecureChannel] Credential grant replay detected for room ${roomId.slice(0, 8)}..., ignoring`);
68708
68717
  return;
68709
68718
  }
68710
68719
  const keys = [];
68711
- for (const cred of grant.credentials || []) {
68720
+ for (const cred of grant2.credentials || []) {
68712
68721
  this._credentialStore.grant(roomId, {
68713
68722
  key: cred.key,
68714
68723
  value: cred.value,
68715
68724
  type: cred.type,
68716
68725
  scope: cred.scope,
68717
- grantedAt: grant.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
68718
- agreementId: grant.agreement_id,
68726
+ grantedAt: grant2.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
68727
+ agreementId: grant2.agreement_id,
68719
68728
  roomId
68720
68729
  });
68721
68730
  keys.push(cred.key);
@@ -68723,21 +68732,21 @@ ${messageText}`;
68723
68732
  console.log(
68724
68733
  `[SecureChannel] Credentials granted for room ${roomId.slice(0, 8)}...: ${keys.join(", ")} (${keys.length} keys)`
68725
68734
  );
68726
- this._sendCredentialAck(roomId, grant.agreement_id, keys, "stored");
68727
- this.emit("credentials_granted", { roomId, keys, agreementId: grant.agreement_id });
68735
+ this._sendCredentialAck(roomId, grant2.agreement_id, keys, "stored");
68736
+ this.emit("credentials_granted", { roomId, keys, agreementId: grant2.agreement_id });
68728
68737
  } else if (messageType === "credential_revoke") {
68729
- const revoke = payload;
68738
+ const revoke2 = payload;
68730
68739
  const revoked = [];
68731
- for (const key of revoke.credential_keys || []) {
68740
+ for (const key of revoke2.credential_keys || []) {
68732
68741
  if (this._credentialStore.revoke(roomId, key)) {
68733
68742
  revoked.push(key);
68734
68743
  }
68735
68744
  }
68736
68745
  console.log(
68737
- `[SecureChannel] Credentials revoked for room ${roomId.slice(0, 8)}...: ${revoked.join(", ")} (reason: ${revoke.reason || "none"})`
68746
+ `[SecureChannel] Credentials revoked for room ${roomId.slice(0, 8)}...: ${revoked.join(", ")} (reason: ${revoke2.reason || "none"})`
68738
68747
  );
68739
- this._sendCredentialAck(roomId, revoke.agreement_id, revoked, "revoked");
68740
- this.emit("credentials_revoked", { roomId, keys: revoked, agreementId: revoke.agreement_id });
68748
+ this._sendCredentialAck(roomId, revoke2.agreement_id, revoked, "revoked");
68749
+ this.emit("credentials_revoked", { roomId, keys: revoked, agreementId: revoke2.agreement_id });
68741
68750
  }
68742
68751
  } catch (err) {
68743
68752
  console.error("[SecureChannel] Error handling credential message:", err);
@@ -97386,7 +97395,7 @@ var init_index = __esm2({
97386
97395
  init_skill_invoker();
97387
97396
  await init_skill_telemetry();
97388
97397
  await init_policy_enforcer();
97389
- VERSION = true ? "0.23.10" : "0.0.0-dev";
97398
+ VERSION = true ? "0.23.11" : "0.0.0-dev";
97390
97399
  }
97391
97400
  });
97392
97401
  await init_index();
@@ -118691,6 +118700,72 @@ function Z_($10, Q4) {
118691
118700
  // src/worker-permission.ts
118692
118701
  import { realpathSync as realpathSync2 } from "node:fs";
118693
118702
  import { resolve as resolve3, dirname as dirname2, basename, sep as sep2, isAbsolute } from "node:path";
118703
+
118704
+ // src/host-trust.ts
118705
+ import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, existsSync as existsSync3, readdirSync as readdirSync2, lstatSync as lstatSync2, readFileSync as readFileSync4 } from "node:fs";
118706
+ import { join as join6 } from "node:path";
118707
+ import { homedir, hostname as hostname3 } from "node:os";
118708
+ var TRUST_SUBDIR = "host-trust";
118709
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
118710
+ var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
118711
+ var HostTrustError = class extends Error {
118712
+ };
118713
+ function trustDir(root3) {
118714
+ return join6(root3 ?? join6(homedir(), ".agentvault"), TRUST_SUBDIR);
118715
+ }
118716
+ function sanitize(deviceId) {
118717
+ if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) {
118718
+ throw new HostTrustError("invalid device-id");
118719
+ }
118720
+ return deviceId;
118721
+ }
118722
+ function isTrusted(deviceId, root3) {
118723
+ try {
118724
+ if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) return false;
118725
+ const p2 = join6(trustDir(root3), deviceId);
118726
+ return lstatSync2(p2).isFile();
118727
+ } catch {
118728
+ return false;
118729
+ }
118730
+ }
118731
+ function grant(deviceId, root3) {
118732
+ const id = sanitize(deviceId);
118733
+ const dir = trustDir(root3);
118734
+ mkdirSync3(dir, { recursive: true });
118735
+ writeFileSync(
118736
+ join6(dir, id),
118737
+ `granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
118738
+ `
118739
+ );
118740
+ }
118741
+ function revoke(deviceId, root3) {
118742
+ const id = sanitize(deviceId);
118743
+ const p2 = join6(trustDir(root3), id);
118744
+ if (!existsSync3(p2)) return false;
118745
+ rmSync2(p2, { force: true, recursive: true });
118746
+ return true;
118747
+ }
118748
+ function listTrusted(root3) {
118749
+ try {
118750
+ return readdirSync2(trustDir(root3)).filter((n10) => ID_RE.test(n10));
118751
+ } catch {
118752
+ return [];
118753
+ }
118754
+ }
118755
+ function readDeviceId(dataDir) {
118756
+ for (const f7 of IDENTITY_FILES) {
118757
+ try {
118758
+ const parsed = JSON.parse(readFileSync4(join6(dataDir, f7), "utf-8"));
118759
+ if (parsed?.deviceId && ID_RE.test(parsed.deviceId)) return parsed.deviceId;
118760
+ } catch {
118761
+ }
118762
+ }
118763
+ throw new HostTrustError(
118764
+ "no device identity found in the data dir \u2014 start the bridge once to enrol, then re-run"
118765
+ );
118766
+ }
118767
+
118768
+ // src/worker-permission.ts
118694
118769
  var PATH_FIELDS = ["file_path", "path", "notebook_path"];
118695
118770
  function canonical(p2) {
118696
118771
  const abs = resolve3(p2);
@@ -118734,6 +118809,12 @@ function gateDecision(toolName, input, opts) {
118734
118809
  if (!opts.isToolTurn()) {
118735
118810
  return { deny: true, reason: "tools are disabled on this turn; reply with the say tool only" };
118736
118811
  }
118812
+ if (FILE_TOOLS.has(toolName)) {
118813
+ const trustRoot = canonical(trustDir());
118814
+ if (pathsOf(input).some((p2) => within(p2, trustRoot))) {
118815
+ return { deny: true, reason: "the host-trust directory is not writable by the agent" };
118816
+ }
118817
+ }
118737
118818
  if (opts.gatesRemoved?.()) {
118738
118819
  return { deny: false };
118739
118820
  }
@@ -118973,7 +119054,7 @@ __export(external_exports, {
118973
119054
  guid: () => guid4,
118974
119055
  hash: () => hash2,
118975
119056
  hex: () => hex4,
118976
- hostname: () => hostname4,
119057
+ hostname: () => hostname5,
118977
119058
  httpUrl: () => httpUrl2,
118978
119059
  includes: () => _includes2,
118979
119060
  instanceof: () => _instanceof2,
@@ -120360,7 +120441,7 @@ __export(regexes_exports2, {
120360
120441
  extendedDuration: () => extendedDuration2,
120361
120442
  guid: () => guid3,
120362
120443
  hex: () => hex3,
120363
- hostname: () => hostname3,
120444
+ hostname: () => hostname4,
120364
120445
  html5Email: () => html5Email2,
120365
120446
  idnEmail: () => idnEmail2,
120366
120447
  integer: () => integer2,
@@ -120437,7 +120518,7 @@ var cidrv43 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5
120437
120518
  var cidrv63 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
120438
120519
  var base643 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
120439
120520
  var base64url3 = /^[A-Za-z0-9_-]*$/;
120440
- var hostname3 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
120521
+ var hostname4 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
120441
120522
  var domain2 = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
120442
120523
  var e1643 = /^\+[1-9]\d{6,14}$/;
120443
120524
  var dateSource2 = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`;
@@ -130807,7 +130888,7 @@ __export(schemas_exports2, {
130807
130888
  guid: () => guid4,
130808
130889
  hash: () => hash2,
130809
130890
  hex: () => hex4,
130810
- hostname: () => hostname4,
130891
+ hostname: () => hostname5,
130811
130892
  httpUrl: () => httpUrl2,
130812
130893
  instanceof: () => _instanceof2,
130813
130894
  int: () => int2,
@@ -131311,7 +131392,7 @@ var ZodCustomStringFormat2 = /* @__PURE__ */ $constructor2("ZodCustomStringForma
131311
131392
  function stringFormat2(format, fnOrRegex, _params = {}) {
131312
131393
  return _stringFormat2(ZodCustomStringFormat2, format, fnOrRegex, _params);
131313
131394
  }
131314
- function hostname4(_params) {
131395
+ function hostname5(_params) {
131315
131396
  return _stringFormat2(ZodCustomStringFormat2, "hostname", regexes_exports2.hostname, _params);
131316
131397
  }
131317
131398
  function hex4(_params) {
@@ -133146,20 +133227,20 @@ var ArmingState = class {
133146
133227
  };
133147
133228
 
133148
133229
  // src/approve-cli.ts
133149
- import { mkdirSync as mkdirSync3, writeFileSync, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
133150
- import { join as join6 } from "node:path";
133230
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync3, existsSync as existsSync4 } from "node:fs";
133231
+ import { join as join7 } from "node:path";
133151
133232
  var APPROVALS_SUBDIR = "arm-approvals";
133152
- var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
133233
+ var ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
133153
133234
  var ApproveArmError = class extends Error {
133154
133235
  };
133155
133236
  function sanitizeRequestId(requestId) {
133156
- if (!ID_RE.test(requestId)) {
133237
+ if (!ID_RE2.test(requestId)) {
133157
133238
  throw new ApproveArmError("invalid request-id");
133158
133239
  }
133159
133240
  return requestId;
133160
133241
  }
133161
133242
  function sanitizeRoomId(roomId) {
133162
- if (!ID_RE.test(roomId)) {
133243
+ if (!ID_RE2.test(roomId)) {
133163
133244
  throw new ApproveArmError("invalid room-id");
133164
133245
  }
133165
133246
  return roomId;
@@ -133167,24 +133248,24 @@ function sanitizeRoomId(roomId) {
133167
133248
  function writeApproval(dataDir, requestId, roomId) {
133168
133249
  const id = sanitizeRequestId(requestId);
133169
133250
  const room = sanitizeRoomId(roomId);
133170
- const dir = join6(dataDir, APPROVALS_SUBDIR);
133171
- mkdirSync3(dir, { recursive: true });
133172
- writeFileSync(join6(dir, id), room);
133251
+ const dir = join7(dataDir, APPROVALS_SUBDIR);
133252
+ mkdirSync4(dir, { recursive: true });
133253
+ writeFileSync2(join7(dir, id), room);
133173
133254
  }
133174
133255
  function drainApprovals(dataDir) {
133175
- const dir = join6(dataDir, APPROVALS_SUBDIR);
133176
- if (!existsSync3(dir)) return [];
133256
+ const dir = join7(dataDir, APPROVALS_SUBDIR);
133257
+ if (!existsSync4(dir)) return [];
133177
133258
  const out = [];
133178
- for (const name of readdirSync2(dir)) {
133179
- if (!ID_RE.test(name)) continue;
133259
+ for (const name of readdirSync3(dir)) {
133260
+ if (!ID_RE2.test(name)) continue;
133180
133261
  let roomId = "";
133181
133262
  try {
133182
- roomId = readFileSync4(join6(dir, name), "utf8").trim();
133263
+ roomId = readFileSync5(join7(dir, name), "utf8").trim();
133183
133264
  } catch {
133184
133265
  }
133185
- out.push({ requestId: name, roomId: ID_RE.test(roomId) ? roomId : "" });
133266
+ out.push({ requestId: name, roomId: ID_RE2.test(roomId) ? roomId : "" });
133186
133267
  try {
133187
- rmSync2(join6(dir, name), { force: true, recursive: true });
133268
+ rmSync3(join7(dir, name), { force: true, recursive: true });
133188
133269
  } catch {
133189
133270
  }
133190
133271
  }
@@ -133208,6 +133289,60 @@ function maybeRunApproveArmSubcommand(argv, dataDir) {
133208
133289
  }
133209
133290
  return true;
133210
133291
  }
133292
+ function maybeRunHostTrustSubcommand(argv, dataDir, root3) {
133293
+ const cmd = argv[2];
133294
+ if (cmd !== "trust-host" && cmd !== "untrust-host") return false;
133295
+ const flags = argv.slice(3);
133296
+ if (cmd === "untrust-host" && flags.includes("--all")) {
133297
+ const ids = listTrusted(root3);
133298
+ for (const id of ids) revoke(id, root3);
133299
+ console.log(`[untrust-host] revoked ${ids.length} host-trust grant(s)`);
133300
+ return true;
133301
+ }
133302
+ let deviceId;
133303
+ try {
133304
+ deviceId = readDeviceId(dataDir);
133305
+ } catch (e7) {
133306
+ console.error(`[${cmd}] ${e7.message}`);
133307
+ process.exitCode = 1;
133308
+ return true;
133309
+ }
133310
+ if (cmd === "untrust-host") {
133311
+ const removed = revoke(deviceId, root3);
133312
+ console.log(
133313
+ removed ? `[untrust-host] revoked host trust for ${deviceId}` : `[untrust-host] no host-trust grant existed for ${deviceId}`
133314
+ );
133315
+ return true;
133316
+ }
133317
+ if (!flags.includes("--yes")) {
133318
+ console.log(
133319
+ `This machine will trust web-granted full access for device ${deviceId}.
133320
+ Writes: ${trustDir(root3)}/${deviceId}
133321
+ Revoke: av-bridge untrust-host
133322
+ Re-run with --yes to confirm.`
133323
+ );
133324
+ return true;
133325
+ }
133326
+ grant(deviceId, root3);
133327
+ console.log(`[trust-host] host trust granted for ${deviceId}`);
133328
+ return true;
133329
+ }
133330
+
133331
+ // src/bsafe-arming.ts
133332
+ function decideArming(opts) {
133333
+ const hostTrusted = isTrusted(opts.deviceId, opts.trustRoot);
133334
+ const gatesRemoved = opts.snapshotGatesRemoved && hostTrusted;
133335
+ if (!opts.osIsolated) {
133336
+ return { gatesRemoved, mode: "authoritative-bpure", recordWebArmed: false, hostTrusted };
133337
+ }
133338
+ if (gatesRemoved) {
133339
+ return { gatesRemoved, mode: "authoritative-under-grant", recordWebArmed: true, hostTrusted };
133340
+ }
133341
+ if (hostTrusted) {
133342
+ return { gatesRemoved: false, mode: "authoritative-bsafe", recordWebArmed: true, hostTrusted };
133343
+ }
133344
+ return { gatesRemoved: false, mode: "c2-disarm-only", recordWebArmed: false, hostTrusted };
133345
+ }
133211
133346
 
133212
133347
  // src/bridge.ts
133213
133348
  var APPROVAL_POLL_MS = 1500;
@@ -133314,11 +133449,13 @@ function wireBridge(channel, session, target, opts = {}) {
133314
133449
  let gatesRemoved = false;
133315
133450
  const gatesRemovedGetter = () => gatesRemoved;
133316
133451
  opts.onGatesRemoved?.(gatesRemovedGetter);
133317
- const webArmedUnderGrant = /* @__PURE__ */ new Set();
133452
+ let hostTrusted = false;
133453
+ const webArmedRevocable = /* @__PURE__ */ new Set();
133318
133454
  channel.on("state", (s10) => {
133319
133455
  if (s10 !== "ready") {
133320
133456
  workAllowed = false;
133321
133457
  gatesRemoved = false;
133458
+ hostTrusted = false;
133322
133459
  }
133323
133460
  });
133324
133461
  const arming = new ArmingState();
@@ -133360,7 +133497,14 @@ function wireBridge(channel, session, target, opts = {}) {
133360
133497
  const osIsolated = opts.osIsolated !== void 0 ? opts.osIsolated : process.env.AV_WORKER_OS_ISOLATED === "1" || process.env.AV_WORKER_OS_ISOLATED === "true";
133361
133498
  const heartbeat = () => {
133362
133499
  try {
133363
- channel.sendWorkerHeartbeat?.({ workerCapable, armedRooms: arming.armedRooms() });
133500
+ channel.sendWorkerHeartbeat?.({
133501
+ workerCapable,
133502
+ armedRooms: arming.armedRooms(),
133503
+ hostTrusted,
133504
+ // Already the conjunction: `gatesRemoved` is assigned from
133505
+ // decideArming, which ANDs the web grant with host trust.
133506
+ gatesEffective: gatesRemoved
133507
+ });
133364
133508
  } catch (err) {
133365
133509
  log(`worker heartbeat send failed (ignored): ${err instanceof Error ? err.message : String(err)}`);
133366
133510
  }
@@ -133369,40 +133513,56 @@ function wireBridge(channel, session, target, opts = {}) {
133369
133513
  channel.on("ready", () => heartbeat());
133370
133514
  const applyArmingSnapshot = (s10) => {
133371
133515
  workAllowed = s10?.workAllowed === true;
133372
- gatesRemoved = s10?.gatesRemoved === true;
133516
+ const prevHostTrusted = hostTrusted;
133517
+ const prevGatesEffective = gatesRemoved;
133518
+ const decision = decideArming({
133519
+ snapshotGatesRemoved: s10?.gatesRemoved === true,
133520
+ osIsolated,
133521
+ deviceId: channel.deviceId ?? "",
133522
+ trustRoot: opts.trustRoot
133523
+ });
133524
+ gatesRemoved = decision.gatesRemoved;
133525
+ hostTrusted = decision.hostTrusted;
133526
+ const trustChanged = hostTrusted !== prevHostTrusted || gatesRemoved !== prevGatesEffective;
133527
+ if (s10?.gatesRemoved === true && !decision.hostTrusted) {
133528
+ log(
133529
+ `"Remove all gates" is ON for this device but this HOST has not approved it. Full access is NOT in effect. Grant on this machine: av-bridge trust-host`
133530
+ );
133531
+ }
133373
133532
  if (!workerCapable) {
133374
133533
  log("arming_snapshot ignored \u2014 not worker-capable");
133534
+ if (trustChanged) heartbeat();
133375
133535
  return;
133376
133536
  }
133377
133537
  const roomIds = s10?.roomIds ?? [];
133378
133538
  try {
133379
133539
  const before = new Set(arming.armedRooms());
133380
- if (osIsolated && gatesRemoved) {
133381
- arming.applyAuthoritative(roomIds);
133382
- for (const r7 of roomIds) if (!before.has(r7)) webArmedUnderGrant.add(r7);
133383
- } else if (osIsolated) {
133384
- if (webArmedUnderGrant.size > 0) {
133385
- const revoked = [...webArmedUnderGrant].filter((r7) => arming.isArmed(r7));
133540
+ if (decision.mode === "c2-disarm-only") {
133541
+ if (webArmedRevocable.size > 0) {
133542
+ const revoked = [...webArmedRevocable].filter((r7) => arming.isArmed(r7));
133386
133543
  for (const r7 of revoked) arming.disarm(r7);
133387
- webArmedUnderGrant.clear();
133544
+ webArmedRevocable.clear();
133388
133545
  if (revoked.length > 0) {
133389
133546
  log(
133390
- `arming_snapshot: "Remove all gates" was revoked \u2014 disarming the rooms it had armed on this OS-isolated worker: [${revoked.map((r7) => r7.slice(0, 8)).join(", ")}]`
133547
+ `arming_snapshot: full access is not in effect \u2014 disarming the rooms it had armed on this OS-isolated worker: [${revoked.map((r7) => r7.slice(0, 8)).join(", ")}]`
133391
133548
  );
133392
133549
  }
133393
133550
  }
133394
133551
  const dropped = arming.reconcileDisarm(roomIds);
133395
133552
  if (dropped.length > 0) {
133396
133553
  log(
133397
- `arming_snapshot: OS-isolated worker \u2014 disarm-only until #20 (web cannot arm shell workers); disarmed: [${dropped.map((r7) => r7.slice(0, 8)).join(", ")}]`
133554
+ `arming_snapshot: OS-isolated worker \u2014 disarm-only without host trust (av-bridge trust-host); disarmed: [${dropped.map((r7) => r7.slice(0, 8)).join(", ")}]`
133398
133555
  );
133399
133556
  }
133400
133557
  } else {
133401
133558
  arming.applyAuthoritative(roomIds);
133559
+ if (decision.recordWebArmed) {
133560
+ for (const r7 of roomIds) if (!before.has(r7)) webArmedRevocable.add(r7);
133561
+ }
133402
133562
  }
133403
133563
  const after = arming.armedRooms();
133404
133564
  const changed = after.length !== before.size || after.some((r7) => !before.has(r7));
133405
- if (changed) heartbeat();
133565
+ if (changed || trustChanged) heartbeat();
133406
133566
  } catch (err) {
133407
133567
  log(`arming_snapshot handling failed (ignored): ${err instanceof Error ? err.message : String(err)}`);
133408
133568
  }
@@ -133458,6 +133618,9 @@ async function main() {
133458
133618
  if (maybeRunApproveArmSubcommand(process.argv, dataDirForSubcommand)) {
133459
133619
  process.exit(process.exitCode ?? 0);
133460
133620
  }
133621
+ if (maybeRunHostTrustSubcommand(process.argv, dataDirForSubcommand)) {
133622
+ process.exit(process.exitCode ?? 0);
133623
+ }
133461
133624
  const { maybeRunServiceSubcommand: maybeRunServiceSubcommand2 } = await Promise.resolve().then(() => (init_subcommand(), subcommand_exports));
133462
133625
  if (maybeRunServiceSubcommand2(process.argv, process.env)) {
133463
133626
  process.exit(process.exitCode ?? 0);
@@ -133469,7 +133632,7 @@ async function main() {
133469
133632
  "[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
133470
133633
  );
133471
133634
  }
133472
- console.error(`[bridge] version: ${true ? "0.6.5" : "dev"}`);
133635
+ console.error(`[bridge] version: ${true ? "0.7.0" : "dev"}`);
133473
133636
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
133474
133637
  console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
133475
133638
  if (cfg.armRoom) {
@@ -133555,9 +133718,9 @@ async function main() {
133555
133718
  `[worker-trap] ${rec.at} ${rec.outcome} after ${rec.ranMs}ms (waited ${rec.waitedMs}ms behind ${rec.queueDepthAtEnqueue}, ${rec.queueDepthAtStart} still queued)` + (rec.session ? ` composed=${rec.session.composedChars} result=${rec.session.sawResult} said=${rec.session.said}` : "")
133556
133719
  );
133557
133720
  try {
133558
- const dir = join10(cfg.dataDir, "logs");
133559
- mkdirSync5(dir, { recursive: true });
133560
- appendFileSync2(join10(dir, "worker-incidents.jsonl"), JSON.stringify(rec) + "\n");
133721
+ const dir = join11(cfg.dataDir, "logs");
133722
+ mkdirSync6(dir, { recursive: true });
133723
+ appendFileSync2(join11(dir, "worker-incidents.jsonl"), JSON.stringify(rec) + "\n");
133561
133724
  } catch (err) {
133562
133725
  console.error(`[worker-trap] could not persist incident: ${err.message}`);
133563
133726
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
6
6
  "main": "dist/index.js",