@agentvault/claude-bridge 0.5.6 → 0.5.8
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/config.d.ts +3 -0
- package/dist/index.js +230 -54
- package/dist/router.d.ts +30 -0
- package/dist/session.d.ts +13 -2
- package/dist/worker-permission.d.ts +8 -0
- package/dist/worker-queue.d.ts +41 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export interface BridgeConfig {
|
|
|
20
20
|
/** Slice 2: arm the pinned room (roomFilter) for worker tools on room turns.
|
|
21
21
|
* Off by default. Valid only with worker + roomFilter + workspaceDir. */
|
|
22
22
|
armRoom: boolean;
|
|
23
|
+
/** Operator attestation that the agent process runs OS-isolated (own user / jail).
|
|
24
|
+
* Only when true is Bash permitted in worker mode (the OS boundary is the fence). */
|
|
25
|
+
osIsolated: boolean;
|
|
23
26
|
}
|
|
24
27
|
export declare function loadConfig(env: NodeJS.ProcessEnv, argv?: string[]): BridgeConfig;
|
|
25
28
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ function loadConfig(env, argv = []) {
|
|
|
40
40
|
const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || "";
|
|
41
41
|
const worker = env.AV_WORKER === "1" || env.AV_WORKER === "true";
|
|
42
42
|
const workspaceDir = env.AV_WORKSPACE_DIR || void 0;
|
|
43
|
+
const osIsolated = env.AV_WORKER_OS_ISOLATED === "1" || env.AV_WORKER_OS_ISOLATED === "true";
|
|
43
44
|
const PERMISSION_MODES = ["auto", "acceptEdits", "bypassPermissions"];
|
|
44
45
|
const permissionMode = env.AV_PERMISSION_MODE ?? "auto";
|
|
45
46
|
if (!PERMISSION_MODES.includes(permissionMode)) {
|
|
@@ -84,7 +85,8 @@ function loadConfig(env, argv = []) {
|
|
|
84
85
|
worker,
|
|
85
86
|
workspaceDir,
|
|
86
87
|
permissionMode,
|
|
87
|
-
armRoom
|
|
88
|
+
armRoom,
|
|
89
|
+
osIsolated
|
|
88
90
|
};
|
|
89
91
|
}
|
|
90
92
|
var CRED_FILES, BACKUP_FILE;
|
|
@@ -68293,13 +68295,13 @@ ${messageText}`;
|
|
|
68293
68295
|
* Looks for OpenClaw workspace config, falls back to default path.
|
|
68294
68296
|
*/
|
|
68295
68297
|
_resolveWorkspaceDir() {
|
|
68296
|
-
const
|
|
68298
|
+
const homedir = osHomedir();
|
|
68297
68299
|
const agentName = this.config.agentName;
|
|
68298
68300
|
if (this._persisted?.agentRole === "lead") {
|
|
68299
|
-
return join4(
|
|
68301
|
+
return join4(homedir, ".openclaw", "workspace");
|
|
68300
68302
|
}
|
|
68301
68303
|
try {
|
|
68302
|
-
const configPath = join4(
|
|
68304
|
+
const configPath = join4(homedir, ".openclaw", "openclaw.json");
|
|
68303
68305
|
const raw = readFileSync(configPath, "utf-8");
|
|
68304
68306
|
const config22 = JSON.parse(raw);
|
|
68305
68307
|
const agents = config22?.agents?.list;
|
|
@@ -68313,9 +68315,9 @@ ${messageText}`;
|
|
|
68313
68315
|
} catch {
|
|
68314
68316
|
}
|
|
68315
68317
|
if (agentName && agentName !== "CLI Agent" && agentName !== "OpenClaw Agent") {
|
|
68316
|
-
return join4(
|
|
68318
|
+
return join4(homedir, ".openclaw", `workspace-${agentName}`);
|
|
68317
68319
|
}
|
|
68318
|
-
return join4(
|
|
68320
|
+
return join4(homedir, ".openclaw", "workspace");
|
|
68319
68321
|
}
|
|
68320
68322
|
/**
|
|
68321
68323
|
* Send a structured JSON reply to a specific conversation.
|
|
@@ -118475,52 +118477,75 @@ function Z_($10, Q4) {
|
|
|
118475
118477
|
|
|
118476
118478
|
// src/worker-permission.ts
|
|
118477
118479
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
118478
|
-
import { resolve as resolve2, dirname, basename, sep } from "node:path";
|
|
118479
|
-
import { homedir } from "node:os";
|
|
118480
|
+
import { resolve as resolve2, dirname, basename, sep, isAbsolute } from "node:path";
|
|
118480
118481
|
var PATH_FIELDS = ["file_path", "path", "notebook_path"];
|
|
118481
118482
|
function canonical(p2) {
|
|
118482
118483
|
const abs = resolve2(p2);
|
|
118483
118484
|
try {
|
|
118484
118485
|
return realpathSync2(abs);
|
|
118485
118486
|
} catch {
|
|
118486
|
-
|
|
118487
|
-
|
|
118488
|
-
|
|
118489
|
-
|
|
118487
|
+
const suffix = [];
|
|
118488
|
+
let dir = abs;
|
|
118489
|
+
for (; ; ) {
|
|
118490
|
+
const parent2 = dirname(dir);
|
|
118491
|
+
suffix.unshift(basename(dir));
|
|
118492
|
+
if (parent2 === dir) return abs;
|
|
118493
|
+
try {
|
|
118494
|
+
return resolve2(realpathSync2(parent2), ...suffix);
|
|
118495
|
+
} catch {
|
|
118496
|
+
dir = parent2;
|
|
118497
|
+
}
|
|
118490
118498
|
}
|
|
118491
118499
|
}
|
|
118492
118500
|
}
|
|
118493
|
-
|
|
118494
|
-
|
|
118501
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set(["Read", "Write", "Edit", "MultiEdit", "NotebookEdit", "Glob", "Grep"]);
|
|
118502
|
+
function pathsOf(input) {
|
|
118503
|
+
const out = [];
|
|
118495
118504
|
for (const f7 of PATH_FIELDS) {
|
|
118496
118505
|
const v5 = input[f7];
|
|
118497
|
-
if (typeof v5 === "string")
|
|
118498
|
-
const canon = canonical(v5);
|
|
118499
|
-
if (canon === canonDir || canon.startsWith(canonDir + sep)) return true;
|
|
118500
|
-
}
|
|
118506
|
+
if (typeof v5 === "string" && v5.length > 0) out.push(canonical(v5));
|
|
118501
118507
|
}
|
|
118502
|
-
|
|
118503
|
-
|
|
118504
|
-
|
|
118505
|
-
|
|
118506
|
-
|
|
118507
|
-
|
|
118508
|
-
|
|
118509
|
-
|
|
118510
|
-
|
|
118511
|
-
|
|
118512
|
-
}
|
|
118513
|
-
return false;
|
|
118508
|
+
return out;
|
|
118509
|
+
}
|
|
118510
|
+
function within(canonTarget, canonRoot) {
|
|
118511
|
+
return canonTarget === canonRoot || canonTarget.startsWith(canonRoot + sep);
|
|
118512
|
+
}
|
|
118513
|
+
function globPatternEscapes(pattern) {
|
|
118514
|
+
if (typeof pattern !== "string" || pattern.length === 0) return true;
|
|
118515
|
+
if (!/^[A-Za-z0-9 _./*?-]+$/.test(pattern)) return true;
|
|
118516
|
+
if (isAbsolute(pattern)) return true;
|
|
118517
|
+
return pattern.split("/").some((seg) => seg === ".." || seg === "");
|
|
118514
118518
|
}
|
|
118515
118519
|
function gateDecision(toolName, input, opts) {
|
|
118516
118520
|
if (toolName === ROOM_SAY_TOOL_NAME) return { deny: false };
|
|
118517
118521
|
if (!opts.isToolTurn()) {
|
|
118518
118522
|
return { deny: true, reason: "tools are disabled on this turn; reply with the say tool only" };
|
|
118519
118523
|
}
|
|
118520
|
-
if (
|
|
118521
|
-
return { deny: true, reason: "
|
|
118524
|
+
if (toolName === "Bash") {
|
|
118525
|
+
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)" };
|
|
118526
|
+
}
|
|
118527
|
+
if (FILE_TOOLS.has(toolName)) {
|
|
118528
|
+
if (!opts.workspaceDir) {
|
|
118529
|
+
return { deny: true, reason: "no workspace is configured; file tools are disabled" };
|
|
118530
|
+
}
|
|
118531
|
+
const paths = pathsOf(input);
|
|
118532
|
+
if (paths.length === 0) {
|
|
118533
|
+
return { deny: true, reason: "file tool call has no resolvable path; blocked (workspace-confined)" };
|
|
118534
|
+
}
|
|
118535
|
+
if (toolName === "Glob" && globPatternEscapes(input.pattern)) {
|
|
118536
|
+
return { deny: true, reason: "glob pattern must be workspace-relative" };
|
|
118537
|
+
}
|
|
118538
|
+
const wsRoot = canonical(opts.workspaceDir);
|
|
118539
|
+
if (!paths.every((p2) => within(p2, wsRoot))) {
|
|
118540
|
+
return { deny: true, reason: "file access is restricted to the workspace directory" };
|
|
118541
|
+
}
|
|
118542
|
+
const dataRoot = canonical(opts.dataDir);
|
|
118543
|
+
if (paths.some((p2) => within(p2, dataRoot))) {
|
|
118544
|
+
return { deny: true, reason: "access to the AgentVault key directory is not permitted" };
|
|
118545
|
+
}
|
|
118546
|
+
return { deny: false };
|
|
118522
118547
|
}
|
|
118523
|
-
return { deny:
|
|
118548
|
+
return { deny: true, reason: `tool ${toolName} is not permitted in worker mode` };
|
|
118524
118549
|
}
|
|
118525
118550
|
function makeWorkerPreToolUseHook(opts) {
|
|
118526
118551
|
return {
|
|
@@ -132412,6 +132437,8 @@ var PersistentClaudeSession = class {
|
|
|
132412
132437
|
turnText = "";
|
|
132413
132438
|
/** In-process room MCP server — hoisted so buildSdkOptions can reference it. */
|
|
132414
132439
|
roomServer;
|
|
132440
|
+
/** AbortController for the in-flight query(); abort() triggers it (queue timeout). */
|
|
132441
|
+
_abort;
|
|
132415
132442
|
/**
|
|
132416
132443
|
* Queue an inbound message for the model.
|
|
132417
132444
|
* @param opts.autoReplyOnText — for 1:1 DMs (where the owner always expects a
|
|
@@ -132448,11 +132475,22 @@ var PersistentClaudeSession = class {
|
|
|
132448
132475
|
if (this.activeReply) await this.activeReply(text);
|
|
132449
132476
|
else if (this.opts.onSay) await this.opts.onSay(text);
|
|
132450
132477
|
}
|
|
132478
|
+
/** Abort the in-flight query() — used by the worker queue on a per-task timeout. */
|
|
132479
|
+
abort() {
|
|
132480
|
+
this._abort?.abort();
|
|
132481
|
+
}
|
|
132451
132482
|
async *input() {
|
|
132452
132483
|
while (true) {
|
|
132453
|
-
|
|
132454
|
-
|
|
132455
|
-
|
|
132484
|
+
let item;
|
|
132485
|
+
if (this.pending.length > 0) {
|
|
132486
|
+
item = this.pending.shift();
|
|
132487
|
+
} else if (this.opts.ephemeral) {
|
|
132488
|
+
return;
|
|
132489
|
+
} else {
|
|
132490
|
+
item = await new Promise((resolve3) => {
|
|
132491
|
+
this.waiting = resolve3;
|
|
132492
|
+
});
|
|
132493
|
+
}
|
|
132456
132494
|
this.activeReply = item.reply;
|
|
132457
132495
|
this.currentAutoReply = item.autoReplyOnText ?? false;
|
|
132458
132496
|
this.currentArmedGetter = item.armed ?? (() => false);
|
|
@@ -132484,6 +132522,8 @@ var PersistentClaudeSession = class {
|
|
|
132484
132522
|
}
|
|
132485
132523
|
const gateOpts = {
|
|
132486
132524
|
dataDir: this.opts.dataDir ?? "",
|
|
132525
|
+
workspaceDir: this.opts.workspaceDir,
|
|
132526
|
+
osIsolated: this.opts.osIsolated ?? false,
|
|
132487
132527
|
// LOAD-BEARING: tools are allowed when this is an owner DM turn
|
|
132488
132528
|
// (currentAutoReply) OR an armed-room turn (currentArmedGetter()). An UNARMED
|
|
132489
132529
|
// room turn keeps both false → the gate denies all tools but room_say.
|
|
@@ -132505,11 +132545,34 @@ var PersistentClaudeSession = class {
|
|
|
132505
132545
|
});
|
|
132506
132546
|
}
|
|
132507
132547
|
};
|
|
132548
|
+
console.error(
|
|
132549
|
+
`[worker-gate] allowlist active \u2014 workspace=${this.opts.workspaceDir ?? "(none: file tools disabled)"}, bash=${this.opts.osIsolated ? "enabled (OS-isolated)" : "disabled"}`
|
|
132550
|
+
);
|
|
132508
132551
|
return {
|
|
132509
132552
|
...base,
|
|
132510
132553
|
permissionMode: this.opts.permissionMode ?? "auto",
|
|
132511
|
-
|
|
132554
|
+
// FACET B (disk isolation). The workspace is writable under the Facet-A
|
|
132555
|
+
// allowlist, so a prior worker/armed turn could plant config files in it that
|
|
132556
|
+
// a later worker would otherwise ingest. We close each disk surface explicitly
|
|
132557
|
+
// (they are governed SEPARATELY by the SDK, so one flag is not enough):
|
|
132558
|
+
// - settingSources:[] — "disable filesystem settings (SDK isolation mode)":
|
|
132559
|
+
// no ~/.claude or project settings.json (⇒ no settings-defined hooks, the
|
|
132560
|
+
// RCE-grade vector: a hook is a shell command run OUTSIDE the Bash gate),
|
|
132561
|
+
// no CLAUDE.md. It also means no .mcp.json APPROVAL state is loaded — and
|
|
132562
|
+
// enableAllProjectMcpServers/enabledMcpjsonServers live only in Settings —
|
|
132563
|
+
// so an unapproved project .mcp.json server is never spawned in headless
|
|
132564
|
+
// mode. Any MCP tool that WERE discovered is still denied by the Facet-A
|
|
132565
|
+
// allowlist (mcp__* is not room_say/a file tool/Bash → final deny).
|
|
132566
|
+
// - skills:[] — settingSources does NOT gate skills discovery; [] enables
|
|
132567
|
+
// zero skills, so a planted <ws>/.claude/skills/*/SKILL.md description
|
|
132568
|
+
// cannot be injected into the worker's context.
|
|
132569
|
+
// Project context, if needed, is injected via the bridge-controlled systemPrompt,
|
|
132570
|
+
// never auto-loaded from the mutable workspace.
|
|
132571
|
+
settingSources: [],
|
|
132572
|
+
skills: [],
|
|
132512
132573
|
cwd: this.opts.workspaceDir,
|
|
132574
|
+
...this.opts.maxTurns != null ? { maxTurns: this.opts.maxTurns } : {},
|
|
132575
|
+
abortController: this._abort,
|
|
132513
132576
|
// PRIMARY gate: a PreToolUse hook fires on EVERY tool call regardless of
|
|
132514
132577
|
// permissionMode. canUseTool alone is bypassed in "auto" mode (the SDK's
|
|
132515
132578
|
// classifier auto-approves without hitting the "ask" path) — verified live.
|
|
@@ -132519,6 +132582,7 @@ var PersistentClaudeSession = class {
|
|
|
132519
132582
|
};
|
|
132520
132583
|
}
|
|
132521
132584
|
async start() {
|
|
132585
|
+
this._abort = new AbortController();
|
|
132522
132586
|
this.roomServer = _s({
|
|
132523
132587
|
name: "room",
|
|
132524
132588
|
version: "0.2.0",
|
|
@@ -132550,6 +132614,96 @@ var PersistentClaudeSession = class {
|
|
|
132550
132614
|
}
|
|
132551
132615
|
};
|
|
132552
132616
|
|
|
132617
|
+
// src/worker-queue.ts
|
|
132618
|
+
var GENERIC_ERROR = "Sorry \u2014 I couldn't complete that request.";
|
|
132619
|
+
var WorkerQueue = class {
|
|
132620
|
+
constructor(deps) {
|
|
132621
|
+
this.deps = deps;
|
|
132622
|
+
}
|
|
132623
|
+
deps;
|
|
132624
|
+
q = [];
|
|
132625
|
+
loop = Promise.resolve();
|
|
132626
|
+
running = false;
|
|
132627
|
+
enqueue(task) {
|
|
132628
|
+
this.q.push(task);
|
|
132629
|
+
if (!this.running) {
|
|
132630
|
+
this.running = true;
|
|
132631
|
+
this.loop = this.drain();
|
|
132632
|
+
}
|
|
132633
|
+
}
|
|
132634
|
+
/** Resolves when the queue has processed everything enqueued so far. */
|
|
132635
|
+
whenDrained() {
|
|
132636
|
+
return this.loop;
|
|
132637
|
+
}
|
|
132638
|
+
async drain() {
|
|
132639
|
+
try {
|
|
132640
|
+
while (this.q.length > 0) {
|
|
132641
|
+
const task = this.q.shift();
|
|
132642
|
+
await this.runOne(task);
|
|
132643
|
+
}
|
|
132644
|
+
} finally {
|
|
132645
|
+
this.running = false;
|
|
132646
|
+
}
|
|
132647
|
+
}
|
|
132648
|
+
async runOne(task) {
|
|
132649
|
+
let session;
|
|
132650
|
+
let timer;
|
|
132651
|
+
try {
|
|
132652
|
+
session = this.deps.makeSession(task);
|
|
132653
|
+
session.push(task.instruction, task.reply, {
|
|
132654
|
+
autoReplyOnText: task.autoReplyOnText,
|
|
132655
|
+
armed: task.armed
|
|
132656
|
+
});
|
|
132657
|
+
const currentSession = session;
|
|
132658
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
132659
|
+
timer = setTimeout(() => {
|
|
132660
|
+
currentSession.abort();
|
|
132661
|
+
reject(new Error("worker task timeout"));
|
|
132662
|
+
}, this.deps.timeoutMs);
|
|
132663
|
+
});
|
|
132664
|
+
await Promise.race([session.start(), timeout]);
|
|
132665
|
+
} catch (e7) {
|
|
132666
|
+
this.deps.log(`[worker-queue] task failed: ${e7.message}`);
|
|
132667
|
+
session?.abort();
|
|
132668
|
+
try {
|
|
132669
|
+
await task.reply(GENERIC_ERROR);
|
|
132670
|
+
} catch (replyErr) {
|
|
132671
|
+
this.deps.log(`[worker-queue] failed to deliver error reply: ${replyErr.message}`);
|
|
132672
|
+
}
|
|
132673
|
+
} finally {
|
|
132674
|
+
if (timer) clearTimeout(timer);
|
|
132675
|
+
}
|
|
132676
|
+
}
|
|
132677
|
+
};
|
|
132678
|
+
|
|
132679
|
+
// src/router.ts
|
|
132680
|
+
function makeRouter(deps) {
|
|
132681
|
+
return {
|
|
132682
|
+
push(text, reply, opts) {
|
|
132683
|
+
const replySink = reply ?? (() => {
|
|
132684
|
+
});
|
|
132685
|
+
if (deps.worker) {
|
|
132686
|
+
const isOwnerDm = opts?.autoReplyOnText === true;
|
|
132687
|
+
const isArmedRoom = opts?.armed?.() === true;
|
|
132688
|
+
if (isOwnerDm) {
|
|
132689
|
+
deps.queue.enqueue({ instruction: text, reply: replySink, autoReplyOnText: true });
|
|
132690
|
+
return;
|
|
132691
|
+
}
|
|
132692
|
+
if (isArmedRoom) {
|
|
132693
|
+
deps.queue.enqueue({
|
|
132694
|
+
instruction: text,
|
|
132695
|
+
reply: replySink,
|
|
132696
|
+
autoReplyOnText: false,
|
|
132697
|
+
armed: opts.armed
|
|
132698
|
+
});
|
|
132699
|
+
return;
|
|
132700
|
+
}
|
|
132701
|
+
}
|
|
132702
|
+
deps.listener.push(text, reply, opts);
|
|
132703
|
+
}
|
|
132704
|
+
};
|
|
132705
|
+
}
|
|
132706
|
+
|
|
132553
132707
|
// src/arming.ts
|
|
132554
132708
|
var ArmingState = class {
|
|
132555
132709
|
armed = /* @__PURE__ */ new Set();
|
|
@@ -132913,7 +133067,7 @@ async function main() {
|
|
|
132913
133067
|
"[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"
|
|
132914
133068
|
);
|
|
132915
133069
|
}
|
|
132916
|
-
console.error(`[bridge] version: ${true ? "0.5.
|
|
133070
|
+
console.error(`[bridge] version: ${true ? "0.5.8" : "dev"}`);
|
|
132917
133071
|
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
132918
133072
|
if (cfg.worker) {
|
|
132919
133073
|
console.error(`[bridge] WORKER MODE \u2014 workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
@@ -132939,9 +133093,14 @@ async function main() {
|
|
|
132939
133093
|
agentName: cfg.agentName,
|
|
132940
133094
|
platform: "node"
|
|
132941
133095
|
});
|
|
132942
|
-
const
|
|
133096
|
+
const agentSystemPrompt = cfg.systemPrompt ?? `You are ${cfg.agentName}, an AI agent on AgentVault. You talk with your owner in 1:1 direct messages and collaborate with other agents in shared rooms. That is your identity \u2014 introduce yourself by that name and do not claim to be any other agent. To speak, call the say tool. In a 1:1 with your owner, reply to what they say. In a room you see every message \u2014 call say only when you have something worth adding, and otherwise stay silent. Keep messages concise.`;
|
|
133097
|
+
const deviceJwt = () => {
|
|
133098
|
+
const c4 = channel;
|
|
133099
|
+
return c4._deviceJwt ?? c4._persisted?.deviceJwt ?? null;
|
|
133100
|
+
};
|
|
133101
|
+
const listener = new PersistentClaudeSession({
|
|
132943
133102
|
model: cfg.model,
|
|
132944
|
-
systemPrompt:
|
|
133103
|
+
systemPrompt: agentSystemPrompt,
|
|
132945
133104
|
// Claude speaks by calling the say tool → the session routes it to the reply
|
|
132946
133105
|
// bound to the message being answered (wireBridge captures that per inbound via
|
|
132947
133106
|
// ActiveTarget.snapshotReply, which also logs the "said to …" line). This
|
|
@@ -132949,22 +133108,35 @@ async function main() {
|
|
|
132949
133108
|
// room when room traffic arrives mid-compose.
|
|
132950
133109
|
// Assistant reasoning that wasn't sent — log a short trace only.
|
|
132951
133110
|
onObserve: (text) => console.error(`[bridge] observed (${text.length} chars, not sent)`),
|
|
132952
|
-
worker:
|
|
132953
|
-
workspaceDir: cfg.workspaceDir,
|
|
132954
|
-
permissionMode: cfg.permissionMode,
|
|
132955
|
-
dataDir: cfg.dataDir,
|
|
132956
|
-
// Slice 2 Plan B: audit self-report context (only used on armed-room turns).
|
|
132957
|
-
roomId: cfg.roomFilter,
|
|
132958
|
-
agentId: channel.deviceId,
|
|
132959
|
-
apiUrl: cfg.apiUrl,
|
|
132960
|
-
deviceJwt: () => {
|
|
132961
|
-
const c4 = channel;
|
|
132962
|
-
return c4._deviceJwt ?? c4._persisted?.deviceJwt ?? null;
|
|
132963
|
-
}
|
|
133111
|
+
worker: false
|
|
132964
133112
|
});
|
|
133113
|
+
const WORKER_MAX_TURNS = 20;
|
|
133114
|
+
const WORKER_TIMEOUT_MS = 5 * 6e4;
|
|
133115
|
+
const workerQueue = new WorkerQueue({
|
|
133116
|
+
makeSession: (task) => new PersistentClaudeSession({
|
|
133117
|
+
model: cfg.model,
|
|
133118
|
+
systemPrompt: agentSystemPrompt,
|
|
133119
|
+
onObserve: (text) => console.error(`[worker] observed (${text.length} chars, not sent)`),
|
|
133120
|
+
worker: true,
|
|
133121
|
+
ephemeral: true,
|
|
133122
|
+
maxTurns: WORKER_MAX_TURNS,
|
|
133123
|
+
workspaceDir: cfg.workspaceDir,
|
|
133124
|
+
osIsolated: cfg.osIsolated,
|
|
133125
|
+
permissionMode: cfg.permissionMode,
|
|
133126
|
+
dataDir: cfg.dataDir,
|
|
133127
|
+
// Slice 2 Plan B audit self-report — only fires on armed-room tasks (isArmedTurn).
|
|
133128
|
+
roomId: cfg.roomFilter,
|
|
133129
|
+
agentId: channel.deviceId,
|
|
133130
|
+
apiUrl: cfg.apiUrl,
|
|
133131
|
+
deviceJwt
|
|
133132
|
+
}),
|
|
133133
|
+
timeoutMs: WORKER_TIMEOUT_MS,
|
|
133134
|
+
log: (m6) => console.error(m6)
|
|
133135
|
+
});
|
|
133136
|
+
const router = makeRouter({ worker: !!cfg.worker, listener, queue: workerQueue });
|
|
132965
133137
|
wireBridge(
|
|
132966
133138
|
channel,
|
|
132967
|
-
{ push: (t7, reply, opts) =>
|
|
133139
|
+
{ push: (t7, reply, opts) => router.push(t7, reply, opts) },
|
|
132968
133140
|
target,
|
|
132969
133141
|
{
|
|
132970
133142
|
roomFilter: cfg.roomFilter,
|
|
@@ -132984,7 +133156,11 @@ async function main() {
|
|
|
132984
133156
|
"room_joined",
|
|
132985
133157
|
(e7) => console.error(`[bridge] joined room ${e7.name} (${e7.roomId})`)
|
|
132986
133158
|
);
|
|
132987
|
-
|
|
133159
|
+
listener.start().catch((err) => {
|
|
133160
|
+
console.error("[bridge] fatal:", err);
|
|
133161
|
+
process.exit(1);
|
|
133162
|
+
});
|
|
133163
|
+
await channel.start();
|
|
132988
133164
|
}
|
|
132989
133165
|
main().catch((err) => {
|
|
132990
133166
|
console.error("[bridge] fatal:", err);
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReplySink } from "./session.js";
|
|
2
|
+
import type { WorkerTask } from "./worker-queue.js";
|
|
3
|
+
/** The push interface wireBridge drives (matches PersistentClaudeSession.push). */
|
|
4
|
+
export interface PushLike {
|
|
5
|
+
push(text: string, reply?: ReplySink, opts?: {
|
|
6
|
+
autoReplyOnText?: boolean;
|
|
7
|
+
armed?: () => boolean;
|
|
8
|
+
}): void;
|
|
9
|
+
}
|
|
10
|
+
export interface RouterDeps {
|
|
11
|
+
/** AV_WORKER=1 → tool-eligible turns run in isolated workers; false → no worker ever. */
|
|
12
|
+
worker: boolean;
|
|
13
|
+
/** Always-on locked listener (tools:[]) for untrusted/non-tool turns. */
|
|
14
|
+
listener: PushLike;
|
|
15
|
+
/** Serial queue of tool-eligible turns. */
|
|
16
|
+
queue: {
|
|
17
|
+
enqueue(task: WorkerTask): void;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Route each inbound turn to EXACTLY ONE lane (facet B). Tool-eligible turns —
|
|
22
|
+
* owner DMs (autoReplyOnText) and armed room turns — go to an isolated ephemeral
|
|
23
|
+
* worker via the queue; everything else stays on the locked listener. A room
|
|
24
|
+
* turn's `armed` getter is read once here at routing time to pick the lane; the
|
|
25
|
+
* same live getter is threaded to the worker task so a mid-turn disarm still
|
|
26
|
+
* denies the next tool call (Facet A D5). Locked agents (worker=false) always
|
|
27
|
+
* route to the listener — a strict no-op vs today.
|
|
28
|
+
*/
|
|
29
|
+
export declare function makeRouter(deps: RouterDeps): PushLike;
|
|
30
|
+
//# sourceMappingURL=router.d.ts.map
|
package/dist/session.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type QueryFn = (args: {
|
|
|
45
45
|
type: string;
|
|
46
46
|
[k: string]: unknown;
|
|
47
47
|
}>;
|
|
48
|
-
type ReplySink = (text: string) => void | Promise<void>;
|
|
48
|
+
export type ReplySink = (text: string) => void | Promise<void>;
|
|
49
49
|
export interface SessionOpts {
|
|
50
50
|
/** Fallback reply sink when a message carries no per-message reply (e.g. a
|
|
51
51
|
* proactive say). Per-message replies passed to push() take precedence. */
|
|
@@ -60,6 +60,8 @@ export interface SessionOpts {
|
|
|
60
60
|
worker?: boolean;
|
|
61
61
|
/** Workspace directory to set as cwd in worker mode. */
|
|
62
62
|
workspaceDir?: string;
|
|
63
|
+
/** True when the process is attested OS-isolated, gating whether Bash is allowed at all. */
|
|
64
|
+
osIsolated?: boolean;
|
|
63
65
|
/** Claude permission mode for worker turns (default: "auto"). */
|
|
64
66
|
permissionMode?: "auto" | "acceptEdits" | "bypassPermissions";
|
|
65
67
|
/** AgentVault data directory — fenced off from worker tool access. */
|
|
@@ -71,6 +73,12 @@ export interface SessionOpts {
|
|
|
71
73
|
apiUrl?: string;
|
|
72
74
|
/** Lazy getter so a JWT refreshed after construction is still picked up. */
|
|
73
75
|
deviceJwt?: () => string | null;
|
|
76
|
+
/** Single-shot ephemeral worker: process exactly the pushed message(s) then
|
|
77
|
+
* terminate the input stream so start() resolves. Used by the worker queue so
|
|
78
|
+
* a per-task query() completes and its state is discarded. */
|
|
79
|
+
ephemeral?: boolean;
|
|
80
|
+
/** Turn cap for a worker query() (bounds a runaway tool loop). */
|
|
81
|
+
maxTurns?: number;
|
|
74
82
|
}
|
|
75
83
|
export declare class PersistentClaudeSession {
|
|
76
84
|
private opts;
|
|
@@ -103,6 +111,8 @@ export declare class PersistentClaudeSession {
|
|
|
103
111
|
private turnText;
|
|
104
112
|
/** In-process room MCP server — hoisted so buildSdkOptions can reference it. */
|
|
105
113
|
private roomServer;
|
|
114
|
+
/** AbortController for the in-flight query(); abort() triggers it (queue timeout). */
|
|
115
|
+
private _abort?;
|
|
106
116
|
constructor(opts: SessionOpts);
|
|
107
117
|
/**
|
|
108
118
|
* Queue an inbound message for the model.
|
|
@@ -124,6 +134,8 @@ export declare class PersistentClaudeSession {
|
|
|
124
134
|
* back to opts.onSay. This is what closes the DM→room leak: the destination is
|
|
125
135
|
* the one captured for the message being answered, not a live global target. */
|
|
126
136
|
deliver(text: string): Promise<void>;
|
|
137
|
+
/** Abort the in-flight query() — used by the worker queue on a per-task timeout. */
|
|
138
|
+
abort(): void;
|
|
127
139
|
private input;
|
|
128
140
|
/**
|
|
129
141
|
* Build the SDK options for this session. Locked mode (default) is safe for
|
|
@@ -134,5 +146,4 @@ export declare class PersistentClaudeSession {
|
|
|
134
146
|
private buildSdkOptions;
|
|
135
147
|
start(): Promise<void>;
|
|
136
148
|
}
|
|
137
|
-
export {};
|
|
138
149
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -8,6 +8,10 @@ import type { CanUseTool, HookCallbackMatcher } from "@anthropic-ai/claude-agent
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function makeWorkerPreToolUseHook(opts: {
|
|
10
10
|
dataDir: string;
|
|
11
|
+
/** The confined workspace root; file tools are denied entirely when unset. */
|
|
12
|
+
workspaceDir?: string;
|
|
13
|
+
/** True when the process is attested OS-isolated, gating whether Bash is allowed at all. */
|
|
14
|
+
osIsolated: boolean;
|
|
11
15
|
isToolTurn: () => boolean;
|
|
12
16
|
/** Slice 2 Plan B: true when this turn comes from an armed room. When set,
|
|
13
17
|
* each tool decision (allow AND deny) is forwarded to onToolDecision for
|
|
@@ -27,6 +31,10 @@ export declare function makeWorkerPreToolUseHook(opts: {
|
|
|
27
31
|
*/
|
|
28
32
|
export declare function makeWorkerPermission(opts: {
|
|
29
33
|
dataDir: string;
|
|
34
|
+
/** The confined workspace root; file tools are denied entirely when unset. */
|
|
35
|
+
workspaceDir?: string;
|
|
36
|
+
/** True when the process is attested OS-isolated, gating whether Bash is allowed at all. */
|
|
37
|
+
osIsolated: boolean;
|
|
30
38
|
isToolTurn: () => boolean;
|
|
31
39
|
}): CanUseTool;
|
|
32
40
|
//# sourceMappingURL=worker-permission.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PersistentClaudeSession, ReplySink } from "./session.js";
|
|
2
|
+
/** One tool-eligible turn to run in an isolated ephemeral worker. */
|
|
3
|
+
export type WorkerTask = {
|
|
4
|
+
/** The single instruction that seeds the worker's conversation. */
|
|
5
|
+
instruction: string;
|
|
6
|
+
/** Where this task's reply (room_say / #416 fallback) is sent. Per-task. */
|
|
7
|
+
reply: ReplySink;
|
|
8
|
+
/** Owner-DM turns want a plain-text reply fallback (#416). */
|
|
9
|
+
autoReplyOnText: boolean;
|
|
10
|
+
/** Armed-room turns: live getter the gate reads per tool decision (mid-turn
|
|
11
|
+
* disarm denies the next call). Absent for owner DMs. */
|
|
12
|
+
armed?: () => boolean;
|
|
13
|
+
};
|
|
14
|
+
export interface WorkerQueueDeps {
|
|
15
|
+
/** Build a fresh ephemeral worker session for this task (not yet started). */
|
|
16
|
+
makeSession: (task: WorkerTask) => PersistentClaudeSession;
|
|
17
|
+
/** Per-task wall-clock cap; on breach the worker is aborted and the queue advances. */
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
log: (m: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Serial FIFO of tool-eligible turns. One worker runs at a time (concurrent tool
|
|
23
|
+
* calls against one device/workspace can interleave destructively; the owner is a
|
|
24
|
+
* single actor). Each task runs in a fresh ephemeral worker seeded with only its
|
|
25
|
+
* instruction; on error or timeout the worker is torn down, a generic message is
|
|
26
|
+
* sent to the task's reply, and the queue advances — a task can never wedge the
|
|
27
|
+
* queue (which would starve the owner's DM lane).
|
|
28
|
+
*/
|
|
29
|
+
export declare class WorkerQueue {
|
|
30
|
+
private deps;
|
|
31
|
+
private q;
|
|
32
|
+
private loop;
|
|
33
|
+
private running;
|
|
34
|
+
constructor(deps: WorkerQueueDeps);
|
|
35
|
+
enqueue(task: WorkerTask): void;
|
|
36
|
+
/** Resolves when the queue has processed everything enqueued so far. */
|
|
37
|
+
whenDrained(): Promise<void>;
|
|
38
|
+
private drain;
|
|
39
|
+
private runOne;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=worker-queue.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentvault/claude-bridge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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",
|