@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 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 homedir2 = osHomedir();
68298
+ const homedir = osHomedir();
68297
68299
  const agentName = this.config.agentName;
68298
68300
  if (this._persisted?.agentRole === "lead") {
68299
- return join4(homedir2, ".openclaw", "workspace");
68301
+ return join4(homedir, ".openclaw", "workspace");
68300
68302
  }
68301
68303
  try {
68302
- const configPath = join4(homedir2, ".openclaw", "openclaw.json");
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(homedir2, ".openclaw", `workspace-${agentName}`);
68318
+ return join4(homedir, ".openclaw", `workspace-${agentName}`);
68317
68319
  }
68318
- return join4(homedir2, ".openclaw", "workspace");
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
- try {
118487
- return resolve2(realpathSync2(dirname(abs)), basename(abs));
118488
- } catch {
118489
- return abs;
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
- function targetsDataDir(_toolName, input, dataDir) {
118494
- const canonDir = canonical(dataDir);
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
- const cmd = input.command;
118503
- if (typeof cmd === "string") {
118504
- if (cmd.includes(canonDir)) return true;
118505
- const home = process.env.HOME || homedir();
118506
- if (home && (canonDir === home || canonDir.startsWith(home + sep))) {
118507
- const rel = canonDir.slice(home.length);
118508
- for (const prefix of ["~", "$HOME", "${HOME}"]) {
118509
- if (cmd.includes(prefix + rel)) return true;
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 (targetsDataDir(toolName, input, opts.dataDir)) {
118521
- return { deny: true, reason: "access to the AgentVault key directory is not permitted" };
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: false };
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
- const item = this.pending.length > 0 ? this.pending.shift() : await new Promise((resolve3) => {
132454
- this.waiting = resolve3;
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
- settingSources: ["user", "project"],
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.6" : "dev"}`);
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 session = new PersistentClaudeSession({
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: 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.`,
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: cfg.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) => session.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
- await Promise.all([channel.start(), session.start()]);
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);
@@ -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.6",
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",