@agentvault/claude-bridge 0.3.2 → 0.3.4
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 +6 -2
- package/dist/config.d.ts +3 -0
- package/dist/index.js +198 -49
- package/dist/session.d.ts +41 -1
- package/dist/worker-permission.d.ts +22 -0
- package/package.json +1 -1
package/dist/bridge.d.ts
CHANGED
|
@@ -35,8 +35,12 @@ export declare class RoomHushState {
|
|
|
35
35
|
}
|
|
36
36
|
export interface RoomSession {
|
|
37
37
|
/** `reply` is the immutable reply sink captured for THIS message (see
|
|
38
|
-
* ActiveTarget.snapshotReply) — the session invokes it when Claude answers.
|
|
39
|
-
|
|
38
|
+
* ActiveTarget.snapshotReply) — the session invokes it when Claude answers.
|
|
39
|
+
* `opts.autoReplyOnText` (set for 1:1 DMs) makes the session fall back to
|
|
40
|
+
* sending plain assistant text when the model never calls the say tool (#416). */
|
|
41
|
+
push(text: string, reply?: (text: string) => Promise<void>, opts?: {
|
|
42
|
+
autoReplyOnText?: boolean;
|
|
43
|
+
}): void;
|
|
40
44
|
}
|
|
41
45
|
/** Where a reply should go: a room (sendToRoom) or a 1:1 DM (send). */
|
|
42
46
|
export type BridgeTarget = {
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export interface BridgeConfig {
|
|
|
8
8
|
roomFilter?: string;
|
|
9
9
|
model?: string;
|
|
10
10
|
systemPrompt?: string;
|
|
11
|
+
worker: boolean;
|
|
12
|
+
workspaceDir?: string;
|
|
13
|
+
permissionMode: "auto" | "acceptEdits" | "bypassPermissions";
|
|
11
14
|
}
|
|
12
15
|
export declare function loadConfig(env: NodeJS.ProcessEnv, argv?: string[]): BridgeConfig;
|
|
13
16
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -45248,20 +45248,20 @@ function multibaseToPublicKey(multibase) {
|
|
|
45248
45248
|
}
|
|
45249
45249
|
async function signDocument(document, privateKey) {
|
|
45250
45250
|
await libsodium_wrappers_default.ready;
|
|
45251
|
-
const
|
|
45251
|
+
const canonical2 = canonicalize(document);
|
|
45252
45252
|
const domainPrefix = libsodium_wrappers_default.from_string(DOMAIN_DID_DOCUMENT);
|
|
45253
|
-
const message = new Uint8Array(domainPrefix.length +
|
|
45253
|
+
const message = new Uint8Array(domainPrefix.length + canonical2.length);
|
|
45254
45254
|
message.set(domainPrefix);
|
|
45255
|
-
message.set(
|
|
45255
|
+
message.set(canonical2, domainPrefix.length);
|
|
45256
45256
|
return libsodium_wrappers_default.crypto_sign_detached(message, privateKey);
|
|
45257
45257
|
}
|
|
45258
45258
|
async function verifyDocumentSignature(document, signature, publicKey) {
|
|
45259
45259
|
await libsodium_wrappers_default.ready;
|
|
45260
|
-
const
|
|
45260
|
+
const canonical2 = canonicalize(document);
|
|
45261
45261
|
const domainPrefix = libsodium_wrappers_default.from_string(DOMAIN_DID_DOCUMENT);
|
|
45262
|
-
const message = new Uint8Array(domainPrefix.length +
|
|
45262
|
+
const message = new Uint8Array(domainPrefix.length + canonical2.length);
|
|
45263
45263
|
message.set(domainPrefix);
|
|
45264
|
-
message.set(
|
|
45264
|
+
message.set(canonical2, domainPrefix.length);
|
|
45265
45265
|
try {
|
|
45266
45266
|
return libsodium_wrappers_default.crypto_sign_verify_detached(signature, message, publicKey);
|
|
45267
45267
|
} catch {
|
|
@@ -96449,6 +96449,20 @@ function resolveDataDir(env) {
|
|
|
96449
96449
|
function loadConfig(env, argv = []) {
|
|
96450
96450
|
const { dataDir, source: dataDirSource } = resolveDataDir(env);
|
|
96451
96451
|
const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || "";
|
|
96452
|
+
const worker = env.AV_WORKER === "1" || env.AV_WORKER === "true";
|
|
96453
|
+
const workspaceDir = env.AV_WORKSPACE_DIR || void 0;
|
|
96454
|
+
const PERMISSION_MODES = ["auto", "acceptEdits", "bypassPermissions"];
|
|
96455
|
+
const permissionMode = env.AV_PERMISSION_MODE ?? "auto";
|
|
96456
|
+
if (!PERMISSION_MODES.includes(permissionMode)) {
|
|
96457
|
+
throw new Error(
|
|
96458
|
+
`invalid AV_PERMISSION_MODE: ${env.AV_PERMISSION_MODE} (expected one of ${PERMISSION_MODES.join(", ")})`
|
|
96459
|
+
);
|
|
96460
|
+
}
|
|
96461
|
+
if (worker && !workspaceDir) {
|
|
96462
|
+
throw new Error(
|
|
96463
|
+
"worker mode requires AV_WORKSPACE_DIR (the project directory the agent works in)"
|
|
96464
|
+
);
|
|
96465
|
+
}
|
|
96452
96466
|
if (!inviteToken && !hasPersistedCreds(dataDir)) {
|
|
96453
96467
|
throw new Error(
|
|
96454
96468
|
"invite token required: pass it as the first argument or set AV_INVITE_TOKEN (or start from a data dir with existing credentials to reconnect)"
|
|
@@ -96465,7 +96479,10 @@ function loadConfig(env, argv = []) {
|
|
|
96465
96479
|
agentName: env.AV_AGENT_NAME ?? "Claude",
|
|
96466
96480
|
roomFilter: env.AV_ROOM_ID || void 0,
|
|
96467
96481
|
model: env.AV_CLAUDE_MODEL || void 0,
|
|
96468
|
-
systemPrompt: env.AV_SYSTEM_PROMPT || void 0
|
|
96482
|
+
systemPrompt: env.AV_SYSTEM_PROMPT || void 0,
|
|
96483
|
+
worker,
|
|
96484
|
+
workspaceDir,
|
|
96485
|
+
permissionMode
|
|
96469
96486
|
};
|
|
96470
96487
|
}
|
|
96471
96488
|
|
|
@@ -117763,6 +117780,81 @@ function Z_($10, Q4) {
|
|
|
117763
117780
|
return null;
|
|
117764
117781
|
}
|
|
117765
117782
|
|
|
117783
|
+
// src/worker-permission.ts
|
|
117784
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
117785
|
+
import { resolve as resolve2, dirname, basename, sep } from "node:path";
|
|
117786
|
+
var PATH_FIELDS = ["file_path", "path", "notebook_path"];
|
|
117787
|
+
function canonical(p2) {
|
|
117788
|
+
const abs = resolve2(p2);
|
|
117789
|
+
try {
|
|
117790
|
+
return realpathSync2(abs);
|
|
117791
|
+
} catch {
|
|
117792
|
+
try {
|
|
117793
|
+
return resolve2(realpathSync2(dirname(abs)), basename(abs));
|
|
117794
|
+
} catch {
|
|
117795
|
+
return abs;
|
|
117796
|
+
}
|
|
117797
|
+
}
|
|
117798
|
+
}
|
|
117799
|
+
function targetsDataDir(_toolName, input, dataDir) {
|
|
117800
|
+
const canonDir = canonical(dataDir);
|
|
117801
|
+
for (const f7 of PATH_FIELDS) {
|
|
117802
|
+
const v5 = input[f7];
|
|
117803
|
+
if (typeof v5 === "string") {
|
|
117804
|
+
const canon = canonical(v5);
|
|
117805
|
+
if (canon === canonDir || canon.startsWith(canonDir + sep)) return true;
|
|
117806
|
+
}
|
|
117807
|
+
}
|
|
117808
|
+
const cmd = input.command;
|
|
117809
|
+
if (typeof cmd === "string" && cmd.includes(canonDir)) return true;
|
|
117810
|
+
return false;
|
|
117811
|
+
}
|
|
117812
|
+
function gateDecision(toolName, input, opts) {
|
|
117813
|
+
if (toolName === ROOM_SAY_TOOL_NAME) return { deny: false };
|
|
117814
|
+
if (!opts.isOwnerTurn()) {
|
|
117815
|
+
return { deny: true, reason: "tools are disabled in rooms; reply with the say tool only" };
|
|
117816
|
+
}
|
|
117817
|
+
if (targetsDataDir(toolName, input, opts.dataDir)) {
|
|
117818
|
+
return { deny: true, reason: "access to the AgentVault key directory is not permitted" };
|
|
117819
|
+
}
|
|
117820
|
+
return { deny: false };
|
|
117821
|
+
}
|
|
117822
|
+
function makeWorkerPreToolUseHook(opts) {
|
|
117823
|
+
return {
|
|
117824
|
+
hooks: [
|
|
117825
|
+
async (input) => {
|
|
117826
|
+
const i2 = input;
|
|
117827
|
+
const verdict = gateDecision(
|
|
117828
|
+
i2.tool_name ?? "",
|
|
117829
|
+
i2.tool_input ?? {},
|
|
117830
|
+
opts
|
|
117831
|
+
);
|
|
117832
|
+
if (verdict.deny) {
|
|
117833
|
+
console.error(`[worker-gate] DENY ${i2.tool_name} \u2014 ${verdict.reason}`);
|
|
117834
|
+
return {
|
|
117835
|
+
hookSpecificOutput: {
|
|
117836
|
+
hookEventName: "PreToolUse",
|
|
117837
|
+
permissionDecision: "deny",
|
|
117838
|
+
permissionDecisionReason: verdict.reason
|
|
117839
|
+
}
|
|
117840
|
+
};
|
|
117841
|
+
}
|
|
117842
|
+
return {};
|
|
117843
|
+
}
|
|
117844
|
+
]
|
|
117845
|
+
};
|
|
117846
|
+
}
|
|
117847
|
+
function makeWorkerPermission(opts) {
|
|
117848
|
+
return async (toolName, input) => {
|
|
117849
|
+
const verdict = gateDecision(toolName, input, opts);
|
|
117850
|
+
if (verdict.deny) {
|
|
117851
|
+
console.error(`[worker-gate] DENY ${toolName} \u2014 ${verdict.reason}`);
|
|
117852
|
+
return { behavior: "deny", message: verdict.reason };
|
|
117853
|
+
}
|
|
117854
|
+
return { behavior: "allow", updatedInput: input };
|
|
117855
|
+
};
|
|
117856
|
+
}
|
|
117857
|
+
|
|
117766
117858
|
// ../../node_modules/zod/v4/classic/external.js
|
|
117767
117859
|
var external_exports = {};
|
|
117768
117860
|
__export(external_exports, {
|
|
@@ -131554,14 +131646,36 @@ var PersistentClaudeSession = class {
|
|
|
131554
131646
|
/** Reply sink bound to the message currently being processed. Set as each
|
|
131555
131647
|
* message is handed to the model so the say tool routes to the right place. */
|
|
131556
131648
|
activeReply;
|
|
131557
|
-
|
|
131649
|
+
/**
|
|
131650
|
+
* Per-turn state for the #416 DM auto-reply fallback.
|
|
131651
|
+
*
|
|
131652
|
+
* LOAD-BEARING for the worker tool gate: `currentAutoReply` is true ONLY on
|
|
131653
|
+
* owner DM turns (`autoReplyOnText:true` from wireBridge) and false on room
|
|
131654
|
+
* turns (`autoReplyOnText:false`). `makeWorkerPermission` calls
|
|
131655
|
+
* `isOwnerTurn: () => this.currentAutoReply` to decide whether tools are
|
|
131656
|
+
* allowed. A room-origin turn MUST keep this false or untrusted input gains
|
|
131657
|
+
* full tool access. See worker-permission.test.ts for the covering assertion.
|
|
131658
|
+
*/
|
|
131659
|
+
currentAutoReply = false;
|
|
131660
|
+
saidThisTurn = false;
|
|
131661
|
+
turnText = "";
|
|
131662
|
+
/** In-process room MCP server — hoisted so buildSdkOptions can reference it. */
|
|
131663
|
+
roomServer;
|
|
131664
|
+
/**
|
|
131665
|
+
* Queue an inbound message for the model.
|
|
131666
|
+
* @param opts.autoReplyOnText — for 1:1 DMs (where the owner always expects a
|
|
131667
|
+
* reply): if the turn produces assistant text but never calls the say tool,
|
|
131668
|
+
* send that text as the reply instead of silently dropping it (#416). Rooms
|
|
131669
|
+
* omit this so the agent can still choose silence.
|
|
131670
|
+
*/
|
|
131671
|
+
push(text, reply, opts) {
|
|
131558
131672
|
const msg = {
|
|
131559
131673
|
type: "user",
|
|
131560
131674
|
message: { role: "user", content: text },
|
|
131561
131675
|
parent_tool_use_id: null,
|
|
131562
131676
|
session_id: ""
|
|
131563
131677
|
};
|
|
131564
|
-
const item = { msg, reply };
|
|
131678
|
+
const item = { msg, reply, autoReplyOnText: opts?.autoReplyOnText };
|
|
131565
131679
|
if (this.waiting) {
|
|
131566
131680
|
const w2 = this.waiting;
|
|
131567
131681
|
this.waiting = null;
|
|
@@ -131574,56 +131688,70 @@ var PersistentClaudeSession = class {
|
|
|
131574
131688
|
* back to opts.onSay. This is what closes the DM→room leak: the destination is
|
|
131575
131689
|
* the one captured for the message being answered, not a live global target. */
|
|
131576
131690
|
async deliver(text) {
|
|
131691
|
+
this.saidThisTurn = true;
|
|
131577
131692
|
if (this.activeReply) await this.activeReply(text);
|
|
131578
131693
|
else if (this.opts.onSay) await this.opts.onSay(text);
|
|
131579
131694
|
}
|
|
131580
131695
|
async *input() {
|
|
131581
131696
|
while (true) {
|
|
131582
|
-
|
|
131583
|
-
|
|
131584
|
-
this.activeReply = item2.reply;
|
|
131585
|
-
yield item2.msg;
|
|
131586
|
-
continue;
|
|
131587
|
-
}
|
|
131588
|
-
const item = await new Promise((resolve2) => {
|
|
131589
|
-
this.waiting = resolve2;
|
|
131697
|
+
const item = this.pending.length > 0 ? this.pending.shift() : await new Promise((resolve3) => {
|
|
131698
|
+
this.waiting = resolve3;
|
|
131590
131699
|
});
|
|
131591
131700
|
this.activeReply = item.reply;
|
|
131701
|
+
this.currentAutoReply = item.autoReplyOnText ?? false;
|
|
131702
|
+
this.saidThisTurn = false;
|
|
131703
|
+
this.turnText = "";
|
|
131592
131704
|
yield item.msg;
|
|
131593
131705
|
}
|
|
131594
131706
|
}
|
|
131707
|
+
/**
|
|
131708
|
+
* Build the SDK options for this session. Locked mode (default) is safe for
|
|
131709
|
+
* untrusted inbound room messages. Worker mode (opts.worker=true) enables full
|
|
131710
|
+
* toolset on owner DMs, project context, workspace cwd, and an AV data-dir
|
|
131711
|
+
* fence via canUseTool.
|
|
131712
|
+
*/
|
|
131713
|
+
buildSdkOptions() {
|
|
131714
|
+
const base = {
|
|
131715
|
+
model: this.opts.model,
|
|
131716
|
+
systemPrompt: this.opts.systemPrompt,
|
|
131717
|
+
mcpServers: { room: this.roomServer },
|
|
131718
|
+
allowedTools: [ROOM_SAY_TOOL_NAME]
|
|
131719
|
+
};
|
|
131720
|
+
if (!this.opts.worker) {
|
|
131721
|
+
return {
|
|
131722
|
+
...base,
|
|
131723
|
+
permissionMode: "dontAsk",
|
|
131724
|
+
tools: [],
|
|
131725
|
+
settingSources: []
|
|
131726
|
+
};
|
|
131727
|
+
}
|
|
131728
|
+
const gateOpts = {
|
|
131729
|
+
dataDir: this.opts.dataDir ?? "",
|
|
131730
|
+
// LOAD-BEARING: this.currentAutoReply is true only on owner DM turns.
|
|
131731
|
+
// A room turn must keep it false or untrusted input gains full tool access.
|
|
131732
|
+
// See worker-permission.test.ts for the covering assertion.
|
|
131733
|
+
isOwnerTurn: () => this.currentAutoReply
|
|
131734
|
+
};
|
|
131735
|
+
return {
|
|
131736
|
+
...base,
|
|
131737
|
+
permissionMode: this.opts.permissionMode ?? "auto",
|
|
131738
|
+
settingSources: ["user", "project"],
|
|
131739
|
+
cwd: this.opts.workspaceDir,
|
|
131740
|
+
// PRIMARY gate: a PreToolUse hook fires on EVERY tool call regardless of
|
|
131741
|
+
// permissionMode. canUseTool alone is bypassed in "auto" mode (the SDK's
|
|
131742
|
+
// classifier auto-approves without hitting the "ask" path) — verified live.
|
|
131743
|
+
hooks: { PreToolUse: [makeWorkerPreToolUseHook(gateOpts)] },
|
|
131744
|
+
// Secondary (defense-in-depth): covers the "ask" path in default/dontAsk modes.
|
|
131745
|
+
canUseTool: makeWorkerPermission(gateOpts)
|
|
131746
|
+
};
|
|
131747
|
+
}
|
|
131595
131748
|
async start() {
|
|
131596
|
-
|
|
131749
|
+
this.roomServer = _s({
|
|
131597
131750
|
name: "room",
|
|
131598
131751
|
version: "0.2.0",
|
|
131599
131752
|
tools: [makeRoomSayTool((text) => this.deliver(text))]
|
|
131600
131753
|
});
|
|
131601
|
-
const sdkOptions =
|
|
131602
|
-
model: this.opts.model,
|
|
131603
|
-
systemPrompt: this.opts.systemPrompt,
|
|
131604
|
-
// SECURITY: this session is driven by UNTRUSTED inbound room messages from
|
|
131605
|
-
// other agents. It must NOT be able to run general tools (shell/file/network)
|
|
131606
|
-
// — that would be a prompt-injection → RCE / key-exfil path on the host. The
|
|
131607
|
-
// ONLY tool it gets is room_say (post text to the active room), which has no
|
|
131608
|
-
// such reach. Defense is capability REMOVAL first, permission mode second:
|
|
131609
|
-
// - tools [] : remove ALL built-in tools (Bash/Read/Write/...) at the
|
|
131610
|
-
// source so an injection can't even reach the permission layer. The
|
|
131611
|
-
// room_say MCP tool comes via mcpServers and is unaffected by this.
|
|
131612
|
-
// - permissionMode "dontAsk" (NEVER "bypassPermissions"): deny anything not
|
|
131613
|
-
// pre-approved — no reliance on a no-TTY-implies-denial assumption.
|
|
131614
|
-
// - allowedTools [ROOM_SAY_TOOL_NAME] : pre-approve ONLY room_say.
|
|
131615
|
-
// - settingSources [] : do not load CLAUDE.md / project agents / skills
|
|
131616
|
-
// from disk into an untrusted-input session.
|
|
131617
|
-
// When CClaude later becomes a tool-using coder, additional tools must only
|
|
131618
|
-
// be enabled INSIDE an isolation boundary (sandboxed dev droplet, no prod/
|
|
131619
|
-
// network access, no access to the device-key dir) AND gated by canUseTool.
|
|
131620
|
-
permissionMode: "dontAsk",
|
|
131621
|
-
tools: [],
|
|
131622
|
-
allowedTools: [ROOM_SAY_TOOL_NAME],
|
|
131623
|
-
mcpServers: { room: roomServer },
|
|
131624
|
-
settingSources: []
|
|
131625
|
-
// maxTurns left unset → persistent multi-turn (NOT maxTurns:1)
|
|
131626
|
-
};
|
|
131754
|
+
const sdkOptions = this.buildSdkOptions();
|
|
131627
131755
|
const q5 = this.opts.queryImpl ? this.opts.queryImpl({
|
|
131628
131756
|
prompt: this.input(),
|
|
131629
131757
|
options: sdkOptions
|
|
@@ -131635,7 +131763,15 @@ var PersistentClaudeSession = class {
|
|
|
131635
131763
|
if (m6.type === "assistant") {
|
|
131636
131764
|
const blocks = m6.message?.content ?? [];
|
|
131637
131765
|
const text = blocks.filter((b5) => b5.type === "text").map((b5) => b5.text ?? "").join("");
|
|
131638
|
-
if (text.trim())
|
|
131766
|
+
if (text.trim()) {
|
|
131767
|
+
this.turnText += text;
|
|
131768
|
+
this.opts.onObserve?.(text);
|
|
131769
|
+
}
|
|
131770
|
+
} else if (m6.type === "result") {
|
|
131771
|
+
const reply = this.activeReply;
|
|
131772
|
+
if (this.currentAutoReply && !this.saidThisTurn && this.turnText.trim() && reply) {
|
|
131773
|
+
void reply(this.turnText);
|
|
131774
|
+
}
|
|
131639
131775
|
}
|
|
131640
131776
|
}
|
|
131641
131777
|
}
|
|
@@ -131741,13 +131877,15 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
131741
131877
|
}
|
|
131742
131878
|
log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)}`);
|
|
131743
131879
|
target.setRoom(e7.roomId);
|
|
131744
|
-
session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log)
|
|
131880
|
+
session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log), {
|
|
131881
|
+
autoReplyOnText: false
|
|
131882
|
+
});
|
|
131745
131883
|
});
|
|
131746
131884
|
channel.on("message", (text, metadata) => {
|
|
131747
131885
|
if (metadata?.roomId) return;
|
|
131748
131886
|
log("inbound 1:1 DM from owner");
|
|
131749
131887
|
target.setDm();
|
|
131750
|
-
session.push(text, target.snapshotReply(channel, log));
|
|
131888
|
+
session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: true });
|
|
131751
131889
|
});
|
|
131752
131890
|
}
|
|
131753
131891
|
|
|
@@ -131760,7 +131898,14 @@ async function main() {
|
|
|
131760
131898
|
"[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"
|
|
131761
131899
|
);
|
|
131762
131900
|
}
|
|
131901
|
+
console.error(`[bridge] version: ${true ? "0.3.4" : "dev"}`);
|
|
131763
131902
|
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
131903
|
+
if (cfg.worker) {
|
|
131904
|
+
console.error(`[bridge] WORKER MODE \u2014 workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
131905
|
+
console.error(
|
|
131906
|
+
"[bridge] WORKER MODE WARNING: worker mode mixes untrusted room context with trusted owner DM turns in one persistent session. The turn-scoped gate (isOwnerTurn) prevents tool use ON a room turn, but does NOT prevent a room message from planting an instruction that fires on a later owner DM turn when tools are enabled (cross-turn injection). Until Slice 2: run a worker bridge DM-only (no room traffic) or under OS-user isolation."
|
|
131907
|
+
);
|
|
131908
|
+
}
|
|
131764
131909
|
const target = new ActiveTarget();
|
|
131765
131910
|
if (cfg.roomFilter) target.setRoom(cfg.roomFilter);
|
|
131766
131911
|
const channel = new SecureChannel({
|
|
@@ -131779,11 +131924,15 @@ async function main() {
|
|
|
131779
131924
|
// per-message binding is what prevents a private 1:1 reply from leaking into a
|
|
131780
131925
|
// room when room traffic arrives mid-compose.
|
|
131781
131926
|
// Assistant reasoning that wasn't sent — log a short trace only.
|
|
131782
|
-
onObserve: (text) => console.error(`[bridge] observed (${text.length} chars, not sent)`)
|
|
131927
|
+
onObserve: (text) => console.error(`[bridge] observed (${text.length} chars, not sent)`),
|
|
131928
|
+
worker: cfg.worker,
|
|
131929
|
+
workspaceDir: cfg.workspaceDir,
|
|
131930
|
+
permissionMode: cfg.permissionMode,
|
|
131931
|
+
dataDir: cfg.dataDir
|
|
131783
131932
|
});
|
|
131784
131933
|
wireBridge(
|
|
131785
131934
|
channel,
|
|
131786
|
-
{ push: (t7, reply) => session.push(t7, reply) },
|
|
131935
|
+
{ push: (t7, reply, opts) => session.push(t7, reply, opts) },
|
|
131787
131936
|
target,
|
|
131788
131937
|
{ roomFilter: cfg.roomFilter, log: (m6) => console.error("[bridge] " + m6) }
|
|
131789
131938
|
);
|
package/dist/session.d.ts
CHANGED
|
@@ -55,6 +55,15 @@ export interface SessionOpts {
|
|
|
55
55
|
model?: string;
|
|
56
56
|
systemPrompt?: string;
|
|
57
57
|
queryImpl?: QueryFn;
|
|
58
|
+
/** When true, session runs as a worker agent: full toolset on owner DMs,
|
|
59
|
+
* project context loaded, workspace cwd set, AV data dir fenced off. */
|
|
60
|
+
worker?: boolean;
|
|
61
|
+
/** Workspace directory to set as cwd in worker mode. */
|
|
62
|
+
workspaceDir?: string;
|
|
63
|
+
/** Claude permission mode for worker turns (default: "auto"). */
|
|
64
|
+
permissionMode?: "auto" | "acceptEdits" | "bypassPermissions";
|
|
65
|
+
/** AgentVault data directory — fenced off from worker tool access. */
|
|
66
|
+
dataDir?: string;
|
|
58
67
|
}
|
|
59
68
|
export declare class PersistentClaudeSession {
|
|
60
69
|
private opts;
|
|
@@ -63,13 +72,44 @@ export declare class PersistentClaudeSession {
|
|
|
63
72
|
/** Reply sink bound to the message currently being processed. Set as each
|
|
64
73
|
* message is handed to the model so the say tool routes to the right place. */
|
|
65
74
|
private activeReply?;
|
|
75
|
+
/**
|
|
76
|
+
* Per-turn state for the #416 DM auto-reply fallback.
|
|
77
|
+
*
|
|
78
|
+
* LOAD-BEARING for the worker tool gate: `currentAutoReply` is true ONLY on
|
|
79
|
+
* owner DM turns (`autoReplyOnText:true` from wireBridge) and false on room
|
|
80
|
+
* turns (`autoReplyOnText:false`). `makeWorkerPermission` calls
|
|
81
|
+
* `isOwnerTurn: () => this.currentAutoReply` to decide whether tools are
|
|
82
|
+
* allowed. A room-origin turn MUST keep this false or untrusted input gains
|
|
83
|
+
* full tool access. See worker-permission.test.ts for the covering assertion.
|
|
84
|
+
*/
|
|
85
|
+
private currentAutoReply;
|
|
86
|
+
private saidThisTurn;
|
|
87
|
+
private turnText;
|
|
88
|
+
/** In-process room MCP server — hoisted so buildSdkOptions can reference it. */
|
|
89
|
+
private roomServer;
|
|
66
90
|
constructor(opts: SessionOpts);
|
|
67
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Queue an inbound message for the model.
|
|
93
|
+
* @param opts.autoReplyOnText — for 1:1 DMs (where the owner always expects a
|
|
94
|
+
* reply): if the turn produces assistant text but never calls the say tool,
|
|
95
|
+
* send that text as the reply instead of silently dropping it (#416). Rooms
|
|
96
|
+
* omit this so the agent can still choose silence.
|
|
97
|
+
*/
|
|
98
|
+
push(text: string, reply?: ReplySink, opts?: {
|
|
99
|
+
autoReplyOnText?: boolean;
|
|
100
|
+
}): void;
|
|
68
101
|
/** Route a say-tool message to the reply bound to the in-flight message, falling
|
|
69
102
|
* back to opts.onSay. This is what closes the DM→room leak: the destination is
|
|
70
103
|
* the one captured for the message being answered, not a live global target. */
|
|
71
104
|
deliver(text: string): Promise<void>;
|
|
72
105
|
private input;
|
|
106
|
+
/**
|
|
107
|
+
* Build the SDK options for this session. Locked mode (default) is safe for
|
|
108
|
+
* untrusted inbound room messages. Worker mode (opts.worker=true) enables full
|
|
109
|
+
* toolset on owner DMs, project context, workspace cwd, and an AV data-dir
|
|
110
|
+
* fence via canUseTool.
|
|
111
|
+
*/
|
|
112
|
+
private buildSdkOptions;
|
|
73
113
|
start(): Promise<void>;
|
|
74
114
|
}
|
|
75
115
|
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CanUseTool, HookCallbackMatcher } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* PRIMARY enforcement: a PreToolUse hook. Unlike canUseTool (the SDK's "ask"
|
|
4
|
+
* path, which `permissionMode:"auto"` BYPASSES by auto-approving benign tools via
|
|
5
|
+
* its classifier — verified live: the canUseTool gate never fired in auto mode),
|
|
6
|
+
* a PreToolUse `deny` fires on EVERY tool call regardless of permission mode.
|
|
7
|
+
* This is what actually enforces the gate at runtime.
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeWorkerPreToolUseHook(opts: {
|
|
10
|
+
dataDir: string;
|
|
11
|
+
isOwnerTurn: () => boolean;
|
|
12
|
+
}): HookCallbackMatcher;
|
|
13
|
+
/**
|
|
14
|
+
* SECONDARY (defense-in-depth): the canUseTool callback covers the SDK's "ask"
|
|
15
|
+
* path (permissionMode `"default"`/`"dontAsk"`). Shares gateDecision with the
|
|
16
|
+
* PreToolUse hook above, so a deny in one is a deny in the other.
|
|
17
|
+
*/
|
|
18
|
+
export declare function makeWorkerPermission(opts: {
|
|
19
|
+
dataDir: string;
|
|
20
|
+
isOwnerTurn: () => boolean;
|
|
21
|
+
}): CanUseTool;
|
|
22
|
+
//# 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.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|