@agentvault/muse-bridge 0.1.30 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bridge.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface RoomMessage {
15
15
  * `plaintext`. The @ gate reads this; absent on emitters < 0.23.42. */
16
16
  caption?: string;
17
17
  senderIsAgent?: boolean;
18
+ /** #1459 — SecureChannel: a human in THIS agent's own account. Absent on
19
+ * emitters < 0.23.55, which reads as "not the owner" (fail closed). */
20
+ senderIsOwner?: boolean;
21
+ /** #1459 — SecureChannel: this turn carries a file from someone other than
22
+ * the owner. Only an explicit `false` lets an owner turn be powered. */
23
+ foreignContent?: boolean;
18
24
  /** ISO send time from the emit (`data.created_at`). Used to stamp a message
19
25
  * held as room context; receipt time stands in when absent. */
20
26
  timestamp?: string;
@@ -108,6 +114,8 @@ export declare function attachLifecycle(channel: LifecycleChannel, opts?: {
108
114
  restart: boolean;
109
115
  }) => void;
110
116
  }): void;
117
+ /** #1459 — appended to an owner turn that lost full access to someone else's file. */
118
+ export declare const FILE_LOCK_NOTE: string;
111
119
  export declare function roomTurnPrefix(roomId: string, senderName: string, roomName?: string): string;
