@agentvault/claude-bridge 0.5.10 → 0.6.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.
package/dist/bridge.d.ts CHANGED
@@ -26,6 +26,8 @@ export interface ArmingSnapshot {
26
26
  * (backend `devices.work_allowed`, Task 2). Optional so older/test payloads
27
27
  * without it don't break — treated as `false` (fail-closed) when absent. */
28
28
  workAllowed?: boolean;
29
+ /** #627: owner's "Remove all gates" grant. Absent ⇒ gates stay in place. */
30
+ gatesRemoved?: boolean;
29
31
  }
30
32
  /** Metadata SecureChannel attaches to a 1:1 `message` event. `roomId` is set only
31
33
  * when the `message` actually originated in a room (handled by room_message), so
@@ -176,9 +178,16 @@ export declare function wireBridge(channel: RoomChannel, session: RoomSession, t
176
178
  * Task 4 (C2 shell-gate): true when this worker runs Bash unconfined
177
179
  * (AV_WORKER_OS_ISOLATED=1 — see config.ts/index.ts, threaded from the
178
180
  * session's `osIsolated`). When true, the `arming_snapshot` handler stays
179
- * disarm-only regardless of B1/worker-capability — a shell worker must
180
- * never be armed by a web-originated flag (RCE risk). Falls back to
181
- * reading the env directly only if the caller doesn't thread this.
181
+ * disarm-only regardless of B1/worker-capability — a shell worker is not
182
+ * armed by a web-originated flag (RCE risk).
183
+ *
184
+ * #627 EXCEPTION: unless the owner has removed all gates. That grant is an
185
+ * explicit, recorded decision to accept exactly this risk, so it lifts C2;
186
+ * revoking it disarms the rooms the web armed under it. See the
187
+ * `applyArmingSnapshot` comment block for the full rationale. Without the
188
+ * grant, C2 is unchanged.
189
+ *
190
+ * Falls back to reading the env directly only if the caller doesn't thread this.
182
191
  */
183
192
  osIsolated?: boolean;
184
193
  /**
@@ -190,5 +199,13 @@ export declare function wireBridge(channel: RoomChannel, session: RoomSession, t
190
199
  * (index.ts, arming-authoritative.test.ts, bridge.test.ts) stays intact.
191
200
  */
192
201
  onWorkAllowed?: (getter: () => boolean) => void;
202
+ /**
203
+ * #627: out-param that receives the live `gatesRemoved()` getter once,
204
+ * synchronously, during wiring — same shape as `onWorkAllowed` above, and
205
+ * for the same reason (keeps wireBridge's return type stable so existing
206
+ * callers are untouched). index.ts captures it and threads it into the
207
+ * worker session's gate.
208
+ */
209
+ onGatesRemoved?: (getter: () => boolean) => void;
193
210
  }): ArmingState;
194
211
  //# sourceMappingURL=bridge.d.ts.map
