@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 +20 -3
- package/dist/index.js +57 -6
- package/dist/session.d.ts +12 -0
- package/dist/worker-permission.d.ts +5 -0
- package/package.json +3 -3
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
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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]
|
|
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")
|
|
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.
|
|
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]
|
|
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.
|
|
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": "
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@anthropic-ai/claude-agent-sdk": "^0.2.114",
|