112
120
  /**
113
121
  * What a held (unaddressed) room message contributes as context: the CAPTION
package/dist/index.js CHANGED
@@ -11798,11 +11798,11 @@ async function joinWithFirstMatchingKp(welcomeBytes, candidates) {
11798
11798
  return null;
11799
11799
  }
11800
11800
  function ownIdentity() {
11801
- const v2 = true ? "0.23.54" : FALLBACK;
11801
+ const v2 = true ? "0.23.55" : FALLBACK;
11802
11802
  return `${PACKAGE}@${v2}`;
11803
11803
  }
11804
11804
  function buildSha() {
11805
- const raw = true ? "caa1937" : "";
11805
+ const raw = true ? "45e77bd" : "";
11806
11806
  const cleaned = (raw ?? "").trim();
11807
11807
  return !cleaned || cleaned === "unknown" ? null : cleaned;
11808
11808
  }
@@ -13215,6 +13215,14 @@ function _isSelfHubAddress(ownHubAddress, target) {
13215
13215
  const tgt = (target ?? "").trim().toLowerCase();
13216
13216
  return own2.length > 0 && own2 === tgt;
13217
13217
  }
13218
+ function keepKnownMemberTenants(previous, next) {
13219
+ const known = /* @__PURE__ */ new Map();
13220
+ for (const m2 of previous) if (m2.tenantId) known.set(m2.deviceId, m2.tenantId);
13221
+ return next.map((m2) => {
13222
+ const tenantId = m2.tenantId ? void 0 : known.get(m2.deviceId);
13223
+ return tenantId ? { ...m2, tenantId } : m2;
13224
+ });
13225
+ }
13218
13226
  function historyErrorReason(error2) {
13219
13227
  let name = "Error";
13220
13228
  let status;
@@ -71809,15 +71817,19 @@ var init_dist = __esm({
71809
71817
  */
71810
71818
  _recentRoomAttachments = /* @__PURE__ */ new Map();
71811
71819
  static RECENT_ROOM_ATTACHMENT_MS = 10 * 60 * 1e3;
71812
- /** #1181: prefix a room turn that carries no attachment with the one saved in this room recently. */
71820
+ /** #1181: prefix a room turn that carries no attachment with the one saved in this room recently.
71821
+ * `from` is who sent that file (#1459), so the turn can be marked as carrying their content. */
71813
71822
  _withRecentRoomAttachmentNote(roomId, text) {
71814
71823
  const recent = this._recentRoomAttachments.get(roomId);
71815
- if (!recent) return text;
71824
+ if (!recent) return { text };
71816
71825
  const ageMs = Date.now() - recent.at;
71817
- if (ageMs > _SecureChannel.RECENT_ROOM_ATTACHMENT_MS) return text;
71818
- return `[Most recent file in this room: ${recent.filePath} (${recent.name}, received ${Math.round(ageMs / 1e3)}s ago). If this message refers to "the file" or "this file", it means that one \u2014 use your Read tool on it.]
71826
+ if (ageMs > _SecureChannel.RECENT_ROOM_ATTACHMENT_MS) return { text };
71827
+ return {
71828
+ text: `[Most recent file in this room: ${recent.filePath} (${recent.name}, received ${Math.round(ageMs / 1e3)}s ago). If this message refers to "the file" or "this file", it means that one \u2014 use your Read tool on it.]
71819
71829
 
71820
- ${text}`;
71830
+ ${text}`,
71831
+ from: recent.senderDeviceId ?? null
71832
+ };
71821
71833
  }
71822
71834
  /**
71823
71835
  * #1161: room attachments whose caption addressed nobody, NOT yet fetched.
@@ -71831,22 +71843,22 @@ ${text}`;
71831
71843
  _deferredRoomAttachments = /* @__PURE__ */ new Map();
71832
71844
  static DEFERRED_ROOM_ATTACHMENT_MAX = 5;
71833
71845
  static DEFERRED_ROOM_ATTACHMENT_MS = 60 * 60 * 1e3;
71834
- _deferRoomAttachment(roomId, info) {
71846
+ _deferRoomAttachment(roomId, info, senderDeviceId) {
71835
71847
  const list = this._deferredRoomAttachments.get(roomId) ?? [];
71836
- list.push({ info, at: Date.now() });
71848
+ list.push({ info, at: Date.now(), senderDeviceId });
71837
71849
  while (list.length > _SecureChannel.DEFERRED_ROOM_ATTACHMENT_MAX) list.shift();
71838
71850
  this._deferredRoomAttachments.set(roomId, list);
71839
71851
  console.log(
71840
71852
  `[SecureChannel] room ${roomId.slice(0, 8)}: attachment ${String(info?.filename ?? "file")} deferred \u2014 caption addresses nobody, fetch on the next @-addressed turn (#1161)`
71841
71853
  );
71842
71854
  }
71843
- /** Drain the room's deferred attachments that are still fresh (oldest first). */
71855
+ /** Drain the room's deferred attachments that are still fresh (oldest first), with who sent each. */
71844
71856
  _takeDeferredRoomAttachments(roomId) {
71845
71857
  const list = this._deferredRoomAttachments.get(roomId);
71846
71858
  if (!list?.length) return [];
71847
71859
  this._deferredRoomAttachments.delete(roomId);
71848
71860
  const cutoff = Date.now() - _SecureChannel.DEFERRED_ROOM_ATTACHMENT_MS;
71849
- return list.filter((d2) => d2.at >= cutoff).map((d2) => d2.info);
71861
+ return list.filter((d2) => d2.at >= cutoff).map((d2) => ({ info: d2.info, senderDeviceId: d2.senderDeviceId }));
71850
71862
  }
71851
71863
  /** In-flight roster-refresh promises (per room) — dedup concurrent refreshes. */
71852
71864
  _roomRosterRefreshInFlight = /* @__PURE__ */ new Map();
@@ -73938,7 +73950,7 @@ ${text}`;
73938
73950
  */
73939
73951
  sendActivitySpan(spanData) {
73940
73952
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
73941
- const pluginVersion = true ? "0.23.54" : "0.0.0-dev";
73953
+ const pluginVersion = true ? "0.23.55" : "0.0.0-dev";
73942
73954
  const agentName = this.config.agentName ?? "Agent";
73943
73955
  const resource = {
73944
73956
  "service.name": "agentvault-agent",
@@ -74089,11 +74101,15 @@ ${text}`;
74089
74101
  }
74090
74102
  const existingMlsGroupId = this._persisted.rooms[roomData.roomId]?.mlsGroupId;
74091
74103
  const existingMlsTs = this._persisted.rooms[roomData.roomId]?.lastMlsMessageTs;
74104
+ const members2 = keepKnownMemberTenants(
74105
+ this._persisted.rooms[roomData.roomId]?.members ?? [],
74106
+ roomData.members
74107
+ );
74092
74108
  this._persisted.rooms[roomData.roomId] = {
74093
74109
  roomId: roomData.roomId,
74094
74110
  name: roomData.name,
74095
74111
  conversationIds,
74096
- members: roomData.members,
74112
+ members: members2,
74097
74113
  ...existingMlsGroupId && { mlsGroupId: existingMlsGroupId },
74098
74114
  ...existingMlsTs && { lastMlsMessageTs: existingMlsTs }
74099
74115
  };
@@ -74598,7 +74614,8 @@ ${text}`;
74598
74614
  entityType: m2.entity_type,
74599
74615
  displayName: m2.display_name,
74600
74616
  identityPublicKey: m2.identity_public_key,
74601
- ephemeralPublicKey: m2.ephemeral_public_key
74617
+ ephemeralPublicKey: m2.ephemeral_public_key,
74618
+ ...typeof m2.tenant_id === "string" ? { tenantId: m2.tenant_id } : {}
74602
74619
  }));
74603
74620
  if (members2.length > 0) {
74604
74621
  const roomState = this._persisted.rooms[roomId];
@@ -76143,7 +76160,7 @@ ${text}`;
76143
76160
  agentVersion: this.config.agentVersion ?? "0.0.0",
76144
76161
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
76145
76162
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
76146
- pluginVersion: true ? "0.23.54" : "0.0.0-dev"
76163
+ pluginVersion: true ? "0.23.55" : "0.0.0-dev"
76147
76164
  });