package/dist/index.js CHANGED
@@ -67424,6 +67424,7 @@ var init_channel = __esm2({
67424
67424
  if (data.event === "arming_snapshot") {
67425
67425
  this.emit("arming_snapshot", {
67426
67426
  workAllowed: data.data?.work_allowed === true,
67427
+ gatesRemoved: data.data?.gates_removed === true,
67427
67428
  roomIds: data.data?.room_ids ?? []
67428
67429
  });
67429
67430
  }
@@ -118580,6 +118581,12 @@ function gateDecision(toolName, input, opts) {
118580
118581
  if (!opts.isToolTurn()) {
118581
118582
  return { deny: true, reason: "tools are disabled on this turn; reply with the say tool only" };
118582
118583
  }
118584
+ if (opts.gatesRemoved?.()) {
118585
+ return { deny: false };
118586
+ }
118587
+ if (opts.isOwnerDmTurn?.()) {
118588
+ return { deny: false };
118589
+ }
118583
118590
  if (toolName === "Bash") {
118584
118591
  return opts.osIsolated ? { deny: false } : { deny: true, reason: "Bash is disabled in worker mode unless the process is OS-isolated (set AV_WORKER_OS_ISOLATED=1)" };
118585
118592
  }
@@ -132612,6 +132619,19 @@ var PersistentClaudeSession = class {
132612
132619
  // default (no getter ⇒ no tools). Read LIVE per tool decision so a Work-off
132613
132620
  // flip / fail-closed reconnect window denies the very next call.
132614
132621
  isToolTurn: () => (this.opts.workAllowed?.() ?? false) && (this.currentAutoReply || this.currentArmedGetter()),
132622
+ // Full-access lane: a 1:1 owner DM, whose ephemeral worker is seeded with
132623
+ // ONLY the owner's own message, so no third party can steer the turn.
132624
+ // Conjoined with !currentArmedGetter() deliberately: if a room turn ever
132625
+ // arrived with autoReplyOnText set, it must fall through to the allowlist
132626
+ // rather than inherit full access. Fail-safe, not merely descriptive.
132627
+ // Read LIVE per tool decision, like isToolTurn.
132628
+ isOwnerDmTurn: () => this.currentAutoReply && !this.currentArmedGetter(),
132629
+ // #627: the owner removed all gates for this device. Evaluated by
132630
+ // gateDecision AFTER isToolTurn, so an unarmed room turn stays chat-only
132631
+ // — this widens what a tool turn may do, never which turns get tools.
132632
+ // Read LIVE per decision so a revoke (or a reconnect reset) denies the
132633
+ // very next call, exactly like workAllowed above.
132634
+ gatesRemoved: () => this.opts.gatesRemoved?.() ?? false,
132615
132635
  // Slice 2 Plan B: emit an audit self-report for each tool decision on an
132616
132636
  // armed-room turn (both allow and deny). room_say is excluded by the hook.
132617
132637
  isArmedTurn: () => this.currentArmedGetter(),
@@ -132627,7 +132647,7 @@ var PersistentClaudeSession = class {
132627
132647
  }
132628
132648
  };
132629
132649
  console.error(
132630
- `[worker-gate] allowlist active \u2014 workspace=${this.opts.workspaceDir ?? "(none: file tools disabled)"}, bash=${this.opts.osIsolated ? "enabled (OS-isolated)" : "disabled"}`
132650
+ `[worker-gate] active \u2014 1:1 owner DM: FULL access, no allowlist, no workspace fence. Armed room: FULL access whenever "Remove all gates" is on (live, owner-controlled, off by default); otherwise allowlist, workspace=${this.opts.workspaceDir ?? "(none: file tools disabled)"}, bash=${this.opts.osIsolated ? "enabled (OS-isolated)" : "disabled"}`
132631
132651
  );
132632
132652
  return {
132633
132653
  ...base,
@@ -132764,7 +132784,7 @@ function makeRouter(deps) {
132764
132784
  push(text, reply, opts) {
132765
132785
  const replySink = reply ?? (() => {
132766
132786
  });
132767
- const isOwnerDm = opts?.autoReplyOnText === true && deps.workAllowed();
132787
+ const isOwnerDm = opts?.autoReplyOnText === true && opts?.armed === void 0 && deps.workAllowed();
132768
132788
  const isArmedRoom = opts?.armed?.() === true && deps.workAllowed();
132769
132789
  if (isOwnerDm) {
132770
132790
  deps.queue.enqueue({
@@ -133068,8 +133088,15 @@ function wireBridge(channel, session, target, opts = {}) {
133068
133088
  let workAllowed = false;
133069
133089
  const workAllowedGetter = () => workAllowed;
133070
133090
  opts.onWorkAllowed?.(workAllowedGetter);
133091
+ let gatesRemoved = false;
133092
+ const gatesRemovedGetter = () => gatesRemoved;
133093
+ opts.onGatesRemoved?.(gatesRemovedGetter);
133094
+ const webArmedUnderGrant = /* @__PURE__ */ new Set();
133071
133095
  channel.on("state", (s10) => {
133072
- if (s10 !== "ready") workAllowed = false;
133096
+ if (s10 !== "ready") {
133097
+ workAllowed = false;
133098
+ gatesRemoved = false;
133099
+ }
133073
133100
  });
133074
133101
  const arming = new ArmingState();
133075
133102
  if (opts.armRoom === true && opts.roomFilter) {
@@ -133119,6 +133146,7 @@ function wireBridge(channel, session, target, opts = {}) {
133119
133146
  channel.on("ready", () => heartbeat());
133120
133147
  const applyArmingSnapshot = (s10) => {
133121
133148
  workAllowed = s10?.workAllowed === true;
133149
+ gatesRemoved = s10?.gatesRemoved === true;
133122
133150
  if (!workerCapable) {
133123
133151
  log("arming_snapshot ignored \u2014 not worker-capable");
133124
133152
  return;
@@ -133126,7 +133154,20 @@ function wireBridge(channel, session, target, opts = {}) {
133126
133154
  const roomIds = s10?.roomIds ?? [];
133127
133155
  try {
133128
133156
  const before = new Set(arming.armedRooms());
133129
- if (osIsolated) {
133157
+ if (osIsolated && gatesRemoved) {
133158
+ arming.applyAuthoritative(roomIds);
133159
+ for (const r7 of roomIds) if (!before.has(r7)) webArmedUnderGrant.add(r7);
133160
+ } else if (osIsolated) {
133161
+ if (webArmedUnderGrant.size > 0) {
133162
+ const revoked = [...webArmedUnderGrant].filter((r7) => arming.isArmed(r7));
133163
+ for (const r7 of revoked) arming.disarm(r7);
133164
+ webArmedUnderGrant.clear();
133165
+ if (revoked.length > 0) {
133166
+ log(
133167
+ `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(", ")}]`
133168
+ );
133169
+ }
133170
+ }
133130
133171
  const dropped = arming.reconcileDisarm(roomIds);
133131
133172
  if (dropped.length > 0) {
133132
133173
  log(
@@ -133205,7 +133246,7 @@ async function main() {
133205
133246
  "[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"
133206
133247
  );
133207
133248
  }
133208
- console.error(`[bridge] version: ${true ? "0.5.10" : "dev"}`);
133249
+ console.error(`[bridge] version: ${true ? "0.6.0" : "dev"}`);
133209
133250
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
133210
133251
  console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
133211
133252
  if (cfg.armRoom) {
@@ -133217,7 +133258,7 @@ async function main() {
133217
133258
  );
133218
133259
  } else {
133219
133260
  console.error(
133220
- '[bridge] tools run on owner DM turns only when Work is allowed (the dashboard "Work" switch). Rooms stay say-only unless armed (AV_ARM_ROOM=1 with a pinned AV_ROOM_ID). Cross-turn injection note: a persistent session mixes room context with DM turns \u2014 run DM-only or OS-isolated if you do not arm a room.'
133261
+ '[bridge] 1:1 owner DMs run with FULL tool access when "Allow tools" is on for this agent (dashboard switch, default off): any command, any file this user account can reach, network, skills and subagents \u2014 equivalent to a terminal on this machine, with no per-action prompt. Rooms are NOT affected: they stay say-only unless armed (AV_ARM_ROOM=1 with a pinned AV_ROOM_ID), and an armed room keeps the confined allowlist. Each tool-enabled turn runs in a fresh session seeded with only that one message, so room text cannot leak into a DM turn.'
133221
133262
  );
133222
133263
  }
133223
133264
  const target = new ActiveTarget();
@@ -133236,6 +133277,8 @@ async function main() {
133236
133277
  };
133237
133278
  let liveWorkAllowed = () => false;
133238
133279
  const workAllowed = () => liveWorkAllowed();
133280
+ let liveGatesRemoved = () => false;
133281
+ const gatesRemoved = () => liveGatesRemoved();
133239
133282
  const listener = new PersistentClaudeSession({
133240
133283
  model: cfg.model,
133241
133284
  systemPrompt: agentSystemPrompt,
@@ -133259,6 +133302,11 @@ async function main() {
133259
133302
  // Task 4: the hard backstop — no tool runs on any worker turn (owner DM or
133260
133303
  // armed room) unless Work is on. Read live per tool decision.
133261
133304
  workAllowed,
133305
+ // #627: only the WORKER session gets this. The listener is the locked
133306
+ // tools:[] facet and must stay that way — a grant that widened the
133307
+ // always-on listener would put an unrestricted shell behind every
133308
+ // untrusted room message, not just tool turns.
133309
+ gatesRemoved,
133262
133310
  ephemeral: true,
133263
133311
  maxTurns: WORKER_MAX_TURNS,
133264
133312
  workspaceDir: cfg.workspaceDir,
@@ -133291,6 +133339,9 @@ async function main() {
133291
133339
  // during wiring) into the indirection the router + worker session read live.
133292
133340
  onWorkAllowed: (getter) => {
133293
133341
  liveWorkAllowed = getter;
133342
+ },
133343
+ onGatesRemoved: (getter) => {
133344
+ liveGatesRemoved = getter;
133294
133345
  }
133295
133346
  }
133296
133347
  );
package/dist/session.d.ts CHANGED
@@ -86,6 +86,18 @@ export interface SessionOpts {
86
86
  * false, even if the router mis-routed. Read LIVE at each tool decision (never
87
87
  * snapshotted). Absent getter ⇒ `?? false` ⇒ fail-closed (no tools). */
88
88
  workAllowed?: () => boolean;
89
+ /**
90
+ * #627: live, fail-closed device-level "Remove all gates" grant. When this
91
+ * returns true the gate permits EVERY tool with no restrictions, in rooms
92
+ * and 1:1 alike — the owner's explicit decision, recorded in #627.
93
+ *
94
+ * NOT a replacement for `isToolTurn`: this is read AFTER it, so an UNARMED
95
+ * room turn is still chat-only. That ordering is what separates "a room you
96
+ * enabled" from "any room somebody added your agent to", and it costs
97
+ * nothing to keep. Read LIVE at each tool decision (never snapshotted);
98
+ * absent getter ⇒ `?? false` ⇒ gates stay in place.
99
+ */
100
+ gatesRemoved?: () => boolean;
89
101
  }
90
102
  export declare class PersistentClaudeSession {
91
103
  private opts;
@@ -13,6 +13,9 @@ export declare function makeWorkerPreToolUseHook(opts: {
13
13
  /** True when the process is attested OS-isolated, gating whether Bash is allowed at all. */
14
14
  osIsolated: boolean;
15
15
  isToolTurn: () => boolean;
16
+ /** True when this turn is a 1:1 owner DM. That lane gets full access; rooms
17
+ * keep the allowlist. Read live per tool call, like isToolTurn. */
18
+ isOwnerDmTurn?: () => boolean;
16
19
  /** Slice 2 Plan B: true when this turn comes from an armed room. When set,
17
20
  * each tool decision (allow AND deny) is forwarded to onToolDecision for
18
21
  * audit self-reporting. room_say is always excluded (it is the reply
@@ -36,5 +39,7 @@ export declare function makeWorkerPermission(opts: {
36
39
  /** True when the process is attested OS-isolated, gating whether Bash is allowed at all. */
37
40
  osIsolated: boolean;
38
41
  isToolTurn: () => boolean;
42
+ /** True when this turn is a 1:1 owner DM (full access lane). */
43
+ isOwnerDmTurn?: () => boolean;
39
44
  }): CanUseTool;
40
45
  //# sourceMappingURL=worker-permission.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.5.10",
3
+ "version": "0.6.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",
@@ -23,10 +23,10 @@
23
23
  }
24
24
  },
25
25
  "scripts": {
26
- "build": "tsc --emitDeclarationOnly && node build.mjs",
26
+ "build": "npm --prefix ../plugin run build && tsc --emitDeclarationOnly && node build.mjs",
27
27
  "start": "node dist/index.js",
28
28
  "test": "vitest run",
29
- "prepublishOnly": "node build.mjs"
29
+ "prepublishOnly": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
32
  "@anthropic-ai/claude-agent-sdk": "^0.2.114",