@agentvault/claude-bridge 0.5.7 → 0.5.10
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/arming.d.ts +44 -14
- package/dist/bridge.d.ts +39 -3
- package/dist/config.d.ts +3 -2
- package/dist/index.js +422 -131
- package/dist/router.d.ts +40 -0
- package/dist/service/spec.d.ts.map +1 -1
- package/dist/session.d.ts +28 -2
- package/dist/worker-queue.d.ts +46 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,8 +10,8 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
|
-
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
14
|
-
import { join as join5 } from "node:path";
|
|
13
|
+
import { existsSync, readFileSync as readFileSync2, mkdirSync, chmodSync } from "node:fs";
|
|
14
|
+
import { join as join5, resolve as resolve2, sep, dirname } from "node:path";
|
|
15
15
|
function hasRecoverableBackup(dataDir) {
|
|
16
16
|
try {
|
|
17
17
|
const parsed = JSON.parse(readFileSync2(join5(dataDir, BACKUP_FILE), "utf-8"));
|
|
@@ -38,8 +38,22 @@ function resolveDataDir(env) {
|
|
|
38
38
|
function loadConfig(env, argv = []) {
|
|
39
39
|
const { dataDir, source: dataDirSource } = resolveDataDir(env);
|
|
40
40
|
const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || "";
|
|
41
|
-
const
|
|
42
|
-
const
|
|
41
|
+
const workspaceDir = env.AV_WORKSPACE_DIR || join5(dirname(dataDir), "workspaces", slugify2(env.AV_AGENT_NAME ?? "claude"));
|
|
42
|
+
const wsReal = resolve2(workspaceDir);
|
|
43
|
+
const ddReal = resolve2(dataDir);
|
|
44
|
+
if (wsReal === ddReal || wsReal.startsWith(ddReal + sep) || ddReal.startsWith(wsReal + sep)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`AV_WORKSPACE_DIR (${workspaceDir}) must be disjoint from the data dir (${dataDir})`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(workspaceDir, { recursive: true, mode: 448 });
|
|
51
|
+
chmodSync(workspaceDir, 448);
|
|
52
|
+
} catch (e7) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Failed to create/secure the agent workspace at ${workspaceDir} (override with AV_WORKSPACE_DIR): ${e7.message}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
43
57
|
const osIsolated = env.AV_WORKER_OS_ISOLATED === "1" || env.AV_WORKER_OS_ISOLATED === "true";
|
|
44
58
|
const PERMISSION_MODES = ["auto", "acceptEdits", "bypassPermissions"];
|
|
45
59
|
const permissionMode = env.AV_PERMISSION_MODE ?? "auto";
|
|
@@ -48,16 +62,8 @@ function loadConfig(env, argv = []) {
|
|
|
48
62
|
`invalid AV_PERMISSION_MODE: ${env.AV_PERMISSION_MODE} (expected one of ${PERMISSION_MODES.join(", ")})`
|
|
49
63
|
);
|
|
50
64
|
}
|
|
51
|
-
if (worker && !workspaceDir) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
"worker mode requires AV_WORKSPACE_DIR (the project directory the agent works in)"
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
65
|
const armRoom = env.AV_ARM_ROOM === "1" || env.AV_ARM_ROOM === "true";
|
|
57
66
|
if (armRoom) {
|
|
58
|
-
if (!worker) {
|
|
59
|
-
throw new Error("AV_ARM_ROOM requires worker mode (AV_WORKER=1)");
|
|
60
|
-
}
|
|
61
67
|
if (!env.AV_ROOM_ID) {
|
|
62
68
|
throw new Error("AV_ARM_ROOM requires a pinned room (AV_ROOM_ID) \u2014 the armed collaboration room");
|
|
63
69
|
}
|
|
@@ -82,7 +88,6 @@ function loadConfig(env, argv = []) {
|
|
|
82
88
|
roomFilter: env.AV_ROOM_ID || void 0,
|
|
83
89
|
model: env.AV_CLAUDE_MODEL || void 0,
|
|
84
90
|
systemPrompt: env.AV_SYSTEM_PROMPT || void 0,
|
|
85
|
-
worker,
|
|
86
91
|
workspaceDir,
|
|
87
92
|
permissionMode,
|
|
88
93
|
armRoom,
|
|
@@ -99,7 +104,7 @@ var init_config = __esm({
|
|
|
99
104
|
});
|
|
100
105
|
|
|
101
106
|
// src/service/launcher.ts
|
|
102
|
-
import { dirname as
|
|
107
|
+
import { dirname as dirname3 } from "node:path";
|
|
103
108
|
function sq2(s10) {
|
|
104
109
|
return `'${s10.replace(/'/g, `'\\''`)}'`;
|
|
105
110
|
}
|
|
@@ -124,7 +129,7 @@ function renderLauncher(opts) {
|
|
|
124
129
|
return lines.join("\n") + "\n";
|
|
125
130
|
}
|
|
126
131
|
function writeLauncher(deps, spec) {
|
|
127
|
-
deps.mkdir(
|
|
132
|
+
deps.mkdir(dirname3(spec.launcherPath));
|
|
128
133
|
deps.writeFile(spec.launcherPath, spec.launcherScript);
|
|
129
134
|
deps.chmod(spec.launcherPath, 493);
|
|
130
135
|
}
|
|
@@ -158,7 +163,6 @@ function buildServiceSpec(cfg, opts) {
|
|
|
158
163
|
AV_API_URL: cfg.apiUrl,
|
|
159
164
|
AV_PERMISSION_MODE: cfg.permissionMode
|
|
160
165
|
};
|
|
161
|
-
if (cfg.worker) env.AV_WORKER = "1";
|
|
162
166
|
if (cfg.workspaceDir) env.AV_WORKSPACE_DIR = cfg.workspaceDir;
|
|
163
167
|
if (cfg.roomFilter) env.AV_ROOM_ID = cfg.roomFilter;
|
|
164
168
|
if (cfg.armRoom) env.AV_ARM_ROOM = "1";
|
|
@@ -171,7 +175,7 @@ function buildServiceSpec(cfg, opts) {
|
|
|
171
175
|
launcherPath,
|
|
172
176
|
launcherScript: renderLauncher({ installPath: opts.path, entrypoint: opts.entrypoint }),
|
|
173
177
|
env,
|
|
174
|
-
workingDir: cfg.
|
|
178
|
+
workingDir: cfg.workspaceDir ? cfg.workspaceDir : opts.home,
|
|
175
179
|
logOut: join7(cfg.dataDir, "logs", "bridge.log"),
|
|
176
180
|
logErr: join7(cfg.dataDir, "logs", "bridge.error.log"),
|
|
177
181
|
keepAliveUnlessCleanExit: true,
|
|
@@ -187,7 +191,7 @@ var init_spec = __esm({
|
|
|
187
191
|
});
|
|
188
192
|
|
|
189
193
|
// src/service/launchd.ts
|
|
190
|
-
import { dirname as
|
|
194
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
191
195
|
function esc3(s10) {
|
|
192
196
|
return s10.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
193
197
|
}
|
|
@@ -241,9 +245,9 @@ var init_launchd = __esm({
|
|
|
241
245
|
}
|
|
242
246
|
install(spec) {
|
|
243
247
|
const path2 = this.plistPath(spec.label);
|
|
244
|
-
this.deps.mkdir(
|
|
248
|
+
this.deps.mkdir(dirname4(spec.logOut));
|
|
245
249
|
writeLauncher(this.deps, spec);
|
|
246
|
-
this.deps.mkdir(
|
|
250
|
+
this.deps.mkdir(dirname4(path2));
|
|
247
251
|
this.deps.writeFile(path2, renderLaunchdPlist(spec));
|
|
248
252
|
const svc = `${this.domain()}/${spec.label}`;
|
|
249
253
|
this.deps.exec("launchctl", ["bootout", svc]);
|
|
@@ -277,7 +281,7 @@ var init_launchd = __esm({
|
|
|
277
281
|
});
|
|
278
282
|
|
|
279
283
|
// src/service/systemd.ts
|
|
280
|
-
import { dirname as
|
|
284
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
281
285
|
function q5(s10) {
|
|
282
286
|
const esc4 = s10.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/%/g, "%%");
|
|
283
287
|
return `"${esc4}"`;
|
|
@@ -318,9 +322,9 @@ var init_systemd = __esm({
|
|
|
318
322
|
}
|
|
319
323
|
install(spec) {
|
|
320
324
|
const path2 = this.unitPath(spec.label);
|
|
321
|
-
this.deps.mkdir(
|
|
325
|
+
this.deps.mkdir(dirname5(spec.logOut));
|
|
322
326
|
writeLauncher(this.deps, spec);
|
|
323
|
-
this.deps.mkdir(
|
|
327
|
+
this.deps.mkdir(dirname5(path2));
|
|
324
328
|
this.deps.writeFile(path2, renderSystemdUnit(spec));
|
|
325
329
|
this.deps.exec("loginctl", ["enable-linger", this.deps.user]);
|
|
326
330
|
this.deps.exec("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -345,7 +349,7 @@ var init_systemd = __esm({
|
|
|
345
349
|
|
|
346
350
|
// src/service/backend.ts
|
|
347
351
|
import { spawnSync } from "node:child_process";
|
|
348
|
-
import { mkdirSync as
|
|
352
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, rmSync as rmSync3, existsSync as existsSync4, chmodSync as chmodSync2 } from "node:fs";
|
|
349
353
|
import { userInfo } from "node:os";
|
|
350
354
|
function defaultDeps() {
|
|
351
355
|
return {
|
|
@@ -357,8 +361,8 @@ function defaultDeps() {
|
|
|
357
361
|
return { code: r7.status ?? 1, stdout: r7.stdout ?? "" };
|
|
358
362
|
},
|
|
359
363
|
writeFile: (p2, d10) => writeFileSync2(p2, d10),
|
|
360
|
-
mkdir: (p2) =>
|
|
361
|
-
chmod: (p2, m6) =>
|
|
364
|
+
mkdir: (p2) => mkdirSync4(p2, { recursive: true }),
|
|
365
|
+
chmod: (p2, m6) => chmodSync2(p2, m6),
|
|
362
366
|
rm: (p2) => rmSync3(p2, { force: true }),
|
|
363
367
|
exists: (p2) => existsSync4(p2)
|
|
364
368
|
};
|
|
@@ -589,7 +593,7 @@ var init_libsodium_sumo = __esm2({
|
|
|
589
593
|
}
|
|
590
594
|
}
|
|
591
595
|
_Module = Module;
|
|
592
|
-
Module.ready = new Promise(function(
|
|
596
|
+
Module.ready = new Promise(function(resolve32, reject) {
|
|
593
597
|
var Module2 = _Module;
|
|
594
598
|
Module2.onAbort = reject;
|
|
595
599
|
Module2.print = function(what) {
|
|
@@ -601,7 +605,7 @@ var init_libsodium_sumo = __esm2({
|
|
|
601
605
|
Module2.onRuntimeInitialized = function() {
|
|
602
606
|
try {
|
|
603
607
|
Module2._crypto_secretbox_keybytes();
|
|
604
|
-
|
|
608
|
+
resolve32();
|
|
605
609
|
} catch (err2) {
|
|
606
610
|
reject(err2);
|
|
607
611
|
}
|
|
@@ -54446,8 +54450,8 @@ var init_mutex = __esm2({
|
|
|
54446
54450
|
}
|
|
54447
54451
|
async lock() {
|
|
54448
54452
|
let releaseLock;
|
|
54449
|
-
const nextLock = new Promise((
|
|
54450
|
-
releaseLock =
|
|
54453
|
+
const nextLock = new Promise((resolve32) => {
|
|
54454
|
+
releaseLock = resolve32;
|
|
54451
54455
|
});
|
|
54452
54456
|
const previousLock = __classPrivateFieldGet(this, _Mutex_locked, "f");
|
|
54453
54457
|
__classPrivateFieldSet(this, _Mutex_locked, nextLock, "f");
|
|
@@ -65246,6 +65250,58 @@ var init_channel = __esm2({
|
|
|
65246
65250
|
}
|
|
65247
65251
|
return void 0;
|
|
65248
65252
|
}
|
|
65253
|
+
/**
|
|
65254
|
+
* True when a conversation is NOT a genuine 1:1 owner DM — i.e. it belongs to a
|
|
65255
|
+
* room OR an A2A channel — and therefore must NEVER surface to the native worker
|
|
65256
|
+
* tool-gate as a no-roomId `message` (the gate treats that as an authenticated
|
|
65257
|
+
* owner DM, tool-enabled, bypassing arming). This is the predicate every fallback
|
|
65258
|
+
* emit guard uses (MLS 1:1, DR-delivery queue, HTTP poll).
|
|
65259
|
+
*
|
|
65260
|
+
* Resolves from LOCAL persisted state — BOTH `_persisted.rooms[*].conversationIds`
|
|
65261
|
+
* and `_persisted.a2aChannels[*].conversationId` — and accepts EITHER the
|
|
65262
|
+
* conversation id or the conversation-group id, so a convGroupId-only room/A2A
|
|
65263
|
+
* payload is still caught while a genuine convGroupId-only 1:1 shared-group DM
|
|
65264
|
+
* (present in neither map) still returns false and emits.
|
|
65265
|
+
*
|
|
65266
|
+
* Some fallback guards additionally honor an AUTHORITATIVE server routing field as
|
|
65267
|
+
* a backstop for the cold-local-state window — but WHICH field each carries differs
|
|
65268
|
+
* per endpoint, so do NOT assume a blanket "backend always populates room_id/
|
|
65269
|
+
* a2a_channel_id" at a new call site:
|
|
65270
|
+
* - HTTP poll (`/devices/{id}/messages`): server `room_id` populated for room-backed
|
|
65271
|
+
* convs (conv_room_map). No `a2a_channel_id` on this endpoint → A2A caught locally.
|
|
65272
|
+
* - DR-delivery (`/dr/delivery` -> dr_delivery_service.pull_pending): server `room_id`
|
|
65273
|
+
* populated (from the conversation); NO `a2a_channel_id` (DrDeliveryQueue is
|
|
65274
|
+
* 1:1-only, never A2A). room_id is null in practice today but real for any future
|
|
65275
|
+
* room-over-DR row.
|
|
65276
|
+
* - MLS 1:1 (`_handleMessageMLS`): room/A2A are routed away UPSTREAM at the
|
|
65277
|
+
* MLS-delivery dispatcher (authoritative `room_id`/`a2a_channel_id`) before this
|
|
65278
|
+
* handler; the fields are absent on `data` here, so this predicate is the belt.
|
|
65279
|
+
* The MLS-delivery *queue* endpoint (mls_delivery_service.pull_pending) is the one that
|
|
65280
|
+
* returns all of room_id/a2a_channel_id/conversation_group_id — but that feeds the
|
|
65281
|
+
* dispatcher, not these emit guards directly.
|
|
65282
|
+
*
|
|
65283
|
+
* No fail-closed drop-unknown guard is used: that would risk dropping a legitimate
|
|
65284
|
+
* 1:1 owner DM that arrives before room/A2A hydration (a North-Star violation). The
|
|
65285
|
+
* reachable residual (truly-cold local state AND the server omitting its routing field
|
|
65286
|
+
* on a room/A2A row) is empty today because the only paths that carry room/A2A rows
|
|
65287
|
+
* (poll + MLS dispatcher) always populate the field; DR-delivery and sync are
|
|
65288
|
+
* structurally 1:1-only.
|
|
65289
|
+
*/
|
|
65290
|
+
_isNonDmConversation(convId, convGroupId) {
|
|
65291
|
+
const ids = [convId, convGroupId].filter((x22) => !!x22);
|
|
65292
|
+
if (ids.length === 0) return false;
|
|
65293
|
+
if (this._persisted?.rooms) {
|
|
65294
|
+
for (const room of Object.values(this._persisted.rooms)) {
|
|
65295
|
+
if (room.conversationIds?.some((c22) => ids.includes(c22))) return true;
|
|
65296
|
+
}
|
|
65297
|
+
}
|
|
65298
|
+
if (this._persisted?.a2aChannels) {
|
|
65299
|
+
for (const ch2 of Object.values(this._persisted.a2aChannels)) {
|
|
65300
|
+
if (ch2.conversationId && ids.includes(ch2.conversationId)) return true;
|
|
65301
|
+
}
|
|
65302
|
+
}
|
|
65303
|
+
return false;
|
|
65304
|
+
}
|
|
65249
65305
|
/**
|
|
65250
65306
|
* Get recent message history for a specific room, for LLM context injection.
|
|
65251
65307
|
* Returns the last N messages tagged with `room:{roomId}`.
|
|
@@ -65499,7 +65555,7 @@ var init_channel = __esm2({
|
|
|
65499
65555
|
*/
|
|
65500
65556
|
sendActivitySpan(spanData) {
|
|
65501
65557
|
if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
|
|
65502
|
-
const pluginVersion = true ? "0.23.
|
|
65558
|
+
const pluginVersion = true ? "0.23.7" : "0.0.0-dev";
|
|
65503
65559
|
const agentName = this.config.agentName ?? "Agent";
|
|
65504
65560
|
const resource = {
|
|
65505
65561
|
"service.name": "agentvault-agent",
|
|
@@ -65575,7 +65631,7 @@ var init_channel = __esm2({
|
|
|
65575
65631
|
* Optional timeout rejects with an Error.
|
|
65576
65632
|
*/
|
|
65577
65633
|
waitForDecision(decisionId, timeoutMs) {
|
|
65578
|
-
return new Promise((
|
|
65634
|
+
return new Promise((resolve32, reject) => {
|
|
65579
65635
|
let timer = null;
|
|
65580
65636
|
const handler = (plaintext, metadata) => {
|
|
65581
65637
|
if (metadata.messageType !== "decision_response") return;
|
|
@@ -65584,7 +65640,7 @@ var init_channel = __esm2({
|
|
|
65584
65640
|
if (parsed.decision?.decision_id === decisionId) {
|
|
65585
65641
|
if (timer) clearTimeout(timer);
|
|
65586
65642
|
this.removeListener("message", handler);
|
|
65587
|
-
|
|
65643
|
+
resolve32({
|
|
65588
65644
|
decision_id: parsed.decision.decision_id,
|
|
65589
65645
|
selected_option_id: parsed.decision.selected_option_id,
|
|
65590
65646
|
resolved_at: parsed.decision.resolved_at,
|
|
@@ -67116,7 +67172,7 @@ var init_channel = __esm2({
|
|
|
67116
67172
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67117
67173
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67118
67174
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67119
|
-
pluginVersion: true ? "0.23.
|
|
67175
|
+
pluginVersion: true ? "0.23.7" : "0.0.0-dev"
|
|
67120
67176
|
});
|
|
67121
67177
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67122
67178
|
}
|
|
@@ -67366,7 +67422,10 @@ var init_channel = __esm2({
|
|
|
67366
67422
|
this.emit("disarm", { roomId: data.data?.room_id });
|
|
67367
67423
|
}
|
|
67368
67424
|
if (data.event === "arming_snapshot") {
|
|
67369
|
-
this.emit("arming_snapshot", {
|
|
67425
|
+
this.emit("arming_snapshot", {
|
|
67426
|
+
workAllowed: data.data?.work_allowed === true,
|
|
67427
|
+
roomIds: data.data?.room_ids ?? []
|
|
67428
|
+
});
|
|
67370
67429
|
}
|
|
67371
67430
|
if (data.event === "policy_blocked") {
|
|
67372
67431
|
this.emit("policy_blocked", data.data);
|
|
@@ -67426,7 +67485,7 @@ var init_channel = __esm2({
|
|
|
67426
67485
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67427
67486
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67428
67487
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67429
|
-
pluginVersion: true ? "0.23.
|
|
67488
|
+
pluginVersion: true ? "0.23.7" : "0.0.0-dev"
|
|
67430
67489
|
});
|
|
67431
67490
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67432
67491
|
}
|
|
@@ -67886,6 +67945,10 @@ var init_channel = __esm2({
|
|
|
67886
67945
|
return;
|
|
67887
67946
|
}
|
|
67888
67947
|
if (messageType === "history_catchup_response") return;
|
|
67948
|
+
if (data.room_id || data.a2a_channel_id || this._isNonDmConversation(convId, convGroupId)) {
|
|
67949
|
+
console.warn(`[SecureChannel] Dropped 1:1 MLS emit for non-DM conv ${(convId ?? convGroupId ?? "?").slice(0, 8)} (room/A2A guard)`);
|
|
67950
|
+
return;
|
|
67951
|
+
}
|
|
67889
67952
|
if (convId) {
|
|
67890
67953
|
const session = this._sessions.get(convId);
|
|
67891
67954
|
if (session && !session.activated) {
|
|
@@ -68732,7 +68795,12 @@ ${messageText}`;
|
|
|
68732
68795
|
senderName: senderLabel,
|
|
68733
68796
|
plaintext: messageText,
|
|
68734
68797
|
messageType,
|
|
68735
|
-
timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
68798
|
+
timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
68799
|
+
// Native bridge (claude-room-bridge) consumes this to decide reply
|
|
68800
|
+
// expectation: a human/owner speaking in a room always expects a reply,
|
|
68801
|
+
// an agent does not. Derived from the room roster (line ~5394) — the same
|
|
68802
|
+
// signal the OpenClaw mention-filter uses for loop prevention.
|
|
68803
|
+
senderIsAgent
|
|
68736
68804
|
});
|
|
68737
68805
|
const contextualMessage = senderIsAgent ? messageText : `[${senderLabel}]: ${messageText}`;
|
|
68738
68806
|
Promise.resolve(this.config.onMessage?.(contextualMessage, metadata)).catch((err) => {
|
|
@@ -69856,16 +69924,7 @@ ${messageText}`;
|
|
|
69856
69924
|
ackedIds.push(msg.queue_id);
|
|
69857
69925
|
continue;
|
|
69858
69926
|
}
|
|
69859
|
-
|
|
69860
|
-
if (this._persisted?.rooms) {
|
|
69861
|
-
for (const room of Object.values(this._persisted.rooms)) {
|
|
69862
|
-
if (room.conversationIds?.includes(msg.conversation_id)) {
|
|
69863
|
-
isRoom = true;
|
|
69864
|
-
break;
|
|
69865
|
-
}
|
|
69866
|
-
}
|
|
69867
|
-
}
|
|
69868
|
-
if (isRoom) {
|
|
69927
|
+
if (msg.room_id || msg.a2a_channel_id || this._isNonDmConversation(msg.conversation_id, msg.conversation_group_id)) {
|
|
69869
69928
|
ackedIds.push(msg.queue_id);
|
|
69870
69929
|
continue;
|
|
69871
69930
|
}
|
|
@@ -70508,7 +70567,7 @@ ${messageText}`;
|
|
|
70508
70567
|
if (msg.sender_device_id === this._deviceId) continue;
|
|
70509
70568
|
const session = this._sessions.get(msg.conversation_id);
|
|
70510
70569
|
if (!session) continue;
|
|
70511
|
-
if (this.
|
|
70570
|
+
if (msg.room_id || this._isNonDmConversation(msg.conversation_id)) {
|
|
70512
70571
|
this._persisted.lastMessageTimestamp = msg.created_at;
|
|
70513
70572
|
continue;
|
|
70514
70573
|
}
|
|
@@ -86826,7 +86885,7 @@ var init_protocol = __esm2({
|
|
|
86826
86885
|
return;
|
|
86827
86886
|
}
|
|
86828
86887
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
86829
|
-
await new Promise((
|
|
86888
|
+
await new Promise((resolve32) => setTimeout(resolve32, pollInterval));
|
|
86830
86889
|
options?.signal?.throwIfAborted();
|
|
86831
86890
|
}
|
|
86832
86891
|
} catch (error210) {
|
|
@@ -86843,7 +86902,7 @@ var init_protocol = __esm2({
|
|
|
86843
86902
|
*/
|
|
86844
86903
|
request(request, resultSchema, options) {
|
|
86845
86904
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
86846
|
-
return new Promise((
|
|
86905
|
+
return new Promise((resolve32, reject) => {
|
|
86847
86906
|
const earlyReject = (error210) => {
|
|
86848
86907
|
reject(error210);
|
|
86849
86908
|
};
|
|
@@ -86921,7 +86980,7 @@ var init_protocol = __esm2({
|
|
|
86921
86980
|
if (!parseResult.success) {
|
|
86922
86981
|
reject(parseResult.error);
|
|
86923
86982
|
} else {
|
|
86924
|
-
|
|
86983
|
+
resolve32(parseResult.data);
|
|
86925
86984
|
}
|
|
86926
86985
|
} catch (error210) {
|
|
86927
86986
|
reject(error210);
|
|
@@ -87182,12 +87241,12 @@ var init_protocol = __esm2({
|
|
|
87182
87241
|
}
|
|
87183
87242
|
} catch {
|
|
87184
87243
|
}
|
|
87185
|
-
return new Promise((
|
|
87244
|
+
return new Promise((resolve32, reject) => {
|
|
87186
87245
|
if (signal.aborted) {
|
|
87187
87246
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
87188
87247
|
return;
|
|
87189
87248
|
}
|
|
87190
|
-
const timeoutId = setTimeout(
|
|
87249
|
+
const timeoutId = setTimeout(resolve32, interval);
|
|
87191
87250
|
signal.addEventListener("abort", () => {
|
|
87192
87251
|
clearTimeout(timeoutId);
|
|
87193
87252
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -90172,7 +90231,7 @@ var require_compile = __commonJS({
|
|
|
90172
90231
|
const schOrFunc = root3.refs[ref];
|
|
90173
90232
|
if (schOrFunc)
|
|
90174
90233
|
return schOrFunc;
|
|
90175
|
-
let _sch =
|
|
90234
|
+
let _sch = resolve32.call(this, root3, ref);
|
|
90176
90235
|
if (_sch === void 0) {
|
|
90177
90236
|
const schema = (_a32 = root3.localRefs) === null || _a32 === void 0 ? void 0 : _a32[ref];
|
|
90178
90237
|
const { schemaId } = this.opts;
|
|
@@ -90199,7 +90258,7 @@ var require_compile = __commonJS({
|
|
|
90199
90258
|
function sameSchemaEnv(s1, s22) {
|
|
90200
90259
|
return s1.schema === s22.schema && s1.root === s22.root && s1.baseId === s22.baseId;
|
|
90201
90260
|
}
|
|
90202
|
-
function
|
|
90261
|
+
function resolve32(root3, ref) {
|
|
90203
90262
|
let sch;
|
|
90204
90263
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
90205
90264
|
ref = sch;
|
|
@@ -90766,7 +90825,7 @@ var require_fast_uri = __commonJS({
|
|
|
90766
90825
|
}
|
|
90767
90826
|
return uri;
|
|
90768
90827
|
}
|
|
90769
|
-
function
|
|
90828
|
+
function resolve32(baseURI, relativeURI, options) {
|
|
90770
90829
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
90771
90830
|
const resolved = resolveComponent(parse32(baseURI, schemelessOptions), parse32(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
90772
90831
|
schemelessOptions.skipEscape = true;
|
|
@@ -90993,7 +91052,7 @@ var require_fast_uri = __commonJS({
|
|
|
90993
91052
|
var fastUri = {
|
|
90994
91053
|
SCHEMES,
|
|
90995
91054
|
normalize,
|
|
90996
|
-
resolve:
|
|
91055
|
+
resolve: resolve32,
|
|
90997
91056
|
resolveComponent,
|
|
90998
91057
|
equal,
|
|
90999
91058
|
serialize,
|
|
@@ -95013,7 +95072,7 @@ var init_mcp = __esm2({
|
|
|
95013
95072
|
let task = createTaskResult.task;
|
|
95014
95073
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
95015
95074
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
95016
|
-
await new Promise((
|
|
95075
|
+
await new Promise((resolve32) => setTimeout(resolve32, pollInterval));
|
|
95017
95076
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
95018
95077
|
if (!updatedTask) {
|
|
95019
95078
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -95954,7 +96013,7 @@ var init_dist2 = __esm2({
|
|
|
95954
96013
|
});
|
|
95955
96014
|
if (!chunk) {
|
|
95956
96015
|
if (i2 === 1) {
|
|
95957
|
-
await new Promise((
|
|
96016
|
+
await new Promise((resolve32) => setTimeout(resolve32));
|
|
95958
96017
|
maxReadCount = 3;
|
|
95959
96018
|
continue;
|
|
95960
96019
|
}
|
|
@@ -96454,9 +96513,9 @@ data:
|
|
|
96454
96513
|
const initRequest = messages.find((m22) => isInitializeRequest(m22));
|
|
96455
96514
|
const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
|
|
96456
96515
|
if (this._enableJsonResponse) {
|
|
96457
|
-
return new Promise((
|
|
96516
|
+
return new Promise((resolve32) => {
|
|
96458
96517
|
this._streamMapping.set(streamId, {
|
|
96459
|
-
resolveJson:
|
|
96518
|
+
resolveJson: resolve32,
|
|
96460
96519
|
cleanup: () => {
|
|
96461
96520
|
this._streamMapping.delete(streamId);
|
|
96462
96521
|
}
|
|
@@ -97173,7 +97232,7 @@ var init_index = __esm2({
|
|
|
97173
97232
|
init_skill_invoker();
|
|
97174
97233
|
await init_skill_telemetry();
|
|
97175
97234
|
await init_policy_enforcer();
|
|
97176
|
-
VERSION = true ? "0.23.
|
|
97235
|
+
VERSION = true ? "0.23.7" : "0.0.0-dev";
|
|
97177
97236
|
}
|
|
97178
97237
|
});
|
|
97179
97238
|
await init_index();
|
|
@@ -118477,21 +118536,21 @@ function Z_($10, Q4) {
|
|
|
118477
118536
|
|
|
118478
118537
|
// src/worker-permission.ts
|
|
118479
118538
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
118480
|
-
import { resolve as
|
|
118539
|
+
import { resolve as resolve3, dirname as dirname2, basename, sep as sep2, isAbsolute } from "node:path";
|
|
118481
118540
|
var PATH_FIELDS = ["file_path", "path", "notebook_path"];
|
|
118482
118541
|
function canonical(p2) {
|
|
118483
|
-
const abs =
|
|
118542
|
+
const abs = resolve3(p2);
|
|
118484
118543
|
try {
|
|
118485
118544
|
return realpathSync2(abs);
|
|
118486
118545
|
} catch {
|
|
118487
118546
|
const suffix = [];
|
|
118488
118547
|
let dir = abs;
|
|
118489
118548
|
for (; ; ) {
|
|
118490
|
-
const parent2 =
|
|
118549
|
+
const parent2 = dirname2(dir);
|
|
118491
118550
|
suffix.unshift(basename(dir));
|
|
118492
118551
|
if (parent2 === dir) return abs;
|
|
118493
118552
|
try {
|
|
118494
|
-
return
|
|
118553
|
+
return resolve3(realpathSync2(parent2), ...suffix);
|
|
118495
118554
|
} catch {
|
|
118496
118555
|
dir = parent2;
|
|
118497
118556
|
}
|
|
@@ -118508,7 +118567,7 @@ function pathsOf(input) {
|
|
|
118508
118567
|
return out;
|
|
118509
118568
|
}
|
|
118510
118569
|
function within(canonTarget, canonRoot) {
|
|
118511
|
-
return canonTarget === canonRoot || canonTarget.startsWith(canonRoot +
|
|
118570
|
+
return canonTarget === canonRoot || canonTarget.startsWith(canonRoot + sep2);
|
|
118512
118571
|
}
|
|
118513
118572
|
function globPatternEscapes(pattern) {
|
|
118514
118573
|
if (typeof pattern !== "string" || pattern.length === 0) return true;
|
|
@@ -132433,10 +132492,21 @@ var PersistentClaudeSession = class {
|
|
|
132433
132492
|
* disarm denies the very next tool call. wireBridge binds it to the ArmingState
|
|
132434
132493
|
* for this turn's room; an unarmed room turn's getter returns false. */
|
|
132435
132494
|
currentArmedGetter = () => false;
|
|
132495
|
+
/** Per-turn: does this turn's sender expect a reply? DECOUPLED from
|
|
132496
|
+
* `currentAutoReply` on purpose — it drives ONLY the #416 plain-text fallback
|
|
132497
|
+
* (deliver assistant text when the model never calls say), NOT the tool gate.
|
|
132498
|
+
* True for owner 1:1 DMs (defaults from autoReplyOnText) AND for a HUMAN/owner
|
|
132499
|
+
* speaking in a room (wireBridge sets replyExpected from !senderIsAgent). An
|
|
132500
|
+
* agent-authored room turn keeps this false → the agent may still stay silent,
|
|
132501
|
+
* which also prevents agent↔agent reply loops. Tool access is unaffected: it
|
|
132502
|
+
* stays gated on currentAutoReply (owner DM) OR currentArmedGetter (armed room). */
|
|
132503
|
+
currentReplyExpected = false;
|
|
132436
132504
|
saidThisTurn = false;
|
|
132437
132505
|
turnText = "";
|
|
132438
132506
|
/** In-process room MCP server — hoisted so buildSdkOptions can reference it. */
|
|
132439
132507
|
roomServer;
|
|
132508
|
+
/** AbortController for the in-flight query(); abort() triggers it (queue timeout). */
|
|
132509
|
+
_abort;
|
|
132440
132510
|
/**
|
|
132441
132511
|
* Queue an inbound message for the model.
|
|
132442
132512
|
* @param opts.autoReplyOnText — for 1:1 DMs (where the owner always expects a
|
|
@@ -132456,7 +132526,13 @@ var PersistentClaudeSession = class {
|
|
|
132456
132526
|
parent_tool_use_id: null,
|
|
132457
132527
|
session_id: ""
|
|
132458
132528
|
};
|
|
132459
|
-
const item = {
|
|
132529
|
+
const item = {
|
|
132530
|
+
msg,
|
|
132531
|
+
reply,
|
|
132532
|
+
autoReplyOnText: opts?.autoReplyOnText,
|
|
132533
|
+
replyExpected: opts?.replyExpected,
|
|
132534
|
+
armed: opts?.armed
|
|
132535
|
+
};
|
|
132460
132536
|
if (this.waiting) {
|
|
132461
132537
|
const w2 = this.waiting;
|
|
132462
132538
|
this.waiting = null;
|
|
@@ -132473,13 +132549,25 @@ var PersistentClaudeSession = class {
|
|
|
132473
132549
|
if (this.activeReply) await this.activeReply(text);
|
|
132474
132550
|
else if (this.opts.onSay) await this.opts.onSay(text);
|
|
132475
132551
|
}
|
|
132552
|
+
/** Abort the in-flight query() — used by the worker queue on a per-task timeout. */
|
|
132553
|
+
abort() {
|
|
132554
|
+
this._abort?.abort();
|
|
132555
|
+
}
|
|
132476
132556
|
async *input() {
|
|
132477
132557
|
while (true) {
|
|
132478
|
-
|
|
132479
|
-
|
|
132480
|
-
|
|
132558
|
+
let item;
|
|
132559
|
+
if (this.pending.length > 0) {
|
|
132560
|
+
item = this.pending.shift();
|
|
132561
|
+
} else if (this.opts.ephemeral) {
|
|
132562
|
+
return;
|
|
132563
|
+
} else {
|
|
132564
|
+
item = await new Promise((resolve4) => {
|
|
132565
|
+
this.waiting = resolve4;
|
|
132566
|
+
});
|
|
132567
|
+
}
|
|
132481
132568
|
this.activeReply = item.reply;
|
|
132482
132569
|
this.currentAutoReply = item.autoReplyOnText ?? false;
|
|
132570
|
+
this.currentReplyExpected = item.replyExpected ?? item.autoReplyOnText ?? false;
|
|
132483
132571
|
this.currentArmedGetter = item.armed ?? (() => false);
|
|
132484
132572
|
this.saidThisTurn = false;
|
|
132485
132573
|
this.turnText = "";
|
|
@@ -132517,7 +132605,13 @@ var PersistentClaudeSession = class {
|
|
|
132517
132605
|
// D5: currentArmedGetter() is called HERE on every tool decision, so a
|
|
132518
132606
|
// mid-turn disarm denies the next call. See worker-permission.test.ts +
|
|
132519
132607
|
// session.test.ts for the covering assertions.
|
|
132520
|
-
|
|
132608
|
+
//
|
|
132609
|
+
// Task 4 (C-1/H-3) HARD BACKSTOP: device-level `workAllowed()` is a top-level
|
|
132610
|
+
// AND over BOTH signals — NO tool runs (owner DM OR armed room) when Work is
|
|
132611
|
+
// off, even if the router ever mis-routed. `?? false` is the fail-closed
|
|
132612
|
+
// default (no getter ⇒ no tools). Read LIVE per tool decision so a Work-off
|
|
132613
|
+
// flip / fail-closed reconnect window denies the very next call.
|
|
132614
|
+
isToolTurn: () => (this.opts.workAllowed?.() ?? false) && (this.currentAutoReply || this.currentArmedGetter()),
|
|
132521
132615
|
// Slice 2 Plan B: emit an audit self-report for each tool decision on an
|
|
132522
132616
|
// armed-room turn (both allow and deny). room_say is excluded by the hook.
|
|
132523
132617
|
isArmedTurn: () => this.currentArmedGetter(),
|
|
@@ -132538,8 +132632,28 @@ var PersistentClaudeSession = class {
|
|
|
132538
132632
|
return {
|
|
132539
132633
|
...base,
|
|
132540
132634
|
permissionMode: this.opts.permissionMode ?? "auto",
|
|
132541
|
-
|
|
132635
|
+
// FACET B (disk isolation). The workspace is writable under the Facet-A
|
|
132636
|
+
// allowlist, so a prior worker/armed turn could plant config files in it that
|
|
132637
|
+
// a later worker would otherwise ingest. We close each disk surface explicitly
|
|
132638
|
+
// (they are governed SEPARATELY by the SDK, so one flag is not enough):
|
|
132639
|
+
// - settingSources:[] — "disable filesystem settings (SDK isolation mode)":
|
|
132640
|
+
// no ~/.claude or project settings.json (⇒ no settings-defined hooks, the
|
|
132641
|
+
// RCE-grade vector: a hook is a shell command run OUTSIDE the Bash gate),
|
|
132642
|
+
// no CLAUDE.md. It also means no .mcp.json APPROVAL state is loaded — and
|
|
132643
|
+
// enableAllProjectMcpServers/enabledMcpjsonServers live only in Settings —
|
|
132644
|
+
// so an unapproved project .mcp.json server is never spawned in headless
|
|
132645
|
+
// mode. Any MCP tool that WERE discovered is still denied by the Facet-A
|
|
132646
|
+
// allowlist (mcp__* is not room_say/a file tool/Bash → final deny).
|
|
132647
|
+
// - skills:[] — settingSources does NOT gate skills discovery; [] enables
|
|
132648
|
+
// zero skills, so a planted <ws>/.claude/skills/*/SKILL.md description
|
|
132649
|
+
// cannot be injected into the worker's context.
|
|
132650
|
+
// Project context, if needed, is injected via the bridge-controlled systemPrompt,
|
|
132651
|
+
// never auto-loaded from the mutable workspace.
|
|
132652
|
+
settingSources: [],
|
|
132653
|
+
skills: [],
|
|
132542
132654
|
cwd: this.opts.workspaceDir,
|
|
132655
|
+
...this.opts.maxTurns != null ? { maxTurns: this.opts.maxTurns } : {},
|
|
132656
|
+
abortController: this._abort,
|
|
132543
132657
|
// PRIMARY gate: a PreToolUse hook fires on EVERY tool call regardless of
|
|
132544
132658
|
// permissionMode. canUseTool alone is bypassed in "auto" mode (the SDK's
|
|
132545
132659
|
// classifier auto-approves without hitting the "ask" path) — verified live.
|
|
@@ -132549,6 +132663,7 @@ var PersistentClaudeSession = class {
|
|
|
132549
132663
|
};
|
|
132550
132664
|
}
|
|
132551
132665
|
async start() {
|
|
132666
|
+
this._abort = new AbortController();
|
|
132552
132667
|
this.roomServer = _s({
|
|
132553
132668
|
name: "room",
|
|
132554
132669
|
version: "0.2.0",
|
|
@@ -132572,7 +132687,7 @@ var PersistentClaudeSession = class {
|
|
|
132572
132687
|
}
|
|
132573
132688
|
} else if (m6.type === "result") {
|
|
132574
132689
|
const reply = this.activeReply;
|
|
132575
|
-
if (this.
|
|
132690
|
+
if (this.currentReplyExpected && !this.saidThisTurn && this.turnText.trim() && reply) {
|
|
132576
132691
|
void reply(this.turnText);
|
|
132577
132692
|
}
|
|
132578
132693
|
}
|
|
@@ -132580,6 +132695,101 @@ var PersistentClaudeSession = class {
|
|
|
132580
132695
|
}
|
|
132581
132696
|
};
|
|
132582
132697
|
|
|
132698
|
+
// src/worker-queue.ts
|
|
132699
|
+
var GENERIC_ERROR = "Sorry \u2014 I couldn't complete that request.";
|
|
132700
|
+
var WorkerQueue = class {
|
|
132701
|
+
constructor(deps) {
|
|
132702
|
+
this.deps = deps;
|
|
132703
|
+
}
|
|
132704
|
+
deps;
|
|
132705
|
+
q = [];
|
|
132706
|
+
loop = Promise.resolve();
|
|
132707
|
+
running = false;
|
|
132708
|
+
enqueue(task) {
|
|
132709
|
+
this.q.push(task);
|
|
132710
|
+
if (!this.running) {
|
|
132711
|
+
this.running = true;
|
|
132712
|
+
this.loop = this.drain();
|
|
132713
|
+
}
|
|
132714
|
+
}
|
|
132715
|
+
/** Resolves when the queue has processed everything enqueued so far. */
|
|
132716
|
+
whenDrained() {
|
|
132717
|
+
return this.loop;
|
|
132718
|
+
}
|
|
132719
|
+
async drain() {
|
|
132720
|
+
try {
|
|
132721
|
+
while (this.q.length > 0) {
|
|
132722
|
+
const task = this.q.shift();
|
|
132723
|
+
await this.runOne(task);
|
|
132724
|
+
}
|
|
132725
|
+
} finally {
|
|
132726
|
+
this.running = false;
|
|
132727
|
+
}
|
|
132728
|
+
}
|
|
132729
|
+
async runOne(task) {
|
|
132730
|
+
let session;
|
|
132731
|
+
let timer;
|
|
132732
|
+
try {
|
|
132733
|
+
session = this.deps.makeSession(task);
|
|
132734
|
+
session.push(task.instruction, task.reply, {
|
|
132735
|
+
autoReplyOnText: task.autoReplyOnText,
|
|
132736
|
+
replyExpected: task.replyExpected,
|
|
132737
|
+
armed: task.armed
|
|
132738
|
+
});
|
|
132739
|
+
const currentSession = session;
|
|
132740
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
132741
|
+
timer = setTimeout(() => {
|
|
132742
|
+
currentSession.abort();
|
|
132743
|
+
reject(new Error("worker task timeout"));
|
|
132744
|
+
}, this.deps.timeoutMs);
|
|
132745
|
+
});
|
|
132746
|
+
await Promise.race([session.start(), timeout]);
|
|
132747
|
+
} catch (e7) {
|
|
132748
|
+
this.deps.log(`[worker-queue] task failed: ${e7.message}`);
|
|
132749
|
+
session?.abort();
|
|
132750
|
+
try {
|
|
132751
|
+
await task.reply(GENERIC_ERROR);
|
|
132752
|
+
} catch (replyErr) {
|
|
132753
|
+
this.deps.log(`[worker-queue] failed to deliver error reply: ${replyErr.message}`);
|
|
132754
|
+
}
|
|
132755
|
+
} finally {
|
|
132756
|
+
if (timer) clearTimeout(timer);
|
|
132757
|
+
}
|
|
132758
|
+
}
|
|
132759
|
+
};
|
|
132760
|
+
|
|
132761
|
+
// src/router.ts
|
|
132762
|
+
function makeRouter(deps) {
|
|
132763
|
+
return {
|
|
132764
|
+
push(text, reply, opts) {
|
|
132765
|
+
const replySink = reply ?? (() => {
|
|
132766
|
+
});
|
|
132767
|
+
const isOwnerDm = opts?.autoReplyOnText === true && deps.workAllowed();
|
|
132768
|
+
const isArmedRoom = opts?.armed?.() === true && deps.workAllowed();
|
|
132769
|
+
if (isOwnerDm) {
|
|
132770
|
+
deps.queue.enqueue({
|
|
132771
|
+
instruction: text,
|
|
132772
|
+
reply: replySink,
|
|
132773
|
+
autoReplyOnText: true,
|
|
132774
|
+
replyExpected: opts?.replyExpected
|
|
132775
|
+
});
|
|
132776
|
+
return;
|
|
132777
|
+
}
|
|
132778
|
+
if (isArmedRoom) {
|
|
132779
|
+
deps.queue.enqueue({
|
|
132780
|
+
instruction: text,
|
|
132781
|
+
reply: replySink,
|
|
132782
|
+
autoReplyOnText: false,
|
|
132783
|
+
replyExpected: opts?.replyExpected,
|
|
132784
|
+
armed: opts.armed
|
|
132785
|
+
});
|
|
132786
|
+
return;
|
|
132787
|
+
}
|
|
132788
|
+
deps.listener.push(text, reply, opts);
|
|
132789
|
+
}
|
|
132790
|
+
};
|
|
132791
|
+
}
|
|
132792
|
+
|
|
132583
132793
|
// src/arming.ts
|
|
132584
132794
|
var ArmingState = class {
|
|
132585
132795
|
armed = /* @__PURE__ */ new Set();
|
|
@@ -132588,26 +132798,58 @@ var ArmingState = class {
|
|
|
132588
132798
|
consumed = /* @__PURE__ */ new Set();
|
|
132589
132799
|
// request-ids already approved (one-shot)
|
|
132590
132800
|
/**
|
|
132591
|
-
* Replace the armed set. Use
|
|
132592
|
-
*
|
|
132801
|
+
* Replace the armed set. Use for the host-local launch seed (AV_ARM_ROOM) —
|
|
132802
|
+
* itself a host-local action and therefore allowed to arm — and as the
|
|
132803
|
+
* primitive `applyAuthoritative` below delegates to for non-shell workers.
|
|
132593
132804
|
*
|
|
132594
|
-
*
|
|
132595
|
-
*
|
|
132596
|
-
*
|
|
132597
|
-
*
|
|
132598
|
-
*
|
|
132805
|
+
* The B-pure posture (Task 4) treats the backend connect/live
|
|
132806
|
+
* `arming_snapshot` the same way — see `applyAuthoritative`'s docstring for
|
|
132807
|
+
* the current arm-from-snapshot rationale. Shell-capable (OS-isolated)
|
|
132808
|
+
* workers do NOT use this path; they stay on `reconcileDisarm` (disarm-only,
|
|
132809
|
+
* see below) until #20.
|
|
132599
132810
|
*/
|
|
132600
132811
|
applySnapshot(roomIds) {
|
|
132601
132812
|
this.armed = new Set(roomIds);
|
|
132602
132813
|
}
|
|
132603
132814
|
/**
|
|
132604
|
-
*
|
|
132605
|
-
*
|
|
132606
|
-
*
|
|
132607
|
-
*
|
|
132608
|
-
*
|
|
132609
|
-
*
|
|
132610
|
-
*
|
|
132815
|
+
* Set the armed set to EXACTLY `intendedArmed`: arms every room in the list
|
|
132816
|
+
* that isn't already armed, and disarms every currently-armed room that is
|
|
132817
|
+
* absent from it. This is the AUTHORITATIVE arm-from-snapshot path (Task 4,
|
|
132818
|
+
* reversing the prior disarm-only safeguard for non-shell workers).
|
|
132819
|
+
*
|
|
132820
|
+
* Why arming from the snapshot is now safe: `intendedArmed` is derived
|
|
132821
|
+
* server-side from `devices.work_allowed`, and that flag can only be flipped
|
|
132822
|
+
* by `PUT /devices/{id}/work_allowed`, which REJECTS any device-bound caller
|
|
132823
|
+
* outright (C1 guard — a human account owner only; an agent, including a
|
|
132824
|
+
* compromised one, cannot self-authorize). So "arm from snapshot" no longer
|
|
132825
|
+
* means "a web session can arm a worker" in the old adversarial sense — it
|
|
132826
|
+
* means "the verified owner's grant is applied end-to-end, live, without
|
|
132827
|
+
* requiring a separate host-local approval step for every reconnect." This is
|
|
132828
|
+
* the accepted B-pure posture; see #20 to harden it further with local
|
|
132829
|
+
* approval for non-shell workers too. Shell-capable (OS-isolated) workers are
|
|
132830
|
+
* carved out of this path entirely (C2) — see the shell-gate in bridge.ts —
|
|
132831
|
+
* because a shell worker armed by a web flag is a live RCE surface even under
|
|
132832
|
+
* an honest owner (a compromised owner web session, or a compromised backend,
|
|
132833
|
+
* would get arbitrary code execution). Those workers stay on `reconcileDisarm`
|
|
132834
|
+
* (disarm-only) until #20 delivers local approval for them as well.
|
|
132835
|
+
*/
|
|
132836
|
+
applyAuthoritative(intendedArmed) {
|
|
132837
|
+
this.applySnapshot(intendedArmed);
|
|
132838
|
+
}
|
|
132839
|
+
/**
|
|
132840
|
+
* Disarm-only reconciliation from the backend connect/live snapshot. Disarms
|
|
132841
|
+
* any currently-armed room whose intent is no longer armed (i.e. NOT in
|
|
132842
|
+
* `intendedArmed`) — so a disarm issued while the bridge was offline still
|
|
132843
|
+
* takes effect on reconnect — but NEVER arms a room. Used for OS-isolated
|
|
132844
|
+
* (shell-capable) workers ONLY (C2): a shell worker must never be armed by a
|
|
132845
|
+
* web-originated flag, because a compromised owner session or backend would
|
|
132846
|
+
* translate directly into host code execution. Those workers can be armed
|
|
132847
|
+
* only by the host-local launch seed (`AV_ARM_ROOM`, via `applySnapshot`)
|
|
132848
|
+
* until #20 delivers a local-approval path for them too. Non-shell workers
|
|
132849
|
+
* use `applyAuthoritative` instead (Task 4) — see its docstring. Returns the
|
|
132850
|
+
* rooms that were disarmed. In-memory arming survives a WS reconnect, so a
|
|
132851
|
+
* genuinely-armed room is unaffected here; only a full process restart
|
|
132852
|
+
* (which clears this state) requires re-arming.
|
|
132611
132853
|
*/
|
|
132612
132854
|
reconcileDisarm(intendedArmed) {
|
|
132613
132855
|
const keep = new Set(intendedArmed);
|
|
@@ -132661,7 +132903,7 @@ var ArmingState = class {
|
|
|
132661
132903
|
};
|
|
132662
132904
|
|
|
132663
132905
|
// src/approve-cli.ts
|
|
132664
|
-
import { mkdirSync as
|
|
132906
|
+
import { mkdirSync as mkdirSync3, writeFileSync, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
|
|
132665
132907
|
import { join as join6 } from "node:path";
|
|
132666
132908
|
var APPROVALS_SUBDIR = "arm-approvals";
|
|
132667
132909
|
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -132683,7 +132925,7 @@ function writeApproval(dataDir, requestId, roomId) {
|
|
|
132683
132925
|
const id = sanitizeRequestId(requestId);
|
|
132684
132926
|
const room = sanitizeRoomId(roomId);
|
|
132685
132927
|
const dir = join6(dataDir, APPROVALS_SUBDIR);
|
|
132686
|
-
|
|
132928
|
+
mkdirSync3(dir, { recursive: true });
|
|
132687
132929
|
writeFileSync(join6(dir, id), room);
|
|
132688
132930
|
}
|
|
132689
132931
|
function drainApprovals(dataDir) {
|
|
@@ -132823,6 +133065,12 @@ function pollApprovalsOnce(arming, dataDir, onArmed, log = () => {
|
|
|
132823
133065
|
function wireBridge(channel, session, target, opts = {}) {
|
|
132824
133066
|
const log = opts.log ?? (() => {
|
|
132825
133067
|
});
|
|
133068
|
+
let workAllowed = false;
|
|
133069
|
+
const workAllowedGetter = () => workAllowed;
|
|
133070
|
+
opts.onWorkAllowed?.(workAllowedGetter);
|
|
133071
|
+
channel.on("state", (s10) => {
|
|
133072
|
+
if (s10 !== "ready") workAllowed = false;
|
|
133073
|
+
});
|
|
132826
133074
|
const arming = new ArmingState();
|
|
132827
133075
|
if (opts.armRoom === true && opts.roomFilter) {
|
|
132828
133076
|
arming.applySnapshot([opts.roomFilter]);
|
|
@@ -132848,6 +133096,7 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
132848
133096
|
target.setRoom(e7.roomId);
|
|
132849
133097
|
session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log), {
|
|
132850
133098
|
autoReplyOnText: false,
|
|
133099
|
+
replyExpected: e7.senderIsAgent === false,
|
|
132851
133100
|
armed: () => arming.isArmed(e7.roomId)
|
|
132852
133101
|
});
|
|
132853
133102
|
});
|
|
@@ -132857,7 +133106,8 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
132857
133106
|
target.setDm();
|
|
132858
133107
|
session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: true });
|
|
132859
133108
|
});
|
|
132860
|
-
const workerCapable = !!
|
|
133109
|
+
const workerCapable = !!opts.workspaceDir;
|
|
133110
|
+
const osIsolated = opts.osIsolated !== void 0 ? opts.osIsolated : process.env.AV_WORKER_OS_ISOLATED === "1" || process.env.AV_WORKER_OS_ISOLATED === "true";
|
|
132861
133111
|
const heartbeat = () => {
|
|
132862
133112
|
try {
|
|
132863
133113
|
channel.sendWorkerHeartbeat?.({ workerCapable, armedRooms: arming.armedRooms() });
|
|
@@ -132867,21 +133117,33 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
132867
133117
|
};
|
|
132868
133118
|
heartbeat();
|
|
132869
133119
|
channel.on("ready", () => heartbeat());
|
|
132870
|
-
|
|
133120
|
+
const applyArmingSnapshot = (s10) => {
|
|
133121
|
+
workAllowed = s10?.workAllowed === true;
|
|
132871
133122
|
if (!workerCapable) {
|
|
132872
133123
|
log("arming_snapshot ignored \u2014 not worker-capable");
|
|
132873
133124
|
return;
|
|
132874
133125
|
}
|
|
133126
|
+
const roomIds = s10?.roomIds ?? [];
|
|
132875
133127
|
try {
|
|
132876
|
-
const
|
|
132877
|
-
if (
|
|
132878
|
-
|
|
132879
|
-
|
|
133128
|
+
const before = new Set(arming.armedRooms());
|
|
133129
|
+
if (osIsolated) {
|
|
133130
|
+
const dropped = arming.reconcileDisarm(roomIds);
|
|
133131
|
+
if (dropped.length > 0) {
|
|
133132
|
+
log(
|
|
133133
|
+
`arming_snapshot: OS-isolated worker \u2014 disarm-only until #20 (web cannot arm shell workers); disarmed: [${dropped.map((r7) => r7.slice(0, 8)).join(", ")}]`
|
|
133134
|
+
);
|
|
133135
|
+
}
|
|
133136
|
+
} else {
|
|
133137
|
+
arming.applyAuthoritative(roomIds);
|
|
132880
133138
|
}
|
|
133139
|
+
const after = arming.armedRooms();
|
|
133140
|
+
const changed = after.length !== before.size || after.some((r7) => !before.has(r7));
|
|
133141
|
+
if (changed) heartbeat();
|
|
132881
133142
|
} catch (err) {
|
|
132882
133143
|
log(`arming_snapshot handling failed (ignored): ${err instanceof Error ? err.message : String(err)}`);
|
|
132883
133144
|
}
|
|
132884
|
-
}
|
|
133145
|
+
};
|
|
133146
|
+
channel.on("arming_snapshot", applyArmingSnapshot);
|
|
132885
133147
|
channel.on("disarm", (d10) => {
|
|
132886
133148
|
if (!workerCapable) {
|
|
132887
133149
|
log("disarm ignored \u2014 not worker-capable");
|
|
@@ -132943,22 +133205,20 @@ async function main() {
|
|
|
132943
133205
|
"[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"
|
|
132944
133206
|
);
|
|
132945
133207
|
}
|
|
132946
|
-
console.error(`[bridge] version: ${true ? "0.5.
|
|
133208
|
+
console.error(`[bridge] version: ${true ? "0.5.10" : "dev"}`);
|
|
132947
133209
|
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
132948
|
-
|
|
132949
|
-
|
|
132950
|
-
|
|
132951
|
-
|
|
132952
|
-
|
|
132953
|
-
|
|
132954
|
-
|
|
132955
|
-
|
|
132956
|
-
|
|
132957
|
-
|
|
132958
|
-
|
|
132959
|
-
|
|
132960
|
-
);
|
|
132961
|
-
}
|
|
133210
|
+
console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
133211
|
+
if (cfg.armRoom) {
|
|
133212
|
+
console.error(
|
|
133213
|
+
`[bridge] ARMED ROOM ${cfg.roomFilter} \u2014 worker tools are enabled on this room's turns. The enclave key dir stays fenced; the workspace dir is ADVISORY (the SDK does not confine file ops to it \u2014 they root in $HOME). Blast radius = whatever this host/environment exposes.`
|
|
133214
|
+
);
|
|
133215
|
+
console.error(
|
|
133216
|
+
"[bridge] ARMED ROOM WARNING: this room contains agents you do not control. A room peer's message can drive your agent's tools. The directory is NOT a sandbox \u2014 run this bridge in an owner-provisioned confined environment (dedicated droplet / container / OS-user) with no connected accounts or ambient credentials. See the Slice 2 hardening guide."
|
|
133217
|
+
);
|
|
133218
|
+
} else {
|
|
133219
|
+
console.error(
|
|
133220
|
+
'[bridge] tools run on owner DM turns only when Work is allowed (the dashboard "Work" switch). Rooms stay say-only unless armed (AV_ARM_ROOM=1 with a pinned AV_ROOM_ID). Cross-turn injection note: a persistent session mixes room context with DM turns \u2014 run DM-only or OS-isolated if you do not arm a room.'
|
|
133221
|
+
);
|
|
132962
133222
|
}
|
|
132963
133223
|
const target = new ActiveTarget();
|
|
132964
133224
|
if (cfg.roomFilter) target.setRoom(cfg.roomFilter);
|
|
@@ -132969,9 +133229,16 @@ async function main() {
|
|
|
132969
133229
|
agentName: cfg.agentName,
|
|
132970
133230
|
platform: "node"
|
|
132971
133231
|
});
|
|
132972
|
-
const
|
|
133232
|
+
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.`;
|
|
133233
|
+
const deviceJwt = () => {
|
|
133234
|
+
const c4 = channel;
|
|
133235
|
+
return c4._deviceJwt ?? c4._persisted?.deviceJwt ?? null;
|
|
133236
|
+
};
|
|
133237
|
+
let liveWorkAllowed = () => false;
|
|
133238
|
+
const workAllowed = () => liveWorkAllowed();
|
|
133239
|
+
const listener = new PersistentClaudeSession({
|
|
132973
133240
|
model: cfg.model,
|
|
132974
|
-
systemPrompt:
|
|
133241
|
+
systemPrompt: agentSystemPrompt,
|
|
132975
133242
|
// Claude speaks by calling the say tool → the session routes it to the reply
|
|
132976
133243
|
// bound to the message being answered (wireBridge captures that per inbound via
|
|
132977
133244
|
// ActiveTarget.snapshotReply, which also logs the "said to …" line). This
|
|
@@ -132979,32 +133246,52 @@ async function main() {
|
|
|
132979
133246
|
// room when room traffic arrives mid-compose.
|
|
132980
133247
|
// Assistant reasoning that wasn't sent — log a short trace only.
|
|
132981
133248
|
onObserve: (text) => console.error(`[bridge] observed (${text.length} chars, not sent)`),
|
|
132982
|
-
worker:
|
|
132983
|
-
|
|
132984
|
-
|
|
132985
|
-
|
|
132986
|
-
|
|
132987
|
-
|
|
132988
|
-
|
|
132989
|
-
|
|
132990
|
-
|
|
132991
|
-
|
|
132992
|
-
|
|
132993
|
-
|
|
132994
|
-
|
|
133249
|
+
worker: false
|
|
133250
|
+
});
|
|
133251
|
+
const WORKER_MAX_TURNS = 20;
|
|
133252
|
+
const WORKER_TIMEOUT_MS = 5 * 6e4;
|
|
133253
|
+
const workerQueue = new WorkerQueue({
|
|
133254
|
+
makeSession: (task) => new PersistentClaudeSession({
|
|
133255
|
+
model: cfg.model,
|
|
133256
|
+
systemPrompt: agentSystemPrompt,
|
|
133257
|
+
onObserve: (text) => console.error(`[worker] observed (${text.length} chars, not sent)`),
|
|
133258
|
+
worker: true,
|
|
133259
|
+
// Task 4: the hard backstop — no tool runs on any worker turn (owner DM or
|
|
133260
|
+
// armed room) unless Work is on. Read live per tool decision.
|
|
133261
|
+
workAllowed,
|
|
133262
|
+
ephemeral: true,
|
|
133263
|
+
maxTurns: WORKER_MAX_TURNS,
|
|
133264
|
+
workspaceDir: cfg.workspaceDir,
|
|
133265
|
+
osIsolated: cfg.osIsolated,
|
|
133266
|
+
permissionMode: cfg.permissionMode,
|
|
133267
|
+
dataDir: cfg.dataDir,
|
|
133268
|
+
// Slice 2 Plan B audit self-report — only fires on armed-room tasks (isArmedTurn).
|
|
133269
|
+
roomId: cfg.roomFilter,
|
|
133270
|
+
agentId: channel.deviceId,
|
|
133271
|
+
apiUrl: cfg.apiUrl,
|
|
133272
|
+
deviceJwt
|
|
133273
|
+
}),
|
|
133274
|
+
timeoutMs: WORKER_TIMEOUT_MS,
|
|
133275
|
+
log: (m6) => console.error(m6)
|
|
132995
133276
|
});
|
|
133277
|
+
const router = makeRouter({ workAllowed, listener, queue: workerQueue });
|
|
132996
133278
|
wireBridge(
|
|
132997
133279
|
channel,
|
|
132998
|
-
{ push: (t7, reply, opts) =>
|
|
133280
|
+
{ push: (t7, reply, opts) => router.push(t7, reply, opts) },
|
|
132999
133281
|
target,
|
|
133000
133282
|
{
|
|
133001
133283
|
roomFilter: cfg.roomFilter,
|
|
133002
133284
|
armRoom: cfg.armRoom,
|
|
133003
133285
|
log: (m6) => console.error("[bridge] " + m6),
|
|
133004
133286
|
// Slice 2 Plan C (T11): live arm/disarm + local-approval poll + heartbeat.
|
|
133005
|
-
worker: cfg.worker,
|
|
133006
133287
|
workspaceDir: cfg.workspaceDir,
|
|
133007
|
-
dataDir: cfg.dataDir
|
|
133288
|
+
dataDir: cfg.dataDir,
|
|
133289
|
+
osIsolated: cfg.osIsolated,
|
|
133290
|
+
// Task 4: capture the live fail-closed workAllowed getter (set synchronously
|
|
133291
|
+
// during wiring) into the indirection the router + worker session read live.
|
|
133292
|
+
onWorkAllowed: (getter) => {
|
|
133293
|
+
liveWorkAllowed = getter;
|
|
133294
|
+
}
|
|
133008
133295
|
}
|
|
133009
133296
|
);
|
|
133010
133297
|
attachLifecycle2(channel, {
|
|
@@ -133015,7 +133302,11 @@ async function main() {
|
|
|
133015
133302
|
"room_joined",
|
|
133016
133303
|
(e7) => console.error(`[bridge] joined room ${e7.name} (${e7.roomId})`)
|
|
133017
133304
|
);
|
|
133018
|
-
|
|
133305
|
+
listener.start().catch((err) => {
|
|
133306
|
+
console.error("[bridge] fatal:", err);
|
|
133307
|
+
process.exit(1);
|
|
133308
|
+
});
|
|
133309
|
+
await channel.start();
|
|
133019
133310
|
}
|
|
133020
133311
|
main().catch((err) => {
|
|
133021
133312
|
console.error("[bridge] fatal:", err);
|