76148
76165
  this._telemetryReporter.startAutoFlush(3e4);
76149
76166
  }
@@ -76299,7 +76316,8 @@ ${text}`;
76299
76316
  entityType: m2.entity_type,
76300
76317
  displayName: m2.display_name,
76301
76318
  identityPublicKey: m2.identity_public_key,
76302
- ephemeralPublicKey: m2.ephemeral_public_key
76319
+ ephemeralPublicKey: m2.ephemeral_public_key,
76320
+ ...typeof m2.tenant_id === "string" ? { tenantId: m2.tenant_id } : {}
76303
76321
  })),
76304
76322
  conversations: (d2.conversations || []).map((c2) => ({
76305
76323
  id: c2.id,
@@ -76474,7 +76492,7 @@ ${text}`;
76474
76492
  agentVersion: this.config.agentVersion ?? "0.0.0",
76475
76493
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
76476
76494
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
76477
- pluginVersion: true ? "0.23.54" : "0.0.0-dev"
76495
+ pluginVersion: true ? "0.23.55" : "0.0.0-dev"
76478
76496
  });
76479
76497
  this._telemetryReporter.startAutoFlush(3e4);
76480
76498
  }
@@ -77331,13 +77349,13 @@ ${messageText}`;
77331
77349
  * Returns the text unchanged when there is no attachment or the fetch fails —
77332
77350
  * but a failure is LOGGED and announced in the text, never silent.
77333
77351
  */
77334
- async _augmentWithAttachment(attachmentInfo, baseText, roomId) {
77352
+ async _augmentWithAttachment(attachmentInfo, baseText, roomId, senderDeviceId) {
77335
77353
  if (!attachmentInfo) return baseText;
77336
77354
  try {
77337
77355
  const { filePath, decrypted } = await this._downloadAndDecryptAttachment(attachmentInfo);
77338
77356
  const mime = String(attachmentInfo.mime ?? "application/octet-stream");
77339
77357
  const name = String(attachmentInfo.filename ?? "attachment");
77340
- if (roomId) this._recentRoomAttachments.set(roomId, { filePath, name, at: Date.now() });
77358
+ if (roomId) this._recentRoomAttachments.set(roomId, { filePath, name, at: Date.now(), senderDeviceId });
77341
77359
  const textMimes = ["text/", "application/json", "application/xml", "application/csv"];
77342
77360
  if (textMimes.some((m2) => mime.startsWith(m2))) {
77343
77361
  const content = new TextDecoder().decode(decrypted);
@@ -77993,22 +78011,34 @@ ${baseText}`;
77993
78011
  }
77994
78012
  const caption = messageText;
77995
78013
  const addressed = parseMentions(messageText).length > 0;
