@securecoms/agent-bridge 0.29.0 → 0.30.0

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/index.js CHANGED
@@ -13317,17 +13317,17 @@ async function makeNobleSignatureImpl(alg) {
13317
13317
  };
13318
13318
  }
13319
13319
  try {
13320
- const { ed25519: ed255194 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
13320
+ const { ed25519: ed255197 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
13321
13321
  return {
13322
13322
  async sign(signKey, message) {
13323
- return ed255194.sign(message, signKey);
13323
+ return ed255197.sign(message, signKey);
13324
13324
  },
13325
13325
  async verify(publicKey, message, signature) {
13326
- return ed255194.verify(signature, message, publicKey);
13326
+ return ed255197.verify(signature, message, publicKey);
13327
13327
  },
13328
13328
  async keygen() {
13329
- const signKey = ed255194.utils.randomSecretKey();
13330
- return { signKey, publicKey: ed255194.getPublicKey(signKey) };
13329
+ const signKey = ed255197.utils.randomSecretKey();
13330
+ return { signKey, publicKey: ed255197.getPublicKey(signKey) };
13331
13331
  }
13332
13332
  };
13333
13333
  } catch (err) {
@@ -17605,11 +17605,11 @@ var MlsOperationQueue = class _MlsOperationQueue {
17605
17605
  const prev = this.chains.get(channelId) ?? Promise.resolve();
17606
17606
  const entry = { group: this.group, kind: this.kind, key: channelId, released: false };
17607
17607
  const parentHeld = als?.getStore() ?? [];
17608
- const guarded = als ? () => als.run([...parentHeld, entry], op) : op;
17608
+ const guarded2 = als ? () => als.run([...parentHeld, entry], op) : op;
17609
17609
  const settle = (p) => p.finally(() => {
17610
17610
  entry.released = true;
17611
17611
  });
17612
- const next = settle(prev.then(guarded, guarded));
17612
+ const next = settle(prev.then(guarded2, guarded2));
17613
17613
  this.chains.set(channelId, next);
17614
17614
  this.inFlight.set(channelId, (this.inFlight.get(channelId) ?? 0) + 1);
17615
17615
  try {
@@ -23342,7 +23342,9 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23342
23342
  retryCount: 0,
23343
23343
  expires_at: row.expires_at,
23344
23344
  voiceMode: row.voice_mode ?? void 0,
23345
- voiceDefaultResponder: row.voice_default_responder ?? void 0
23345
+ voiceDefaultResponder: row.voice_default_responder ?? void 0,
23346
+ ...typeof row.identity_signature === "string" && row.identity_signature ? { identitySignature: row.identity_signature } : {},
23347
+ payloadHex: row.payload
23346
23348
  };
23347
23349
  const custodiedAt = this.nowMs();
23348
23350
  this.custody.set(row.queue_id, entry);
@@ -25542,7 +25544,9 @@ var FakeConnectBackend = class _FakeConnectBackend {
25542
25544
  epoch: p.epoch,
25543
25545
  payload: bytesToHex22(canonical),
25544
25546
  connect_channel_id: p.channelId,
25545
- created_at: ""
25547
+ created_at: "",
25548
+ identity_signature: null
25549
+ // the envelope above is unsigned
25546
25550
  });
25547
25551
  }
25548
25552
  }
@@ -25554,7 +25558,11 @@ var FakeConnectBackend = class _FakeConnectBackend {
25554
25558
  // edge stores the canonical envelope bytes (hex) and fans out one delivery row
25555
25559
  // per active member device except the sender's. We resolve the sender's device
25556
25560
  // from sender_id (recorded at registerDevice); if unknown, every member gets it.
25557
- async ingestApplication(envelope) {
25561
+ //
25562
+ // Owner-signed tool grants (spec §7.0): like Plan 0's backend, each row carries
25563
+ // the envelope's `identity_proof.signature` as `identity_signature` beside the
25564
+ // unsigned canonical payload. `omitSignature` models a pre-Plan-0 row.
25565
+ async ingestApplication(envelope, opts) {
25558
25566
  const channelId = envelope.channel_id;
25559
25567
  const canonical = await canonicalEnvelopeBytes(envelope);
25560
25568
  const members = this.membersByChannel.get(channelId) ?? [];
@@ -25572,7 +25580,8 @@ var FakeConnectBackend = class _FakeConnectBackend {
25572
25580
  epoch: envelope.mls_group_epoch,
25573
25581
  payload: bytesToHex22(canonical),
25574
25582
  connect_channel_id: channelId,
25575
- created_at: ""
25583
+ created_at: "",
25584
+ identity_signature: opts?.omitSignature ? null : envelope.identity_proof.signature || null
25576
25585
  });
25577
25586
  }
25578
25587
  }
@@ -25944,11 +25953,15 @@ function openSchema(db) {
25944
25953
  ensureUnifiedDmOwner(db);
25945
25954
  ensureConversationLastAbsentAt(db);
25946
25955
  ensureRecallForgetTables(db);
25956
+ ensureAgentMemoryOrigin(db);
25947
25957
  ensureRuntimeInboxDeliveryAttempts(db);
25948
25958
  ensureRuntimeReplyIntent(db);
25949
25959
  ensureRuntimeReplyIntentEnvelopeGroup(db);
25950
25960
  ensureGroupLink(db);
25951
25961
  ensureMlsCustody(db);
25962
+ ensureToolGrantTables(db);
25963
+ ensureToolGrantAppliedTable(db);
25964
+ ensureRoomSearchCapTable(db);
25952
25965
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
25953
25966
  }
25954
25967
  function ensureMlsCustody(db) {
@@ -26043,6 +26056,11 @@ function ensureRecallStateColumns(db) {
26043
26056
  }
26044
26057
  }
26045
26058
  }
26059
+ function ensureAgentMemoryOrigin(db) {
26060
+ const cols = db.prepare("PRAGMA table_info(agent_memory)").all();
26061
+ if (cols.length > 0 && !cols.some((c) => c.name === "origin"))
26062
+ db.exec("ALTER TABLE agent_memory ADD COLUMN origin TEXT");
26063
+ }
26046
26064
  function ensureRecallForgetTables(db) {
26047
26065
  db.exec(`
26048
26066
  CREATE TABLE IF NOT EXISTS agent_memory_recall_forgotten (
@@ -26118,6 +26136,67 @@ function ensureUnifiedStateColumns(db) {
26118
26136
  }
26119
26137
  }
26120
26138
  }
26139
+ function ensureToolGrantAppliedTable(db) {
26140
+ db.exec(`
26141
+ CREATE TABLE IF NOT EXISTS tool_grant_applied (
26142
+ identity_id TEXT NOT NULL,
26143
+ pin_key_b64 TEXT NOT NULL,
26144
+ sealed_applied BLOB NOT NULL,
26145
+ updated_at INTEGER NOT NULL,
26146
+ PRIMARY KEY (identity_id, pin_key_b64)
26147
+ );
26148
+ `);
26149
+ }
26150
+ function ensureRoomSearchCapTable(db) {
26151
+ db.exec(`
26152
+ CREATE TABLE IF NOT EXISTS room_search_cap (
26153
+ identity_id TEXT NOT NULL,
26154
+ channel_id TEXT NOT NULL,
26155
+ utc_day TEXT NOT NULL,
26156
+ granted_turns INTEGER NOT NULL,
26157
+ reached_by TEXT,
26158
+ notice_posted INTEGER NOT NULL,
26159
+ PRIMARY KEY (identity_id, channel_id)
26160
+ );
26161
+ `);
26162
+ }
26163
+ function ensureToolGrantTables(db) {
26164
+ db.exec(`
26165
+ CREATE TABLE IF NOT EXISTS tool_grant_manifest (
26166
+ identity_id TEXT NOT NULL,
26167
+ pin_key_b64 TEXT NOT NULL,
26168
+ counter INTEGER NOT NULL,
26169
+ sealed_manifest BLOB NOT NULL,
26170
+ updated_at INTEGER NOT NULL,
26171
+ PRIMARY KEY (identity_id, pin_key_b64)
26172
+ );
26173
+ CREATE TABLE IF NOT EXISTS owner_replay (
26174
+ identity_id TEXT NOT NULL,
26175
+ channel_id TEXT NOT NULL,
26176
+ message_id TEXT NOT NULL,
26177
+ group_id TEXT NOT NULL,
26178
+ epoch INTEGER NOT NULL,
26179
+ processed_at INTEGER NOT NULL,
26180
+ PRIMARY KEY (identity_id, channel_id, message_id)
26181
+ );
26182
+ CREATE INDEX IF NOT EXISTS owner_replay_age ON owner_replay (identity_id, channel_id, processed_at);
26183
+ CREATE TABLE IF NOT EXISTS owner_verify_failed (
26184
+ identity_id TEXT NOT NULL,
26185
+ channel_id TEXT NOT NULL,
26186
+ message_id TEXT NOT NULL,
26187
+ failed_at INTEGER NOT NULL,
26188
+ PRIMARY KEY (identity_id, message_id)
26189
+ );
26190
+ CREATE TABLE IF NOT EXISTS owner_replay_closed_epoch (
26191
+ identity_id TEXT NOT NULL,
26192
+ channel_id TEXT NOT NULL,
26193
+ group_id TEXT NOT NULL,
26194
+ epoch INTEGER NOT NULL,
26195
+ closed_at INTEGER NOT NULL,
26196
+ PRIMARY KEY (identity_id, channel_id, group_id, epoch)
26197
+ );
26198
+ `);
26199
+ }
26121
26200
 
26122
26201
  // ../connect-mls-store/dist/errors.js
26123
26202
  var MlsStoreCorruptError = class extends Error {
@@ -26272,7 +26351,16 @@ function itemVecAad(identityId, f, modelId) {
26272
26351
  return enc4.encode(["agent_memory_item_vec", identityId, f.scopeKind, f.scopeId, f.originId, String(f.chunkIndex), modelId].join(NUL));
26273
26352
  }
26274
26353
  function encodeItemRecord(r) {
26275
- return enc4.encode(JSON.stringify({ v: 1, t: r.text, s: r.source, tr: r.trust, ...r.sourceRef !== void 0 ? { sr: r.sourceRef } : {}, ...r.chunks !== void 0 ? { c: r.chunks } : {} }));
26354
+ return enc4.encode(JSON.stringify({
26355
+ v: 1,
26356
+ t: r.text,
26357
+ s: r.source,
26358
+ tr: r.trust,
26359
+ ...r.sourceRef !== void 0 ? { sr: r.sourceRef } : {},
26360
+ ...r.chunks !== void 0 ? { c: r.chunks } : {},
26361
+ ...r.role !== void 0 ? { ro: r.role } : {},
26362
+ ...r.origin !== void 0 ? { or: r.origin } : {}
26363
+ }));
26276
26364
  }
26277
26365
  function decodeItemRecord(b) {
26278
26366
  let o;
@@ -26283,7 +26371,7 @@ function decodeItemRecord(b) {
26283
26371
  }
26284
26372
  if (!o || typeof o !== "object" || Array.isArray(o))
26285
26373
  return null;
26286
- const { v, t, s, tr, sr, c } = o;
26374
+ const { v, t, s, tr, sr, c, ro, or } = o;
26287
26375
  if (v !== 1 || typeof t !== "string" || typeof s !== "string" || !ITEM_SOURCES.includes(s))
26288
26376
  return null;
26289
26377
  if (tr !== "owner" && tr !== "reference")
@@ -26294,7 +26382,19 @@ function decodeItemRecord(b) {
26294
26382
  return null;
26295
26383
  if (c !== void 0 && (typeof c !== "number" || !Number.isSafeInteger(c) || c < 1))
26296
26384
  return null;
26297
- return { text: t, source: s, trust: tr, ...sr !== void 0 ? { sourceRef: sr } : {}, ...c !== void 0 ? { chunks: c } : {} };
26385
+ if (ro !== void 0 && ro !== "peer" && ro !== "agent")
26386
+ return null;
26387
+ if (or !== void 0 && or !== "fast_path" && or !== "reply" && or !== "untrusted")
26388
+ return null;
26389
+ return {
26390
+ text: t,
26391
+ source: s,
26392
+ trust: tr,
26393
+ ...sr !== void 0 ? { sourceRef: sr } : {},
26394
+ ...c !== void 0 ? { chunks: c } : {},
26395
+ ...ro !== void 0 ? { role: ro } : {},
26396
+ ...or !== void 0 ? { origin: or } : {}
26397
+ };
26298
26398
  }
26299
26399
  var noteSourceRef = (conversationId, noteId) => `agent_memory_note:${conversationId}:${noteId}`;
26300
26400
  var recallSourceRef = (conversationId, messageId) => `agent_memory_recall:${conversationId}:${messageId}`;
@@ -26338,6 +26438,14 @@ function bytesEqual(a, b) {
26338
26438
  function sameTag(prior, tag) {
26339
26439
  return prior !== null && prior.groupId === tag.groupId && prior.epoch === tag.epoch;
26340
26440
  }
26441
+ function metaOf(r) {
26442
+ return {
26443
+ ...r.message_id !== null ? { messageId: r.message_id } : {},
26444
+ modality: r.modality,
26445
+ ...isOrigin(r.origin) ? { origin: r.origin } : {}
26446
+ };
26447
+ }
26448
+ var isOrigin = (v) => v === "fast_path" || v === "reply" || v === "untrusted";
26341
26449
  var UTF8 = new TextDecoder();
26342
26450
  var DAY_MS = 24 * 60 * 60 * 1e3;
26343
26451
  var DEFAULT_MEMORY_RETENTION_MS = 30 * DAY_MS;
@@ -26386,6 +26494,8 @@ function cachePreparedStatements(db) {
26386
26494
  return st;
26387
26495
  };
26388
26496
  }
26497
+ var OWNER_REPLAY_MAX_PER_CHANNEL = 1e4;
26498
+ var OWNER_REPLAY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
26389
26499
  var SqliteMlsStateStore = class _SqliteMlsStateStore {
26390
26500
  db;
26391
26501
  id;
@@ -26759,7 +26869,13 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
26759
26869
  * same message (the bridge's ingest stamp). The staged row is never
26760
26870
  * rewritten: on a match the FIRST staging stays authoritative.
26761
26871
  */
26762
- async stageRuntimeInbox(messageId, payload, receivedAt = Date.now(), samePayload = bytesEqual) {
26872
+ /**
26873
+ * Owner-signed tool grants (P-3, plan 1 task 4): when `ownerReplay` is given,
26874
+ * the owner replay entry and the runtime_inbox row are written in ONE
26875
+ * BEGIN IMMEDIATE transaction, so a failed stage records no replay entry and a
26876
+ * recorded replay entry always has its staged message.
26877
+ */
26878
+ async stageRuntimeInbox(messageId, payload, receivedAt = Date.now(), samePayload = bytesEqual, ownerReplay) {
26763
26879
  const prior = this.db.prepare("SELECT sealed_message FROM runtime_inbox WHERE identity_id=? AND message_id=?").get(this.id, messageId);
26764
26880
  if (prior) {
26765
26881
  const priorPayload = await this.openRuntimeInboxPayload(prior.sealed_message);
@@ -26769,9 +26885,20 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
26769
26885
  return false;
26770
26886
  }
26771
26887
  const sealed = await this.cipher.seal(payload);
26772
- const info = this.db.prepare(`INSERT OR IGNORE INTO runtime_inbox
26773
- (identity_id, message_id, sealed_message, received_at)
26774
- VALUES (?, ?, ?, ?)`).run(this.id, messageId, sealed, receivedAt);
26888
+ const info = this.immediate(() => {
26889
+ if (ownerReplay) {
26890
+ this.db.prepare(`INSERT OR IGNORE INTO owner_replay
26891
+ (identity_id, channel_id, message_id, group_id, epoch, processed_at)
26892
+ VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, ownerReplay.channelId, messageId, ownerReplay.groupId, ownerReplay.epoch, ownerReplay.processedAt);
26893
+ }
26894
+ if (this.failNextRuntimeInboxInsert) {
26895
+ this.failNextRuntimeInboxInsert = false;
26896
+ throw new Error("runtime_inbox insert failed (test)");
26897
+ }
26898
+ return this.db.prepare(`INSERT OR IGNORE INTO runtime_inbox
26899
+ (identity_id, message_id, sealed_message, received_at)
26900
+ VALUES (?, ?, ?, ?)`).run(this.id, messageId, sealed, receivedAt);
26901
+ });
26775
26902
  if (info.changes === 0) {
26776
26903
  const raced = this.db.prepare("SELECT sealed_message FROM runtime_inbox WHERE identity_id=? AND message_id=?").get(this.id, messageId);
26777
26904
  const racedPayload = await this.openRuntimeInboxPayload(raced.sealed_message);
@@ -27054,10 +27181,10 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27054
27181
  return { seq: 0, deduped: false, dropped: true };
27055
27182
  }
27056
27183
  const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
27057
- (identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
27184
+ (identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at, origin)
27058
27185
  VALUES (?, ?,
27059
27186
  (SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
27060
- ?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, now);
27187
+ ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, now, isOrigin(input.origin) ? input.origin : null);
27061
27188
  if (info.changes === 0) {
27062
27189
  const row2 = this.db.prepare("SELECT seq FROM agent_memory WHERE identity_id=? AND conversation_id=? AND dedup_key=?").get(this.id, input.conversationId, dedupKey);
27063
27190
  return { seq: row2.seq, deduped: true };
@@ -27081,6 +27208,29 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27081
27208
  facts.push(new TextDecoder().decode(await this.cipher.open(f.sealed_fact)));
27082
27209
  return { recent, facts };
27083
27210
  }
27211
+ /** Owner grants plan 4 Task 16: message id, modality and origin of the given lines (clear columns only, no text). */
27212
+ memoryLineMeta(conversationId, seqs) {
27213
+ const out = /* @__PURE__ */ new Map();
27214
+ const stmt = this.db.prepare("SELECT message_id, modality, origin FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq=?");
27215
+ for (const seq of seqs) {
27216
+ const r = stmt.get(this.id, conversationId, seq);
27217
+ if (r)
27218
+ out.set(seq, metaOf(r));
27219
+ }
27220
+ return out;
27221
+ }
27222
+ /** Owner grants plan 4 Task 16 (audit r2 M-1): every peer row's message id in the conversation, not the 50-row window. */
27223
+ memoryPeerMessageIds(conversationId) {
27224
+ return this.db.prepare("SELECT message_id FROM agent_memory WHERE identity_id=? AND conversation_id=? AND role='peer' AND message_id IS NOT NULL").all(this.id, conversationId).map((r) => r.message_id);
27225
+ }
27226
+ /** Owner grants plan 4 Task 16 (OQ-9): the history row an imported_history item came from, or null when it is gone. */
27227
+ memoryRowForTrace(conversationId, at) {
27228
+ const cols = "role, peer_id, message_id, modality, origin, recorded_at";
27229
+ const r = at.seq !== void 0 ? this.db.prepare(`SELECT ${cols} FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq=?`).get(this.id, conversationId, at.seq) : at.messageId !== void 0 ? this.db.prepare(`SELECT ${cols} FROM agent_memory WHERE identity_id=? AND conversation_id=? AND message_id=?`).get(this.id, conversationId, at.messageId) : void 0;
27230
+ if (!r)
27231
+ return null;
27232
+ return { role: r.role === "agent" ? "agent" : "peer", ts: Number(r.recorded_at), ...r.peer_id ? { peerId: r.peer_id } : {}, ...metaOf(r) };
27233
+ }
27084
27234
  async recordMemoryFact(conversationId, fact) {
27085
27235
  const now = this.memoryNow();
27086
27236
  const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
@@ -28108,7 +28258,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
28108
28258
  }
28109
28259
  /** §14.1 (R-U12): one conversation's history in seq order, opened under the EMPTY AAD. Batched by `afterSeq`; `next` is {at: recorded_at, id: seq}. */
28110
28260
  async readHistoryForMigration(conversationId, sinceMs, afterSeq, limit) {
28111
- const raw = this.db.prepare(`SELECT seq, message_id, role, sealed_text, recorded_at FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at>=? AND seq>?
28261
+ const raw = this.db.prepare(`SELECT seq, message_id, role, sealed_text, recorded_at, modality, origin FROM agent_memory WHERE identity_id=? AND conversation_id=? AND recorded_at>=? AND seq>?
28112
28262
  ORDER BY seq ASC LIMIT ?`).all(this.id, conversationId, sinceMs, afterSeq, limit);
28113
28263
  const rows = [];
28114
28264
  let unreadable = 0;
@@ -28117,7 +28267,15 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
28117
28267
  await yieldToLoop();
28118
28268
  try {
28119
28269
  const text = UTF8.decode(await this.cipher.open(r.sealed_text));
28120
- rows.push({ seq: Number(r.seq), messageId: r.message_id ?? null, role: r.role === "agent" ? "agent" : "peer", text, recordedAt: Number(r.recorded_at) });
28270
+ rows.push({
28271
+ seq: Number(r.seq),
28272
+ messageId: r.message_id ?? null,
28273
+ role: r.role === "agent" ? "agent" : "peer",
28274
+ text,
28275
+ recordedAt: Number(r.recorded_at),
28276
+ modality: r.modality,
28277
+ ...isOrigin(r.origin) ? { origin: r.origin } : {}
28278
+ });
28121
28279
  } catch {
28122
28280
  unreadable++;
28123
28281
  }
@@ -28519,6 +28677,156 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
28519
28677
  const res = expectCreatedAt === void 0 ? this.db.prepare("DELETE FROM mls_owed_followup WHERE identity_id=? AND channel_id=?").run(this.id, channelId) : this.db.prepare("DELETE FROM mls_owed_followup WHERE identity_id=? AND channel_id=? AND created_at=?").run(this.id, channelId, expectCreatedAt);
28520
28678
  return Number(res.changes) > 0;
28521
28679
  }
28680
+ // ---------------------------------------------------------------------------
28681
+ // Owner-signed tool grants, step 1 (plan 1 task 4; spec §7.3, §7.11; D-2).
28682
+ /** The last good manifest wrapper stored for this pinned public key, or null. */
28683
+ async loadToolGrantManifest(pinKeyB64) {
28684
+ const row = this.db.prepare("SELECT counter, sealed_manifest FROM tool_grant_manifest WHERE identity_id=? AND pin_key_b64=?").get(this.id, pinKeyB64);
28685
+ if (!row)
28686
+ return null;
28687
+ return { counter: Number(row.counter), wrapper: await this.unsealJson(row.sealed_manifest) };
28688
+ }
28689
+ /** Stores a verified wrapper. The counter can never fall or repeat, whatever the
28690
+ * caller does: the upsert's WHERE refuses it and this throws. */
28691
+ async saveToolGrantManifest(pinKeyB64, counter, wrapper) {
28692
+ const sealed = await this.sealJson(wrapper);
28693
+ const info = this.db.prepare(`INSERT INTO tool_grant_manifest (identity_id, pin_key_b64, counter, sealed_manifest, updated_at)
28694
+ VALUES (?, ?, ?, ?, ?)
28695
+ ON CONFLICT(identity_id, pin_key_b64) DO UPDATE SET
28696
+ counter = excluded.counter, sealed_manifest = excluded.sealed_manifest, updated_at = excluded.updated_at
28697
+ WHERE excluded.counter > tool_grant_manifest.counter`).run(this.id, pinKeyB64, counter, sealed, Date.now());
28698
+ if (Number(info.changes) === 0)
28699
+ throw new Error("tool grant counter not increasing");
28700
+ }
28701
+ /** Counter rule §6.6: state is per pinned key; a new key starts a new counter. */
28702
+ dropToolGrantManifestsExcept(pinKeyB64) {
28703
+ return Number(this.db.prepare("DELETE FROM tool_grant_manifest WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
28704
+ }
28705
+ // Owner-signed tool grants plan 4, Task 12 (spec §7.5): the room web_search backstop, per room per UTC day.
28706
+ /** A row of another day reads as zero. */
28707
+ roomSearchCapState(channelId, utcDay) {
28708
+ const row = this.db.prepare("SELECT granted_turns, reached_by, notice_posted FROM room_search_cap WHERE identity_id=? AND channel_id=? AND utc_day=?").get(this.id, channelId, utcDay);
28709
+ if (!row)
28710
+ return { grantedTurns: 0, reachedBy: null, noticePosted: false };
28711
+ const by = row.reached_by === "bridge" || row.reached_by === "runtime" ? row.reached_by : null;
28712
+ return { grantedTurns: Number(row.granted_turns), reachedBy: by, noticePosted: Number(row.notice_posted) === 1 };
28713
+ }
28714
+ /** One statement: a row of an earlier day is reset, then +1. Returns the new count. */
28715
+ countRoomSearchGrant(channelId, utcDay) {
28716
+ const row = this.db.prepare(`INSERT INTO room_search_cap (identity_id, channel_id, utc_day, granted_turns, reached_by, notice_posted)
28717
+ VALUES (?, ?, ?, 1, NULL, 0)
28718
+ ON CONFLICT(identity_id, channel_id) DO UPDATE SET
28719
+ granted_turns = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.granted_turns + 1 ELSE 1 END,
28720
+ reached_by = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.reached_by ELSE NULL END,
28721
+ notice_posted = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.notice_posted ELSE 0 END,
28722
+ utc_day = excluded.utc_day
28723
+ RETURNING granted_turns`).get(this.id, channelId, utcDay);
28724
+ return Number(row.granted_turns);
28725
+ }
28726
+ /** True when this call set the flag (the first time today). */
28727
+ markRoomSearchCapReached(channelId, utcDay, by) {
28728
+ const info = this.db.prepare(`INSERT INTO room_search_cap (identity_id, channel_id, utc_day, granted_turns, reached_by, notice_posted)
28729
+ VALUES (?, ?, ?, 0, ?, 0)
28730
+ ON CONFLICT(identity_id, channel_id) DO UPDATE SET
28731
+ granted_turns = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.granted_turns ELSE 0 END,
28732
+ notice_posted = CASE WHEN room_search_cap.utc_day = excluded.utc_day THEN room_search_cap.notice_posted ELSE 0 END,
28733
+ reached_by = excluded.reached_by,
28734
+ utc_day = excluded.utc_day
28735
+ WHERE room_search_cap.utc_day <> excluded.utc_day OR room_search_cap.reached_by IS NULL`).run(this.id, channelId, utcDay, by);
28736
+ return Number(info.changes) > 0;
28737
+ }
28738
+ markRoomSearchCapNoticePosted(channelId, utcDay) {
28739
+ this.db.prepare("UPDATE room_search_cap SET notice_posted = 1 WHERE identity_id=? AND channel_id=? AND utc_day=?").run(this.id, channelId, utcDay);
28740
+ }
28741
+ /** Deletes rows of earlier days; called at start(). */
28742
+ pruneRoomSearchCap(utcDay) {
28743
+ return Number(this.db.prepare("DELETE FROM room_search_cap WHERE identity_id=? AND utc_day < ?").run(this.id, utcDay).changes);
28744
+ }
28745
+ // Owner-signed tool grants plan 4, Task 11 (rulings item 3): the last applied good search block.
28746
+ /** The applied record for this pinned key, or null. `search: null` means "applied with no search block". */
28747
+ async loadToolGrantApplied(pinKeyB64) {
28748
+ const row = this.db.prepare("SELECT sealed_applied FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64=?").get(this.id, pinKeyB64);
28749
+ if (!row)
28750
+ return null;
28751
+ return await this.unsealJson(row.sealed_applied);
28752
+ }
28753
+ async saveToolGrantApplied(pinKeyB64, applied) {
28754
+ const sealed = await this.sealJson({ counter: applied.counter, search: applied.search });
28755
+ this.db.prepare(`INSERT INTO tool_grant_applied (identity_id, pin_key_b64, sealed_applied, updated_at)
28756
+ VALUES (?, ?, ?, ?)
28757
+ ON CONFLICT(identity_id, pin_key_b64) DO UPDATE SET
28758
+ sealed_applied = excluded.sealed_applied, updated_at = excluded.updated_at`).run(this.id, pinKeyB64, sealed, Date.now());
28759
+ }
28760
+ /** Counter rule §6.6: a new pinned key drops the old key's applied record. */
28761
+ dropToolGrantAppliedExcept(pinKeyB64) {
28762
+ return Number(this.db.prepare("DELETE FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
28763
+ }
28764
+ hasOwnerMessage(channelId, messageId) {
28765
+ return this.db.prepare("SELECT 1 FROM owner_replay WHERE identity_id=? AND channel_id=? AND message_id=?").get(this.id, channelId, messageId) !== void 0;
28766
+ }
28767
+ isOwnerEpochClosed(channelId, groupId, epoch) {
28768
+ return this.db.prepare("SELECT 1 FROM owner_replay_closed_epoch WHERE identity_id=? AND channel_id=? AND group_id=? AND epoch=?").get(this.id, channelId, groupId, epoch) !== void 0;
28769
+ }
28770
+ recordOwnerVerifyFailure(channelId, messageId, failedAt = Date.now()) {
28771
+ this.db.prepare("INSERT OR IGNORE INTO owner_verify_failed (identity_id, channel_id, message_id, failed_at) VALUES (?, ?, ?, ?)").run(this.id, channelId, messageId, failedAt);
28772
+ }
28773
+ isOwnerVerifyFailed(messageId) {
28774
+ return this.db.prepare("SELECT 1 FROM owner_verify_failed WHERE identity_id=? AND message_id=?").get(this.id, messageId) !== void 0;
28775
+ }
28776
+ listOwnerReplayChannels() {
28777
+ return this.db.prepare("SELECT DISTINCT channel_id FROM owner_replay WHERE identity_id=? ORDER BY channel_id").all(this.id).map((r) => r.channel_id);
28778
+ }
28779
+ /**
28780
+ * Decision D-2. Per channel with a known current (group, epoch): rows of the
28781
+ * current pair are never pruned; among the others, drop rows older than
28782
+ * `maxAgeMs`, then every row past the newest `maxPerChannel`, oldest first.
28783
+ * Every pair that lost a row is recorded closed. A channel with no current
28784
+ * position is skipped this pass. Old `owner_verify_failed` rows go too.
28785
+ * Returns the number of owner_replay rows removed.
28786
+ */
28787
+ pruneOwnerReplay(nowMs, currentOf, opts = {}) {
28788
+ const maxPerChannel = opts.maxPerChannel ?? OWNER_REPLAY_MAX_PER_CHANNEL;
28789
+ const maxAgeMs = opts.maxAgeMs ?? OWNER_REPLAY_MAX_AGE_MS;
28790
+ const cutoff = nowMs - maxAgeMs;
28791
+ return this.immediate(() => {
28792
+ let removed = 0;
28793
+ for (const channelId of this.listOwnerReplayChannels()) {
28794
+ const cur = currentOf.get(channelId);
28795
+ if (!cur)
28796
+ continue;
28797
+ const others = this.db.prepare(`SELECT rowid AS rid, group_id, epoch, processed_at FROM owner_replay
28798
+ WHERE identity_id=? AND channel_id=? AND NOT (group_id=? AND epoch=?)
28799
+ ORDER BY processed_at DESC, rowid DESC`).all(this.id, channelId, cur.groupId, cur.epoch);
28800
+ const young = others.filter((r) => Number(r.processed_at) >= cutoff);
28801
+ const victims = [...others.filter((r) => Number(r.processed_at) < cutoff), ...young.slice(maxPerChannel)];
28802
+ const closed = /* @__PURE__ */ new Map();
28803
+ const del = this.db.prepare("DELETE FROM owner_replay WHERE rowid=?");
28804
+ for (const v of victims) {
28805
+ removed += Number(del.run(v.rid).changes);
28806
+ closed.set(`${v.group_id}\0${v.epoch}`, { groupId: v.group_id, epoch: Number(v.epoch) });
28807
+ }
28808
+ const close = this.db.prepare(`INSERT OR IGNORE INTO owner_replay_closed_epoch (identity_id, channel_id, group_id, epoch, closed_at)
28809
+ VALUES (?, ?, ?, ?, ?)`);
28810
+ for (const p of closed.values()) {
28811
+ if (p.groupId === cur.groupId && p.epoch === cur.epoch)
28812
+ continue;
28813
+ close.run(this.id, channelId, p.groupId, p.epoch, nowMs);
28814
+ }
28815
+ }
28816
+ this.db.prepare("DELETE FROM owner_verify_failed WHERE identity_id=? AND failed_at < ?").run(this.id, cutoff);
28817
+ return removed;
28818
+ });
28819
+ }
28820
+ failNextRuntimeInboxInsert = false;
28821
+ /** @internal test-only: the next runtime_inbox INSERT inside the staging transaction throws once. */
28822
+ _failNextRuntimeInboxInsertForTest() {
28823
+ this.failNextRuntimeInboxInsert = true;
28824
+ }
28825
+ /** @internal test-only */
28826
+ _insertOwnerReplayForTest(channelId, messageId, groupId, epoch, processedAt) {
28827
+ this.db.prepare(`INSERT INTO owner_replay (identity_id, channel_id, message_id, group_id, epoch, processed_at)
28828
+ VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, channelId, messageId, groupId, epoch, processedAt);
28829
+ }
28522
28830
  /** @internal test-only */
28523
28831
  _rawMainForTest(identityId, channelId) {
28524
28832
  const row = this.db.prepare("SELECT sealed_main FROM mls_group_state WHERE identity_id=? AND channel_id=?").get(identityId, channelId);
@@ -28671,8 +28979,8 @@ function loadPackageMeta(importMetaUrl) {
28671
28979
  }
28672
28980
 
28673
28981
  // src/bridge-agent.ts
28674
- import { promises as fsp, statSync as statSync4 } from "node:fs";
28675
- import { join as join16, dirname as dirname4 } from "node:path";
28982
+ import { existsSync as existsSync5, promises as fsp, statSync as statSync4 } from "node:fs";
28983
+ import { join as join17, dirname as dirname5 } from "node:path";
28676
28984
  import { fileURLToPath as fileURLToPath2 } from "node:url";
28677
28985
 
28678
28986
  // src/latency.ts
@@ -29263,8 +29571,8 @@ async function prepareAttachment(filePath2, caption, channelId, deps) {
29263
29571
  async function materializeAttachment(body, ctx, deps) {
29264
29572
  await deps.ready();
29265
29573
  const ciphertext = await deps.downloadBlob(body.blobId);
29266
- const sha25611 = await deps.computeFileDigest(ciphertext);
29267
- if (sha25611 !== body.digest) {
29574
+ const sha25612 = await deps.computeFileDigest(ciphertext);
29575
+ if (sha25612 !== body.digest) {
29268
29576
  throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
29269
29577
  }
29270
29578
  const plain = await deps.decryptFile(
@@ -29277,7 +29585,7 @@ async function materializeAttachment(body, ctx, deps) {
29277
29585
  const path = `${dir}/${ctx.messageId}-${safeName}`;
29278
29586
  await deps.mkdirp(dir);
29279
29587
  await deps.writeFile(path, plain);
29280
- return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha25611 };
29588
+ return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha25612 };
29281
29589
  }
29282
29590
 
29283
29591
  // src/sodium-ready.ts
@@ -29866,11 +30174,11 @@ function quoteFromMessage(note, record) {
29866
30174
  }
29867
30175
  function renderNotes(notes) {
29868
30176
  if (notes.length === 0) return void 0;
29869
- const lines = notes.map((n) => {
30177
+ const lines2 = notes.map((n) => {
29870
30178
  const one = oneLine(n.text);
29871
30179
  return `- [n${n.id}] ${/^[#\-\[]/.test(one) ? `\\${one}` : one}`;
29872
30180
  });
29873
- return [NOTES_HEADING, ...lines].join("\n");
30181
+ return [NOTES_HEADING, ...lines2].join("\n");
29874
30182
  }
29875
30183
 
29876
30184
  // src/memory/secret-scan.ts
@@ -30619,7 +30927,7 @@ function gcModels(root3, keepModelIds, now, maxAgeMs = THIRTY_DAYS) {
30619
30927
 
30620
30928
  // src/memory/embedder/manifest.ts
30621
30929
  var MODEL_ORIGIN = "https://downloads.securecoms.ai";
30622
- var file = (modelId, name, sha25611, size) => Object.freeze({ name, urlPath: `/models/embeddings/${modelId}/${sha25611}/${name}`, sha256: sha25611, size });
30930
+ var file = (modelId, name, sha25612, size) => Object.freeze({ name, urlPath: `/models/embeddings/${modelId}/${sha25612}/${name}`, sha256: sha25612, size });
30623
30931
  var MODEL_ID = "minilm-l6-v2-int8";
30624
30932
  var EMBEDDING_MANIFEST = Object.freeze({
30625
30933
  modelId: MODEL_ID,
@@ -30963,8 +31271,8 @@ import { join as join11, win32 } from "node:path";
30963
31271
  var WINDOWS_SEMANTIC_ENABLED = false;
30964
31272
  var windowsSemanticOn = (platform, enabled = WINDOWS_SEMANTIC_ENABLED) => platform !== "win32" || enabled;
30965
31273
  function parseSaveSddl(saveFile) {
30966
- const lines = saveFile.toString("utf16le").replace(/^\uFEFF/, "").split(/\r?\n/);
30967
- return (lines[1] ?? "").trim();
31274
+ const lines2 = saveFile.toString("utf16le").replace(/^\uFEFF/, "").split(/\r?\n/);
31275
+ return (lines2[1] ?? "").trim();
30968
31276
  }
30969
31277
  function aclAllows(sddl, ownerSid) {
30970
31278
  const allowed = /* @__PURE__ */ new Set([ownerSid, "SY", "BA"]);
@@ -32359,7 +32667,15 @@ var ItemBook = class {
32359
32667
  originAt: r.originAt,
32360
32668
  storedAt
32361
32669
  });
32362
- const rec = (text, chunks) => ({ text, source: r.source, trust: r.trust, ...r.sourceRef !== void 0 ? { sourceRef: r.sourceRef } : {}, ...chunks ? { chunks } : {} });
32670
+ const rec = (text, chunks) => ({
32671
+ text,
32672
+ source: r.source,
32673
+ trust: r.trust,
32674
+ ...r.sourceRef !== void 0 ? { sourceRef: r.sourceRef } : {},
32675
+ ...chunks ? { chunks } : {},
32676
+ ...r.role !== void 0 ? { role: r.role } : {},
32677
+ ...r.origin !== void 0 ? { origin: r.origin } : {}
32678
+ });
32363
32679
  if (chunked) parts.push({ itemId: first, chunkIndex: 0, parentId: null, parentOriginId: "", sealedRecord: await this.d.store.sealItemRecord(fields(0), rec("", texts.length)), sealedVec: null, modelId: null, tokenMacs: [], embeddable: false });
32364
32680
  const inlineOk = r.pendingOnly !== true && !this.d.paused() && this.d.embedOn() && this.d.embedder.state().state === "ready";
32365
32681
  for (const [i, text] of texts.entries()) {
@@ -32638,7 +32954,7 @@ var UnifiedReadBook = class {
32638
32954
  this.entries.set(p.messageId, { channelId: p.channelId, scopeId: p.scopeId, capture, pins: pins?.pins ?? [], pinnedMore: pins?.more ?? 0, items });
32639
32955
  }
32640
32956
  /** ws flush (synchronous): the entry's own channel and owner scope, its capture still current (H-4, U11). */
32641
- deliverable(messageId, channelId, scopeIdNow) {
32957
+ deliverable(messageId, channelId, scopeIdNow, keep = () => true) {
32642
32958
  const e = this.entries.get(messageId);
32643
32959
  if (!e || e.channelId !== channelId || e.scopeId !== scopeIdNow) return void 0;
32644
32960
  if (!same(e.capture, this.d.store.itemCapture(channelId))) {
@@ -32647,8 +32963,9 @@ var UnifiedReadBook = class {
32647
32963
  }
32648
32964
  const at = (i) => ({ id: i.itemId, source: i.source, at: i.originAt, text: clampText(i.text) });
32649
32965
  const all = [
32650
- ...e.pins.map((i) => ({ block: "pinned", ...at(i) })),
32651
- ...e.items.map((i) => ({ block: i.trust === "owner" ? "owner" : "reference", ...at(i) }))
32966
+ // Owner grants plan 4 Task 16: `keep` is the voice fence on a pinned agent (never a SQL read here; the bridge supplies it).
32967
+ ...e.pins.filter(keep).map((i) => ({ block: "pinned", ...at(i) })),
32968
+ ...e.items.filter(keep).map((i) => ({ block: i.trust === "owner" ? "owner" : "reference", ...at(i) }))
32652
32969
  ];
32653
32970
  const items = all.slice(0, FRAME_ITEMS_MAX);
32654
32971
  const pinsCut = Math.max(0, e.pins.length - FRAME_ITEMS_MAX);
@@ -32693,6 +33010,56 @@ var UnifiedReadBook = class {
32693
33010
  }
32694
33011
  };
32695
33012
 
33013
+ // src/grants/voice-fence.ts
33014
+ import { createHash as createHash4 } from "node:crypto";
33015
+ var FAST_ID_EPOCH_MS = 17887392e5;
33016
+ function legacyFastReplyId(messageId) {
33017
+ const h = createHash4("sha256").update(`fast:${messageId}`).digest();
33018
+ h[6] = h[6] & 15 | 64;
33019
+ h[8] = h[8] & 63 | 128;
33020
+ const x = h.subarray(0, 16).toString("hex");
33021
+ return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
33022
+ }
33023
+ function classifyAgentLine(line, ctx) {
33024
+ if (line.origin === "fast_path" || line.origin === "reply" || line.origin === "untrusted") return line.origin;
33025
+ if (line.modality === "voice") return "fast_path";
33026
+ if (line.messageId !== void 0 && ctx.fastIds.has(line.messageId)) return "fast_path";
33027
+ if (line.ts !== void 0 && line.ts < FAST_ID_EPOCH_MS) return "fast_path";
33028
+ if (line.modality === "text") return "reply";
33029
+ return "fast_path";
33030
+ }
33031
+ function isRelayClassPeer(line, ownerId) {
33032
+ return line.modality === "voice" || line.peerId !== ownerId;
33033
+ }
33034
+ function parseHistoryRef(ref) {
33035
+ const m = /^agent_memory:(.+):(\d+)$/.exec(ref);
33036
+ return m ? { conversationId: m[1], seq: Number(m[2]) } : null;
33037
+ }
33038
+ function classifyUnifiedItem(item, trace, ctx, strict, keepUntrusted = false) {
33039
+ if (strict?.turnChannelId !== void 0 && item.channelId !== strict.turnChannelId) {
33040
+ if (item.source === "owner_typed") return "keep";
33041
+ if (item.source !== "imported_history") return "exclude";
33042
+ const row2 = traceItem(item, trace);
33043
+ return row2 !== null && row2.role === "peer" && !isRelayClassPeer(row2, strict.ownerId) ? "keep" : "exclude";
33044
+ }
33045
+ if (strict && item.source === "owner_voice") return "exclude";
33046
+ if (item.role === "peer" && !(strict && item.source === "imported_history")) return "keep";
33047
+ if (item.role !== "peer" && item.origin !== void 0) return item.origin === "reply" || keepUntrusted && item.origin === "untrusted" ? "keep" : "exclude";
33048
+ if (item.source === "agent_reply") return "keep";
33049
+ if (item.source !== "imported_history") return "keep";
33050
+ const row = traceItem(item, trace);
33051
+ if (row === null) return "exclude";
33052
+ if (row.role === "peer") return strict && isRelayClassPeer(row, strict.ownerId) ? "exclude" : "keep";
33053
+ const origin = classifyAgentLine(row, ctx);
33054
+ return origin === "reply" || keepUntrusted && origin === "untrusted" ? "keep" : "exclude";
33055
+ }
33056
+ function traceItem(item, trace) {
33057
+ const ref = item.sourceRef !== void 0 ? parseHistoryRef(item.sourceRef) : null;
33058
+ if (ref !== null) return trace(ref.conversationId, { seq: ref.seq });
33059
+ const seqForm = /^seq:(\d+)$/.exec(item.originId);
33060
+ return seqForm ? trace(item.channelId, { seq: Number(seqForm[1]) }) : trace(item.channelId, { messageId: item.originId });
33061
+ }
33062
+
32696
33063
  // src/memory/unified/migrate.ts
32697
33064
  var OLD_TABLES_PRESENT = true;
32698
33065
  var MIGRATION_BATCH = 100;
@@ -32845,7 +33212,10 @@ async function runMigration(d, o = {}) {
32845
33212
  text: r.text,
32846
33213
  sourceRef: historySourceRef(c, r.seq),
32847
33214
  pendingOnly: true,
32848
- expect
33215
+ expect,
33216
+ // Owner grants plan 4 Task 16: the row's role and, for an agent row, its classified origin.
33217
+ role: r.role,
33218
+ ...r.role === "agent" ? { origin: d.classifyHistoryOrigin ? d.classifyHistoryOrigin(c, r) : classifyAgentLine({ origin: r.origin, modality: r.modality, ...r.messageId ? { messageId: r.messageId } : {}, ts: r.recordedAt }, { fastIds: /* @__PURE__ */ new Set() }) } : {}
32849
33219
  }),
32850
33220
  (r) => r.recordedAt,
32851
33221
  t
@@ -32903,9 +33273,9 @@ async function recordInboundMemory(sink, channelId, senderId, text, messageId, m
32903
33273
  } catch {
32904
33274
  }
32905
33275
  }
32906
- async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text", expectPurgeEpoch) {
33276
+ async function recordOutboundMemory(sink, channelId, text, messageId, modality = "text", expectPurgeEpoch, origin) {
32907
33277
  try {
32908
- await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId, ...expectPurgeEpoch === void 0 ? {} : { expectPurgeEpoch } });
33278
+ await sink.record({ conversationId: channelId, role: "agent", text, modality, messageId, ...expectPurgeEpoch === void 0 ? {} : { expectPurgeEpoch }, ...origin ? { origin } : {} });
32909
33279
  } catch {
32910
33280
  }
32911
33281
  }
@@ -33261,10 +33631,10 @@ function splitFastReply(raw, maxChars = 200, maxSentences = 2) {
33261
33631
  return { reply: normalizeFastReply(raw.replace(DEFER_RE, " "), maxChars, maxSentences), defer };
33262
33632
  }
33263
33633
  function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
33264
- const lines = raw.split(/\r?\n+/).map(
33634
+ const lines2 = raw.split(/\r?\n+/).map(
33265
33635
  (l) => l.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").replace(/^\s*#{1,6}\s+/, "").replace(/[*_`]+/g, "").trim()
33266
33636
  ).filter(Boolean);
33267
- const sentences = lines.flatMap((l) => l.split(SENTENCE_END)).map((s) => s.trim()).filter(Boolean);
33637
+ const sentences = lines2.flatMap((l) => l.split(SENTENCE_END)).map((s) => s.trim()).filter(Boolean);
33268
33638
  return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
33269
33639
  }
33270
33640
 
@@ -33476,18 +33846,18 @@ function parseSummarizeBody(parsed, selfIdentityId, log = () => {
33476
33846
  function transcriptToPrompt(transcript) {
33477
33847
  const names = /* @__PURE__ */ new Map();
33478
33848
  for (const p of transcript.participants ?? []) names.set(p.identity_id, p.display_name);
33479
- const lines = transcript.turns.map(
33849
+ const lines2 = transcript.turns.map(
33480
33850
  (t) => `${names.get(t.speaker_identity_id) ?? t.speaker_identity_id}: ${t.text}`
33481
33851
  );
33482
- return `${SUMMARY_PROMPT}${lines.join("\n")}
33852
+ return `${SUMMARY_PROMPT}${lines2.join("\n")}
33483
33853
  `;
33484
33854
  }
33485
33855
  function parseSummaryOutput(text) {
33486
- const lines = text.split("\n");
33487
- const idx = lines.findIndex((line) => /^\s*Action items:\s*$/i.test(line));
33856
+ const lines2 = text.split("\n");
33857
+ const idx = lines2.findIndex((line) => /^\s*Action items:\s*$/i.test(line));
33488
33858
  if (idx < 0) return { paragraph: text.trim(), actionItems: [] };
33489
- const paragraph = lines.slice(0, idx).join("\n").trim();
33490
- const actionItems = lines.slice(idx + 1).map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").trim()).filter((line) => line.length > 0);
33859
+ const paragraph = lines2.slice(0, idx).join("\n").trim();
33860
+ const actionItems = lines2.slice(idx + 1).map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").trim()).filter((line) => line.length > 0);
33491
33861
  return { paragraph, actionItems };
33492
33862
  }
33493
33863
  function buildSummaryBody(request, out, now) {
@@ -33527,7 +33897,7 @@ async function runSummarize(opts) {
33527
33897
  }
33528
33898
 
33529
33899
  // src/service/runtime-config-dir.ts
33530
- import { createHash as createHash4, randomUUID } from "node:crypto";
33900
+ import { createHash as createHash5, randomUUID } from "node:crypto";
33531
33901
  import {
33532
33902
  chmodSync as chmodSync4,
33533
33903
  existsSync as existsSync4,
@@ -33562,7 +33932,24 @@ function assertManagedName(name) {
33562
33932
  }
33563
33933
  function isLocalOnlyName(name) {
33564
33934
  const n = name.toLowerCase().replace(/[. ]+$/, "");
33565
- return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets";
33935
+ return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE;
33936
+ }
33937
+ var MANIFEST_POLICY_FILE = "manifest-policy.json";
33938
+ function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
33939
+ try {
33940
+ deps.unlink(join15(runtimeConfigDir(home, identityId), "llm-source.json"));
33941
+ } catch (e) {
33942
+ if (e?.code !== "ENOENT") throw e;
33943
+ }
33944
+ }
33945
+ function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
33946
+ const parent3 = join15(home, ".securecoms", "runtime-config");
33947
+ const dir = runtimeConfigDir(home, identityId);
33948
+ for (const d of [join15(home, ".securecoms"), parent3, dir]) {
33949
+ deps.mkdir(d, 448);
33950
+ deps.chmod(d, 448);
33951
+ }
33952
+ atomicWrite(join15(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
33566
33953
  }
33567
33954
  function atomicWrite(path, body, deps) {
33568
33955
  const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
@@ -33618,7 +34005,7 @@ function checkConfiguredPaths(skillsJson, statFn = (p) => {
33618
34005
  });
33619
34006
  }
33620
34007
  function lineHash(line) {
33621
- return createHash4("sha256").update(line.trim(), "utf8").digest("hex");
34008
+ return createHash5("sha256").update(line.trim(), "utf8").digest("hex");
33622
34009
  }
33623
34010
  var LOCAL_REPORT_FILE = "local-config.json";
33624
34011
  var SECRET_PATH_COUNT_MAX = 1e4;
@@ -33647,7 +34034,7 @@ var LEGACY_MEMORY_REPORT_MAX_SOURCES = 100;
33647
34034
  var LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
33648
34035
  var LEGACY_MEMORY_FACT_DECISION_MAX = 500;
33649
34036
  function legacySourceId(source, bytes) {
33650
- return createHash4("sha256").update(source.kind).update("\0").update(source.channel_id ?? "").update("\0").update(source.path).update("\0").update(bytes).digest("hex");
34037
+ return createHash5("sha256").update(source.kind).update("\0").update(source.channel_id ?? "").update("\0").update(source.path).update("\0").update(bytes).digest("hex");
33651
34038
  }
33652
34039
  function localLegacySources(doc) {
33653
34040
  if (!Array.isArray(doc.legacy_memory)) return [];
@@ -33819,6 +34206,568 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
33819
34206
  return removed;
33820
34207
  }
33821
34208
 
34209
+ // src/grants/binding.ts
34210
+ import { sha256 as sha25611 } from "@noble/hashes/sha256";
34211
+
34212
+ // src/grants/manifest.ts
34213
+ import { ed25519 as ed255195 } from "@noble/curves/ed25519";
34214
+
34215
+ // src/grants/pin.ts
34216
+ import { lstatSync as lstatSync7, readFileSync as readFileSync9 } from "node:fs";
34217
+ import { ed25519 as ed255194 } from "@noble/curves/ed25519";
34218
+ import { dirname as dirname4, join as join16 } from "node:path";
34219
+ var PIN_FILE_NAME = "owner-signing-key.json";
34220
+ var UUID_LOWER = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
34221
+ var PIN_KEYS = ["key_algo", "owner_identity_id", "public_key"];
34222
+ function canonicalBase64Bytes(v, len) {
34223
+ if (typeof v !== "string" || v.length === 0) return null;
34224
+ const buf = Buffer.from(v, "base64");
34225
+ if (buf.length !== len || buf.toString("base64") !== v) return null;
34226
+ return new Uint8Array(buf);
34227
+ }
34228
+ function parsePinObject(v) {
34229
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return { ok: false, state: "malformed" };
34230
+ const keys = Object.keys(v).sort();
34231
+ if (keys.length !== PIN_KEYS.length || keys.some((k, i) => k !== PIN_KEYS[i])) return { ok: false, state: "malformed" };
34232
+ const o = v;
34233
+ if (typeof o.key_algo !== "string") return { ok: false, state: "malformed" };
34234
+ if (o.key_algo !== "ed25519") return { ok: false, state: "unsupported_algo" };
34235
+ if (typeof o.owner_identity_id !== "string" || !UUID_LOWER.test(o.owner_identity_id)) return { ok: false, state: "malformed" };
34236
+ const publicKey = canonicalBase64Bytes(o.public_key, 32);
34237
+ if (!publicKey) return { ok: false, state: "malformed" };
34238
+ try {
34239
+ if (ed255194.ExtendedPoint.fromHex(publicKey).isSmallOrder()) return { ok: false, state: "malformed" };
34240
+ } catch {
34241
+ return { ok: false, state: "malformed" };
34242
+ }
34243
+ return {
34244
+ ok: true,
34245
+ pin: Object.freeze({ ownerIdentityId: o.owner_identity_id, keyAlgo: "ed25519", publicKey, publicKeyB64: o.public_key })
34246
+ };
34247
+ }
34248
+ var NODE_FS = { lstat: (p) => lstatSync7(p), readFile: (p) => readFileSync9(p, "utf8") };
34249
+ function fromValue(v) {
34250
+ const r = parsePinObject(v);
34251
+ return r.ok ? { kind: "ok", pin: r.pin } : { kind: "bad", state: r.state };
34252
+ }
34253
+ function readPinFile(path, fs) {
34254
+ let st;
34255
+ try {
34256
+ st = fs.lstat(path);
34257
+ } catch (e) {
34258
+ if (e?.code === "ENOENT") return { kind: "absent" };
34259
+ return { kind: "bad", state: "unreadable" };
34260
+ }
34261
+ if (st.isSymbolicLink()) return { kind: "bad", state: "symlink_refused" };
34262
+ let text;
34263
+ try {
34264
+ text = fs.readFile(path);
34265
+ } catch {
34266
+ return { kind: "bad", state: "unreadable" };
34267
+ }
34268
+ if (st.mode & 63) writeServiceLog2("warn", "config.pin_permissions_unsafe");
34269
+ let json2;
34270
+ try {
34271
+ json2 = JSON.parse(text);
34272
+ } catch {
34273
+ return { kind: "bad", state: "malformed" };
34274
+ }
34275
+ return fromValue(json2);
34276
+ }
34277
+ function samePin(a, b) {
34278
+ return a.ownerIdentityId === b.ownerIdentityId && a.keyAlgo === b.keyAlgo && a.publicKeyB64 === b.publicKeyB64;
34279
+ }
34280
+ function loadOwnerGrantPin(opts) {
34281
+ const fs = opts.fs ?? NODE_FS;
34282
+ const file2 = readPinFile(join16(dirname4(opts.credentialPath), PIN_FILE_NAME), fs);
34283
+ const field = "owner_signing_key" in opts.credentialJson && opts.credentialJson.owner_signing_key !== void 0 ? fromValue(opts.credentialJson.owner_signing_key) : { kind: "absent" };
34284
+ if (file2.kind === "bad") return { state: file2.state, pin: null };
34285
+ if (field.kind === "bad") return { state: field.state, pin: null };
34286
+ if (file2.kind === "absent" && field.kind === "absent") return { state: "no_pin", pin: null };
34287
+ if (file2.kind === "ok" && field.kind === "ok") {
34288
+ return samePin(file2.pin, field.pin) ? { state: "ok", pin: file2.pin } : { state: "pin_conflict", pin: null };
34289
+ }
34290
+ const one = file2.kind === "ok" ? file2.pin : field.pin;
34291
+ return { state: "ok", pin: one };
34292
+ }
34293
+
34294
+ // src/grants/manifest.ts
34295
+ var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
34296
+ var MANIFEST_KNOWN_TOOLS = Object.freeze({
34297
+ owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
34298
+ team_room: Object.freeze(["web_search"])
34299
+ });
34300
+ function manifestSignedBytes(manifest) {
34301
+ const domain = new TextEncoder().encode(TOOL_GRANTS_DOMAIN);
34302
+ const body = canonicalize(manifest);
34303
+ const out = new Uint8Array(domain.length + body.length);
34304
+ out.set(domain, 0);
34305
+ out.set(body, domain.length);
34306
+ return out;
34307
+ }
34308
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
34309
+ function wellTyped(v) {
34310
+ if (typeof v === "string") return true;
34311
+ if (typeof v === "number") return Number.isSafeInteger(v) && v >= 0;
34312
+ if (Array.isArray(v)) return v.every(wellTyped);
34313
+ if (isPlainObject(v)) return Object.values(v).every(wellTyped);
34314
+ return false;
34315
+ }
34316
+ var WRAPPER_KEYS = ["algo", "manifest", "signature"];
34317
+ var HEX64 = /^[0-9a-f]{64}$/;
34318
+ var PRINTABLE_ASCII = /^[\x21-\x7e]+$/;
34319
+ var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII.test(v);
34320
+ var exactKeys = (o, required, optional = []) => required.every((k) => k in o) && Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
34321
+ var isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
34322
+ function parseEndpoints(v) {
34323
+ if (v === void 0) return {};
34324
+ if (!isPlainObject(v)) return null;
34325
+ const out = {};
34326
+ for (const [name, raw] of Object.entries(v)) {
34327
+ if (!isPlainObject(raw)) return null;
34328
+ const e = { ...raw };
34329
+ if (e.headersFile === "") delete e.headersFile;
34330
+ if (!exactKeys(e, ["baseUrl"], ["headersFile"])) return null;
34331
+ if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII.test(e.baseUrl)) return null;
34332
+ let url;
34333
+ try {
34334
+ url = new URL(e.baseUrl);
34335
+ } catch {
34336
+ return null;
34337
+ }
34338
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
34339
+ if (url.username !== "" || url.password !== "" || url.href !== e.baseUrl) return null;
34340
+ if ("headersFile" in e && !absolutePath(e.headersFile)) return null;
34341
+ out[name] = Object.freeze({ baseUrl: e.baseUrl, ...typeof e.headersFile === "string" ? { headersFile: e.headersFile } : {} });
34342
+ }
34343
+ return out;
34344
+ }
34345
+ function parseDatabases(v) {
34346
+ if (v === void 0) return {};
34347
+ if (!isPlainObject(v)) return null;
34348
+ const out = {};
34349
+ for (const [name, d] of Object.entries(v)) {
34350
+ if (!isPlainObject(d)) return null;
34351
+ const keys = Object.keys(d);
34352
+ if (keys.length !== 1 || keys[0] !== "sqlite" && keys[0] !== "postgresUrlFile") return null;
34353
+ const path = d[keys[0]];
34354
+ if (!absolutePath(path)) return null;
34355
+ out[name] = Object.freeze(keys[0] === "sqlite" ? { sqlite: path } : { postgresUrlFile: path });
34356
+ }
34357
+ return out;
34358
+ }
34359
+ function parseConfig(v) {
34360
+ if (!isPlainObject(v)) return null;
34361
+ if (!exactKeys(v, ["persona_sha256", "flags", "memory", "legacy_assign_sha256"], ["llm", "search", "tools_allow"])) return null;
34362
+ if (typeof v.persona_sha256 !== "string" || !HEX64.test(v.persona_sha256)) return null;
34363
+ if (typeof v.legacy_assign_sha256 !== "string" || !HEX64.test(v.legacy_assign_sha256)) return null;
34364
+ if (!isStringArray(v.flags)) return null;
34365
+ if ("tools_allow" in v && !isStringArray(v.tools_allow)) return null;
34366
+ if ("llm" in v) {
34367
+ const l = v.llm;
34368
+ if (!isPlainObject(l) || !exactKeys(l, ["provider", "model"]) || typeof l.provider !== "string" || !l.provider || typeof l.model !== "string" || !l.model) return null;
34369
+ }
34370
+ if ("search" in v) {
34371
+ const sr = v.search;
34372
+ if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]) || sr.provider !== "brave" && sr.provider !== "tavily") return null;
34373
+ }
34374
+ const mem = v.memory;
34375
+ if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
34376
+ if (mem.mode !== "markdown" && mem.mode !== "semantic" && mem.mode !== "off") return null;
34377
+ if (mem.mode === "semantic" !== "recall_days" in mem) return null;
34378
+ if ("recall_days" in mem && mem.recall_days !== 30 && mem.recall_days !== 90 && mem.recall_days !== 365) return null;
34379
+ return Object.freeze({
34380
+ persona_sha256: v.persona_sha256,
34381
+ ...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
34382
+ ...isPlainObject(v.search) ? { search: Object.freeze({ provider: v.search.provider }) } : {},
34383
+ ...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
34384
+ flags: Object.freeze([...v.flags]),
34385
+ memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
34386
+ legacy_assign_sha256: v.legacy_assign_sha256
34387
+ });
34388
+ }
34389
+ function verifyToolGrantManifest(raw, ctx) {
34390
+ if (!isPlainObject(raw)) return { state: "malformed" };
34391
+ const keys = Object.keys(raw).sort();
34392
+ if (keys.length !== WRAPPER_KEYS.length || keys.some((k, i) => k !== WRAPPER_KEYS[i])) return { state: "malformed" };
34393
+ if (typeof raw.algo !== "string") return { state: "malformed" };
34394
+ if (raw.algo !== "ed25519") return { state: "unsupported_algo" };
34395
+ const m = raw.manifest;
34396
+ if (!isPlainObject(m)) return { state: "malformed" };
34397
+ if (m.type !== "securecoms.tool-grants" || m.v !== 1) return { state: "malformed" };
34398
+ if (!wellTyped(m)) return { state: "malformed" };
34399
+ if (typeof m.agent_identity_id !== "string" || typeof m.owner_identity_id !== "string") return { state: "malformed" };
34400
+ for (const k of ["owner_dm_channel_id", "owner_dm_relay_identity_id"]) {
34401
+ if (k in m && typeof m[k] !== "string") return { state: "malformed" };
34402
+ }
34403
+ if (!isPlainObject(m.grants)) return { state: "malformed" };
34404
+ for (const list of Object.values(m.grants)) {
34405
+ if (!Array.isArray(list) || !list.every((t) => typeof t === "string")) return { state: "malformed" };
34406
+ }
34407
+ const endpoints = parseEndpoints(m.endpoints);
34408
+ const databases = parseDatabases(m.databases);
34409
+ const config = parseConfig(m.config);
34410
+ if (endpoints === null || databases === null || config === null) return { state: "malformed" };
34411
+ const sig = canonicalBase64Bytes(raw.signature, 64);
34412
+ if (!sig) return { state: "malformed" };
34413
+ let good;
34414
+ try {
34415
+ good = ed255195.verify(sig, manifestSignedBytes(m), ctx.pin.publicKey, { zip215: false });
34416
+ } catch {
34417
+ return { state: "malformed" };
34418
+ }
34419
+ if (!good) return { state: "bad_signature" };
34420
+ if (m.agent_identity_id !== ctx.agentIdentityId) return { state: "wrong_agent" };
34421
+ if (m.owner_identity_id !== ctx.pin.ownerIdentityId) return { state: "wrong_owner" };
34422
+ const counter = m.counter;
34423
+ if (typeof counter !== "number" || !Number.isSafeInteger(counter) || counter < 1) return { state: "malformed" };
34424
+ if (ctx.storedCounter !== null) {
34425
+ if (counter === ctx.storedCounter) return { state: "unchanged" };
34426
+ if (counter < ctx.storedCounter) return { state: "rollback" };
34427
+ }
34428
+ let droppedUnknown = false;
34429
+ const grants = {};
34430
+ for (const [audience, list] of Object.entries(m.grants)) {
34431
+ if (audience !== "owner_dm" && audience !== "team_room") {
34432
+ droppedUnknown = true;
34433
+ continue;
34434
+ }
34435
+ const known = MANIFEST_KNOWN_TOOLS[audience];
34436
+ const kept = list.filter((t) => known.includes(t));
34437
+ if (kept.length !== list.length) droppedUnknown = true;
34438
+ grants[audience] = Object.freeze(kept);
34439
+ }
34440
+ const manifest = Object.freeze({
34441
+ type: "securecoms.tool-grants",
34442
+ v: 1,
34443
+ agent_identity_id: m.agent_identity_id,
34444
+ owner_identity_id: m.owner_identity_id,
34445
+ ...typeof m.owner_dm_channel_id === "string" ? { owner_dm_channel_id: m.owner_dm_channel_id } : {},
34446
+ ...typeof m.owner_dm_relay_identity_id === "string" ? { owner_dm_relay_identity_id: m.owner_dm_relay_identity_id } : {},
34447
+ counter,
34448
+ grants: Object.freeze(grants),
34449
+ endpoints: Object.freeze(endpoints),
34450
+ databases: Object.freeze(databases),
34451
+ config
34452
+ });
34453
+ return { state: "ok", manifest, droppedUnknown, wrapper: raw };
34454
+ }
34455
+
34456
+ // src/grants/persona-render.ts
34457
+ var PERSONA_MAX_BYTES = 8192;
34458
+ var TONE_FIELD_ORDER = ["humor", "length", "warmth", "formality"];
34459
+ var TONE_WORDS = Object.freeze({
34460
+ warmth: { reserved: "Be plain and businesslike.", plain: "Be plain and businesslike.", warm: "Be warm.", friendly: "Be friendly." },
34461
+ length: { brief: "Keep answers short.", thorough: "Give the full picture when it matters." },
34462
+ formality: { casual: "Write casually.", formal: "Write formally." },
34463
+ humor: { none: "Do not use humor.", light: "Use light humor when it fits.", playful: "Be playful." }
34464
+ });
34465
+ var HANDOFF_TEXT = Object.freeze({
34466
+ money: "money is moving",
34467
+ legal: "someone wants a legal commitment",
34468
+ upset: "the person is upset"
34469
+ });
34470
+ var TOOL_PARAGRAPH = "Use a tool when the answer depends on something you cannot know. Which tools you have depends on the channel, so rely only on the tools offered in this turn, not on anything said earlier in a conversation. When a request needs a tool you were not offered, say so plainly and never claim you looked something up. When you searched or fetched a page, answer from what it says and name the source; never reply with only a link, and never read a URL aloud.";
34471
+ var PY_SPACE = /* @__PURE__ */ new Set([
34472
+ 9,
34473
+ 10,
34474
+ 11,
34475
+ 12,
34476
+ 13,
34477
+ 28,
34478
+ 29,
34479
+ 30,
34480
+ 31,
34481
+ 32,
34482
+ 133,
34483
+ 160,
34484
+ 5760,
34485
+ 8192,
34486
+ 8193,
34487
+ 8194,
34488
+ 8195,
34489
+ 8196,
34490
+ 8197,
34491
+ 8198,
34492
+ 8199,
34493
+ 8200,
34494
+ 8201,
34495
+ 8202,
34496
+ 8232,
34497
+ 8233,
34498
+ 8239,
34499
+ 8287,
34500
+ 12288
34501
+ ]);
34502
+ function pyStrip(s) {
34503
+ let a = 0;
34504
+ let b = s.length;
34505
+ while (a < b && PY_SPACE.has(s.charCodeAt(a))) a++;
34506
+ while (b > a && PY_SPACE.has(s.charCodeAt(b - 1))) b--;
34507
+ return s.slice(a, b);
34508
+ }
34509
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
34510
+ var optString = (v) => v === void 0 || v === null || typeof v === "string";
34511
+ var optStrings = (v) => v === void 0 || v === null || Array.isArray(v) && v.every((x) => typeof x === "string");
34512
+ function lines(items) {
34513
+ return (items ?? []).map(pyStrip).filter((x) => x !== "").map((x) => `- ${x}`).join("\n");
34514
+ }
34515
+ function renderPersonaFiles(persona) {
34516
+ if (!isObj(persona)) return { ok: false, reason: "shape" };
34517
+ const p = persona;
34518
+ for (const k of ["name", "business", "role", "manner"]) if (!optString(p[k])) return { ok: false, reason: "shape" };
34519
+ for (const k of ["can", "cannot", "handoff"]) if (!optStrings(p[k])) return { ok: false, reason: "shape" };
34520
+ const toneRaw = p.tone;
34521
+ if (!(toneRaw === void 0 || toneRaw === null || isObj(toneRaw) && Object.values(toneRaw).every((v) => typeof v === "string"))) {
34522
+ return { ok: false, reason: "shape" };
34523
+ }
34524
+ const name = pyStrip(p.name || "");
34525
+ const business = pyStrip(p.business || "");
34526
+ const role = pyStrip(p.role || "");
34527
+ const identity = [];
34528
+ if (name && business) identity.push(`You are ${name}, an agent for ${business}.`);
34529
+ else if (name) identity.push(`You are ${name}.`);
34530
+ if (role) identity.push(role);
34531
+ const identityMd = identity.join("\n\n");
34532
+ const tone = toneRaw ?? {};
34533
+ const soul = [];
34534
+ for (const k of TONE_FIELD_ORDER) {
34535
+ if (!Object.prototype.hasOwnProperty.call(tone, k)) continue;
34536
+ const words = TONE_WORDS[k];
34537
+ const v = tone[k];
34538
+ if (Object.prototype.hasOwnProperty.call(words, v)) soul.push(words[v]);
34539
+ }
34540
+ const manner = pyStrip(p.manner || "");
34541
+ let soulMd = soul.join(" ");
34542
+ if (manner) soulMd = pyStrip(`${soulMd}
34543
+
34544
+ ${manner}`);
34545
+ const agents = [];
34546
+ const can = lines(p.can);
34547
+ if (can) agents.push(`What you can do:
34548
+ ${can}`);
34549
+ const cannot = lines(p.cannot);
34550
+ if (cannot) agents.push(`What you cannot do:
34551
+ ${cannot}`);
34552
+ const handoff = (p.handoff ?? []).filter((h) => pyStrip(h) !== "").map((h) => Object.prototype.hasOwnProperty.call(HANDOFF_TEXT, h) ? HANDOFF_TEXT[h] : pyStrip(h));
34553
+ if (handoff.length > 0) agents.push("Hand off to a person when " + handoff.join("; when ") + ". Say plainly that you are doing so.");
34554
+ agents.push(TOOL_PARAGRAPH);
34555
+ const agentsMd = agents.join("\n\n");
34556
+ const files = { "IDENTITY.md": identityMd, "SOUL.md": soulMd, "AGENTS.md": agentsMd };
34557
+ for (const body of Object.values(files)) {
34558
+ if (Buffer.byteLength(body, "utf8") > PERSONA_MAX_BYTES) return { ok: false, reason: "over_cap" };
34559
+ }
34560
+ return { ok: true, files };
34561
+ }
34562
+
34563
+ // src/grants/binding.ts
34564
+ var CONFIG_BINDING_FIELDS = ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"];
34565
+ var ASSIGN_PREFIX = "legacy-fact-assign:";
34566
+ function legacyAssignSha256(prunes) {
34567
+ const set = [...new Set(prunes.filter((p) => typeof p === "string" && p.startsWith(ASSIGN_PREFIX)))].sort();
34568
+ return sha256Hex(canonicalize(set));
34569
+ }
34570
+ function legacyAssignToken(d) {
34571
+ return `${ASSIGN_PREFIX}${d.source_id}:${d.fact_hash}:${d.channel_id}`;
34572
+ }
34573
+ var PERSONA_FILES = ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
34574
+ var LLM_SOURCE = "llm-source.json";
34575
+ var FAIL = Symbol("bind_fail");
34576
+ function guarded(f) {
34577
+ try {
34578
+ return f();
34579
+ } catch {
34580
+ return FAIL;
34581
+ }
34582
+ }
34583
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
34584
+ var strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? [...v] : void 0;
34585
+ var sameStrings = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((x, i) => x === b[i]);
34586
+ var sha256Hex = (bytes) => Buffer.from(sha25611(bytes)).toString("hex");
34587
+ function endpointEquals(raw, signed) {
34588
+ if (!isObj2(raw)) return false;
34589
+ const e = { ...raw };
34590
+ if (e.headersFile === "") delete e.headersFile;
34591
+ const keys = Object.keys(e).sort().join(",");
34592
+ const want = "headersFile" in signed ? "baseUrl,headersFile" : "baseUrl";
34593
+ return keys === want && e.baseUrl === signed.baseUrl && e.headersFile === signed.headersFile;
34594
+ }
34595
+ function databaseEquals(raw, signed) {
34596
+ if (!isObj2(raw)) return false;
34597
+ const keys = Object.keys(raw);
34598
+ if (keys.length !== 1) return false;
34599
+ return "sqlite" in signed ? raw.sqlite === signed.sqlite : raw.postgresUrlFile === signed.postgresUrlFile;
34600
+ }
34601
+ function bindTargets(raw, signed, equals) {
34602
+ if (raw === void 0) return { written: {}, mismatches: 0 };
34603
+ if (!isObj2(raw)) return { written: {}, mismatches: 1 };
34604
+ const written = {};
34605
+ let mismatches = 0;
34606
+ for (const [name, entry] of Object.entries(raw)) {
34607
+ const s = Object.prototype.hasOwnProperty.call(signed, name) ? signed[name] : void 0;
34608
+ if (s !== void 0 && equals(entry, s)) written[name] = s;
34609
+ else mismatches++;
34610
+ }
34611
+ return { written, mismatches };
34612
+ }
34613
+ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }) {
34614
+ const mismatches = /* @__PURE__ */ new Set();
34615
+ const cfg = manifest.config;
34616
+ const out = {};
34617
+ const files = isObj2(payload.files) ? payload.files : {};
34618
+ const personaOk = guarded(() => sha256Hex(canonicalize(payload.persona)) === cfg.persona_sha256);
34619
+ const render = guarded(() => renderPersonaFiles(payload.persona));
34620
+ if (personaOk === true && render !== FAIL && render.ok) {
34621
+ for (const f of PERSONA_FILES) out[f] = render.files[f];
34622
+ } else {
34623
+ mismatches.add("persona");
34624
+ }
34625
+ const delivered = files[LLM_SOURCE];
34626
+ const llmSame = guarded(() => {
34627
+ if (delivered === void 0) return cfg.llm === void 0;
34628
+ if (typeof delivered !== "string") return false;
34629
+ const parsed = JSON.parse(delivered);
34630
+ if (!isObj2(parsed)) return false;
34631
+ if (cfg.llm === void 0) return parsed.provider === void 0 && parsed.model === void 0;
34632
+ return parsed.provider === cfg.llm.provider && parsed.model === cfg.llm.model;
34633
+ });
34634
+ if (llmSame !== true) mismatches.add("llm");
34635
+ if (cfg.llm !== void 0) {
34636
+ out[LLM_SOURCE] = JSON.stringify({ provider: cfg.llm.provider, model: cfg.llm.model, version: payload.version }) + "\n";
34637
+ }
34638
+ if (!isObj2(payload.files) && payload.files !== void 0) mismatches.add("files");
34639
+ for (const name of Object.keys(files)) {
34640
+ if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
34641
+ }
34642
+ const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
34643
+ const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
34644
+ const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
34645
+ const payloadFlags = strings(sj?.flags);
34646
+ if (!sameStrings(payloadFlags, cfg.flags)) mismatches.add("flags");
34647
+ const tools = isObj2(sj?.tools) ? sj.tools : void 0;
34648
+ if (!sameStrings(strings(tools?.allow), cfg.tools_allow)) mismatches.add("tools_allow");
34649
+ const deny = strings(tools?.deny);
34650
+ const ps = isObj2(sj?.search) ? sj.search : void 0;
34651
+ const payloadProvider = ps?.provider;
34652
+ let searchBlock;
34653
+ let appliedSearch;
34654
+ if (payloadProvider === cfg.search?.provider && ps === void 0 === (cfg.search === void 0)) {
34655
+ searchBlock = ps === void 0 ? null : Object.freeze({ provider: ps.provider, ...typeof ps.keyFile === "string" ? { keyFile: ps.keyFile } : {} });
34656
+ appliedSearch = searchBlock;
34657
+ } else {
34658
+ mismatches.add("search");
34659
+ searchBlock = (prior.search?.provider ?? void 0) === cfg.search?.provider ? prior.search : void 0;
34660
+ }
34661
+ const skillsDoc = {
34662
+ endpoints: eps.written,
34663
+ databases: dbs.written,
34664
+ flags: [...cfg.flags],
34665
+ ...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
34666
+ ...searchBlock ? { search: searchBlock } : {}
34667
+ };
34668
+ out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
34669
+ const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
34670
+ let memory;
34671
+ if (isObj2(payload.memory)) {
34672
+ const pm = payload.memory;
34673
+ const sameMode = pm.mode === cfg.memory.mode;
34674
+ const sameDays = cfg.memory.mode === "semantic" ? pm.recall_days === cfg.memory.recall_days : !("recall_days" in pm);
34675
+ if (!sameMode || !sameDays) mismatches.add("memory");
34676
+ const { recall_days: _r, mode: _m, ...rest } = pm;
34677
+ memory = { ...signedMemory, ...rest };
34678
+ } else {
34679
+ mismatches.add("memory");
34680
+ memory = { ...signedMemory };
34681
+ }
34682
+ const prunes = payload.memory_prunes === void 0 ? [] : payload.memory_prunes;
34683
+ const decisions = Array.isArray(payload.legacy_memory_fact_decisions) ? payload.legacy_memory_fact_decisions : [];
34684
+ const assignMatch = guarded(() => Array.isArray(prunes) && legacyAssignSha256(prunes) === cfg.legacy_assign_sha256);
34685
+ let legacyDecisions;
34686
+ if (assignMatch === true) {
34687
+ const verified = new Set(prunes.filter((p) => typeof p === "string" && p.startsWith(ASSIGN_PREFIX)));
34688
+ legacyDecisions = decisions.filter((d) => {
34689
+ if (!isObj2(d) || d.action !== "assign") return true;
34690
+ if (typeof d.source_id !== "string" || typeof d.fact_hash !== "string" || typeof d.channel_id !== "string") return false;
34691
+ return verified.has(legacyAssignToken({ source_id: d.source_id, fact_hash: d.fact_hash, channel_id: d.channel_id }));
34692
+ });
34693
+ } else {
34694
+ mismatches.add("legacy_assign");
34695
+ legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
34696
+ }
34697
+ const signedTools = (a) => MANIFEST_KNOWN_TOOLS[a].filter((t) => (manifest.grants[a] ?? []).includes(t));
34698
+ const policy = {
34699
+ v: 1,
34700
+ manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
34701
+ secret_path_mode: "enforce"
34702
+ };
34703
+ return {
34704
+ mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
34705
+ targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
34706
+ files: out,
34707
+ memory,
34708
+ signedTargets: { endpoints: Object.keys(eps.written).length, databases: Object.keys(dbs.written).length },
34709
+ policy,
34710
+ appliedSearch,
34711
+ legacyDecisions
34712
+ };
34713
+ }
34714
+
34715
+ // src/grants/room-search-cap.ts
34716
+ var ROOM_SEARCH_DAILY_CAP = 50;
34717
+ var ROOM_SEARCH_CAP_ENV = "SECURECOMS_ROOM_SEARCH_DAILY_CAP";
34718
+ function parseRoomSearchCap(v, warn) {
34719
+ if (v === void 0) return ROOM_SEARCH_DAILY_CAP;
34720
+ if (/^\d+$/.test(v)) {
34721
+ const n = Number(v);
34722
+ if (n <= ROOM_SEARCH_DAILY_CAP) return n;
34723
+ }
34724
+ warn?.();
34725
+ return ROOM_SEARCH_DAILY_CAP;
34726
+ }
34727
+ var AGENT_ROOM_SEARCH_DAILY_CAP = 200;
34728
+ var AGENT_ROOM_SEARCH_CAP_ENV = "SECURECOMS_AGENT_ROOM_SEARCH_DAILY_CAP";
34729
+ function parseAgentRoomSearchCap(v, warn) {
34730
+ if (v === void 0) return AGENT_ROOM_SEARCH_DAILY_CAP;
34731
+ if (/^\d+$/.test(v)) {
34732
+ const n = Number(v);
34733
+ if (n <= AGENT_ROOM_SEARCH_DAILY_CAP) return n;
34734
+ }
34735
+ warn?.();
34736
+ return AGENT_ROOM_SEARCH_DAILY_CAP;
34737
+ }
34738
+ var AGENT_TOTAL_KEY = "agent-total";
34739
+
34740
+ // src/grants/effective.ts
34741
+ var TOOL_GRANT_CODE_CEILING = MANIFEST_KNOWN_TOOLS;
34742
+ var CURRENT_TIME = Object.freeze({ tool: "current_time", mode: "read" });
34743
+ function effectiveToolGrants(i) {
34744
+ if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false };
34745
+ if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false };
34746
+ const ceiling = TOOL_GRANT_CODE_CEILING[i.audience];
34747
+ const listed = new Set(i.manifest.grants[i.audience] ?? []);
34748
+ const signed = ceiling.filter((t) => listed.has(t));
34749
+ if (!i.eligible || !i.manifestSwitchOn) {
34750
+ const fallback = i.base.filter((g) => g.tool !== "current_time" && signed.includes(g.tool));
34751
+ return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false };
34752
+ }
34753
+ let withheld = false;
34754
+ const tools = signed.filter((t) => {
34755
+ if (i.disabled.has(t)) return false;
34756
+ if (t === "http_request" && i.signedTargets.endpoints === 0) return false;
34757
+ if (t === "sql_query" && i.signedTargets.databases === 0) return false;
34758
+ if (i.audience === "team_room" && t === "web_search" && i.roomSearchCapped) {
34759
+ withheld = true;
34760
+ return false;
34761
+ }
34762
+ return true;
34763
+ });
34764
+ return {
34765
+ grants: Object.freeze([CURRENT_TIME, ...tools.map((tool) => Object.freeze({ tool, mode: "read", source: "manifest" }))]),
34766
+ source: "manifest",
34767
+ withheldBySearchCap: withheld
34768
+ };
34769
+ }
34770
+
33822
34771
  // src/bridge-agent.ts
33823
34772
  import { homedir as homedir4 } from "node:os";
33824
34773
 
@@ -33907,6 +34856,50 @@ function roomIsCrossOrg(rows) {
33907
34856
  return unknown ? null : false;
33908
34857
  }
33909
34858
 
34859
+ // src/grants/owner-turn.ts
34860
+ import { ed25519 as ed255196 } from "@noble/curves/ed25519";
34861
+ var HEX = /^(?:[0-9a-fA-F]{2})+$/;
34862
+ function verifyOwnerSignature(entry, pin) {
34863
+ if (!entry.identitySignature || !entry.payloadHex) return { state: "no_signature" };
34864
+ if (!HEX.test(entry.payloadHex)) return { state: "malformed" };
34865
+ const sig = canonicalBase64Bytes(entry.identitySignature, 64);
34866
+ if (!sig) return { state: "malformed" };
34867
+ const bytes = new Uint8Array(Buffer.from(entry.payloadHex, "hex"));
34868
+ let envelope;
34869
+ try {
34870
+ envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
34871
+ } catch {
34872
+ return { state: "malformed" };
34873
+ }
34874
+ const algo = envelope?.identity_proof?.algo;
34875
+ if (typeof envelope !== "object" || envelope === null || algo === void 0 || algo === null) return { state: "malformed" };
34876
+ if (algo !== "ed25519") return { state: "unknown_algo" };
34877
+ let good;
34878
+ try {
34879
+ good = ed255196.verify(sig, bytes, pin.publicKey, { zip215: false });
34880
+ } catch {
34881
+ return { state: "malformed" };
34882
+ }
34883
+ return good ? { state: "ok", envelope } : { state: "bad_signature" };
34884
+ }
34885
+ function classifyOwnerDmLeaves(leaves, allowed) {
34886
+ if (leaves.some((l) => !l.basic || l.identity === null)) return "foreign";
34887
+ if (!leaves.some((l) => l.identity === allowed.ownerIdentityId)) return "foreign";
34888
+ const unexpected = /* @__PURE__ */ new Set();
34889
+ for (const l of leaves) {
34890
+ const id = l.identity;
34891
+ if (id === allowed.ownerIdentityId || id === allowed.agentIdentityId) continue;
34892
+ if (allowed.relayIdentityId !== void 0 && id === allowed.relayIdentityId) continue;
34893
+ unexpected.add(id);
34894
+ }
34895
+ if (unexpected.size === 0) return "ok";
34896
+ if (unexpected.size === 1) {
34897
+ const [only] = unexpected;
34898
+ if (allowed.isServerRelay(only)) return "unsigned_relay";
34899
+ }
34900
+ return "foreign";
34901
+ }
34902
+
33910
34903
  // src/bridge-agent.ts
33911
34904
  function logSendSupersededAdoption(kind2) {
33912
34905
  switch (kind2) {
@@ -33959,6 +34952,8 @@ var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
33959
34952
  Object.freeze({ tool: "current_time", mode: "read" })
33960
34953
  ]);
33961
34954
  var NO_TOOL_GRANTS = Object.freeze([]);
34955
+ var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
34956
+ var OWNER_TURN_UNCHECKED = Object.freeze({ kind: "unchecked" });
33962
34957
  function ownerDmDefaultGrantsEnabled(env = process.env) {
33963
34958
  const v = env.SECURECOMS_OWNER_DM_DEFAULT_GRANTS;
33964
34959
  return v === void 0 || v === "on";
@@ -33967,6 +34962,14 @@ function roomDefaultGrantsEnabled(env = process.env) {
33967
34962
  const v = env.SECURECOMS_ROOM_DEFAULT_GRANTS;
33968
34963
  return v === void 0 || v === "on";
33969
34964
  }
34965
+ function ownerDmManifestGrantsEnabled(env = process.env) {
34966
+ const v = env.SECURECOMS_OWNER_DM_MANIFEST_GRANTS;
34967
+ return v === void 0 || v === "on";
34968
+ }
34969
+ function roomManifestGrantsEnabled(env = process.env) {
34970
+ const v = env.SECURECOMS_ROOM_MANIFEST_GRANTS;
34971
+ return v === void 0 || v === "on";
34972
+ }
33970
34973
  var MAX_PENDING_NOTE_REQUESTS = 8;
33971
34974
  var INGEST_STAMP_KEY = "_sc_notes_ingest_generation";
33972
34975
  function validIngestStamp(v) {
@@ -34053,6 +35056,17 @@ function redeliveryDelayMs(attempts) {
34053
35056
  return 2 ** Math.min(Math.max(attempts, 0), 16) * REDELIVERY_BACKOFF_BASE_MS;
34054
35057
  }
34055
35058
  var DEAD_LETTER_NOTICE = "I couldn't process a message. Please resend it.";
35059
+ function holdsManifestGrant(a) {
35060
+ return a?.tool_grants?.some((g) => g.mode === "read" && g.source === "manifest") ?? false;
35061
+ }
35062
+ function greetingAuthorization(a) {
35063
+ const { memory_write: _w, ...rest } = a;
35064
+ return Object.freeze({ ...rest, tool_grants: NO_TOOL_GRANTS });
35065
+ }
35066
+ function utcDayOf(ms) {
35067
+ return new Date(ms).toISOString().slice(0, 10);
35068
+ }
35069
+ var ROOM_SEARCH_CAP_NOTICE = "I've used today's web searches for this room. I can still help without searching. Searches are available again tomorrow (UTC).";
34056
35070
  var DEAD_LETTER_NOTICE_INTERVAL_MS = 60 * 6e4;
34057
35071
  var PAUSE_RECHECK_MS = 3e4;
34058
35072
  var SUPERSEDE_RECHECK_MS = 3e4;
@@ -34181,7 +35195,7 @@ var FileOwnerAuthorityStore = class {
34181
35195
  async save(enclaveId, pin) {
34182
35196
  const all = await this.readAll();
34183
35197
  all[enclaveId] = pin;
34184
- await fsp.mkdir(dirname4(this.path), { recursive: true });
35198
+ await fsp.mkdir(dirname5(this.path), { recursive: true });
34185
35199
  await fsp.writeFile(this.path, JSON.stringify(all), "utf8");
34186
35200
  }
34187
35201
  };
@@ -34340,6 +35354,12 @@ var BridgeAgent = class _BridgeAgent {
34340
35354
  });
34341
35355
  this.fetchImpl = deps.fetchImpl ?? fetch;
34342
35356
  this.home = deps.home ?? homedir4();
35357
+ this.grantWriters = {
35358
+ writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
35359
+ writeManagedFiles: deps.grantWriters?.writeManagedFiles ?? writeManagedFiles,
35360
+ bindRuntimeConfig: deps.grantWriters?.bindRuntimeConfig ?? bindRuntimeConfig,
35361
+ removeManagedLlmSource: deps.grantWriters?.removeManagedLlmSource ?? removeManagedLlmSource
35362
+ };
34343
35363
  this.platform = deps.platform ?? process.platform;
34344
35364
  this.machinePaths = deps.hostMachine === true || deps.home !== void 0;
34345
35365
  this.unifiedOn = unifiedPart1On(deps.unifiedMemoryPart1);
@@ -34354,7 +35374,7 @@ var BridgeAgent = class _BridgeAgent {
34354
35374
  threshold: EMBEDDING_MANIFEST.similarityThreshold,
34355
35375
  warm: windowsSemanticOn(this.platform)
34356
35376
  };
34357
- return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname4(fileURLToPath2(import.meta.url)));
35377
+ return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname5(fileURLToPath2(import.meta.url)));
34358
35378
  }
34359
35379
  });
34360
35380
  this.embedder = deps.embedder ?? (deps.hostMachine === true && deps.home === void 0 ? new EmbedderClient({ identityId: cfg.identityId, home: homedir4(), source: cfg.modelSource, fetchImpl: this.fetchImpl, platform: this.platform }) : INERT_EMBEDDER);
@@ -34365,7 +35385,7 @@ var BridgeAgent = class _BridgeAgent {
34365
35385
  modelId: EMBEDDING_MANIFEST.modelId,
34366
35386
  threshold: EMBEDDING_MANIFEST.similarityThreshold
34367
35387
  });
34368
- this.heartbeat = new CallHeartbeat(this.machinePaths ? join16(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
35388
+ this.heartbeat = new CallHeartbeat(this.machinePaths ? join17(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
34369
35389
  this.reembed = new ReembedQueue({
34370
35390
  store: this.store,
34371
35391
  embedder: this.embedder,
@@ -34447,8 +35467,8 @@ var BridgeAgent = class _BridgeAgent {
34447
35467
  const j = await res.json();
34448
35468
  return String(j.attachment_id);
34449
35469
  };
34450
- this.attachmentsDirImpl = deps.attachmentsDir ?? join16(dirname4(cfg.storePath), "attachments");
34451
- this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join16(dirname4(cfg.storePath), "vault-owner-pins.json"));
35470
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join17(dirname5(cfg.storePath), "attachments");
35471
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join17(dirname5(cfg.storePath), "vault-owner-pins.json"));
34452
35472
  this.action = {
34453
35473
  begin: (channelId) => this.actionBegin(channelId),
34454
35474
  start: (opts) => this.actionStart(opts),
@@ -34466,6 +35486,30 @@ var BridgeAgent = class _BridgeAgent {
34466
35486
  fetchImpl;
34467
35487
  /** Round 5: home for the managed runtime-config directory (test seam). */
34468
35488
  home;
35489
+ grantWriters;
35490
+ /** Owner grants (spec §7.5, OQ-2): the operator cap, read once; the same variable the runtime reads. 0 = no room search. */
35491
+ roomSearchDailyCap = parseRoomSearchCap(process.env[ROOM_SEARCH_CAP_ENV], () => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("cap_invalid", ["cap_invalid"]) }));
35492
+ /** Review M-3 (ERIC APPROVALS 3): the per-agent total across rooms, read once; the same variable the runtime reads. */
35493
+ agentRoomSearchDailyCap = parseAgentRoomSearchCap(process.env[AGENT_ROOM_SEARCH_CAP_ENV], () => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("agent_cap_invalid", ["agent_cap_invalid"]) }));
35494
+ /** `${day}\n${channel}` keys already logged reached today (in memory; the store holds the durable flag). */
35495
+ roomCapLogged = /* @__PURE__ */ new Set();
35496
+ /** Rooms whose K-14 send is in flight: a second frame posts nothing more. */
35497
+ roomCapNoticePending = /* @__PURE__ */ new Set();
35498
+ /** Held entries whose staged copy lost web_search to the backstop; applied again at release. */
35499
+ backstopStripped = /* @__PURE__ */ new Set();
35500
+ /**
35501
+ * Owner grants plan 4 Task 11 (P-4, P-6): what the managed directory holds under the current manifest.
35502
+ * Manifest grants require state "bound" at the applied counter (Task 13). null at startup, while a
35503
+ * refetch narrows it, and after any throw (the fallback).
35504
+ */
35505
+ manifestBinding = null;
35506
+ /** skills.json tools.deny from the latest fetch, manifest or not (spec §5.2 `disabled`). */
35507
+ payloadToolDeny = /* @__PURE__ */ new Set();
35508
+ /** Spec §7.7 (OQ-5): the latest fetch's binding outcome, reported in tool_grant_status (Task 15). */
35509
+ configBindingMismatches = [];
35510
+ targetMismatches = { endpoint: 0, database: 0 };
35511
+ /** P-5 (Task 14): the legacy fact decisions the latest bound fetch may apply (assigns verified against the signed set). */
35512
+ boundLegacyDecisions = [];
34469
35513
  joinState = /* @__PURE__ */ new Map();
34470
35514
  /**
34471
35515
  * Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
@@ -34769,11 +35813,29 @@ var BridgeAgent = class _BridgeAgent {
34769
35813
  ownerDmDefaultGrants = ownerDmDefaultGrantsEnabled();
34770
35814
  /** Read once at startup; see roomDefaultGrantsEnabled. */
34771
35815
  roomDefaultGrants = roomDefaultGrantsEnabled();
35816
+ /** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
35817
+ ownerDmManifestGrants = ownerDmManifestGrantsEnabled();
35818
+ roomManifestGrants = roomManifestGrantsEnabled();
34772
35819
  /**
34773
35820
  * Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
34774
35821
  * cycle's refresh (refreshRostersForCycle). Cleared at each cycle's start.
34775
35822
  */
34776
35823
  ownerDmCycleFresh = /* @__PURE__ */ new Set();
35824
+ // Owner-signed tool grants, step 1 (plan 1 task 7). The verified manifest is
35825
+ // RECORDED here and in the sealed store; no grant code reads it in step 1.
35826
+ toolGrantManifest = null;
35827
+ toolGrantCounter = null;
35828
+ manifestState = "no_manifest";
35829
+ /** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
35830
+ ownerVerifyFailures = 0;
35831
+ /** Verified owner turns of this ingest cycle, recorded in owner_replay when staged (P-3). */
35832
+ ownerReplayPending = /* @__PURE__ */ new Map();
35833
+ /** D-9: ids of owner turns that failed verification (never `replay`, audit E-3), id -> channel. Persisted too. */
35834
+ ownerVerifyFailedIds = /* @__PURE__ */ new Map();
35835
+ serverOwnerMismatchLogged = false;
35836
+ /** Audit L-2: the replay prune runs at most once per interval, on a monotonic clock. */
35837
+ monoNow = () => performance.now();
35838
+ lastOwnerReplayPruneAt = -Infinity;
34777
35839
  /**
34778
35840
  * Audit M-B1: room-granted turns, keyed by the inbound message id, with the
34779
35841
  * channel, MLS group and epoch the room was classified at (review I-1: an
@@ -35216,14 +36278,38 @@ var BridgeAgent = class _BridgeAgent {
35216
36278
  return;
35217
36279
  }
35218
36280
  const version = payload.version;
35219
- await this.applyPayloadMemory(payload.memory);
36281
+ const skillsForDeny = payload.skills_json;
36282
+ const denyRaw = skillsForDeny?.tools?.deny;
36283
+ this.payloadToolDeny = new Set(Array.isArray(denyRaw) ? denyRaw.filter((t) => typeof t === "string") : []);
36284
+ let manifestStepThrew = false;
36285
+ try {
36286
+ await this.applyToolGrantManifest(payload.tool_grant_manifest);
36287
+ } catch (e) {
36288
+ manifestStepThrew = true;
36289
+ writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
36290
+ }
36291
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
36292
+ const inForce = pin !== null ? this.toolGrantManifest : null;
36293
+ if (inForce !== null && pin !== null) {
36294
+ const applied = manifestStepThrew ? await this.bindFailed(inForce, payload.memory, void 0) : await this.applyUnderManifest(inForce, pin.publicKeyB64, payload, version);
36295
+ if (!applied) {
36296
+ await this.postRuntimeConfigReport(base, version, payload.skills_json ?? {});
36297
+ return;
36298
+ }
36299
+ } else {
36300
+ this.manifestBinding = null;
36301
+ this.configBindingMismatches = [];
36302
+ this.targetMismatches = { endpoint: 0, database: 0 };
36303
+ await this.applyPayloadMemory(payload.memory);
36304
+ this.overwriteStalePolicy();
36305
+ }
35220
36306
  if (!version) {
35221
36307
  writeManagedFiles(this.home, this.cfg.identityId, {});
35222
36308
  writeServiceLog2("info", "runtime_config.not_saved");
35223
36309
  }
35224
36310
  const skillsJson = payload.skills_json ?? {};
35225
- if (version) {
35226
- writeManagedFiles(this.home, this.cfg.identityId, {
36311
+ if (version && inForce === null) {
36312
+ this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
35227
36313
  ...payload.files ?? {},
35228
36314
  "skills.json": `${JSON.stringify(skillsJson, null, 2)}
35229
36315
  `
@@ -35235,12 +36321,13 @@ var BridgeAgent = class _BridgeAgent {
35235
36321
  if (payload.legacy_memory_deletes?.length) {
35236
36322
  deleteLegacyMemorySources(this.home, this.cfg.identityId, payload.legacy_memory_deletes);
35237
36323
  }
35238
- if (payload.legacy_memory_fact_decisions?.length) {
36324
+ const legacyDecisions = inForce !== null ? this.boundLegacyDecisions : payload.legacy_memory_fact_decisions;
36325
+ if (legacyDecisions?.length) {
35239
36326
  const provider = this.cfg.memoryProvider ?? "securecoms";
35240
36327
  const result = await applyLegacyMemoryFactDecisions(
35241
36328
  this.home,
35242
36329
  this.cfg.identityId,
35243
- payload.legacy_memory_fact_decisions,
36330
+ legacyDecisions,
35244
36331
  async (channelId, fact, assignmentKey) => {
35245
36332
  if (provider === "none" || provider === "hermes") {
35246
36333
  throw new Error("configured memory provider has no protected channel-fact import path");
@@ -35259,72 +36346,177 @@ var BridgeAgent = class _BridgeAgent {
35259
36346
  });
35260
36347
  }
35261
36348
  }
35262
- const local = readLocalReport(this.home, this.cfg.identityId);
35263
- const report = {
35264
- applied_version: version,
35265
- paths: checkConfiguredPaths(skillsJson),
35266
- // Read AFTER the prunes above, so the owner's next view of memory is
35267
- // the file as it now stands, not the one they just pruned from.
35268
- memory: readManagedMemory(this.home, this.cfg.identityId),
35269
- // E13: pass through what the runtime reported. The daemon does not
35270
- // interpret any of it.
35271
- local_persona: local?.persona ?? null,
35272
- // Only the runtime knows whether a search key file is configured — it
35273
- // is an environment value on that process, invisible to the daemon and
35274
- // to the backend. The backend needs it to generate a truthful tool
35275
- // paragraph, so it rides along here.
35276
- has_search_key: local?.has_search_key ?? false,
35277
- // Same idea, for the BYOK LLM key file: only the runtime can see
35278
- // whether one is configured in its own environment.
35279
- has_llm_key: local?.has_llm_key ?? false,
35280
- // The backend uses this only to expose target channels compatible with
35281
- // the actual protected-memory adapter. It is an enum, never a path or
35282
- // provider credential.
35283
- memory_provider: this.cfg.memoryProvider ?? "securecoms",
35284
- // Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
35285
- // unchanged. memory_notes is true only when notes are actually on (the
35286
- // provider is securecoms and the parsed payload.memory keeps them on);
35287
- // the generation is the store's applied value, never the payload's.
35288
- // Final review I-1: and only once a notes-reading runtime said hello.
35289
- memory_notes: this.notesProviderOn() && this.notesConsumerSeen,
35290
- // Unified memory (plan Task 31): only while the compiled part 1 switch is on, so a
35291
- // 0.28-equivalent bridge never makes the web show the unified panel. Numbers and
35292
- // booleans only (U1).
35293
- ...this.reportedUnified(),
35294
- ...this.reportedClearGeneration(),
35295
- // 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
35296
- ...this.reportedRecall(),
35297
- // Inert legacy files only. `readLocalReport` revalidates the current
35298
- // regular-file bytes and strips every local path before this leaves the
35299
- // machine.
35300
- legacy_memory: local?.legacy_memory ?? [],
35301
- // §B.4: counts of server-named secret paths inside / allowlisted /
35302
- // outside the secrets directory, and the runtime's mode. Numbers and
35303
- // one enum only; readLocalReport rebuilds it so no path rides along.
35304
- secret_paths: local?.secret_paths ?? null
35305
- };
35306
- const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
35307
- method: "POST",
35308
- headers: {
35309
- authorization: `Bearer ${this.cfg.apiKey}`,
35310
- "content-type": "application/json"
35311
- },
35312
- body: JSON.stringify(report)
35313
- });
35314
- if (!posted.ok) {
35315
- if (version) {
35316
- writeServiceLog2("warn", "runtime_config.report_failed", { status: posted.status, version });
35317
- } else {
35318
- writeServiceLog2("warn", "runtime_config.legacy_memory_report_failed", { status: posted.status });
35319
- }
35320
- return;
35321
- }
35322
- if (version) writeServiceLog2("info", "runtime_config.applied", { version });
35323
- else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
36349
+ await this.postRuntimeConfigReport(base, version, skillsJson);
35324
36350
  } catch (e) {
35325
36351
  writeServiceLog2("warn", "runtime_config.refetch_failed", {}, e);
35326
36352
  }
35327
36353
  }
36354
+ /**
36355
+ * Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
36356
+ * after the memory was applied; the caller then writes nothing else. Throws only from a file write, with
36357
+ * the binding left null.
36358
+ */
36359
+ async applyUnderManifest(manifest, pinKeyB64, payload, version) {
36360
+ let prior;
36361
+ try {
36362
+ prior = (await this.store.loadToolGrantApplied(pinKeyB64))?.search;
36363
+ } catch (e) {
36364
+ prior = void 0;
36365
+ writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("applied_read_failed", ["applied_read_failed"]) }, e);
36366
+ }
36367
+ let bound;
36368
+ try {
36369
+ bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
36370
+ } catch (e) {
36371
+ bound = null;
36372
+ return this.bindFailed(manifest, payload.memory, e);
36373
+ }
36374
+ const counter = manifest.counter;
36375
+ const state = bound.mismatches.length === 0 ? "bound" : "mismatch";
36376
+ const old = this.manifestBinding;
36377
+ if (!(old !== null && old.state === "bound" && old.counter === counter && state === "bound")) this.manifestBinding = null;
36378
+ this.configBindingMismatches = bound.mismatches;
36379
+ this.boundLegacyDecisions = bound.legacyDecisions;
36380
+ this.logConfigBinding(bound);
36381
+ this.targetMismatches = { endpoint: bound.targetMismatches.endpoint, database: bound.targetMismatches.database };
36382
+ await this.applyPayloadMemory(bound.memory);
36383
+ if (!version) {
36384
+ this.manifestBinding = null;
36385
+ return true;
36386
+ }
36387
+ try {
36388
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
36389
+ this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
36390
+ if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
36391
+ } catch (e) {
36392
+ this.manifestBinding = null;
36393
+ throw e;
36394
+ }
36395
+ this.manifestBinding = { counter, state, signedTargets: { ...bound.signedTargets } };
36396
+ if (bound.appliedSearch !== void 0) {
36397
+ try {
36398
+ await this.store.saveToolGrantApplied(pinKeyB64, { counter, search: bound.appliedSearch });
36399
+ } catch (e) {
36400
+ writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("applied_save_failed", ["applied_save_failed"]) }, e);
36401
+ }
36402
+ }
36403
+ return true;
36404
+ }
36405
+ /** Spec §7.6 (plan 4 Task 15): content-free. One line per mismatched field, one per dropped target. */
36406
+ logConfigBinding(bound) {
36407
+ if (bound.mismatches.length === 0) {
36408
+ writeServiceLog2("info", "authz.config_binding", { state: ServiceLogEnum.of("ok", ["ok"]) });
36409
+ }
36410
+ for (const field of bound.mismatches) {
36411
+ writeServiceLog2("warn", "authz.config_binding", {
36412
+ state: ServiceLogEnum.of("mismatch", ["mismatch"]),
36413
+ field: ServiceLogEnum.of(field, ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"])
36414
+ });
36415
+ }
36416
+ for (const kind2 of ["endpoint", "database"]) {
36417
+ for (let i = 0; i < bound.targetMismatches[kind2]; i++) {
36418
+ writeServiceLog2("warn", "authz.target_mismatch", { kind: ServiceLogEnum.of(kind2, ["endpoint", "database"]) });
36419
+ }
36420
+ }
36421
+ }
36422
+ /** H-4: the binding could not be computed. The memory still applies, with the signed mode; nothing else is written. */
36423
+ async bindFailed(manifest, rawMemory, e) {
36424
+ this.manifestBinding = null;
36425
+ writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
36426
+ const signed = { mode: manifest.config.memory.mode, ...manifest.config.memory.recall_days !== void 0 ? { recall_days: manifest.config.memory.recall_days } : {} };
36427
+ let memory = signed;
36428
+ if (rawMemory !== null && typeof rawMemory === "object" && !Array.isArray(rawMemory)) {
36429
+ const { mode: _m, recall_days: _r, ...rest } = rawMemory;
36430
+ memory = { ...signed, ...rest };
36431
+ }
36432
+ await this.applyPayloadMemory(memory);
36433
+ return false;
36434
+ }
36435
+ /** No manifest in force: a manifest-policy.json left by an earlier one is replaced by an empty tool list. */
36436
+ overwriteStalePolicy() {
36437
+ if (!existsSync5(join17(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
36438
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, { v: 1, manifest_tools: [], secret_path_mode: "enforce" });
36439
+ }
36440
+ /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
36441
+ async postRuntimeConfigReport(base, version, skillsJson) {
36442
+ const local = readLocalReport(this.home, this.cfg.identityId);
36443
+ const report = {
36444
+ applied_version: version,
36445
+ paths: checkConfiguredPaths(skillsJson),
36446
+ // Read AFTER the prunes above, so the owner's next view of memory is
36447
+ // the file as it now stands, not the one they just pruned from.
36448
+ memory: readManagedMemory(this.home, this.cfg.identityId),
36449
+ // E13: pass through what the runtime reported. The daemon does not
36450
+ // interpret any of it.
36451
+ local_persona: local?.persona ?? null,
36452
+ // Only the runtime knows whether a search key file is configured — it
36453
+ // is an environment value on that process, invisible to the daemon and
36454
+ // to the backend. The backend needs it to generate a truthful tool
36455
+ // paragraph, so it rides along here.
36456
+ has_search_key: local?.has_search_key ?? false,
36457
+ // Same idea, for the BYOK LLM key file: only the runtime can see
36458
+ // whether one is configured in its own environment.
36459
+ has_llm_key: local?.has_llm_key ?? false,
36460
+ // The backend uses this only to expose target channels compatible with
36461
+ // the actual protected-memory adapter. It is an enum, never a path or
36462
+ // provider credential.
36463
+ memory_provider: this.cfg.memoryProvider ?? "securecoms",
36464
+ // Spec §11.1-11.2 (OQ-E): flat keys; the string `memory` above is
36465
+ // unchanged. memory_notes is true only when notes are actually on (the
36466
+ // provider is securecoms and the parsed payload.memory keeps them on);
36467
+ // the generation is the store's applied value, never the payload's.
36468
+ // Final review I-1: and only once a notes-reading runtime said hello.
36469
+ memory_notes: this.notesProviderOn() && this.notesConsumerSeen,
36470
+ // Unified memory (plan Task 31): only while the compiled part 1 switch is on, so a
36471
+ // 0.28-equivalent bridge never makes the web show the unified panel. Numbers and
36472
+ // booleans only (U1).
36473
+ ...this.reportedUnified(),
36474
+ ...this.reportedClearGeneration(),
36475
+ // 7b spec §9.2 (Task 19): capability versus state. Read-only: the report never starts the embedder.
36476
+ ...this.reportedRecall(),
36477
+ // Inert legacy files only. `readLocalReport` revalidates the current
36478
+ // regular-file bytes and strips every local path before this leaves the
36479
+ // machine.
36480
+ legacy_memory: local?.legacy_memory ?? [],
36481
+ // §B.4: counts of server-named secret paths inside / allowlisted /
36482
+ // outside the secrets directory, and the runtime's mode. Numbers and
36483
+ // one enum only; readLocalReport rebuilds it so no path rides along.
36484
+ secret_paths: local?.secret_paths ?? null,
36485
+ // Owner-signed tool grants (spec §7.7, D-6): enums and numbers only. It
36486
+ // authorizes nothing and a compromised backend can forge what it shows
36487
+ // (residual 9). The backend drops it until Plan 2 or 3 stores it.
36488
+ tool_grant_status: {
36489
+ pin: this.cfg.ownerGrantPin?.state ?? "no_pin",
36490
+ manifest: this.manifestState,
36491
+ applied_counter: this.toolGrantCounter,
36492
+ owner_dm_verify_failures: this.ownerVerifyFailures,
36493
+ // Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
36494
+ config_binding_mismatches: [...this.configBindingMismatches],
36495
+ target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
36496
+ capability: this.lastConsumerCapability
36497
+ }
36498
+ };
36499
+ const reportedVerifyFailures = this.ownerVerifyFailures;
36500
+ const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
36501
+ method: "POST",
36502
+ headers: {
36503
+ authorization: `Bearer ${this.cfg.apiKey}`,
36504
+ "content-type": "application/json"
36505
+ },
36506
+ body: JSON.stringify(report)
36507
+ });
36508
+ if (!posted.ok) {
36509
+ if (version) {
36510
+ writeServiceLog2("warn", "runtime_config.report_failed", { status: posted.status, version });
36511
+ } else {
36512
+ writeServiceLog2("warn", "runtime_config.legacy_memory_report_failed", { status: posted.status });
36513
+ }
36514
+ return;
36515
+ }
36516
+ this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
36517
+ if (version) writeServiceLog2("info", "runtime_config.applied", { version });
36518
+ else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
36519
+ }
35328
36520
  /**
35329
36521
  * Enforce the inbound-buffer cap with a DROP-OLDEST policy.
35330
36522
  *
@@ -35347,6 +36539,12 @@ var BridgeAgent = class _BridgeAgent {
35347
36539
  async start() {
35348
36540
  this.startedAt = Date.now();
35349
36541
  await this.restoreRuntimeInbox();
36542
+ await this.loadToolGrantState();
36543
+ try {
36544
+ this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
36545
+ } catch (e) {
36546
+ writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
36547
+ }
35350
36548
  try {
35351
36549
  this.store.pruneRecall();
35352
36550
  this.store.reconcileRecallClearGeneration();
@@ -36480,8 +37678,10 @@ var BridgeAgent = class _BridgeAgent {
36480
37678
  if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
36481
37679
  const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
36482
37680
  const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
36483
- if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch);
36484
- if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null);
37681
+ if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
37682
+ const origin = this.spokenReplyIds.has(id) ? "fast_path" : !this.taintScope(channelId) ? "reply" : opts.inReplyTo !== void 0 && this.cleanTurns.has(opts.inReplyTo) && !this.taintedTurns.has(opts.inReplyTo) ? "reply" : "untrusted";
37683
+ if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
37684
+ if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
36485
37685
  }
36486
37686
  /**
36487
37687
  * Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
@@ -36496,8 +37696,9 @@ var BridgeAgent = class _BridgeAgent {
36496
37696
  return { ...this.store.itemCapture(channelId), clearGeneration: stamp };
36497
37697
  }
36498
37698
  /** Section 7.1 agent reply, section 24.2: the owner-DM predicate for the channel at write (no sender), else nothing. Reference trust always (U18). OQ-P2: bridge-authored Pinned:/Forgot: lines are filtered by the caller. */
36499
- spawnUnifiedReply(channelId, text, messageId, expect) {
37699
+ spawnUnifiedReply(channelId, text, messageId, expect, origin) {
36500
37700
  if (expect === null) return;
37701
+ if (origin === "fast_path") return;
36501
37702
  const work = (async () => {
36502
37703
  try {
36503
37704
  if (!this.unifiedProviderOn()) return;
@@ -36513,7 +37714,9 @@ var BridgeAgent = class _BridgeAgent {
36513
37714
  source: "agent_reply",
36514
37715
  trust: "reference",
36515
37716
  text,
36516
- expect
37717
+ expect,
37718
+ role: "agent",
37719
+ origin
36517
37720
  });
36518
37721
  } catch {
36519
37722
  writeServiceLog2("warn", "memory.unified_reply_failed", {});
@@ -37239,6 +38442,13 @@ var BridgeAgent = class _BridgeAgent {
37239
38442
  );
37240
38443
  return humanCounterparts.length === 1 && humanCounterparts[0][1].is_agent_owner;
37241
38444
  }
38445
+ /**
38446
+ * Decision D-9 (audit H-3): a turn that failed owner verification gets only
38447
+ * its own text, never the prior-turn history snapshot, for every provider.
38448
+ */
38449
+ mayReadHistoryForTurn(messageId, authorization) {
38450
+ return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
38451
+ }
37242
38452
  /** An inbound turn may receive OpenClaw memory only from an active owner DM. */
37243
38453
  mayReadOpenClawMemoryForTurn(authorization) {
37244
38454
  return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
@@ -37248,16 +38458,119 @@ var BridgeAgent = class _BridgeAgent {
37248
38458
  * stays owner-DM-only; every provider failure degrades to empty context and
37249
38459
  * never falls back to the runtime's retired plaintext JSON files.
37250
38460
  */
37251
- async memoryForTurn(channelId, authorization, limit = 50) {
37252
- if (!this.mayReadOpenClawMemoryForTurn(authorization)) {
38461
+ async memoryForTurn(channelId, messageId, authorization, limit = 50) {
38462
+ if (!this.mayReadHistoryForTurn(messageId, authorization)) {
37253
38463
  return { recent: [], facts: [] };
37254
38464
  }
37255
38465
  try {
37256
- return await this.memory.context(channelId, limit);
38466
+ return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
37257
38467
  } catch {
37258
38468
  return { recent: [], facts: [] };
37259
38469
  }
37260
38470
  }
38471
+ /**
38472
+ * Owner grants plan 4 Task 16 (T-30, audit r2 H-1): the voice fence is on whenever a pin is loaded, on EVERY runtime
38473
+ * turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
38474
+ */
38475
+ voiceFenceOn() {
38476
+ return this.cfg.ownerGrantPin?.pin != null;
38477
+ }
38478
+ /** The keyed and legacy fast-reply ids of every peer row of the conversation (audit r2 M-1; never the 50-row window). */
38479
+ fenceContext(channelId) {
38480
+ let ids;
38481
+ const build = () => {
38482
+ const out = /* @__PURE__ */ new Set();
38483
+ for (const p of this.store.memoryPeerMessageIds(channelId)) {
38484
+ out.add(fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, p));
38485
+ out.add(legacyFastReplyId(p));
38486
+ }
38487
+ return out;
38488
+ };
38489
+ return { get fastIds() {
38490
+ return ids ??= build();
38491
+ } };
38492
+ }
38493
+ /**
38494
+ * Drop agent lines classified fast_path, untrusted or unknown from a turn's memory (every turn of a pinned agent).
38495
+ * `strict` (review H-3, a turn that holds a manifest grant): relay-class peer lines go too (sender not the pinned
38496
+ * owner, or voice modality). Otherwise peer lines are never dropped.
38497
+ */
38498
+ fenceMemory(channelId, ctx, manifestTurn = false) {
38499
+ if (!this.voiceFenceOn()) return ctx;
38500
+ const scoped = this.taintScope(channelId);
38501
+ const strict = manifestTurn && scoped;
38502
+ if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
38503
+ const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
38504
+ const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
38505
+ const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => strict || t.role === "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
38506
+ const fence = this.fenceContext(channelId);
38507
+ const recent = ctx.recent.filter((t) => {
38508
+ const m = meta.get(t.seq);
38509
+ if (t.role !== "agent") return !(strict && isRelayClassPeer({ peerId: t.peerId, modality: m?.modality }, ownerId));
38510
+ const origin = classifyAgentLine({ ...m ?? {}, ts: t.ts }, fence);
38511
+ return origin === "reply" || origin === "untrusted" && !scoped;
38512
+ });
38513
+ return recent.length === ctx.recent.length ? ctx : { ...ctx, recent };
38514
+ }
38515
+ /**
38516
+ * Review H-3: true when a delivered frame holds a line a manifest turn would exclude (a relay-class peer line; agent
38517
+ * lines in a frame are already fenced). Such a turn's reply is recorded "untrusted".
38518
+ */
38519
+ frameHoldsRelayClass(m) {
38520
+ const recent = m.memory?.recent ?? [];
38521
+ if (recent.length === 0) return false;
38522
+ const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
38523
+ const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
38524
+ const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
38525
+ return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
38526
+ }
38527
+ /**
38528
+ * Review H-3 attribution: message ids of turns delivered on a pinned agent whose frame held no relay-class line.
38529
+ * A reply is recorded "reply" only when its in-reply-to is here; anything else (tainted, unknown after a restart,
38530
+ * no in-reply-to) is "untrusted". Bounded; the oldest are forgotten first (forgetting only makes a reply untrusted).
38531
+ */
38532
+ cleanTurns = /* @__PURE__ */ new Set();
38533
+ noteDeliveredFrame(m, unifiedTainted = false) {
38534
+ if (!this.taintScope(m.channel_id)) return;
38535
+ const tainted = unifiedTainted || this.turnIsRelayClass(m) || !holdsManifestGrant(m.authorization) && this.frameHoldsRelayClass(m);
38536
+ if (tainted) {
38537
+ this.cleanTurns.delete(m.message_id);
38538
+ this.taintedTurns.add(m.message_id);
38539
+ if (this.taintedTurns.size > 5e3) this.taintedTurns.delete(this.taintedTurns.values().next().value);
38540
+ return;
38541
+ }
38542
+ if (this.taintedTurns.has(m.message_id)) return;
38543
+ this.cleanTurns.add(m.message_id);
38544
+ if (this.cleanTurns.size > 5e3) this.cleanTurns.delete(this.cleanTurns.values().next().value);
38545
+ }
38546
+ taintedTurns = /* @__PURE__ */ new Set();
38547
+ /**
38548
+ * Review R2-H2: on a pinned agent a 7a note or a unified memory_note is written only from a turn delivered with a clean
38549
+ * frame in the owner DM (cleanTurns, not taintedTurns), so a tainted fallback turn cannot plant a note that a later
38550
+ * manifest turn reads. Taint is noted at delivery, before any note frame for the turn can arrive.
38551
+ */
38552
+ noteTurnClean(messageId) {
38553
+ return !this.voiceFenceOn() || this.cleanTurns.has(messageId) && !this.taintedTurns.has(messageId);
38554
+ }
38555
+ /**
38556
+ * Review R2-H1: the turn itself is relay-class (its frame memory is prior-turn only, so its own sender is never in it):
38557
+ * a sender that is not the pinned owner, a relay speaker claim, or a voice turn.
38558
+ */
38559
+ turnIsRelayClass(m) {
38560
+ const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId;
38561
+ return m.sender_id !== ownerId || m.authorization?.relay_speaker !== void 0 || m.voice === true || m.transcribed === true || m.speaker !== void 0;
38562
+ }
38563
+ /**
38564
+ * R2-M1 ruling: the channel where the H-3 taint and the `untrusted` exclusion apply, on a pinned agent: the signed
38565
+ * owner_dm_channel_id of the manifest in force. Before any manifest is held there is no signed DM, so every channel
38566
+ * not known to be a room counts (fail closed: a reply recorded there before the first manifest must not reach it).
38567
+ */
38568
+ taintScope(channelId) {
38569
+ if (!this.voiceFenceOn()) return false;
38570
+ const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
38571
+ if (signedDm !== void 0) return signedDm === channelId;
38572
+ return (this.freshMeta(channelId)?.is_dm ?? this.store.getChannelMeta(channelId)?.isDm) !== false;
38573
+ }
37261
38574
  /**
37262
38575
  * Spec §11.2 (OQ-L), §6.5: apply one parsed `payload.memory`. An unreadable
37263
38576
  * value turns notes off and deletes nothing. A readable one goes through
@@ -37713,10 +39026,16 @@ var BridgeAgent = class _BridgeAgent {
37713
39026
  if (isDm !== true || ownerId === void 0) return { kind: "unknown" };
37714
39027
  return this.store.recordDmOwner(c, ownerId) ? { kind: "owner", ownerId } : { kind: "unknown" };
37715
39028
  };
39029
+ const fences = /* @__PURE__ */ new Map();
39030
+ const classifyHistoryOrigin = (c, r) => {
39031
+ const f = fences.get(c) ?? (fences.set(c, this.fenceContext(c)), fences.get(c));
39032
+ return classifyAgentLine({ origin: r.origin, modality: r.modality, ...r.messageId ? { messageId: r.messageId } : {}, ts: r.recordedAt }, f);
39033
+ };
37716
39034
  const out = await runMigration({
37717
39035
  store: this.store,
37718
39036
  items: this.items,
37719
39037
  classify,
39038
+ classifyHistoryOrigin,
37720
39039
  stopping: () => this.closingDown,
37721
39040
  pinsChanged: () => this.notes.exclusive(async () => {
37722
39041
  this.unifiedReads?.bumpGeneration();
@@ -37736,7 +39055,24 @@ var BridgeAgent = class _BridgeAgent {
37736
39055
  const a = m.authorization;
37737
39056
  if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
37738
39057
  const scopeId = this.ownerScopeNow(m.channel_id);
37739
- return scopeId === null ? void 0 : this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId);
39058
+ if (scopeId === null) return void 0;
39059
+ if (!this.voiceFenceOn()) return this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId);
39060
+ const contexts = /* @__PURE__ */ new Map();
39061
+ const ctxFor = (c) => contexts.get(c) ?? (contexts.set(c, this.fenceContext(c)), contexts.get(c));
39062
+ const trace = (c, at) => this.store.memoryRowForTrace(c, at);
39063
+ const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
39064
+ const scoped = this.taintScope(m.channel_id);
39065
+ const strict = scoped && holdsManifestGrant(m.authorization);
39066
+ const strictRule = { ownerId, turnChannelId: m.channel_id };
39067
+ let tainted = false;
39068
+ const out = this.unifiedReads.deliverable(m.message_id, m.channel_id, scopeId, (i) => {
39069
+ const facts = { channelId: i.channelId, source: i.source, originId: i.originId, role: i.role, origin: i.origin, sourceRef: i.sourceRef };
39070
+ const keep = classifyUnifiedItem(facts, trace, ctxFor(i.channelId), strict ? strictRule : void 0, !scoped) === "keep";
39071
+ if (keep && scoped && !strict && classifyUnifiedItem(facts, trace, ctxFor(i.channelId), strictRule) === "exclude") tainted = true;
39072
+ return keep;
39073
+ });
39074
+ if (tainted) this.noteDeliveredFrame(m, true);
39075
+ return out;
37740
39076
  }
37741
39077
  /** ws-server: the hello echoes memory_unified only with the part 1 switch compiled on. */
37742
39078
  unifiedFeatureOn() {
@@ -38038,6 +39374,7 @@ var BridgeAgent = class _BridgeAgent {
38038
39374
  if (rec === void 0) return refuse("message");
38039
39375
  const channelId = rec.channel_id;
38040
39376
  if (rec.authorization?.memory_write !== true) return refuse("audience");
39377
+ if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
38041
39378
  const held = await this.heldPosition(channelId);
38042
39379
  const now = this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false });
38043
39380
  if (now.memory_write !== true) return refuse("audience");
@@ -38141,6 +39478,7 @@ var BridgeAgent = class _BridgeAgent {
38141
39478
  if (rec === void 0) return refuse("message");
38142
39479
  const channelId = rec.channel_id;
38143
39480
  if (rec.authorization?.memory_write !== true) return refuse("audience");
39481
+ if (!this.noteTurnClean(rec.message_id)) return refuse("audience");
38144
39482
  const held = await this.heldPosition(channelId);
38145
39483
  if (this.authorizationFor(channelId, rec.sender_id, void 0, held.epoch, held.groupId, { log: false }).memory_write !== true) return refuse("audience");
38146
39484
  const scopeId = this.ownerScopeNow(channelId);
@@ -38224,6 +39562,9 @@ var BridgeAgent = class _BridgeAgent {
38224
39562
  return { type: "memory_note_result", request_id: f.request_id, message_id: f.message_id, ok: true };
38225
39563
  }
38226
39564
  async memoryContext(conversationId, limit) {
39565
+ for (const [id, channelId] of this.ownerVerifyFailedIds) {
39566
+ if (channelId === conversationId && this.runtimePendingIds.has(id)) return { recent: [], facts: [] };
39567
+ }
38227
39568
  if (!await this.mayReadOpenClawMemory(conversationId)) {
38228
39569
  return { recent: [], facts: [] };
38229
39570
  }
@@ -38371,7 +39712,7 @@ var BridgeAgent = class _BridgeAgent {
38371
39712
  const fastLlm = this.fastLlm;
38372
39713
  if (!fastLlm || !this.cfg.voice) return "failed";
38373
39714
  let memory;
38374
- if (!this.mayReadOpenClawMemoryForTurn(m.authorization)) {
39715
+ if (!this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
38375
39716
  memory = { recent: [], facts: [] };
38376
39717
  } else {
38377
39718
  try {
@@ -38419,15 +39760,17 @@ var BridgeAgent = class _BridgeAgent {
38419
39760
  * frame (the "fast path failed" semantics), so a crash mid-call never loses
38420
39761
  * the turn.
38421
39762
  */
38422
- async stageHeldRuntimeMessage(message) {
38423
- if (this.runtimePendingIds.has(message.message_id)) return true;
39763
+ async stageHeldRuntimeMessage(held, opts = {}) {
39764
+ if (this.runtimePendingIds.has(held.message_id)) return true;
38424
39765
  if (this.runtimePendingIds.size >= this.maxBuffer) {
38425
39766
  writeServiceLog2("warn", "runtime_inbox.full");
38426
39767
  return false;
38427
39768
  }
39769
+ const message = opts.absorbed ? held : this.applyRoomSearchBackstop(held);
38428
39770
  try {
38429
39771
  await this.stageSealed(message);
38430
39772
  this.runtimePendingIds.add(message.message_id);
39773
+ if (message !== held) this.backstopStripped.add(message.message_id);
38431
39774
  return true;
38432
39775
  } catch (error) {
38433
39776
  writeServiceLog2("error", "runtime_inbox.stage_failed", {}, error);
@@ -38443,7 +39786,8 @@ var BridgeAgent = class _BridgeAgent {
38443
39786
  }
38444
39787
  const memory = await this.memoryAtRelease(message);
38445
39788
  if (this.closingDown || !this.runtimePendingIds.has(message.message_id)) return;
38446
- this.buffer.push(memory === message.memory ? message : { ...message, memory });
39789
+ const out = this.backstopStripped.delete(message.message_id) ? this.withoutWebSearch(message) : message;
39790
+ this.buffer.push(memory === message.memory ? out : { ...out, memory });
38447
39791
  this.wakeListeners();
38448
39792
  }
38449
39793
  /**
@@ -38461,7 +39805,7 @@ var BridgeAgent = class _BridgeAgent {
38461
39805
  */
38462
39806
  async memoryAtRelease(message) {
38463
39807
  const prior = message.memory;
38464
- if (!prior || !this.mayReadOpenClawMemoryForTurn(message.authorization)) return prior;
39808
+ if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
38465
39809
  let fresh;
38466
39810
  try {
38467
39811
  fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
@@ -38469,7 +39813,7 @@ var BridgeAgent = class _BridgeAgent {
38469
39813
  return prior;
38470
39814
  }
38471
39815
  const after = prior.recent.reduce((max, turn) => Math.max(max, turn.ts ?? 0), message.ts);
38472
- const spoken = fresh.recent.filter((turn) => turn.role === "agent" && turn.ts !== void 0 && turn.ts > after);
39816
+ const spoken = this.fenceMemory(message.channel_id, fresh, holdsManifestGrant(message.authorization)).recent.filter((turn) => turn.role === "agent" && turn.ts !== void 0 && turn.ts > after);
38473
39817
  if (spoken.length === 0) return prior;
38474
39818
  return {
38475
39819
  recent: [...prior.recent, ...spoken].slice(-_BridgeAgent.TURN_MEMORY_LIMIT),
@@ -38752,6 +40096,11 @@ var BridgeAgent = class _BridgeAgent {
38752
40096
  });
38753
40097
  }
38754
40098
  this.channelRoster.set(channelId, { members, at: Date.now(), epoch, groupId, crossOrg: roomIsCrossOrg(rows) });
40099
+ const grantPin = this.cfg.ownerGrantPin?.pin;
40100
+ if (grantPin && !this.serverOwnerMismatchLogged && liveRows.some((r) => r.is_agent_owner === true && !r.is_agent && r.identity_id !== grantPin.ownerIdentityId)) {
40101
+ this.serverOwnerMismatchLogged = true;
40102
+ writeServiceLog2("warn", "authz.pin", { state: ServiceLogEnum.of("server_owner_mismatch", ["server_owner_mismatch"]) });
40103
+ }
38755
40104
  const self = members.get(this.cfg.identityId);
38756
40105
  if (self?.name) this.lastOwnDisplayName = self.name;
38757
40106
  await this.resolveAndPinOwner(channelId, liveRows);
@@ -38778,11 +40127,200 @@ var BridgeAgent = class _BridgeAgent {
38778
40127
  }
38779
40128
  }
38780
40129
  }
40130
+ /**
40131
+ * Owner-signed tool grants, step 1 (spec §7.2; decisions D-1, D-2, D-4, D-8,
40132
+ * D-9; audits H-1, M-2, M-4, E-1..E-3). The verdict for one delivered turn.
40133
+ *
40134
+ * `unchecked` when no pin is loaded or the sender is not the pinned owner.
40135
+ * Otherwise, in order, the first failure wins: the identity signature over the
40136
+ * raw payload bytes with the pinned key; the verified bytes must be the
40137
+ * decrypted envelope; in a DM only (D-1), the signed DM channel and the MLS
40138
+ * leaf set; then the replay set and closed epochs, keyed by the agent's OWN
40139
+ * held group (never the backend's row label, M-2). NEVER throws (M-4): any
40140
+ * exception is `failed verify_error`, and the row is acked like any other turn.
40141
+ */
40142
+ async verifyOwnerTurn(m) {
40143
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
40144
+ if (!pin || m.envelope.sender_id !== pin.ownerIdentityId) return OWNER_TURN_UNCHECKED;
40145
+ try {
40146
+ const sig = verifyOwnerSignature({ payloadHex: m.payloadHex, identitySignature: m.identitySignature }, pin);
40147
+ if (sig.state !== "ok") return this.ownerTurnFailed(m, this.isAckedOwnerOriginal(m) ? "replay" : sig.state);
40148
+ const v = sig.envelope;
40149
+ if (v.message_id !== m.envelope.message_id || v.channel_id !== m.envelope.channel_id || v.sender_id !== m.envelope.sender_id || v.mls_group_epoch !== m.envelope.mls_group_epoch || v.payload_encrypted !== m.envelope.payload_encrypted) {
40150
+ return this.ownerTurnFailed(m, "malformed");
40151
+ }
40152
+ if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
40153
+ let leafClass = "ok";
40154
+ if (this.freshMeta(m.channelId)?.is_dm === true) {
40155
+ const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
40156
+ if (signedDm !== void 0 && signedDm !== m.channelId) return this.ownerTurnFailed(m, "wrong_dm_channel");
40157
+ leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
40158
+ ownerIdentityId: pin.ownerIdentityId,
40159
+ agentIdentityId: this.cfg.identityId,
40160
+ relayIdentityId: this.toolGrantManifest?.owner_dm_relay_identity_id,
40161
+ isServerRelay: (id) => this.freshRoster(m.channelId)?.get(id)?.is_relay === true
40162
+ });
40163
+ if (leafClass === "foreign") return this.ownerTurnFailed(m, "not_owner_dm_leaves");
40164
+ }
40165
+ const held = await this.heldPosition(m.channelId);
40166
+ if (held.groupId === null) return this.ownerTurnFailed(m, "verify_error");
40167
+ if (!this.runtimePendingIds.has(m.messageId) && (this.store.hasOwnerMessage(m.channelId, m.messageId) || this.store.isOwnerEpochClosed(m.channelId, held.groupId, v.mls_group_epoch))) {
40168
+ return this.ownerTurnFailed(m, "replay");
40169
+ }
40170
+ const replay = Object.freeze({ channelId: m.channelId, groupId: held.groupId, epoch: v.mls_group_epoch });
40171
+ this.ownerReplayPending.set(m.messageId, replay);
40172
+ return Object.freeze({ kind: leafClass === "unsigned_relay" ? "constants_only" : "verified", replay });
40173
+ } catch {
40174
+ return this.ownerTurnFailed(m, "verify_error");
40175
+ }
40176
+ }
40177
+ /** Log, count and (except a replay, audit E-3) record one failed owner-turn check. Never throws (audit E-2). */
40178
+ ownerTurnFailed(m, reason) {
40179
+ writeServiceLog2("warn", "authz.owner_dm_verify", {
40180
+ state: ServiceLogEnum.of(reason, [
40181
+ "bad_signature",
40182
+ "malformed",
40183
+ "unknown_algo",
40184
+ "no_signature",
40185
+ "not_owner_dm_leaves",
40186
+ "wrong_dm_channel",
40187
+ "replay",
40188
+ "verify_error"
40189
+ ])
40190
+ });
40191
+ this.ownerVerifyFailures += 1;
40192
+ if (reason !== "replay" && !this.isVerifiedOwnerOriginal(m)) {
40193
+ this.ownerVerifyFailedIds.set(m.messageId, m.channelId);
40194
+ try {
40195
+ this.store.recordOwnerVerifyFailure(m.channelId, m.messageId, Date.now());
40196
+ } catch {
40197
+ writeServiceLog2("warn", "authz.owner_verify_record_failed");
40198
+ }
40199
+ }
40200
+ return Object.freeze({ kind: "failed", reason });
40201
+ }
40202
+ /** Review M-A: a verified original of this id was staged and is still pending, or was recorded in the replay set. */
40203
+ isVerifiedOwnerOriginal(m) {
40204
+ return this.runtimePendingIds.has(m.messageId) || this.isAckedOwnerOriginal(m);
40205
+ }
40206
+ /** Review M-A: a verified original of this id was staged and acked (it is in the replay set, no longer pending). */
40207
+ isAckedOwnerOriginal(m) {
40208
+ if (this.runtimePendingIds.has(m.messageId)) return false;
40209
+ try {
40210
+ return this.store.hasOwnerMessage(m.channelId, m.messageId);
40211
+ } catch {
40212
+ return false;
40213
+ }
40214
+ }
40215
+ /** D-9: memory first, then the store (a restart before the reply). Unpinned: never. A store read that throws withholds (fail closed). */
40216
+ isOwnerVerifyFailed(messageId) {
40217
+ if (this.ownerVerifyFailedIds.has(messageId)) return true;
40218
+ if (!this.cfg.ownerGrantPin?.pin) return false;
40219
+ try {
40220
+ return this.store.isOwnerVerifyFailed(messageId);
40221
+ } catch {
40222
+ return true;
40223
+ }
40224
+ }
40225
+ /**
40226
+ * Decision D-2, audit L-2: prune the owner replay set at most once per
40227
+ * OWNER_REPLAY_PRUNE_INTERVAL_MS, keeping each channel's current (group,
40228
+ * epoch). A channel whose held position is unknown is left out (not pruned).
40229
+ */
40230
+ async maybePruneOwnerReplay() {
40231
+ if (!this.cfg.ownerGrantPin?.pin) return;
40232
+ const now = this.monoNow();
40233
+ if (now - this.lastOwnerReplayPruneAt < OWNER_REPLAY_PRUNE_INTERVAL_MS) return;
40234
+ this.lastOwnerReplayPruneAt = now;
40235
+ try {
40236
+ const currentOf = /* @__PURE__ */ new Map();
40237
+ for (const channelId of this.store.listOwnerReplayChannels()) {
40238
+ const held = await this.heldPosition(channelId);
40239
+ if (held.groupId !== null && held.epoch !== null) currentOf.set(channelId, { groupId: held.groupId, epoch: held.epoch });
40240
+ }
40241
+ this.store.pruneOwnerReplay(Date.now(), currentOf);
40242
+ } catch (e) {
40243
+ writeServiceLog2("warn", "authz.replay_prune_failed", {}, e);
40244
+ }
40245
+ }
40246
+ /** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
40247
+ async loadToolGrantState() {
40248
+ const pin = this.cfg.ownerGrantPin?.pin;
40249
+ if (!pin) return;
40250
+ try {
40251
+ this.store.dropToolGrantManifestsExcept(pin.publicKeyB64);
40252
+ this.store.dropToolGrantAppliedExcept(pin.publicKeyB64);
40253
+ const saved = await this.store.loadToolGrantManifest(pin.publicKeyB64);
40254
+ if (saved) {
40255
+ const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null });
40256
+ if (v.state === "ok") {
40257
+ this.toolGrantManifest = v.manifest;
40258
+ this.toolGrantCounter = v.manifest.counter;
40259
+ } else {
40260
+ this.logManifest("malformed");
40261
+ }
40262
+ }
40263
+ } catch (e) {
40264
+ writeServiceLog2("warn", "authz.manifest_load_failed", {}, e);
40265
+ }
40266
+ await this.maybePruneOwnerReplay();
40267
+ }
40268
+ /**
40269
+ * Spec §7.1: verify and record a served manifest. Never writes a file and
40270
+ * never changes a grant in step 1. A save that throws keeps the old state.
40271
+ */
40272
+ async applyToolGrantManifest(raw) {
40273
+ const pin = this.cfg.ownerGrantPin?.pin;
40274
+ if (!pin) {
40275
+ this.logManifest("no_pin");
40276
+ return;
40277
+ }
40278
+ if (raw === void 0 || raw === null) {
40279
+ this.logManifest("no_manifest");
40280
+ return;
40281
+ }
40282
+ const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter });
40283
+ if (v.state === "ok") {
40284
+ try {
40285
+ await this.store.saveToolGrantManifest(pin.publicKeyB64, v.manifest.counter, v.wrapper);
40286
+ } catch {
40287
+ this.logManifest("kept_last_good");
40288
+ return;
40289
+ }
40290
+ this.toolGrantManifest = v.manifest;
40291
+ this.toolGrantCounter = v.manifest.counter;
40292
+ this.logManifest("ok");
40293
+ if (v.droppedUnknown) this.logManifest("unknown_entry");
40294
+ return;
40295
+ }
40296
+ this.logManifest(v.state);
40297
+ if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
40298
+ }
40299
+ logManifest(state) {
40300
+ this.manifestState = state;
40301
+ const quiet = state === "ok" || state === "unchanged" || state === "no_pin" || state === "no_manifest";
40302
+ writeServiceLog2(quiet ? "info" : "warn", "authz.manifest", {
40303
+ state: ServiceLogEnum.of(state, [
40304
+ "ok",
40305
+ "unchanged",
40306
+ "no_pin",
40307
+ "no_manifest",
40308
+ "malformed",
40309
+ "unsupported_algo",
40310
+ "bad_signature",
40311
+ "wrong_agent",
40312
+ "wrong_owner",
40313
+ "rollback",
40314
+ "unknown_entry",
40315
+ "kept_last_good"
40316
+ ])
40317
+ });
40318
+ }
38781
40319
  /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
38782
- async authorizeTurn(channelId, messageId, senderId, relaySpeakerId) {
40320
+ async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
38783
40321
  const held = await this.heldPosition(channelId);
38784
- const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId);
38785
- if (auth.tool_grants !== ROOM_DEFAULT_READ_GRANTS) return auth;
40322
+ const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
40323
+ if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
38786
40324
  if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
38787
40325
  writeServiceLog2("warn", "room_grant.untracked");
38788
40326
  return Object.freeze({ ...auth, tool_grants: NO_TOOL_GRANTS });
@@ -38864,9 +40402,48 @@ var BridgeAgent = class _BridgeAgent {
38864
40402
  if (this.roomDefaultGrants && this.perToolConsumer !== null && epochFresh && meta?.is_dm === false && audience === "team_room" && entry.crossOrg === false) {
38865
40403
  tool_grants = ROOM_DEFAULT_READ_GRANTS;
38866
40404
  }
40405
+ let grantSource = tool_grants.length ? "constant" : "none";
40406
+ const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
40407
+ const binding = this.manifestBinding;
40408
+ const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
40409
+ const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
40410
+ if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
40411
+ const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null;
40412
+ const r = effectiveToolGrants({
40413
+ audience: "owner_dm",
40414
+ base: OWNER_DM_DEFAULT_READ_GRANTS,
40415
+ manifest,
40416
+ eligible,
40417
+ manifestSwitchOn: this.ownerDmManifestGrants,
40418
+ disabled: this.payloadToolDeny,
40419
+ signedTargets,
40420
+ roomSearchCapped: false
40421
+ });
40422
+ tool_grants = r.grants;
40423
+ grantSource = r.source;
40424
+ } else if (tool_grants === ROOM_DEFAULT_READ_GRANTS) {
40425
+ const r = effectiveToolGrants({
40426
+ audience: "team_room",
40427
+ base: ROOM_DEFAULT_READ_GRANTS,
40428
+ manifest,
40429
+ eligible: boundNow && this.manifestConsumer !== null,
40430
+ manifestSwitchOn: this.roomManifestGrants,
40431
+ disabled: this.payloadToolDeny,
40432
+ signedTargets,
40433
+ roomSearchCapped: manifest !== null && this.roomSearchCapped(channelId)
40434
+ });
40435
+ tool_grants = r.grants;
40436
+ grantSource = r.source;
40437
+ }
40438
+ if (opts.ownerVerdict?.kind === "failed") {
40439
+ tool_grants = NO_TOOL_GRANTS;
40440
+ memoryWrite = false;
40441
+ relaySpeaker = void 0;
40442
+ grantSource = "none";
40443
+ }
38867
40444
  const roomClass = meta?.is_dm !== false ? "na" : entry?.crossOrg === false ? "internal" : entry?.crossOrg === true ? "cross_org" : "unknown";
38868
40445
  if (opts.log !== false) {
38869
- this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite);
40446
+ this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite, grantSource);
38870
40447
  }
38871
40448
  return Object.freeze({
38872
40449
  version: 1,
@@ -38902,8 +40479,10 @@ var BridgeAgent = class _BridgeAgent {
38902
40479
  * enforcement is attached
38903
40480
  * memory_write the turn carries memory_write (Markdown memory
38904
40481
  * 7a, spec §8.3): a boolean, never content
40482
+ * grant_source where the grants came from: constant | none in
40483
+ * step 1 (owner-signed tool grants §7.6)
38905
40484
  */
38906
- logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants, roomClass, epochFresh, memoryWrite) {
40485
+ logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants, roomClass, epochFresh, memoryWrite, grantSource) {
38907
40486
  const meta = this.freshMeta(channelId);
38908
40487
  const roster = this.freshRoster(channelId);
38909
40488
  const sender = roster?.get(senderId);
@@ -38936,7 +40515,8 @@ var BridgeAgent = class _BridgeAgent {
38936
40515
  epoch_fresh: epochFresh,
38937
40516
  room_kill_switch: ServiceLogEnum.of(this.roomDefaultGrants ? "on" : "off", ["on", "off"]),
38938
40517
  per_tool_consumer: this.perToolConsumer !== null,
38939
- memory_write: memoryWrite
40518
+ memory_write: memoryWrite,
40519
+ grant_source: ServiceLogEnum.of(grantSource, ["constant", "manifest", "fallback", "none"])
38940
40520
  });
38941
40521
  }
38942
40522
  /**
@@ -39529,6 +41109,7 @@ var BridgeAgent = class _BridgeAgent {
39529
41109
  this.joinState.set(cid, "JOINED");
39530
41110
  }
39531
41111
  if (!entries.length) return;
41112
+ await this.maybePruneOwnerReplay();
39532
41113
  const receivedAt = Date.now();
39533
41114
  const purgeEpochAtReceipt = /* @__PURE__ */ new Map();
39534
41115
  const itemCaptureAtReceipt = /* @__PURE__ */ new Map();
@@ -39621,7 +41202,7 @@ var BridgeAgent = class _BridgeAgent {
39621
41202
  acked.push(m.queueId);
39622
41203
  continue;
39623
41204
  }
39624
- const refAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id);
41205
+ const refAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id, await this.verifyOwnerTurn(m));
39625
41206
  await this.prepareNotesForTurn(m.channelId, refAuth);
39626
41207
  const vaultRefMessage = {
39627
41208
  channel_id: m.channelId,
@@ -39681,7 +41262,7 @@ var BridgeAgent = class _BridgeAgent {
39681
41262
  { attachmentsDir: this.attachmentsDirImpl, channelId: m.channelId, messageId: m.messageId },
39682
41263
  this.receiveDeps()
39683
41264
  );
39684
- const shareAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id);
41265
+ const shareAuth = await this.authorizeTurn(m.channelId, m.messageId, m.envelope.sender_id, await this.verifyOwnerTurn(m));
39685
41266
  await this.prepareNotesForTurn(m.channelId, shareAuth);
39686
41267
  const runtimeMessage = {
39687
41268
  channel_id: m.channelId,
@@ -39749,14 +41330,16 @@ var BridgeAgent = class _BridgeAgent {
39749
41330
  const senderName = this.freshRoster(m.channelId)?.get(
39750
41331
  m.envelope.sender_id
39751
41332
  )?.name;
41333
+ const ownerVerdict = await this.verifyOwnerTurn(m);
39752
41334
  const turnAuthorization = await this.authorizeTurn(
39753
41335
  m.channelId,
39754
41336
  m.messageId,
39755
41337
  m.envelope.sender_id,
41338
+ ownerVerdict,
39756
41339
  speaker?.identityId
39757
41340
  );
39758
41341
  await this.prepareNotesForTurn(m.channelId, turnAuthorization);
39759
- const runtimeMemory = this.memoryForTurn(m.channelId, turnAuthorization);
41342
+ const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
39760
41343
  const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
39761
41344
  const humanTurn = senderIsAgent !== true || speaker !== void 0;
39762
41345
  const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
@@ -39787,7 +41370,9 @@ var BridgeAgent = class _BridgeAgent {
39787
41370
  sender_id: m.envelope.sender_id,
39788
41371
  text: turnText,
39789
41372
  ts: m.receivedAt,
39790
- authorization: turnAuthorization,
41373
+ // Owner grants plan 4 Task 16 (M-6): under a manifest a call-start greeting entry carries no grants and no
41374
+ // memory_write, on every branch (skip, held, heldAfterAck spreads held). Unpinned agents unchanged (G-4).
41375
+ authorization: fastGreeting && this.toolGrantManifest !== null ? greetingAuthorization(turnAuthorization) : turnAuthorization,
39791
41376
  memory,
39792
41377
  // A relay-attributed turn is the SPEAKER's message to the runtime:
39793
41378
  // name it after them and never flag it as agent-authored.
@@ -39811,7 +41396,7 @@ var BridgeAgent = class _BridgeAgent {
39811
41396
  open2.ids.push(m.messageId);
39812
41397
  open2.parts.push(turnText);
39813
41398
  const staging = (async () => {
39814
- const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory));
41399
+ const ok = await this.stageHeldRuntimeMessage(runtimeEntry(false, await runtimeMemory), { absorbed: true });
39815
41400
  if (!ok) {
39816
41401
  const i = open2.ids.indexOf(m.messageId);
39817
41402
  if (i > 0) {
@@ -39950,11 +41535,11 @@ var BridgeAgent = class _BridgeAgent {
39950
41535
  void work.finally(() => this.unifiedWork.delete(work));
39951
41536
  }
39952
41537
  }
39953
- } else if (this.recallProviderOn()) {
41538
+ } else if (this.recallProviderOn() && ownerVerdict.kind !== "failed") {
39954
41539
  const recallTap = this.captureRecallTap(m.messageId, m.channelId);
39955
41540
  this.spawnRecallTap({ message: runtimeEntry(false, { recent: [], facts: [] }), captured: recallTap });
39956
41541
  }
39957
- void recordInboundMemory(
41542
+ if (ownerVerdict.kind !== "failed") void recordInboundMemory(
39958
41543
  this.memory,
39959
41544
  m.channelId,
39960
41545
  m.envelope.sender_id,
@@ -39977,6 +41562,7 @@ var BridgeAgent = class _BridgeAgent {
39977
41562
  } catch {
39978
41563
  }
39979
41564
  }
41565
+ this.ownerReplayPending.clear();
39980
41566
  this.latency.record("recv.cycle", performance.now() - cycleStart);
39981
41567
  if (this.buffer.length) {
39982
41568
  for (const l of this.listeners) l();
@@ -40015,7 +41601,46 @@ var BridgeAgent = class _BridgeAgent {
40015
41601
  return () => this.listeners.delete(listener);
40016
41602
  }
40017
41603
  /** Synchronous splice of buffered messages (all, or one channel). Shared by drain + SSE. */
41604
+ /** Review H-1: every buffer exit narrows manifest grants again to the state at delivery. */
40018
41605
  takeBuffered(channelId) {
41606
+ return this.takeBufferedRaw(channelId).map((m) => this.delivering(m));
41607
+ }
41608
+ takeDeliverable(busyChannels) {
41609
+ return this.takeDeliverableRaw(busyChannels).map((m) => this.delivering(m));
41610
+ }
41611
+ /** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
41612
+ delivering(m) {
41613
+ const out = this.renarrowManifestGrants(m);
41614
+ this.noteDeliveredFrame(out);
41615
+ return out;
41616
+ }
41617
+ /**
41618
+ * Review H-1 (kill switches reach staged and restored turns): a grant marked source "manifest" survives delivery
41619
+ * only while its audience's manifest switch is on, the binding is bound at the applied counter, and the current
41620
+ * manifest still signs the tool. Otherwise it becomes the fallback: kept unmarked if it is a constant of the
41621
+ * audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
41622
+ */
41623
+ renarrowManifestGrants(m) {
41624
+ const a = m.authorization;
41625
+ if (!a || !a.tool_grants.some((g) => g.mode === "read" && g.source === "manifest")) return m;
41626
+ const audience = a.audience === "owner_dm" || a.audience === "team_room" ? a.audience : null;
41627
+ const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
41628
+ const b = this.manifestBinding;
41629
+ const bound = b !== null && b.state === "bound" && b.counter === this.toolGrantCounter;
41630
+ const switchOn = audience === "owner_dm" ? this.ownerDmManifestGrants : audience === "team_room" ? this.roomManifestGrants : false;
41631
+ const signedNow = new Set(audience && manifest ? manifest.grants[audience] ?? [] : []);
41632
+ const constants4 = new Set(audience === "owner_dm" ? OWNER_DM_DEFAULT_READ_GRANTS.map((g) => g.tool) : audience === "team_room" ? ROOM_DEFAULT_READ_GRANTS.map((g) => g.tool) : []);
41633
+ let changed = false;
41634
+ const out = a.tool_grants.flatMap((g) => {
41635
+ if (!(g.mode === "read" && g.source === "manifest")) return [g];
41636
+ if (switchOn && bound && signedNow.has(g.tool)) return [g];
41637
+ changed = true;
41638
+ return constants4.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
41639
+ });
41640
+ if (!changed) return m;
41641
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(out) }) };
41642
+ }
41643
+ takeBufferedRaw(channelId) {
40019
41644
  if (channelId === void 0 && this.pausedChannels.size === 0 && this.supersededChannels.size === 0) {
40020
41645
  return this.buffer.splice(0);
40021
41646
  }
@@ -40038,7 +41663,7 @@ var BridgeAgent = class _BridgeAgent {
40038
41663
  * working on — which it used to refuse with a "busy" reply and ack — and a
40039
41664
  * backlog replays one message per channel at a time after a restart.
40040
41665
  */
40041
- takeDeliverable(busyChannels) {
41666
+ takeDeliverableRaw(busyChannels) {
40042
41667
  const picked = /* @__PURE__ */ new Set();
40043
41668
  const out = [];
40044
41669
  const keep = [];
@@ -40198,6 +41823,107 @@ var BridgeAgent = class _BridgeAgent {
40198
41823
  }
40199
41824
  this.noticeDeadLetter(message.channel_id);
40200
41825
  }
41826
+ /** Spec §7.5: the room is capped today: cap 0, reached (by the runtime or the backstop), or at the cap. Fails closed. */
41827
+ roomSearchCapped(channelId) {
41828
+ if (this.roomSearchDailyCap === 0 || this.agentRoomSearchDailyCap === 0) return true;
41829
+ try {
41830
+ const day = utcDayOf(Date.now());
41831
+ const st = this.store.roomSearchCapState(channelId, day);
41832
+ if (this.store.roomSearchCapState(AGENT_TOTAL_KEY, day).grantedTurns >= this.agentRoomSearchDailyCap) return true;
41833
+ return st.reachedBy !== null || st.grantedTurns >= this.roomSearchDailyCap;
41834
+ } catch {
41835
+ return true;
41836
+ }
41837
+ }
41838
+ /** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
41839
+ noteRoomCapReached(channelId) {
41840
+ if (typeof channelId !== "string" || this.closingDown) return;
41841
+ if (this.roomSearchDailyCap === 0) return;
41842
+ if (this.freshMeta(channelId)?.is_dm !== false || this.channelRoster.get(channelId)?.crossOrg !== false) return;
41843
+ const day = utcDayOf(Date.now());
41844
+ try {
41845
+ if (this.store.markRoomSearchCapReached(channelId, day, "runtime")) this.logRoomCap(day, channelId, "runtime");
41846
+ if (this.store.roomSearchCapState(channelId, day).noticePosted) return;
41847
+ } catch (e) {
41848
+ writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
41849
+ return;
41850
+ }
41851
+ this.noticeRoomSearchCap(channelId, day);
41852
+ }
41853
+ /** K-14, once per room per UTC day; the flag is persisted only after the send resolves. Called only from noteRoomCapReached. */
41854
+ noticeRoomSearchCap(channelId, day) {
41855
+ const key = `${day}
41856
+ ${channelId}`;
41857
+ if (this.roomCapNoticePending.has(key)) return;
41858
+ this.roomCapNoticePending.add(key);
41859
+ let pending;
41860
+ try {
41861
+ pending = this.send(channelId, ROOM_SEARCH_CAP_NOTICE, void 0, { agentTurn: false });
41862
+ } catch (e) {
41863
+ pending = Promise.reject(e);
41864
+ }
41865
+ void pending.then(
41866
+ () => {
41867
+ try {
41868
+ this.store.markRoomSearchCapNoticePosted(channelId, day);
41869
+ } catch (e) {
41870
+ writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("store_failed", ["store_failed"]) }, e);
41871
+ }
41872
+ writeServiceLog2("info", "authz.room_cap", { state: ServiceLogEnum.of("notice_sent", ["notice_sent"]) });
41873
+ },
41874
+ (e) => writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("notice_failed", ["notice_failed"]) }, e)
41875
+ ).finally(() => this.roomCapNoticePending.delete(key));
41876
+ }
41877
+ logRoomCap(day, channelId, by) {
41878
+ const key = `${day}
41879
+ ${channelId}`;
41880
+ if (this.roomCapLogged.has(key)) return;
41881
+ if (this.roomCapLogged.size > 5e3) this.roomCapLogged.clear();
41882
+ this.roomCapLogged.add(key);
41883
+ writeServiceLog2("info", "authz.room_cap", {
41884
+ state: ServiceLogEnum.of("reached", ["reached"]),
41885
+ by: ServiceLogEnum.of(by, ["runtime", "bridge", "operator"])
41886
+ });
41887
+ }
41888
+ /**
41889
+ * Spec §7.5 (T-14b, audit r1 H-3): the silent bridge backstop. Counts a team_room turn that actually goes out
41890
+ * to the runtime with web_search, once per message id (callers run it after their runtimePendingIds early
41891
+ * return). Past the cap, and on a store failure (fail closed), the entry loses web_search. Never posts K-14.
41892
+ */
41893
+ applyRoomSearchBackstop(entry) {
41894
+ const a = entry.authorization;
41895
+ if (!a || a.audience !== "team_room" || !a.tool_grants.some((g) => g.tool === "web_search")) return entry;
41896
+ const day = utcDayOf(Date.now());
41897
+ if (this.roomSearchDailyCap === 0) {
41898
+ this.logRoomCap(day, entry.channel_id, "operator");
41899
+ return this.withoutWebSearch(entry);
41900
+ }
41901
+ if (this.agentRoomSearchDailyCap === 0) {
41902
+ this.logRoomCap(day, AGENT_TOTAL_KEY, "operator");
41903
+ return this.withoutWebSearch(entry);
41904
+ }
41905
+ try {
41906
+ const n = this.store.countRoomSearchGrant(entry.channel_id, day);
41907
+ if (n === this.roomSearchDailyCap && this.store.markRoomSearchCapReached(entry.channel_id, day, "bridge")) {
41908
+ this.logRoomCap(day, entry.channel_id, "bridge");
41909
+ }
41910
+ if (n > this.roomSearchDailyCap) return this.withoutWebSearch(entry);
41911
+ const total = this.store.countRoomSearchGrant(AGENT_TOTAL_KEY, day);
41912
+ if (total === this.agentRoomSearchDailyCap) this.logRoomCap(day, AGENT_TOTAL_KEY, "bridge");
41913
+ if (total > this.agentRoomSearchDailyCap) return this.withoutWebSearch(entry);
41914
+ return entry;
41915
+ } catch (e) {
41916
+ writeServiceLog2("warn", "authz.room_cap", { state: ServiceLogEnum.of("count_failed", ["count_failed"]) }, e);
41917
+ return this.withoutWebSearch(entry);
41918
+ }
41919
+ }
41920
+ withoutWebSearch(entry) {
41921
+ const a = entry.authorization;
41922
+ return {
41923
+ ...entry,
41924
+ authorization: Object.freeze({ ...a, tool_grants: Object.freeze(a.tool_grants.filter((g) => g.tool !== "web_search")) })
41925
+ };
41926
+ }
40201
41927
  noticeDeadLetter(channelId) {
40202
41928
  if (this.closingDown) return;
40203
41929
  const now = Date.now();
@@ -40419,13 +42145,18 @@ var BridgeAgent = class _BridgeAgent {
40419
42145
  modeEpoch: this.store.recallState().modeEpoch
40420
42146
  };
40421
42147
  let persisted = { stamp, recallPurgeEpoch: recall.purgeEpoch, recallModeEpoch: recall.modeEpoch };
40422
- await this.store.stageRuntimeInbox(message.message_id, this.stagedPayload(message, stamp, recall), message.ts, (prior, next) => {
40423
- const a = splitIngestStamp(prior);
40424
- const b = splitIngestStamp(next);
40425
- if (a === null || b === null || a.message !== b.message) return false;
40426
- persisted = a.marks;
40427
- return true;
40428
- });
42148
+ const replay = this.ownerReplayPending.get(message.message_id);
42149
+ try {
42150
+ await this.store.stageRuntimeInbox(message.message_id, this.stagedPayload(message, stamp, recall), message.ts, (prior, next) => {
42151
+ const a = splitIngestStamp(prior);
42152
+ const b = splitIngestStamp(next);
42153
+ if (a === null || b === null || a.message !== b.message) return false;
42154
+ persisted = a.marks;
42155
+ return true;
42156
+ }, replay ? { ...replay, processedAt: Date.now() } : void 0);
42157
+ } finally {
42158
+ this.ownerReplayPending.delete(message.message_id);
42159
+ }
40429
42160
  this.applyStagedMarks(message.message_id, persisted);
40430
42161
  }
40431
42162
  /** In-memory mirror of the persisted marks (staging and restore). */
@@ -40445,6 +42176,9 @@ var BridgeAgent = class _BridgeAgent {
40445
42176
  const message = rest;
40446
42177
  this.applyStagedMarks(message.message_id, marks);
40447
42178
  this.runtimePendingIds.add(message.message_id);
42179
+ if (this.cfg.ownerGrantPin?.pin && this.isOwnerVerifyFailed(message.message_id)) {
42180
+ this.ownerVerifyFailedIds.set(message.message_id, message.channel_id);
42181
+ }
40448
42182
  if (this.unifiedOn) this.deferredUnifiedIds.add(message.message_id);
40449
42183
  else this.deferredRecallIds.add(message.message_id);
40450
42184
  if (row.deliveryAttempts > 0) {
@@ -40461,12 +42195,13 @@ var BridgeAgent = class _BridgeAgent {
40461
42195
  * Returning false is backpressure: the caller leaves the backend row unacked
40462
42196
  * so it remains the durable queue rather than shedding accepted work.
40463
42197
  */
40464
- async stageRuntimeMessage(message) {
40465
- if (this.runtimePendingIds.has(message.message_id)) return true;
42198
+ async stageRuntimeMessage(staging, opts = {}) {
42199
+ if (this.runtimePendingIds.has(staging.message_id)) return true;
40466
42200
  if (this.runtimePendingIds.size >= this.maxBuffer) {
40467
42201
  writeServiceLog2("warn", "runtime_inbox.full");
40468
42202
  return false;
40469
42203
  }
42204
+ const message = opts.absorbed ? staging : this.applyRoomSearchBackstop(staging);
40470
42205
  try {
40471
42206
  await this.stageSealed(message);
40472
42207
  this.runtimePendingIds.add(message.message_id);
@@ -40572,6 +42307,37 @@ var BridgeAgent = class _BridgeAgent {
40572
42307
  perToolConsumerAttached() {
40573
42308
  return this.perToolConsumer !== null;
40574
42309
  }
42310
+ /**
42311
+ * Owner grants (spec §7.9, plan 4 Task 5): the attached per-tool runtime socket whose hello
42312
+ * declared manifest_grants_v1, or null. A manifest grant is issued only while one is attached
42313
+ * (Task 13); every other socket receives manifest grants narrowed to the constants.
42314
+ * Newest wins, like perToolConsumer.
42315
+ */
42316
+ manifestConsumer = null;
42317
+ /** OQ-5: the latest hello's class; "missing" until any hello. Reported in tool_grant_status (Task 15). */
42318
+ lastConsumerCapability = "missing";
42319
+ noteManifestConsumer(token) {
42320
+ this.manifestConsumer = token;
42321
+ }
42322
+ clearManifestConsumer(token) {
42323
+ if (this.manifestConsumer === token) this.manifestConsumer = null;
42324
+ }
42325
+ manifestConsumerAttached() {
42326
+ return this.manifestConsumer !== null;
42327
+ }
42328
+ noteConsumerCapability(state) {
42329
+ this.lastConsumerCapability = state;
42330
+ if (state !== "ok") {
42331
+ writeServiceLog2("info", "authz.capability", { state: ServiceLogEnum.of(state, ["missing", "harness_consumer"]) });
42332
+ }
42333
+ }
42334
+ /**
42335
+ * Harness re-rule (2026-10-12, audit r2 H-3): true exactly when this agent has a pin loaded and a good
42336
+ * manifest. A harness consumer then receives [] in every audience (consumer-grants.ts withHarnessGrants).
42337
+ */
42338
+ harnessGrantsWithheld() {
42339
+ return this.cfg.ownerGrantPin?.pin != null && this.toolGrantManifest !== null;
42340
+ }
40575
42341
  health() {
40576
42342
  let joined = 0;
40577
42343
  for (const s of this.joinState.values()) if (s === "JOINED") joined++;
@@ -40802,11 +42568,11 @@ var BridgeAgent = class _BridgeAgent {
40802
42568
  };
40803
42569
 
40804
42570
  // src/config.ts
40805
- import { readFileSync as readFileSync9, statSync as statSync5 } from "node:fs";
40806
- import { dirname as dirname5, join as join17 } from "node:path";
42571
+ import { readFileSync as readFileSync10, statSync as statSync5 } from "node:fs";
42572
+ import { dirname as dirname6, join as join18 } from "node:path";
40807
42573
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
40808
42574
  function loadConfig(opts) {
40809
- const raw = readFileSync9(opts.credentialPath, "utf8");
42575
+ const raw = readFileSync10(opts.credentialPath, "utf8");
40810
42576
  try {
40811
42577
  const mode = statSync5(opts.credentialPath).mode & 511;
40812
42578
  if (mode & 63) writeServiceLog2("warn", "config.credential_permissions_unsafe");
@@ -40819,7 +42585,11 @@ function loadConfig(opts) {
40819
42585
  throw new Error("credential file is not valid JSON");
40820
42586
  }
40821
42587
  for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
40822
- const storePath = opts.storePath ?? join17(dirname5(opts.credentialPath), "bridge-state.db");
42588
+ const storePath = opts.storePath ?? join18(dirname6(opts.credentialPath), "bridge-state.db");
42589
+ const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
42590
+ writeServiceLog2(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
42591
+ state: ServiceLogEnum.of(ownerGrantPin.state, ["ok", "no_pin", "pin_conflict", "malformed", "unsupported_algo", "symlink_refused", "unreadable"])
42592
+ });
40823
42593
  const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
40824
42594
  const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
40825
42595
  const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
@@ -40877,7 +42647,8 @@ function loadConfig(opts) {
40877
42647
  voice,
40878
42648
  voiceFastPathMode: parseFastPathMode(json2.voice_fast_path_mode),
40879
42649
  modelSource,
40880
- memoryEnabled
42650
+ memoryEnabled,
42651
+ ownerGrantPin
40881
42652
  };
40882
42653
  }
40883
42654
  function arg(argv, flag) {
@@ -40921,11 +42692,11 @@ import { z as z3 } from "zod";
40921
42692
 
40922
42693
  // src/auth.ts
40923
42694
  import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
40924
- import { existsSync as existsSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync2 } from "node:fs";
42695
+ import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "node:fs";
40925
42696
  function resolveToken(opts) {
40926
42697
  if (opts.token) return opts.token;
40927
42698
  const f = opts.tokenFile;
40928
- if (f && existsSync5(f)) return readFileSync10(f, "utf8").trim();
42699
+ if (f && existsSync6(f)) return readFileSync11(f, "utf8").trim();
40929
42700
  const tok = randomBytes11(32).toString("base64url");
40930
42701
  if (f) writeFileSync2(f, tok, { mode: 384 });
40931
42702
  return tok;
@@ -41017,6 +42788,29 @@ function withConsumerGrants(m, perToolConsumer) {
41017
42788
  const { memory_write: _dropped, ...rest } = a;
41018
42789
  return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
41019
42790
  }
42791
+ var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
42792
+ var CONSTANT_TOOL_NAMES = Object.freeze({
42793
+ owner_dm: Object.freeze(["current_time", "web_search"]),
42794
+ team_room: Object.freeze(["current_time"])
42795
+ });
42796
+ var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
42797
+ function withManifestCapability(m, manifestConsumer) {
42798
+ if (manifestConsumer) return m;
42799
+ const a = m.authorization;
42800
+ if (!a || !a.tool_grants.some(isManifestGrant)) return m;
42801
+ const constants4 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
42802
+ const narrowed = a.tool_grants.flatMap((g) => {
42803
+ if (!isManifestGrant(g)) return [g];
42804
+ return constants4.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
42805
+ });
42806
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
42807
+ }
42808
+ function withHarnessGrants(m, withheld) {
42809
+ if (!withheld) return m;
42810
+ const a = m.authorization;
42811
+ if (!a || a.tool_grants.length === 0) return m;
42812
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
42813
+ }
41020
42814
 
41021
42815
  // src/http-server.ts
41022
42816
  function writeSse(res, msgs, onWriteError) {
@@ -41038,7 +42832,8 @@ data: ${JSON.stringify(msgs[i])}
41038
42832
  return [];
41039
42833
  }
41040
42834
  function deliverSseBatch(res, batch, agent, onWriteError) {
41041
- const left3 = writeSse(res, batch.map((m) => withConsumerGrants(m, false)), onWriteError);
42835
+ const withheld = agent.harnessGrantsWithheld();
42836
+ const left3 = writeSse(res, batch.map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false)), onWriteError);
41042
42837
  const leftIds = new Set(left3.map((message) => message.message_id));
41043
42838
  agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
41044
42839
  if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
@@ -41305,7 +43100,7 @@ function createHttpServer(agent, opts) {
41305
43100
  if (method === "GET" && path === "/v1/messages") {
41306
43101
  const channel = url.searchParams.get("channel_id") ?? void 0;
41307
43102
  const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
41308
- return json(res, 200, (await agent.drain(channel, wait)).map((m) => withConsumerGrants(m, false)));
43103
+ return json(res, 200, (await agent.drain(channel, wait)).map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), agent.harnessGrantsWithheld()), false)));
41309
43104
  }
41310
43105
  if (method === "GET" && path === "/v1/events") {
41311
43106
  const channel = url.searchParams.get("channel_id") ?? void 0;
@@ -41472,6 +43267,7 @@ function attachAgentWebSocket(server, agent, opts) {
41472
43267
  const session = {
41473
43268
  consumer: "unknown",
41474
43269
  perToolGrants: false,
43270
+ manifestGrants: false,
41475
43271
  // helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
41476
43272
  deliveryOpen: helloWaitMs <= 0,
41477
43273
  deliveryToken: /* @__PURE__ */ new Map(),
@@ -41524,7 +43320,15 @@ function attachAgentWebSocket(server, agent, opts) {
41524
43320
  retired = true;
41525
43321
  continue;
41526
43322
  }
41527
- const out = withConsumerNotes(withConsumerGrants(batch[i], session.perToolGrants), batch[i], session, agent);
43323
+ const out = withConsumerNotes(
43324
+ withManifestCapability(
43325
+ withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
43326
+ session.manifestGrants
43327
+ ),
43328
+ batch[i],
43329
+ session,
43330
+ agent
43331
+ );
41528
43332
  const recallOut = withConsumerRecall(out.message, batch[i], session, agent);
41529
43333
  const unifiedOut = withConsumerUnified(recallOut.message, batch[i], session, agent);
41530
43334
  const frame = { type: "message", ...unifiedOut.message };
@@ -41573,6 +43377,7 @@ function attachAgentWebSocket(server, agent, opts) {
41573
43377
  if (current?.sock === sock) current = null;
41574
43378
  if (session.harness) agent.clearHarnessReport(session.harness);
41575
43379
  if (session.perToolGrants) agent.clearPerToolConsumer(session);
43380
+ if (session.manifestGrants) agent.clearManifestConsumer(session);
41576
43381
  };
41577
43382
  current = { sock, cleanup, sweepStale };
41578
43383
  sock.on("close", cleanup);
@@ -41652,6 +43457,15 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
41652
43457
  session.perToolGrants = false;
41653
43458
  agent.clearPerToolConsumer(session);
41654
43459
  }
43460
+ const manifest = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(MANIFEST_GRANTS_FEATURE);
43461
+ if (manifest && !session.manifestGrants) {
43462
+ session.manifestGrants = true;
43463
+ agent.noteManifestConsumer(session);
43464
+ } else if (!manifest && session.manifestGrants) {
43465
+ session.manifestGrants = false;
43466
+ agent.clearManifestConsumer(session);
43467
+ }
43468
+ agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
41655
43469
  if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
41656
43470
  agent.noteNotesConsumer();
41657
43471
  }
@@ -41659,7 +43473,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
41659
43473
  if (session.recallConsumer) agent.noteRecallConsumer();
41660
43474
  session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
41661
43475
  if (session.unifiedConsumer) agent.noteUnifiedConsumer();
41662
- sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : []] });
43476
+ sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
41663
43477
  if (!session.deliveryOpen) {
41664
43478
  session.deliveryOpen = true;
41665
43479
  flush();
@@ -41872,8 +43686,17 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
41872
43686
  }
41873
43687
  return;
41874
43688
  }
43689
+ if (m.type === "room_cap_reached") {
43690
+ if (!session.manifestGrants || typeof m.channel_id !== "string" || !LOWER_UUID.test(m.channel_id)) {
43691
+ sendJson(sock, wsErrorFrame("invalid_request"));
43692
+ return;
43693
+ }
43694
+ agent.noteRoomCapReached(m.channel_id);
43695
+ return;
43696
+ }
41875
43697
  sendJson(sock, wsErrorFrame("invalid_request"));
41876
43698
  }
43699
+ var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
41877
43700
  export {
41878
43701
  BRIDGE_ERRORS,
41879
43702
  BRIDGE_VERSION,