78014
+ const foldedFrom = [];
77996
78015
  if (roomAttachment && !addressed) {
77997
- this._deferRoomAttachment(resolvedRoomId, roomAttachment);
78016
+ this._deferRoomAttachment(resolvedRoomId, roomAttachment, senderDeviceId);
77998
78017
  } else {
77999
78018
  const deferred = addressed ? this._takeDeferredRoomAttachments(resolvedRoomId) : [];
78000
- for (const info of deferred) {
78001
- messageText = await this._augmentWithAttachment(info, messageText, resolvedRoomId);
78019
+ for (const d2 of deferred) {
78020
+ messageText = await this._augmentWithAttachment(d2.info, messageText, resolvedRoomId, d2.senderDeviceId);
78021
+ foldedFrom.push(d2.senderDeviceId ?? null);
78002
78022
  }
78003
78023
  if (roomAttachment) {
78004
- messageText = await this._augmentWithAttachment(roomAttachment, messageText, resolvedRoomId);
78024
+ messageText = await this._augmentWithAttachment(roomAttachment, messageText, resolvedRoomId, senderDeviceId);
78025
+ foldedFrom.push(senderDeviceId ?? null);
78005
78026
  } else if (deferred.length === 0) {
78006
- messageText = this._withRecentRoomAttachmentNote(resolvedRoomId, messageText);
78027
+ const noted = this._withRecentRoomAttachmentNote(resolvedRoomId, messageText);
78028
+ messageText = noted.text;
78029
+ if (noted.from !== void 0) foldedFrom.push(noted.from);
78007
78030
  }
78008
78031
  }
78009
78032
  const roomMembers = this._persisted?.rooms?.[resolvedRoomId]?.members ?? [];
78010
78033
  const senderMember = roomMembers.find((m2) => m2.deviceId === senderDeviceId);
78011
78034
  const senderIsAgent = senderMember?.entityType === "agent" ? true : senderMember?.entityType === "owner" || senderMember?.entityType === "human" ? false : void 0;
78035
+ const ownTenantId = this._deviceJwt ? this._decodeTenantIdForRecovery(this._deviceJwt) : null;
78036
+ const isOwnerMember = (deviceId) => {
78037
+ const member = deviceId ? roomMembers.find((m2) => m2.deviceId === deviceId) : void 0;
78038
+ return (member?.entityType === "owner" || member?.entityType === "human") && ownTenantId !== null && typeof member?.tenantId === "string" && member.tenantId.toLowerCase() === ownTenantId;
78039
+ };
78040
+ const senderIsOwner = senderIsAgent === false && isOwnerMember(senderDeviceId);
78041
+ const foreignContent = foldedFrom.some((from) => !isOwnerMember(from));
78012
78042
  const metadata = {
78013
78043
  messageId: data.message_id ?? "",
78014
78044
  conversationId: "",
@@ -78017,6 +78047,8 @@ ${baseText}`;
78017
78047
  roomId: resolvedRoomId,
78018
78048
  senderDeviceId,
78019
78049
  senderIsAgent,
78050
+ senderIsOwner,
78051
+ foreignContent,
78020
78052
  senderName: senderMember?.displayName || "unknown",
78021
78053
  roomName: this._persisted?.rooms?.[resolvedRoomId]?.name,
78022
78054
  caption
@@ -78042,7 +78074,10 @@ ${baseText}`;
78042
78074
  timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
78043
78075
  // Bridges require an explicit human classification for room tools.
78044
78076
  // Reply policy separately uses this field for agent-loop prevention.
78045
- senderIsAgent
78077
+ senderIsAgent,
78078
+ // #1459 — owner-only power keys on these, never on senderIsAgent.
78079
+ senderIsOwner,
78080
+ foreignContent
78046
78081
  });
78047
78082
  const contextualMessage = senderIsAgent ? messageText : `[${senderLabel}]: ${messageText}`;
78048
78083
  Promise.resolve(this.config.onMessage?.(contextualMessage, metadata)).catch((err) => {
@@ -102874,7 +102909,7 @@ data:
102874
102909
  await init_policy_enforcer();
102875
102910
  init_room_protocol();
102876
102911
  init_tool_telemetry();
102877
- VERSION = true ? "0.23.54" : "0.0.0-dev";
102912
+ VERSION = true ? "0.23.55" : "0.0.0-dev";
102878
102913
  }
102879
102914
  });
102880
102915
  await init_index();
@@ -103595,7 +103630,8 @@ var MuseSession = class {
103595
103630
  text,
103596
103631
  reply,
103597
103632
  autoReplyOnText: opts?.replyExpected ?? opts?.autoReplyOnText,
103598
- sayAllowed: opts?.replyExpected ?? true
103633
+ sayAllowed: opts?.replyExpected ?? true,
103634
+ ownerTurn: opts?.ownerTurn === true
103599
103635
  });
103600
103636
  if (!this.running) this.drain = this.run();
103601
103637
  }
@@ -103646,7 +103682,7 @@ var MuseSession = class {
103646
103682
  const prompt = this.opts.identity ? `${this.opts.identity}
103647
103683
 
103648
103684
  ${item.text}` : item.text;
103649
- const result = await this.driver.runTurn(prompt);
103685
+ const result = await this.driver.runTurn(prompt, { ownerTurn: item.ownerTurn });
103650
103686
  await this.maybeFallback(item, result);
103651
103687
  } catch {
103652
103688
  this.log("turn failed reason=driver");
@@ -103743,7 +103779,7 @@ function museSpawnArgs(opts) {
103743
103779
  );
103744
103780
  }
103745
103781
  const provider = opts.provider ?? "meta";
103746
- const ownerOn = opts.workAllowed === true && opts.gatesRemoved === true;
103782
+ const ownerOn = opts.workAllowed === true && opts.gatesRemoved === true && opts.ownerTurn === true;
103747
103783
  const powerFlags = ownerOn ? ["--yolo"] : ["--disable-shell", "--disable-write"];
103748
103784
  const args = [
103749
103785
  "exec",
@@ -104019,6 +104055,7 @@ async function createMuseProviderAdapter(opts) {
104019
104055
 
104020
104056
  // src/muse-driver.ts
104021
104057
  var SESSION_FILE = "muse-session-id";
104058
+ var OWNER_SESSION_FILE = "muse-owner-session-id";
104022
104059
  var DEFAULT_TURN_TIMEOUT_MS = 3e5;
104023
104060
  var SIGKILL_GRACE_MS = 2e3;
104024
104061
  var MuseDriver = class {
@@ -104031,37 +104068,33 @@ var MuseDriver = class {
104031
104068
  spawnFn;
104032
104069
  turnTimeoutMs;
104033
104070
  persistedId = null;
104071
+ ownerId = null;
104034
104072
  current = null;
104035
104073
  child = null;
104036
104074
  probed = false;
104037
104075
  adapter = null;
104038
- /** Load or create `<dataDir>/muse-session-id`. Does not spawn muse. */
104076
+ /** Load or create `<dataDir>/muse-session-id` and the owner lane's id. Does not spawn muse. */
104039
104077
  async start() {
104040
104078
  mkdirSync3(this.opts.dataDir, { recursive: true });
104041
- const file = join11(this.opts.dataDir, SESSION_FILE);
104042
- if (existsSync4(file)) {
104043
- const existing = readFileSync5(file, "utf8").trim();
104044
- if (existing) {
104045
- this.persistedId = existing;
104046
- return existing;
104047
- }
104048
- }
104049
- this.persistedId = randomUUID5();
104050
- writeFileSync2(file, this.persistedId + "\n", { encoding: "utf8" });
104079
+ this.persistedId = loadOrMintId(join11(this.opts.dataDir, SESSION_FILE));
104080
+ this.ownerId = loadOrMintId(join11(this.opts.dataDir, OWNER_SESSION_FILE));
104051
104081
  return this.persistedId;
104052
104082
  }
104053
104083
  sessionId() {
104054
104084
  return this.persistedId;
104055
104085
  }
104056
- runTurn(prompt) {
104057
- if (!this.persistedId) {
104086
+ ownerSessionId() {
104087
+ return this.ownerId;
104088
+ }
104089
+ runTurn(prompt, opts = {}) {
104090
+ if (!this.persistedId || !this.ownerId) {
104058
104091
  throw new Error("MuseDriver.runTurn before start()");
104059
104092
  }
104060
104093
  if (this.current || this.child) {
104061
104094
  throw new Error("MuseDriver.runTurn called while a turn is in flight");
104062
104095
  }
104063
104096
  this.current = { spoke: false, cancelled: false };
104064
- return this.executeTurn(prompt).finally(() => {
104097
+ return this.executeTurn(prompt, opts.ownerTurn === true).finally(() => {
104065
104098
  this.current = null;
104066
104099
  });
104067
104100
  }
@@ -104084,8 +104117,8 @@ var MuseDriver = class {
104084
104117
  const dead = await waitForExit(child, SIGKILL_GRACE_MS);
104085
104118
  if (!dead) child.kill("SIGKILL");
104086
104119
  }
104087
- async executeTurn(prompt) {
104088
- const sessionId = this.persistedId;
104120
+ async executeTurn(prompt, ownerTurn) {
104121
+ const sessionId = ownerTurn ? this.ownerId : this.persistedId;
104089
104122
  if (!sessionId) {
104090
104123
  throw new Error("MuseDriver.runTurn before start()");
104091
104124
  }
@@ -104099,7 +104132,8 @@ var MuseDriver = class {
104099
104132
  prompt,
104100
104133
  apiKey: this.opts.apiKey,
104101
104134
  workAllowed: readFlag(this.opts.workAllowed),
104102
- gatesRemoved: readFlag(this.opts.gatesRemoved)
104135
+ gatesRemoved: readFlag(this.opts.gatesRemoved),
104136
+ ownerTurn
104103
104137
  });
104104
104138
  await this.probeUsable(spawn2);
104105
104139
  if (this.current?.cancelled) throw new Error("muse turn stopped");
@@ -104252,6 +104286,15 @@ function defaultSpawn(command, args, options) {
104252
104286
  stdio: ["ignore", "pipe", "pipe"]
104253
104287
  });
104254
104288
  }
104289
+ function loadOrMintId(file) {
104290
+ if (existsSync4(file)) {
104291
+ const existing = readFileSync5(file, "utf8").trim();
104292
+ if (existing) return existing;
104293
+ }
104294
+ const id = randomUUID5();
104295
+ writeFileSync2(file, id + "\n", { encoding: "utf8" });
104296
+ return id;
104297
+ }
104255
104298
  function readFlag(value) {
104256
104299
  return typeof value === "function" ? value() : value;
104257
104300
  }
@@ -104805,6 +104848,7 @@ function attachLifecycle2(channel, opts = {}) {
104805
104848
  if (/reconnect loop detected/i.test(msg)) terminal("reconnect_loop", true);
104806
104849
  });
104807
104850
  }
104851
+ var FILE_LOCK_NOTE = "[AgentVault: this turn has no commands and no file changes, because it includes a file another room member shared. If you were asked to run or change something, say so, and that it will work in a message that doesn't refer to that file, or in a 1:1 chat.]";
104808
104852
  function roomTurnPrefix(roomId, senderName, roomName) {
104809
104853
  return `[room ${roomLabel(roomId, roomName)} | ${sanitizeLabel(senderName)}]`;
104810
104854
  }
@@ -104905,19 +104949,33 @@ function wireBridge(channel, session, target, opts = {}) {
104905
104949
  log(`inbound from ${e.senderName} in ${e.roomId.slice(0, 8)}`);
104906
104950
  target.setRoom(e.roomId);
104907
104951
  const held = roomContext.peek(e.roomId);
104908
- const contextBlock = formatRoomContext(held.entries, roomLabel(e.roomId, roomNames.get(e.roomId)), held.dropped);
104909
104952
  const turnText = `${roomTurnPrefix(e.roomId, e.senderName, roomNames.get(e.roomId))}: ${e.plaintext}`;
104953
+ const ownerPowered = e.senderIsAgent === false && e.senderIsOwner === true && e.foreignContent === false && workAllowed() && gatesRemoved();
104954
+ const contextBlock = ownerPowered ? "" : formatRoomContext(held.entries, roomLabel(e.roomId, roomNames.get(e.roomId)), held.dropped);
104955
+ const fileLocked = e.senderIsOwner === true && e.foreignContent === true && workAllowed() && gatesRemoved();
104956
+ if (fileLocked) log(`room ${e.roomId.slice(0, 8)}: owner turn WITHOUT full access \u2014 it carries a file another member shared (#1459)`);
104957
+ const body = fileLocked ? `${turnText}
104958
+
104959
+ ${FILE_LOCK_NOTE}` : turnText;
104910
104960
  session.push(
104911
104961
  contextBlock ? `${contextBlock}
104912
104962
 
104913
- ${turnText}` : turnText,
104963
+ ${body}` : body,
104914
104964
  target.snapshotReply(channel, log),
104915
104965
  {
104916
104966
  autoReplyOnText: false,
104917
- replyExpected: owes
104967
+ replyExpected: owes,
104968
+ ownerTurn: ownerPowered
104918
104969
  }
104919
104970
  );
104920
- if (held.entries.length > 0) {
104971
+ if (ownerPowered) {
104972
+ log(`room ${e.roomId.slice(0, 8)}: owner turn with full access`);
104973
+ if (held.entries.length > 0) {
104974
+ log(
104975
+ `room ${e.roomId.slice(0, 8)}: ${held.entries.length} held message(s) kept for the next turn without full access (a full-access turn never carries room context)`
104976
+ );
104977
+ }
104978
+ } else if (held.entries.length > 0) {
104921
104979
  roomContext.clear(e.roomId);
104922
104980
  log(`room ${e.roomId.slice(0, 8)}: delivered ${held.entries.length} held message(s) as context with this turn`);
104923
104981
  }
@@ -104931,7 +104989,10 @@ ${turnText}` : turnText,
104931
104989
  log("inbound 1:1 DM from owner");
104932
104990
  }
104933
104991
  target.setDm(metadata?.conversationId, metadata?.conversationGroupId);
104934
- session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: ownerAttested });
104992
+ session.push(text, target.snapshotReply(channel, log), {
104993
+ autoReplyOnText: ownerAttested,
104994
+ ownerTurn: ownerAttested
104995
+ });
104935
104996
  });
104936
104997
  }
104937
104998
 
@@ -104989,7 +105050,7 @@ function mkdir700(path2) {
104989
105050
  chmodSync5(path2, 448);
104990
105051
  }
104991
105052
  function packageVersion() {
104992
- return true ? "0.1.30" : void 0;
105053
+ return true ? "0.1.31" : void 0;
104993
105054
  }
104994
105055
  function helpText(version2) {
104995
105056
  return [
@@ -40,15 +40,19 @@ export declare class MuseDriver {
40
40
  private readonly spawnFn;
41
41
  private readonly turnTimeoutMs;
42
42
  private persistedId;
43
+ private ownerId;
43
44
  private current;
44
45
  private child;
45
46
  private probed;
46
47
  private adapter;
47
48
  constructor(opts: MuseDriverOpts);
48
- /** Load or create `<dataDir>/muse-session-id`. Does not spawn muse. */
49
+ /** Load or create `<dataDir>/muse-session-id` and the owner lane's id. Does not spawn muse. */
49
50
  start(): Promise<string>;
50
51
  sessionId(): string | null;
51
- runTurn(prompt: string): Promise<{
52
+ ownerSessionId(): string | null;
53
+ runTurn(prompt: string, opts?: {
54
+ ownerTurn?: boolean;
55
+ }): Promise<{
52
56
  text: string;
53
57
  spoke: boolean;
54
58
  }>;
@@ -2,10 +2,13 @@ export interface RoomSession {
2
2
  push(text: string, reply?: (text: string) => Promise<void>, opts?: {
3
3
  autoReplyOnText?: boolean;
4
4
  replyExpected?: boolean;
5
+ ownerTurn?: boolean;
5
6
  }): void;
6
7
  }
7
8
  export interface DriverLike {
8
- runTurn(prompt: string): Promise<{
9
+ runTurn(prompt: string, opts?: {
10
+ ownerTurn?: boolean;
11
+ }): Promise<{
9
12
  text: string;
10
13
  spoke: boolean;
11
14
  }>;
@@ -33,6 +36,7 @@ export declare class MuseSession implements RoomSession {
33
36
  push(text: string, reply?: (text: string) => Promise<void>, opts?: {
34
37
  autoReplyOnText?: boolean;
35
38
  replyExpected?: boolean;
39
+ ownerTurn?: boolean;
36
40
  }): void;
37
41
  sayHandler(): (text: string) => void;
38
42
  idle(): Promise<void>;
@@ -14,6 +14,13 @@ export interface MuseSpawnOpts {
14
14
  apiKey?: string;
15
15
  workAllowed: boolean;
16
16
  gatesRemoved: boolean;
17
+ /**
18
+ * #1459 — this turn came from the owner: an owner-attested 1:1 DM, or a room
19
+ * message whose sender SecureChannel placed in this agent's own account.
20
+ * Owner power needs all three; the owner's switches alone used to grant it
21
+ * to whoever @-mentioned the agent.
22
+ */
23
+ ownerTurn: boolean;
17
24
  }
18
25
  export interface MuseSpawnEnv {
19
26
  HOME: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/muse-bridge",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "description": "AgentVault Muse Bridge — daemon bridging a Muse agent into E2E-encrypted AgentVault direct messages and rooms.",
6
6
  "main": "dist/index.js",