@securecoms/agent-bridge 0.30.2 → 0.31.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/README.md +53 -0
- package/dist/bin.js +571 -91
- package/dist/bridge-agent.d.ts +149 -15
- package/dist/consumer-grants.d.ts +15 -0
- package/dist/grants/binding.d.ts +30 -2
- package/dist/grants/manifest.d.ts +11 -1
- package/dist/grants/search-providers.d.ts +9 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/index.js +550 -71
- package/dist/preflight-upgrade.d.cts +1 -0
- package/dist/preflight.cjs +1 -0
- package/dist/service/runtime-config-dir.d.ts +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/voice/call-buffer.d.ts +118 -0
- package/dist/voice/persona.d.ts +9 -0
- package/dist/voice/voice-scope.d.ts +31 -0
- package/dist/ws-server.d.ts +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -7556,7 +7556,7 @@ __export(import_bundle_exports, {
|
|
|
7556
7556
|
importModelBundle: () => importModelBundle
|
|
7557
7557
|
});
|
|
7558
7558
|
import { createHash as createHash6, randomBytes as randomBytes12 } from "node:crypto";
|
|
7559
|
-
import { readFileSync as
|
|
7559
|
+
import { readFileSync as readFileSync16, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7560
7560
|
import { join as join24 } from "node:path";
|
|
7561
7561
|
async function importModelBundle(srcDir, root3, m) {
|
|
7562
7562
|
const verified = [];
|
|
@@ -7574,7 +7574,7 @@ async function importModelBundle(srcDir, root3, m) {
|
|
|
7574
7574
|
if (size !== f.size) throw mismatch;
|
|
7575
7575
|
let bytes;
|
|
7576
7576
|
try {
|
|
7577
|
-
bytes =
|
|
7577
|
+
bytes = readFileSync16(src);
|
|
7578
7578
|
} catch {
|
|
7579
7579
|
throw new ModelImportError(`${f.name} is missing from the import directory`);
|
|
7580
7580
|
}
|
|
@@ -7742,7 +7742,9 @@ function settingsToVoiceConfig(s, proxy) {
|
|
|
7742
7742
|
model: "securecoms-fast",
|
|
7743
7743
|
apiKey: proxy.apiKey,
|
|
7744
7744
|
timeoutMs: 2500,
|
|
7745
|
-
maxTokens: 80
|
|
7745
|
+
maxTokens: 80,
|
|
7746
|
+
// Spec 7.2: tells the proxy this bridge scopes voice turns. Sent to the SecureComs proxy only.
|
|
7747
|
+
headers: { "x-securecoms-bridge-caps": "voice-scope-v1" }
|
|
7746
7748
|
},
|
|
7747
7749
|
settings: s
|
|
7748
7750
|
};
|
|
@@ -7750,6 +7752,7 @@ function settingsToVoiceConfig(s, proxy) {
|
|
|
7750
7752
|
|
|
7751
7753
|
// src/voice/persona.ts
|
|
7752
7754
|
var CONTINUITY_CLAUSE = "The text messages in this chat and this voice call are one and the same ongoing conversation, and the earlier turns you can see ARE those messages. If asked whether you remember or received a message, answer from that history \u2014 never say you cannot access previous messages.";
|
|
7755
|
+
var VOICE_CALL_CLAUSE = "This is a live voice call. The earlier turns you can see are from this call only; during a call you cannot see the typed chat, saved notes or memory. If asked about a message, file or note you cannot see, say plainly that you can't see the chat while on a call and offer to pick it up in the chat afterwards. Never guess at what it said.";
|
|
7753
7756
|
var schema = z2.object({
|
|
7754
7757
|
persona: z2.object({
|
|
7755
7758
|
display_name: z2.string().min(1).optional(),
|
|
@@ -29882,7 +29885,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
29882
29885
|
}
|
|
29883
29886
|
|
|
29884
29887
|
// src/bridge-agent.ts
|
|
29885
|
-
import { existsSync as existsSync5, promises as fsp, statSync as statSync5 } from "node:fs";
|
|
29888
|
+
import { existsSync as existsSync5, promises as fsp, readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
|
|
29886
29889
|
import { join as join19, dirname as dirname7 } from "node:path";
|
|
29887
29890
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29888
29891
|
|
|
@@ -34056,7 +34059,8 @@ var FastLlmClient = class {
|
|
|
34056
34059
|
try {
|
|
34057
34060
|
res = await this.fetchImpl(`${this.cfg.baseUrl}${this.cfg.path ?? "/chat/completions"}`, {
|
|
34058
34061
|
method: "POST",
|
|
34059
|
-
|
|
34062
|
+
// Final fix T8: configured headers first, so they can never displace content-type or the bearer.
|
|
34063
|
+
headers: { ...this.cfg.headers, "content-type": "application/json", ...this.key ? { authorization: `Bearer ${this.key}` } : {} },
|
|
34060
34064
|
body: JSON.stringify({ model: this.cfg.model, messages, max_tokens: this.cfg.maxTokens, temperature: 0.4 }),
|
|
34061
34065
|
signal: ac.signal
|
|
34062
34066
|
});
|
|
@@ -34160,7 +34164,7 @@ function buildFastMessages(args2) {
|
|
|
34160
34164
|
const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
|
|
34161
34165
|
const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
|
|
34162
34166
|
const notesLine = args2.notesDeferLine ? ` ${FAST_NOTES_DEFER_LINE}` : "";
|
|
34163
|
-
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${
|
|
34167
|
+
const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${VOICE_CALL_CLAUSE}`;
|
|
34164
34168
|
const recent = args2.memory.recent.map((t) => ({
|
|
34165
34169
|
role: t.role === "agent" ? "assistant" : "user",
|
|
34166
34170
|
content: t.text
|
|
@@ -34197,6 +34201,160 @@ function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
|
|
|
34197
34201
|
return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
|
|
34198
34202
|
}
|
|
34199
34203
|
|
|
34204
|
+
// src/voice/voice-scope.ts
|
|
34205
|
+
function isVoiceScopedTurn(t) {
|
|
34206
|
+
return t.speaker !== void 0 || t.transcribed === true || t.senderIsRelay || t.text.trim() === VOICE_CALL_START_MARKER;
|
|
34207
|
+
}
|
|
34208
|
+
function admitsToCallBuffer(t) {
|
|
34209
|
+
return t.speaker !== void 0 || t.transcribed === true;
|
|
34210
|
+
}
|
|
34211
|
+
|
|
34212
|
+
// src/voice/call-buffer.ts
|
|
34213
|
+
var CALL_BUFFER_CAP = 50;
|
|
34214
|
+
var CALL_BUFFER_IDLE_MS = 30 * 6e4;
|
|
34215
|
+
var CALL_BUFFER_ID_CAP = 5e3;
|
|
34216
|
+
var CallBuffer = class {
|
|
34217
|
+
constructor(opts) {
|
|
34218
|
+
this.opts = opts;
|
|
34219
|
+
}
|
|
34220
|
+
channels = /* @__PURE__ */ new Map();
|
|
34221
|
+
/** messageId -> channelId, insertion ordered (oldest first) for the cap. */
|
|
34222
|
+
voiceIds = /* @__PURE__ */ new Map();
|
|
34223
|
+
/** messageId -> { channelId, the channel's bufSeq at the mark, the channel's generation then }. */
|
|
34224
|
+
seqNotes = /* @__PURE__ */ new Map();
|
|
34225
|
+
/**
|
|
34226
|
+
* Final fix I-1: messageId -> { channelId, the channel's bufSeq just before the turn's own peer entry }. A row the
|
|
34227
|
+
* backend redelivers (a failed ack, runtime_inbox.full backpressure) is ingested again; its id here means its text is
|
|
34228
|
+
* already in the call, so it is not appended again and its snapshot stops before its own entry (A-6).
|
|
34229
|
+
*/
|
|
34230
|
+
admitted = /* @__PURE__ */ new Map();
|
|
34231
|
+
/** The last bufSeq handed out per channel (kept across resets, so the sequence never runs backwards). */
|
|
34232
|
+
lastSeq = /* @__PURE__ */ new Map();
|
|
34233
|
+
nextSeq = 1;
|
|
34234
|
+
/** Per-channel reset generation (advanced by every reset, the idle rule included). */
|
|
34235
|
+
gens = /* @__PURE__ */ new Map();
|
|
34236
|
+
/**
|
|
34237
|
+
* Append a spoken turn's text, once per turn id (final fix I-1). Returns false (and appends nothing) when the id was
|
|
34238
|
+
* already admitted in this call, or when the buffer is disabled.
|
|
34239
|
+
*/
|
|
34240
|
+
appendPeer(channelId, text, messageId) {
|
|
34241
|
+
if (!this.opts.enabled) return false;
|
|
34242
|
+
if (this.admittedAt(messageId) !== void 0) return false;
|
|
34243
|
+
const before = this.seqAt(channelId);
|
|
34244
|
+
this.append(channelId, { role: "peer", text });
|
|
34245
|
+
this.admitted.set(messageId, { channelId, before });
|
|
34246
|
+
if (this.admitted.size > CALL_BUFFER_ID_CAP) this.admitted.delete(this.admitted.keys().next().value);
|
|
34247
|
+
return true;
|
|
34248
|
+
}
|
|
34249
|
+
/**
|
|
34250
|
+
* Final fix I-1: for a turn already admitted in this call, the channel's bufSeq just before its own entry (the end seq
|
|
34251
|
+
* of its first snapshot); undefined for a turn not admitted (or forgotten by a reset, the idle rule or the id cap).
|
|
34252
|
+
*/
|
|
34253
|
+
admittedAt(messageId) {
|
|
34254
|
+
const a = this.admitted.get(messageId);
|
|
34255
|
+
if (a === void 0) return void 0;
|
|
34256
|
+
this.expireIfIdle(a.channelId);
|
|
34257
|
+
return this.admitted.get(messageId)?.before;
|
|
34258
|
+
}
|
|
34259
|
+
appendAgent(channelId, text, origin) {
|
|
34260
|
+
this.append(channelId, { role: "agent", text, origin });
|
|
34261
|
+
}
|
|
34262
|
+
/**
|
|
34263
|
+
* Record a voice-scoped turn's id (for reply admission only, A-4) and note the channel's seq at this point. Final fix
|
|
34264
|
+
* I-1: a turn already admitted (a redelivered row) keeps the seq of its first admission, so the note never moves past
|
|
34265
|
+
* the turn's own entry.
|
|
34266
|
+
*/
|
|
34267
|
+
markVoiceScoped(channelId, messageId) {
|
|
34268
|
+
if (!this.opts.enabled) return;
|
|
34269
|
+
this.expireIfIdle(channelId);
|
|
34270
|
+
this.voiceIds.delete(messageId);
|
|
34271
|
+
this.voiceIds.set(messageId, channelId);
|
|
34272
|
+
if (this.voiceIds.size > CALL_BUFFER_ID_CAP) this.voiceIds.delete(this.voiceIds.keys().next().value);
|
|
34273
|
+
const seq = this.admittedAt(messageId) ?? this.seqAt(channelId);
|
|
34274
|
+
this.seqNotes.delete(messageId);
|
|
34275
|
+
this.seqNotes.set(messageId, { channelId, seq, generation: this.generation(channelId) });
|
|
34276
|
+
if (this.seqNotes.size > CALL_BUFFER_ID_CAP) this.seqNotes.delete(this.seqNotes.keys().next().value);
|
|
34277
|
+
}
|
|
34278
|
+
/** A-4: read ONLY by the bridge's `afterPublish` reply admission. */
|
|
34279
|
+
isVoiceScopedId(messageId) {
|
|
34280
|
+
const channelId = this.voiceIds.get(messageId);
|
|
34281
|
+
if (channelId === void 0) return false;
|
|
34282
|
+
this.expireIfIdle(channelId);
|
|
34283
|
+
return this.voiceIds.has(messageId);
|
|
34284
|
+
}
|
|
34285
|
+
/** The channel's mark noted when `messageId` was marked (the release re-read), or undefined (e.g. after a restart). */
|
|
34286
|
+
seqNoted(messageId) {
|
|
34287
|
+
const note = this.seqNotes.get(messageId);
|
|
34288
|
+
if (note === void 0) return void 0;
|
|
34289
|
+
this.expireIfIdle(note.channelId);
|
|
34290
|
+
const kept = this.seqNotes.get(messageId);
|
|
34291
|
+
return kept === void 0 ? void 0 : { seq: kept.seq, generation: kept.generation };
|
|
34292
|
+
}
|
|
34293
|
+
/** The bufSeq of the channel's newest entry (0 before any append). A snapshot's end seq. */
|
|
34294
|
+
seqAt(channelId) {
|
|
34295
|
+
this.expireIfIdle(channelId);
|
|
34296
|
+
return this.lastSeq.get(channelId) ?? 0;
|
|
34297
|
+
}
|
|
34298
|
+
/**
|
|
34299
|
+
* The channel's last `limit` entries, oldest first. With `upTo` (final fix I-1, a redelivered turn's re-snapshot),
|
|
34300
|
+
* only entries at or below that bufSeq.
|
|
34301
|
+
*/
|
|
34302
|
+
recent(channelId, limit, upTo) {
|
|
34303
|
+
if (limit <= 0) return [];
|
|
34304
|
+
this.expireIfIdle(channelId);
|
|
34305
|
+
const all = this.channels.get(channelId)?.entries ?? [];
|
|
34306
|
+
const entries = upTo === void 0 ? all : all.filter((e) => e.bufSeq <= upTo);
|
|
34307
|
+
return entries.slice(-limit).map(deliver);
|
|
34308
|
+
}
|
|
34309
|
+
/**
|
|
34310
|
+
* The channel's AGENT entries appended after `since.seq` (lines spoken or answered since a snapshot), oldest first.
|
|
34311
|
+
* Final fix M-4: nothing when the channel has been reset since the mark (a later call's lines).
|
|
34312
|
+
*/
|
|
34313
|
+
agentAfter(channelId, since) {
|
|
34314
|
+
if (this.generation(channelId) !== since.generation) return [];
|
|
34315
|
+
const entries = this.channels.get(channelId)?.entries ?? [];
|
|
34316
|
+
return entries.filter((e) => e.role === "agent" && e.bufSeq > since.seq).map(deliver);
|
|
34317
|
+
}
|
|
34318
|
+
/**
|
|
34319
|
+
* The channel's reset generation: it changes on every reset (explicit or idle) and on nothing else. A caller that
|
|
34320
|
+
* holds text across an await (the coalesce window) compares it to skip an append into a call that has since ended.
|
|
34321
|
+
*/
|
|
34322
|
+
generation(channelId) {
|
|
34323
|
+
this.expireIfIdle(channelId);
|
|
34324
|
+
return this.gens.get(channelId) ?? 0;
|
|
34325
|
+
}
|
|
34326
|
+
/** Clear the channel: its entries, its voice-scoped ids, its seq notes and its admitted ids. Only ever removes. */
|
|
34327
|
+
reset(channelId) {
|
|
34328
|
+
this.gens.set(channelId, (this.gens.get(channelId) ?? 0) + 1);
|
|
34329
|
+
this.channels.delete(channelId);
|
|
34330
|
+
for (const [id, c] of this.voiceIds) if (c === channelId) this.voiceIds.delete(id);
|
|
34331
|
+
for (const [id, n] of this.seqNotes) if (n.channelId === channelId) this.seqNotes.delete(id);
|
|
34332
|
+
for (const [id, a] of this.admitted) if (a.channelId === channelId) this.admitted.delete(id);
|
|
34333
|
+
}
|
|
34334
|
+
append(channelId, e) {
|
|
34335
|
+
if (!this.opts.enabled) return;
|
|
34336
|
+
this.expireIfIdle(channelId);
|
|
34337
|
+
let ch = this.channels.get(channelId);
|
|
34338
|
+
if (ch === void 0) {
|
|
34339
|
+
ch = { entries: [], lastAppend: 0 };
|
|
34340
|
+
this.channels.set(channelId, ch);
|
|
34341
|
+
}
|
|
34342
|
+
const bufSeq = this.nextSeq++;
|
|
34343
|
+
this.lastSeq.set(channelId, bufSeq);
|
|
34344
|
+
ch.entries.push({ ...e, bufSeq });
|
|
34345
|
+
if (ch.entries.length > this.opts.cap) ch.entries.splice(0, ch.entries.length - this.opts.cap);
|
|
34346
|
+
ch.lastAppend = this.opts.now();
|
|
34347
|
+
}
|
|
34348
|
+
/** Reset (c): more than `idleMs` since the channel's last append clears it, checked on every read and append. */
|
|
34349
|
+
expireIfIdle(channelId) {
|
|
34350
|
+
const ch = this.channels.get(channelId);
|
|
34351
|
+
if (ch !== void 0 && this.opts.now() - ch.lastAppend > this.opts.idleMs) this.reset(channelId);
|
|
34352
|
+
}
|
|
34353
|
+
};
|
|
34354
|
+
function deliver(e) {
|
|
34355
|
+
return { role: e.role, text: e.text, seq: 0, ...e.origin ? { origin: e.origin } : {} };
|
|
34356
|
+
}
|
|
34357
|
+
|
|
34200
34358
|
// src/vault.ts
|
|
34201
34359
|
var VaultIntegrityError = class extends Error {
|
|
34202
34360
|
constructor(message, cause) {
|
|
@@ -34397,6 +34555,15 @@ import {
|
|
|
34397
34555
|
writeFileSync
|
|
34398
34556
|
} from "node:fs";
|
|
34399
34557
|
import { basename as basename4, isAbsolute, join as join18 } from "node:path";
|
|
34558
|
+
|
|
34559
|
+
// src/grants/search-providers.ts
|
|
34560
|
+
var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
|
|
34561
|
+
var OWNER_DM_SEARCH_DAILY_CAP_MAX = 2e3;
|
|
34562
|
+
function isSearchProvider(value) {
|
|
34563
|
+
return typeof value === "string" && SEARCH_PROVIDERS.has(value);
|
|
34564
|
+
}
|
|
34565
|
+
|
|
34566
|
+
// src/service/runtime-config-dir.ts
|
|
34400
34567
|
var MEMORY_FILE = "MEMORY.md";
|
|
34401
34568
|
var REAL_FS = {
|
|
34402
34569
|
mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
|
|
@@ -34579,10 +34746,14 @@ function readLocalReport(home, identityId) {
|
|
|
34579
34746
|
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
34580
34747
|
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
34581
34748
|
) : {};
|
|
34749
|
+
const capRaw = doc.owner_dm_search_cap_effective;
|
|
34750
|
+
const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
|
|
34582
34751
|
return {
|
|
34583
34752
|
persona,
|
|
34584
34753
|
has_search_key: hasSearchKey,
|
|
34585
34754
|
has_llm_key: hasLlmKey,
|
|
34755
|
+
has_extract_provider: doc.has_extract_provider === true,
|
|
34756
|
+
...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
|
|
34586
34757
|
legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
|
|
34587
34758
|
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
34588
34759
|
};
|
|
@@ -34700,7 +34871,8 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
|
34700
34871
|
import { ed25519 as ed255195 } from "@noble/curves/ed25519";
|
|
34701
34872
|
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
34702
34873
|
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
34703
|
-
owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
|
|
34874
|
+
owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
|
|
34875
|
+
// web_extract: owner DM only, never team_room (spec §5.4, D5)
|
|
34704
34876
|
team_room: Object.freeze(["web_search"])
|
|
34705
34877
|
});
|
|
34706
34878
|
function manifestSignedBytes(manifest) {
|
|
@@ -34775,7 +34947,11 @@ function parseConfig(v) {
|
|
|
34775
34947
|
}
|
|
34776
34948
|
if ("search" in v) {
|
|
34777
34949
|
const sr = v.search;
|
|
34778
|
-
if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]
|
|
34950
|
+
if (!isPlainObject(sr) || !exactKeys(sr, ["provider"], ["owner_dm_daily_cap"]) || !isSearchProvider(sr.provider)) return null;
|
|
34951
|
+
if ("owner_dm_daily_cap" in sr) {
|
|
34952
|
+
const cap = sr.owner_dm_daily_cap;
|
|
34953
|
+
if (typeof cap !== "number" || !Number.isSafeInteger(cap) || cap < 0 || cap > OWNER_DM_SEARCH_DAILY_CAP_MAX) return null;
|
|
34954
|
+
}
|
|
34779
34955
|
}
|
|
34780
34956
|
const mem = v.memory;
|
|
34781
34957
|
if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
|
|
@@ -34785,7 +34961,12 @@ function parseConfig(v) {
|
|
|
34785
34961
|
return Object.freeze({
|
|
34786
34962
|
persona_sha256: v.persona_sha256,
|
|
34787
34963
|
...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
|
|
34788
|
-
...isPlainObject(v.search) ? {
|
|
34964
|
+
...isPlainObject(v.search) ? {
|
|
34965
|
+
search: Object.freeze({
|
|
34966
|
+
provider: v.search.provider,
|
|
34967
|
+
..."owner_dm_daily_cap" in v.search ? { owner_dm_daily_cap: v.search.owner_dm_daily_cap } : {}
|
|
34968
|
+
})
|
|
34969
|
+
} : {},
|
|
34789
34970
|
...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
|
|
34790
34971
|
flags: Object.freeze([...v.flags]),
|
|
34791
34972
|
memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
|
|
@@ -35016,7 +35197,20 @@ function bindTargets(raw, signed, equals) {
|
|
|
35016
35197
|
}
|
|
35017
35198
|
return { written, mismatches };
|
|
35018
35199
|
}
|
|
35019
|
-
function
|
|
35200
|
+
function signedManifestTools(manifest, audience, searchProvidersCapable) {
|
|
35201
|
+
return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && (t !== "web_extract" || searchProvidersCapable));
|
|
35202
|
+
}
|
|
35203
|
+
function manifestPolicyFor(manifest, switches, searchProvidersCapable) {
|
|
35204
|
+
return {
|
|
35205
|
+
v: 1,
|
|
35206
|
+
manifest_tools: [.../* @__PURE__ */ new Set([
|
|
35207
|
+
...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", searchProvidersCapable) : [],
|
|
35208
|
+
...switches.room ? signedManifestTools(manifest, "team_room", searchProvidersCapable) : []
|
|
35209
|
+
])].sort(),
|
|
35210
|
+
secret_path_mode: "enforce"
|
|
35211
|
+
};
|
|
35212
|
+
}
|
|
35213
|
+
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, searchProvidersCapable = false) {
|
|
35020
35214
|
const mismatches = /* @__PURE__ */ new Set();
|
|
35021
35215
|
const cfg = manifest.config;
|
|
35022
35216
|
const out = {};
|
|
@@ -35045,6 +35239,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35045
35239
|
for (const name of Object.keys(files)) {
|
|
35046
35240
|
if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
|
|
35047
35241
|
}
|
|
35242
|
+
const ownerDmTools = signedManifestTools(manifest, "owner_dm", searchProvidersCapable);
|
|
35048
35243
|
const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
|
|
35049
35244
|
const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
|
|
35050
35245
|
const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
|
|
@@ -35062,14 +35257,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35062
35257
|
appliedSearch = searchBlock;
|
|
35063
35258
|
} else {
|
|
35064
35259
|
mismatches.add("search");
|
|
35065
|
-
|
|
35260
|
+
const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
|
|
35261
|
+
searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
|
|
35066
35262
|
}
|
|
35067
35263
|
const skillsDoc = {
|
|
35068
35264
|
endpoints: eps.written,
|
|
35069
35265
|
databases: dbs.written,
|
|
35070
35266
|
flags: [...cfg.flags],
|
|
35071
35267
|
...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
|
|
35072
|
-
...searchBlock ? { search: searchBlock } : {}
|
|
35268
|
+
...searchBlock ? { search: searchBlock } : {},
|
|
35269
|
+
// D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
|
|
35270
|
+
search_signed: cfg.search !== void 0,
|
|
35271
|
+
// D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
|
|
35272
|
+
// otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
|
|
35273
|
+
...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
|
|
35073
35274
|
};
|
|
35074
35275
|
out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
|
|
35075
35276
|
const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
|
|
@@ -35100,12 +35301,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35100
35301
|
mismatches.add("legacy_assign");
|
|
35101
35302
|
legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
|
|
35102
35303
|
}
|
|
35103
|
-
const
|
|
35104
|
-
const policy = {
|
|
35105
|
-
v: 1,
|
|
35106
|
-
manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
|
|
35107
|
-
secret_path_mode: "enforce"
|
|
35108
|
-
};
|
|
35304
|
+
const policy = manifestPolicyFor(manifest, switches, searchProvidersCapable);
|
|
35109
35305
|
return {
|
|
35110
35306
|
mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
|
|
35111
35307
|
targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
|
|
@@ -35244,6 +35440,8 @@ var TOOL_ROOM_CLASSES = Object.freeze({
|
|
|
35244
35440
|
current_time: Object.freeze(["room_safe_read"]),
|
|
35245
35441
|
echo: Object.freeze(["room_safe_read"]),
|
|
35246
35442
|
web_search: Object.freeze(["owner_credential"]),
|
|
35443
|
+
web_extract: Object.freeze(["owner_credential", "egress"]),
|
|
35444
|
+
// D5: spends the owner's key and sends URLs out
|
|
35247
35445
|
fetch_url: Object.freeze(["egress"]),
|
|
35248
35446
|
sql_query: Object.freeze(["owner_credential"]),
|
|
35249
35447
|
http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
|
|
@@ -35357,6 +35555,10 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
|
35357
35555
|
var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
|
|
35358
35556
|
Object.freeze({ tool: "current_time", mode: "read" })
|
|
35359
35557
|
]);
|
|
35558
|
+
var RELAYED_VOICE_GRANTS = Object.freeze([
|
|
35559
|
+
Object.freeze({ tool: "current_time", mode: "read" }),
|
|
35560
|
+
Object.freeze({ tool: "web_search", mode: "read" })
|
|
35561
|
+
]);
|
|
35360
35562
|
var NO_TOOL_GRANTS = Object.freeze([]);
|
|
35361
35563
|
var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
|
|
35362
35564
|
var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -35665,6 +35867,8 @@ function hexToBytes4(h) {
|
|
|
35665
35867
|
return new Uint8Array(Buffer.from(h, "hex"));
|
|
35666
35868
|
}
|
|
35667
35869
|
var BOOT_DRAIN_WAIT_MS_DEFAULT = 6e4;
|
|
35870
|
+
var SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS = 1e3;
|
|
35871
|
+
var SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS = 1e4;
|
|
35668
35872
|
var BootDrainRotationRefusedError = class extends Error {
|
|
35669
35873
|
constructor() {
|
|
35670
35874
|
super("key-package rotation refused: the boot drain has not settled");
|
|
@@ -35714,6 +35918,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35714
35918
|
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
35715
35919
|
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
35716
35920
|
});
|
|
35921
|
+
this.callBuffer = new CallBuffer({
|
|
35922
|
+
cap: CALL_BUFFER_CAP,
|
|
35923
|
+
idleMs: CALL_BUFFER_IDLE_MS,
|
|
35924
|
+
now: () => Date.now(),
|
|
35925
|
+
enabled: (this.cfg.memoryProvider ?? "securecoms") !== "none"
|
|
35926
|
+
});
|
|
35717
35927
|
this.provider = new MlsGroupKeyProvider({
|
|
35718
35928
|
store: this.store,
|
|
35719
35929
|
backend: this.backend,
|
|
@@ -35860,6 +36070,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
35860
36070
|
this.actionClock = deps.actionThrottleNow ?? Date.now;
|
|
35861
36071
|
this.rng = deps.rng ?? Math.random;
|
|
35862
36072
|
this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
|
|
36073
|
+
this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
|
|
36074
|
+
this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
|
|
35863
36075
|
this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve4) => {
|
|
35864
36076
|
const t = setTimeout(resolve4, ms);
|
|
35865
36077
|
t.unref?.();
|
|
@@ -36298,6 +36510,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36298
36510
|
bootSweepTimer;
|
|
36299
36511
|
rng;
|
|
36300
36512
|
bootDrainWaitMs;
|
|
36513
|
+
searchCapabilityRefetchDebounceMs;
|
|
36514
|
+
searchCapabilityRefetchMaxWaitMs;
|
|
36301
36515
|
/** Resolved (and cleared) each time an ingest cycle settles: see start(). */
|
|
36302
36516
|
ingestSettledWaiters = [];
|
|
36303
36517
|
/** The boot key-package publish, when start() had to defer it. Test seam. */
|
|
@@ -36533,6 +36747,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36533
36747
|
startedAt = 0;
|
|
36534
36748
|
store;
|
|
36535
36749
|
memory;
|
|
36750
|
+
/**
|
|
36751
|
+
* Voice fast-path privacy, Task 4b (spec §2.1, OQ-1 ruled): the current calls' spoken turns, in process only. The
|
|
36752
|
+
* ONLY history a voice-scoped turn sees; read through voiceScopedHistory, once per turn, at ingest.
|
|
36753
|
+
*/
|
|
36754
|
+
callBuffer;
|
|
36536
36755
|
/**
|
|
36537
36756
|
* Markdown memory 7a: the one owner of every note mutation and of the
|
|
36538
36757
|
* delivery cache (spec §6.3, §7.2). One per store, so one per identity.
|
|
@@ -36743,9 +36962,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36743
36962
|
}
|
|
36744
36963
|
const skillsJson = payload.skills_json ?? {};
|
|
36745
36964
|
if (version && inForce === null) {
|
|
36965
|
+
let asDelivered = skillsJson;
|
|
36966
|
+
if (skillsJson !== null && typeof skillsJson === "object" && !Array.isArray(skillsJson)) {
|
|
36967
|
+
const { owner_dm_search_cap: _cap, search_signed: _signed, ...rest } = skillsJson;
|
|
36968
|
+
asDelivered = rest;
|
|
36969
|
+
}
|
|
36746
36970
|
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
|
|
36747
36971
|
...payload.files ?? {},
|
|
36748
|
-
"skills.json": `${JSON.stringify(
|
|
36972
|
+
"skills.json": `${JSON.stringify(asDelivered, null, 2)}
|
|
36749
36973
|
`
|
|
36750
36974
|
});
|
|
36751
36975
|
}
|
|
@@ -36800,7 +37024,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36800
37024
|
}
|
|
36801
37025
|
let bound;
|
|
36802
37026
|
try {
|
|
36803
|
-
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
|
|
37027
|
+
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable());
|
|
36804
37028
|
} catch (e) {
|
|
36805
37029
|
bound = null;
|
|
36806
37030
|
return this.bindFailed(manifest, payload.memory, e);
|
|
@@ -36819,9 +37043,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36819
37043
|
return true;
|
|
36820
37044
|
}
|
|
36821
37045
|
try {
|
|
36822
|
-
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
|
|
36823
37046
|
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
|
|
36824
37047
|
if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
|
|
37048
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable()));
|
|
36825
37049
|
} catch (e) {
|
|
36826
37050
|
this.manifestBinding = null;
|
|
36827
37051
|
throw e;
|
|
@@ -36874,6 +37098,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36874
37098
|
/** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
|
|
36875
37099
|
async postRuntimeConfigReport(base, version, skillsJson) {
|
|
36876
37100
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
37101
|
+
const reportedSearchProviders = this.searchProvidersCapable();
|
|
36877
37102
|
const report = {
|
|
36878
37103
|
applied_version: version,
|
|
36879
37104
|
paths: checkConfiguredPaths(skillsJson),
|
|
@@ -36888,6 +37113,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36888
37113
|
// to the backend. The backend needs it to generate a truthful tool
|
|
36889
37114
|
// paragraph, so it rides along here.
|
|
36890
37115
|
has_search_key: local?.has_search_key ?? false,
|
|
37116
|
+
// L-2 (spec §7.9): only the runtime knows whether an extract-capable provider key is readable; never derived
|
|
37117
|
+
// from has_search_key. An older runtime omits it: false.
|
|
37118
|
+
// Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
|
|
37119
|
+
// the bridge withholds web_extract from the runtime.
|
|
37120
|
+
has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
|
|
37121
|
+
// D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
|
|
37122
|
+
...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
|
|
36891
37123
|
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
36892
37124
|
// whether one is configured in its own environment.
|
|
36893
37125
|
has_llm_key: local?.has_llm_key ?? false,
|
|
@@ -36927,7 +37159,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36927
37159
|
// Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
|
|
36928
37160
|
config_binding_mismatches: [...this.configBindingMismatches],
|
|
36929
37161
|
target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
|
|
36930
|
-
capability: this.lastConsumerCapability
|
|
37162
|
+
capability: this.lastConsumerCapability,
|
|
37163
|
+
// D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
|
|
37164
|
+
search_providers_v2: reportedSearchProviders,
|
|
37165
|
+
// D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
|
|
37166
|
+
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
|
|
36931
37167
|
}
|
|
36932
37168
|
};
|
|
36933
37169
|
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
@@ -36948,6 +37184,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36948
37184
|
return;
|
|
36949
37185
|
}
|
|
36950
37186
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
37187
|
+
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
36951
37188
|
if (version) writeServiceLog("info", "runtime_config.applied", { version });
|
|
36952
37189
|
else writeServiceLog("info", "runtime_config.legacy_memory_reported");
|
|
36953
37190
|
}
|
|
@@ -37017,6 +37254,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37017
37254
|
this.startedAt = Date.now();
|
|
37018
37255
|
await this.restoreRuntimeInbox();
|
|
37019
37256
|
await this.loadToolGrantState();
|
|
37257
|
+
this.recomputeManifestPolicy(false);
|
|
37020
37258
|
try {
|
|
37021
37259
|
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
37022
37260
|
} catch (e) {
|
|
@@ -38161,6 +38399,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38161
38399
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
38162
38400
|
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
|
|
38163
38401
|
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
38402
|
+
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(body.text) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
|
|
38403
|
+
this.callBuffer.appendAgent(channelId, body.text, "reply");
|
|
38404
|
+
}
|
|
38164
38405
|
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";
|
|
38165
38406
|
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
|
|
38166
38407
|
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
|
|
@@ -38931,6 +39172,27 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38931
39172
|
mayReadHistoryForTurn(messageId, authorization) {
|
|
38932
39173
|
return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
|
|
38933
39174
|
}
|
|
39175
|
+
/**
|
|
39176
|
+
* Voice fast-path privacy (spec §2.1, OQ-1 ruled): the ONLY history source of a voice-scoped turn, the call buffer.
|
|
39177
|
+
* Never reads the memory provider, the store, the unified store or the 7b index. A-6: called once per turn, at ingest
|
|
39178
|
+
* (takeVoiceSnapshot), after the reset signals and before the turn's own append.
|
|
39179
|
+
*/
|
|
39180
|
+
voiceScopedHistory(channelId, limit, upTo) {
|
|
39181
|
+
return this.callBuffer.recent(channelId, limit, upTo);
|
|
39182
|
+
}
|
|
39183
|
+
/**
|
|
39184
|
+
* Task 4b (A-6): the turn's snapshot, taken BEFORE its own text is appended (audit finding 2). Final fix I-1: a turn
|
|
39185
|
+
* already admitted to this call (a redelivered row) is snapshotted up to just before its own entry, as it was the
|
|
39186
|
+
* first time, so its snapshot never contains itself.
|
|
39187
|
+
*/
|
|
39188
|
+
takeVoiceSnapshot(channelId, messageId) {
|
|
39189
|
+
const upTo = this.callBuffer.admittedAt(messageId);
|
|
39190
|
+
return {
|
|
39191
|
+
recent: this.voiceScopedHistory(channelId, _BridgeAgent.TURN_MEMORY_LIMIT, upTo),
|
|
39192
|
+
endSeq: upTo ?? this.callBuffer.seqAt(channelId),
|
|
39193
|
+
generation: this.callBuffer.generation(channelId)
|
|
39194
|
+
};
|
|
39195
|
+
}
|
|
38934
39196
|
/** An inbound turn may receive OpenClaw memory only from an active owner DM. */
|
|
38935
39197
|
mayReadOpenClawMemoryForTurn(authorization) {
|
|
38936
39198
|
return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
|
|
@@ -38940,16 +39202,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38940
39202
|
* stays owner-DM-only; every provider failure degrades to empty context and
|
|
38941
39203
|
* never falls back to the runtime's retired plaintext JSON files.
|
|
38942
39204
|
*/
|
|
38943
|
-
async memoryForTurn(channelId, messageId, authorization, limit = 50) {
|
|
39205
|
+
async memoryForTurn(channelId, messageId, authorization, limit = 50, voice) {
|
|
38944
39206
|
if (!this.mayReadHistoryForTurn(messageId, authorization)) {
|
|
38945
39207
|
return { recent: [], facts: [] };
|
|
38946
39208
|
}
|
|
39209
|
+
if (voice) return { recent: this.voiceFrameTurns(voice.snapshot?.recent ?? []), facts: [] };
|
|
38947
39210
|
try {
|
|
38948
39211
|
return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
|
|
38949
39212
|
} catch {
|
|
38950
39213
|
return { recent: [], facts: [] };
|
|
38951
39214
|
}
|
|
38952
39215
|
}
|
|
39216
|
+
/**
|
|
39217
|
+
* Task 6 (spec §2.2 A-1, N1): call-buffer entries as runtime-frame turns. Under the voice fence (a pin is loaded) the
|
|
39218
|
+
* fast model's own lines (origin "fast_path") are dropped, as fenceMemory drops them from store history; the fast
|
|
39219
|
+
* prompt keeps them. The internal origin is not carried into the frame; `seq` stays 0 (A-5).
|
|
39220
|
+
*/
|
|
39221
|
+
voiceFrameTurns(entries) {
|
|
39222
|
+
const fenced = this.voiceFenceOn();
|
|
39223
|
+
return entries.filter((e) => !(fenced && e.origin === "fast_path")).map((e) => ({ seq: e.seq, role: e.role, text: e.text }));
|
|
39224
|
+
}
|
|
38953
39225
|
/**
|
|
38954
39226
|
* 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
|
|
38955
39227
|
* turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
|
|
@@ -38984,7 +39256,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38984
39256
|
if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
|
|
38985
39257
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
38986
39258
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
38987
|
-
const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => strict || t.role === "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
39259
|
+
const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => (strict || t.role === "agent") && t.seq !== 0).map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
38988
39260
|
const fence = this.fenceContext(channelId);
|
|
38989
39261
|
const recent = ctx.recent.filter((t) => {
|
|
38990
39262
|
const m = meta.get(t.seq);
|
|
@@ -39003,7 +39275,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39003
39275
|
if (recent.length === 0) return false;
|
|
39004
39276
|
const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
|
|
39005
39277
|
const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
|
|
39006
|
-
const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
39278
|
+
const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent" && t.seq !== 0).map((t) => t.seq)) : /* @__PURE__ */ new Map();
|
|
39007
39279
|
return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
|
|
39008
39280
|
}
|
|
39009
39281
|
/**
|
|
@@ -39299,9 +39571,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39299
39571
|
writeServiceLog("warn", "memory.recall_prepare_failed", {}, e);
|
|
39300
39572
|
}
|
|
39301
39573
|
}
|
|
39574
|
+
/**
|
|
39575
|
+
* Voice fast-path privacy, Task 7 (spec §4): the frame-keyed delivery check. A voice-scoped frame gets no notes, no
|
|
39576
|
+
* recall and no unified items, whatever its authorization says. Keys on the frame's own marks, NEVER on the call
|
|
39577
|
+
* buffer's id set (a reset evicts ids; the staged frame must stay scoped).
|
|
39578
|
+
*/
|
|
39579
|
+
frameIsVoiceScoped(m) {
|
|
39580
|
+
return m.voice_scoped === true || m.transcribed === true || m.speaker !== void 0;
|
|
39581
|
+
}
|
|
39302
39582
|
/** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
|
|
39303
39583
|
recallForDelivery(m) {
|
|
39304
39584
|
if (this.unifiedOn) return void 0;
|
|
39585
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39305
39586
|
if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
|
|
39306
39587
|
return this.recall.deliverable(m.message_id, m.channel_id);
|
|
39307
39588
|
}
|
|
@@ -39534,6 +39815,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39534
39815
|
/** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
|
|
39535
39816
|
unifiedForDelivery(m) {
|
|
39536
39817
|
if (!this.unifiedProviderOn()) return void 0;
|
|
39818
|
+
if (this.frameIsVoiceScoped(m)) return void 0;
|
|
39537
39819
|
const a = m.authorization;
|
|
39538
39820
|
if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
|
|
39539
39821
|
const scopeId = this.ownerScopeNow(m.channel_id);
|
|
@@ -39769,6 +40051,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39769
40051
|
notesForDelivery(m) {
|
|
39770
40052
|
if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
|
|
39771
40053
|
const generation = this.notes.appliedGeneration();
|
|
40054
|
+
if (this.frameIsVoiceScoped(m)) return { generation };
|
|
39772
40055
|
if (!this.notesReadable(m.authorization)) return { generation };
|
|
39773
40056
|
const d = this.notes.deliverable(m.channel_id);
|
|
39774
40057
|
if (d.miss) {
|
|
@@ -40128,7 +40411,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40128
40411
|
* deferring fast reply was actually published — F3).
|
|
40129
40412
|
*/
|
|
40130
40413
|
async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
|
|
40131
|
-
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn
|
|
40414
|
+
const outcome = plan.kind === "ack" ? await this.sendFastLine(turn, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
|
|
40132
40415
|
if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
|
|
40133
40416
|
else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
|
|
40134
40417
|
}
|
|
@@ -40147,19 +40430,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40147
40430
|
async settleFastReplies() {
|
|
40148
40431
|
while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
|
|
40149
40432
|
}
|
|
40150
|
-
/**
|
|
40151
|
-
|
|
40433
|
+
/**
|
|
40434
|
+
* Send a line the voice fast path speaks: its memory record is "voice" (H1).
|
|
40435
|
+
* Task 4b (spec §2.1 agent appends (1), A-1, A-2): once published, the line enters the call buffer as "fast_path",
|
|
40436
|
+
* unless the turn it answers failed owner verification (its id, or any fragment of a coalesced turn). A line answering
|
|
40437
|
+
* a turn the ingest did not find voice-scoped is not appended either (defense in depth: the fast path fires only on
|
|
40438
|
+
* voice-scoped turns, Task 5).
|
|
40439
|
+
*/
|
|
40440
|
+
async sendSpoken(channelId, text, messageId, turn) {
|
|
40152
40441
|
this.spokenReplyIds.add(messageId);
|
|
40153
40442
|
try {
|
|
40154
40443
|
await this.send(channelId, text, messageId);
|
|
40155
40444
|
} finally {
|
|
40156
40445
|
this.spokenReplyIds.delete(messageId);
|
|
40157
40446
|
}
|
|
40447
|
+
if (turn.voiceScoped === true && turn.anyFragmentVerifyFailed !== true && !this.isOwnerVerifyFailed(turn.messageId)) {
|
|
40448
|
+
this.callBuffer.appendAgent(channelId, text, "fast_path");
|
|
40449
|
+
}
|
|
40158
40450
|
}
|
|
40159
40451
|
/** Speak a fixed acknowledgement line; failures are counted, never thrown. */
|
|
40160
|
-
async sendFastLine(
|
|
40452
|
+
async sendFastLine(turn, line) {
|
|
40453
|
+
const { channelId, messageId } = turn;
|
|
40161
40454
|
try {
|
|
40162
|
-
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
|
|
40455
|
+
await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId), turn);
|
|
40163
40456
|
this.fastPathSent++;
|
|
40164
40457
|
return true;
|
|
40165
40458
|
} catch (e) {
|
|
@@ -40173,8 +40466,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40173
40466
|
* the fast model's reply — or, in `ack_only`, the greeting template — is
|
|
40174
40467
|
* spoken, and the outcome decides whether the runtime answers next.
|
|
40175
40468
|
*
|
|
40176
|
-
* MUST NEVER throw: every failure path (
|
|
40177
|
-
*
|
|
40469
|
+
* MUST NEVER throw: every failure path (the LLM call, normalization,
|
|
40470
|
+
* or the send itself) is caught, counted in
|
|
40178
40471
|
* `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
|
|
40179
40472
|
* The `fastReplyId(...)` send id is deterministic, so a duplicate that
|
|
40180
40473
|
* slipped past the in-memory dedupe (e.g. a restart) resolves to the same
|
|
@@ -40188,20 +40481,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40188
40481
|
try {
|
|
40189
40482
|
if ((this.cfg.voiceFastPathMode ?? "full") === "ack_only") {
|
|
40190
40483
|
if (!isGreeting) return "failed";
|
|
40191
|
-
const ok = await this.sendFastLine(m
|
|
40484
|
+
const ok = await this.sendFastLine(m, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
|
|
40192
40485
|
return ok ? "answered" : "failed";
|
|
40193
40486
|
}
|
|
40194
40487
|
const fastLlm = this.fastLlm;
|
|
40195
40488
|
if (!fastLlm || !this.cfg.voice) return "failed";
|
|
40196
|
-
let memory;
|
|
40197
|
-
|
|
40198
|
-
|
|
40199
|
-
|
|
40200
|
-
|
|
40201
|
-
|
|
40202
|
-
}
|
|
40203
|
-
memory = { recent: [], facts: [] };
|
|
40204
|
-
}
|
|
40489
|
+
let memory = { recent: [], facts: [] };
|
|
40490
|
+
const snapshot = m.voiceSnapshot;
|
|
40491
|
+
if (m.voiceScoped === true && snapshot !== void 0 && m.anyFragmentVerifyFailed !== true && this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
|
|
40492
|
+
memory = {
|
|
40493
|
+
recent: [...snapshot.recent, ...this.callBuffer.agentAfter(m.channelId, { seq: snapshot.endSeq, generation: snapshot.generation })].slice(-FAST_PATH_MEMORY_TURNS),
|
|
40494
|
+
facts: []
|
|
40495
|
+
};
|
|
40205
40496
|
}
|
|
40206
40497
|
const messages = buildFastMessages({
|
|
40207
40498
|
persona: this.cfg.voice.persona,
|
|
@@ -40224,7 +40515,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40224
40515
|
const refusal = !isGreeting && isRefusal(split3.reply);
|
|
40225
40516
|
const defer = split3.defer || refusal;
|
|
40226
40517
|
const reply = refusal ? deferAck(m.messageId) : split3.reply;
|
|
40227
|
-
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
|
|
40518
|
+
await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId), m);
|
|
40228
40519
|
this.fastPathSent++;
|
|
40229
40520
|
if (defer) this.fastPathDeferred++;
|
|
40230
40521
|
if (refusal) this.fastPathRefusalDeferred++;
|
|
@@ -40288,6 +40579,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40288
40579
|
async memoryAtRelease(message) {
|
|
40289
40580
|
const prior = message.memory;
|
|
40290
40581
|
if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
|
|
40582
|
+
if (this.frameIsVoiceScoped(message)) {
|
|
40583
|
+
const tails = this.voiceCoalescedTails.get(message.message_id) ?? [];
|
|
40584
|
+
if (tails.some((id) => this.isOwnerVerifyFailed(id))) return prior;
|
|
40585
|
+
const noted = this.callBuffer.seqNoted(message.message_id);
|
|
40586
|
+
if (noted === void 0) return prior;
|
|
40587
|
+
const spoken2 = this.voiceFrameTurns(this.callBuffer.agentAfter(message.channel_id, noted));
|
|
40588
|
+
if (spoken2.length === 0) return prior;
|
|
40589
|
+
return { recent: [...prior.recent, ...spoken2].slice(-_BridgeAgent.TURN_MEMORY_LIMIT), facts: [] };
|
|
40590
|
+
}
|
|
40291
40591
|
let fresh;
|
|
40292
40592
|
try {
|
|
40293
40593
|
fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
|
|
@@ -40799,9 +41099,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40799
41099
|
});
|
|
40800
41100
|
}
|
|
40801
41101
|
/** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
|
|
40802
|
-
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
|
|
41102
|
+
async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
|
|
40803
41103
|
const held = await this.heldPosition(channelId);
|
|
40804
|
-
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
|
|
41104
|
+
const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
|
|
40805
41105
|
if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
|
|
40806
41106
|
if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
|
|
40807
41107
|
writeServiceLog("warn", "room_grant.untracked");
|
|
@@ -40816,17 +41116,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40816
41116
|
* guess. The returned object and grant array are frozen before they cross the
|
|
40817
41117
|
* daemon boundary so one consumer cannot widen a later execution check.
|
|
40818
41118
|
*
|
|
40819
|
-
* Grants (architect ruling 2026-10-02):
|
|
40820
|
-
* the kill switch is on, the channel is a DM, and EITHER
|
|
41119
|
+
* Grants (architect ruling 2026-10-02; P-5 and G-1, spec §9): an owner-DM
|
|
41120
|
+
* constant only when the kill switch is on, the channel is a DM, and EITHER
|
|
40821
41121
|
* (a) audience is owner_dm AND the sender is the channel's pinned DM
|
|
40822
41122
|
* counterpart (`channelOwner`, never "anomalous"). A server-marked
|
|
40823
41123
|
* `is_agent_owner` on a member who is NOT the DM's single human
|
|
40824
41124
|
* counterpart gets nothing; on the counterpart it is trusted (see
|
|
40825
|
-
* LIMIT below);
|
|
41125
|
+
* LIMIT below). OWNER_DM_DEFAULT_READ_GRANTS on a typed turn;
|
|
41126
|
+
* RELAYED_VOICE_GRANTS and no memory_write when `opts.voiceScoped`
|
|
41127
|
+
* (G-1: the ingest predicate isVoiceScopedTurn, spec §1); OR
|
|
40826
41128
|
* (b) the sender is this channel's server-marked relay (`is_relay`) and the
|
|
40827
41129
|
* relayed `speaker_identity_id` (passed by the caller only for relay
|
|
40828
41130
|
* turns) is that same pinned owner, whose roster row is the agent's
|
|
40829
|
-
* owner. A speaker claim from any non-relay
|
|
41131
|
+
* owner: RELAYED_VOICE_GRANTS. A speaker claim from any non-relay
|
|
41132
|
+
* sender gets nothing.
|
|
41133
|
+
* RELAYED_VOICE_GRANTS is never manifest-eligible: effectiveToolGrants gives
|
|
41134
|
+
* the constant by reference, or the fallback when a good manifest is held.
|
|
40830
41135
|
* Both need the roster fetched at `heldEpoch` (the agent's current MLS
|
|
40831
41136
|
* epoch for the channel), or the DM marked epoch-fresh at this ingest
|
|
40832
41137
|
* cycle's refresh (audit M-B4).
|
|
@@ -40870,9 +41175,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40870
41175
|
const ownerDirect = audience === "owner_dm" && senderId === ownerId;
|
|
40871
41176
|
const owner = roster?.get(ownerId);
|
|
40872
41177
|
const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
|
|
40873
|
-
if (ownerDirect
|
|
41178
|
+
if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
|
|
41179
|
+
if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
|
|
40874
41180
|
if (ownerRelayed) relaySpeaker = "owner";
|
|
40875
|
-
if (ownerDirect && relaySpeakerId === void 0 &&
|
|
41181
|
+
if (ownerDirect && relaySpeakerId === void 0 && // G-1: never on a voice-scoped turn.
|
|
41182
|
+
opts.voiceScoped !== true && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
|
|
40876
41183
|
// runtime does not offer memory_note at all. Not memoryConfigConfirmed:
|
|
40877
41184
|
// a turn the boot drain pulls before the boot refetch keeps its mark,
|
|
40878
41185
|
// and the read and write gates (notesProviderOn) hold it until then.
|
|
@@ -40889,11 +41196,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40889
41196
|
const binding = this.manifestBinding;
|
|
40890
41197
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
40891
41198
|
const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
|
|
40892
|
-
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
|
|
40893
|
-
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null;
|
|
41199
|
+
if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS || tool_grants === RELAYED_VOICE_GRANTS) {
|
|
41200
|
+
const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null && tool_grants !== RELAYED_VOICE_GRANTS;
|
|
40894
41201
|
const r = effectiveToolGrants({
|
|
40895
41202
|
audience: "owner_dm",
|
|
40896
|
-
base:
|
|
41203
|
+
base: tool_grants,
|
|
40897
41204
|
manifest,
|
|
40898
41205
|
eligible,
|
|
40899
41206
|
manifestSwitchOn: this.ownerDmManifestGrants,
|
|
@@ -41615,6 +41922,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41615
41922
|
const acked = [];
|
|
41616
41923
|
for (const m of entries) {
|
|
41617
41924
|
try {
|
|
41925
|
+
if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
|
|
41618
41926
|
const encId = m.envelope.enclave_id;
|
|
41619
41927
|
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
41620
41928
|
this.channelEnclaveRecorded.add(m.channelId);
|
|
@@ -41772,6 +42080,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41772
42080
|
continue;
|
|
41773
42081
|
}
|
|
41774
42082
|
if (typeof parsed.text === "string") {
|
|
42083
|
+
if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
|
|
41775
42084
|
const meta = this.freshMeta(m.channelId);
|
|
41776
42085
|
const senderType = m.envelope.sender_type;
|
|
41777
42086
|
const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
|
|
@@ -41813,23 +42122,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41813
42122
|
const senderName = this.freshRoster(m.channelId)?.get(
|
|
41814
42123
|
m.envelope.sender_id
|
|
41815
42124
|
)?.name;
|
|
42125
|
+
const voiceScoped = isVoiceScopedTurn({
|
|
42126
|
+
speaker,
|
|
42127
|
+
transcribed: parsed.transcribed,
|
|
42128
|
+
senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
|
|
42129
|
+
text: parsed.text
|
|
42130
|
+
});
|
|
41816
42131
|
const ownerVerdict = await this.verifyOwnerTurn(m);
|
|
41817
42132
|
const turnAuthorization = await this.authorizeTurn(
|
|
41818
42133
|
m.channelId,
|
|
41819
42134
|
m.messageId,
|
|
41820
42135
|
m.envelope.sender_id,
|
|
41821
42136
|
ownerVerdict,
|
|
41822
|
-
speaker?.identityId
|
|
42137
|
+
speaker?.identityId,
|
|
42138
|
+
voiceScoped
|
|
41823
42139
|
);
|
|
41824
42140
|
await this.prepareNotesForTurn(m.channelId, turnAuthorization);
|
|
41825
|
-
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
|
|
41826
42141
|
const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
|
|
41827
42142
|
const humanTurn = senderIsAgent !== true || speaker !== void 0;
|
|
41828
|
-
const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
42143
|
+
const fastPathEligible = Boolean(voiceScoped && m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
|
|
41829
42144
|
const turnText = parsed.text;
|
|
41830
42145
|
const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
|
|
41831
42146
|
const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
|
|
41832
42147
|
const coalescible = this.voiceCoalesceWindowMs > 0 && windowEligible;
|
|
42148
|
+
const callPeer = admitsToCallBuffer({ speaker, transcribed: parsed.transcribed });
|
|
42149
|
+
let voiceSnapshot;
|
|
42150
|
+
if (voiceScoped) {
|
|
42151
|
+
this.callBuffer.markVoiceScoped(m.channelId, m.messageId);
|
|
42152
|
+
if (!coalescible) {
|
|
42153
|
+
voiceSnapshot = this.takeVoiceSnapshot(m.channelId, m.messageId);
|
|
42154
|
+
if (callPeer && !this.isOwnerVerifyFailed(m.messageId)) this.callBuffer.appendPeer(m.channelId, parsed.text, m.messageId);
|
|
42155
|
+
}
|
|
42156
|
+
}
|
|
42157
|
+
const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization, void 0, voiceScoped ? { snapshot: voiceSnapshot } : void 0);
|
|
41833
42158
|
if (!coalescible) {
|
|
41834
42159
|
const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
|
|
41835
42160
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
|
|
@@ -41845,7 +42170,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41845
42170
|
const fastGreeting = isCallStart && fastPathFiring;
|
|
41846
42171
|
const runtimeVoice = (ackSent) => m.voiceMode ? {
|
|
41847
42172
|
voice: true,
|
|
41848
|
-
|
|
42173
|
+
// Task 6 (OQ-3): a voice-scoped frame's guidance ends with VOICE_CALL_CLAUSE, on all three frames
|
|
42174
|
+
// (held, coalesced after-ack, after-ack); a typed turn during a call keeps its guidance.
|
|
42175
|
+
guidance: `${ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance}${voiceScoped ? ` ${VOICE_CALL_CLAUSE}` : ""}`
|
|
41849
42176
|
} : {};
|
|
41850
42177
|
const runtimeEntry = (ackSent, memory) => ({
|
|
41851
42178
|
channel_id: m.channelId,
|
|
@@ -41866,6 +42193,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41866
42193
|
// identify: the envelope's own claim (relay or on-device
|
|
41867
42194
|
// transcript) or a relay-attributed turn.
|
|
41868
42195
|
...parsed.transcribed === true || speaker !== void 0 ? { transcribed: true } : {},
|
|
42196
|
+
...voiceScoped ? { voice_scoped: true } : {},
|
|
41869
42197
|
...runtimeVoice(ackSent)
|
|
41870
42198
|
});
|
|
41871
42199
|
if (coalescible) {
|
|
@@ -41895,6 +42223,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41895
42223
|
acked.push(m.queueId);
|
|
41896
42224
|
continue;
|
|
41897
42225
|
}
|
|
42226
|
+
const bufferGeneration = this.callBuffer.generation(m.channelId);
|
|
41898
42227
|
const held = runtimeEntry(false, await runtimeMemory);
|
|
41899
42228
|
if (!await this.stageHeldRuntimeMessage(held)) continue;
|
|
41900
42229
|
this.noteVoiceCoalesced(m.messageId);
|
|
@@ -41905,7 +42234,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41905
42234
|
closed: false,
|
|
41906
42235
|
pending: Promise.resolve(),
|
|
41907
42236
|
purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
|
|
41908
|
-
itemCapture: itemCaptureAtReceipt.get(m.channelId)
|
|
42237
|
+
itemCapture: itemCaptureAtReceipt.get(m.channelId),
|
|
42238
|
+
bufferGeneration
|
|
41909
42239
|
};
|
|
41910
42240
|
this.voiceCoalescing.set(windowKey, win);
|
|
41911
42241
|
const openedAt = Date.now();
|
|
@@ -41921,9 +42251,18 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41921
42251
|
const [firstId, ...tailIds] = win.ids;
|
|
41922
42252
|
if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
|
|
41923
42253
|
const text = win.parts.join(" ");
|
|
42254
|
+
const sameCall = this.callBuffer.generation(channelId) === win.bufferGeneration;
|
|
42255
|
+
const anyFragmentVerifyFailed = win.ids.some((id) => this.isOwnerVerifyFailed(id));
|
|
42256
|
+
let windowSnapshot;
|
|
42257
|
+
if (sameCall) {
|
|
42258
|
+
this.callBuffer.markVoiceScoped(channelId, firstId);
|
|
42259
|
+
windowSnapshot = this.takeVoiceSnapshot(channelId, firstId);
|
|
42260
|
+
}
|
|
42261
|
+
if (callPeer && sameCall && !anyFragmentVerifyFailed) this.callBuffer.appendPeer(channelId, text, firstId);
|
|
41924
42262
|
if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41925
42263
|
else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
|
|
41926
|
-
const
|
|
42264
|
+
const windowMemory = await this.memoryForTurn(channelId, firstId, turnAuthorization, void 0, { snapshot: anyFragmentVerifyFailed ? void 0 : windowSnapshot });
|
|
42265
|
+
const merged = { ...held, text, memory: windowMemory };
|
|
41927
42266
|
if (this.unifiedOn) {
|
|
41928
42267
|
const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
|
|
41929
42268
|
const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
|
|
@@ -41951,7 +42290,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41951
42290
|
const release = this.reserveFastReply(channelId);
|
|
41952
42291
|
try {
|
|
41953
42292
|
await this.speakAndSettle(
|
|
41954
|
-
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization },
|
|
42293
|
+
{ channelId, messageId: firstId, senderId, authorization: turnAuthorization, voiceScoped: voiceScoped && sameCall, voiceSnapshot: windowSnapshot, anyFragmentVerifyFailed },
|
|
41955
42294
|
plan,
|
|
41956
42295
|
merged,
|
|
41957
42296
|
{ ...merged, ...runtimeVoice(true) },
|
|
@@ -41983,7 +42322,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41983
42322
|
channelId: m.channelId,
|
|
41984
42323
|
messageId: m.messageId,
|
|
41985
42324
|
senderId: m.envelope.sender_id,
|
|
41986
|
-
authorization: turnAuthorization
|
|
42325
|
+
authorization: turnAuthorization,
|
|
42326
|
+
voiceScoped,
|
|
42327
|
+
voiceSnapshot
|
|
41987
42328
|
};
|
|
41988
42329
|
const guidanceForFast = voiceGuidance;
|
|
41989
42330
|
const greeting = fastGreeting;
|
|
@@ -42808,6 +43149,115 @@ ${channelId}`;
|
|
|
42808
43149
|
manifestConsumerAttached() {
|
|
42809
43150
|
return this.manifestConsumer !== null;
|
|
42810
43151
|
}
|
|
43152
|
+
/**
|
|
43153
|
+
* D28 (B-4): every socket that said hello, with whether its latest hello declared search_providers_v2 on the
|
|
43154
|
+
* manifest_grants_v1 predicate (ws-server.ts). The agent value is the AND over all of them, absent (false) with
|
|
43155
|
+
* none: unlike manifestConsumer, never newest wins. The per-delivery filter judges each socket by its own hello.
|
|
43156
|
+
*/
|
|
43157
|
+
searchProvidersSockets = /* @__PURE__ */ new Map();
|
|
43158
|
+
/**
|
|
43159
|
+
* The D28 value the files were last recomputed for; null until the first socket observation, so that observation
|
|
43160
|
+
* always recomputes (final review I-1), even when it is non-capable.
|
|
43161
|
+
*/
|
|
43162
|
+
searchProvidersState = null;
|
|
43163
|
+
searchCapabilityRefetchTimer = null;
|
|
43164
|
+
/** When the current burst's first change happened (valid while the timer is set). */
|
|
43165
|
+
searchCapabilityBurstStart = 0;
|
|
43166
|
+
/** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
|
|
43167
|
+
lastReportedSearchProviders = null;
|
|
43168
|
+
noteSearchProvidersSocket(token, declared) {
|
|
43169
|
+
this.searchProvidersSockets.set(token, declared);
|
|
43170
|
+
this.onSearchProvidersMaybeChanged();
|
|
43171
|
+
}
|
|
43172
|
+
clearSearchProvidersSocket(token) {
|
|
43173
|
+
if (this.searchProvidersSockets.delete(token)) this.onSearchProvidersMaybeChanged();
|
|
43174
|
+
}
|
|
43175
|
+
/** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
|
|
43176
|
+
searchProvidersCapable() {
|
|
43177
|
+
if (this.searchProvidersSockets.size === 0) return false;
|
|
43178
|
+
for (const v of this.searchProvidersSockets.values()) if (!v) return false;
|
|
43179
|
+
return true;
|
|
43180
|
+
}
|
|
43181
|
+
/**
|
|
43182
|
+
* D19 item 3 (B-6): the search provider signed in the stored verified manifest in force (a pin loaded and a good
|
|
43183
|
+
* manifest), never the server payload; undefined with no pin, no manifest, or no signed search block. The
|
|
43184
|
+
* per-delivery filter (consumer-grants.ts withSearchCapability) and the S-28 report reason both read this.
|
|
43185
|
+
*/
|
|
43186
|
+
signedSearchProvider() {
|
|
43187
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
43188
|
+
return pin !== null ? this.toolGrantManifest?.config.search?.provider : void 0;
|
|
43189
|
+
}
|
|
43190
|
+
/** D19 item 4, D34: the in-force signed provider is not brave, and the attached runtime(s) lack the capability. */
|
|
43191
|
+
searchProviderUnsupported(capable) {
|
|
43192
|
+
if (capable) return false;
|
|
43193
|
+
const provider = this.signedSearchProvider();
|
|
43194
|
+
return provider !== void 0 && provider !== "brave";
|
|
43195
|
+
}
|
|
43196
|
+
/**
|
|
43197
|
+
* D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
|
|
43198
|
+
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
43199
|
+
*/
|
|
43200
|
+
onSearchProvidersMaybeChanged() {
|
|
43201
|
+
const capable = this.searchProvidersCapable();
|
|
43202
|
+
const prev = this.searchProvidersState;
|
|
43203
|
+
if (capable === prev) return;
|
|
43204
|
+
this.searchProvidersState = capable;
|
|
43205
|
+
this.recomputeManifestPolicy(capable);
|
|
43206
|
+
if (this.closingDown) return;
|
|
43207
|
+
if (prev === null && !capable) return;
|
|
43208
|
+
const now = Date.now();
|
|
43209
|
+
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
43210
|
+
else this.searchCapabilityBurstStart = now;
|
|
43211
|
+
const delay = Math.max(0, Math.min(this.searchCapabilityRefetchDebounceMs, this.searchCapabilityBurstStart + this.searchCapabilityRefetchMaxWaitMs - now));
|
|
43212
|
+
this.searchCapabilityRefetchTimer = setTimeout(() => {
|
|
43213
|
+
this.searchCapabilityRefetchTimer = null;
|
|
43214
|
+
if (this.searchCapabilityRefetchNeeded()) void this.refetchRuntimeConfig();
|
|
43215
|
+
}, delay);
|
|
43216
|
+
this.searchCapabilityRefetchTimer.unref?.();
|
|
43217
|
+
}
|
|
43218
|
+
/**
|
|
43219
|
+
* A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
|
|
43220
|
+
* the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
|
|
43221
|
+
*/
|
|
43222
|
+
searchCapabilityRefetchNeeded() {
|
|
43223
|
+
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
43224
|
+
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
43225
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
|
|
43226
|
+
}
|
|
43227
|
+
/**
|
|
43228
|
+
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
43229
|
+
* so a policy is never written ahead of its skills.json. Narrowing never waits: with no binding in force, a file that
|
|
43230
|
+
* lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
|
|
43231
|
+
* Content-free; a write failure is logged and the debounced refetch tries again.
|
|
43232
|
+
*/
|
|
43233
|
+
recomputeManifestPolicy(capable) {
|
|
43234
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
43235
|
+
const manifest = pin !== null ? this.toolGrantManifest : null;
|
|
43236
|
+
try {
|
|
43237
|
+
if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
|
|
43238
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, capable));
|
|
43239
|
+
return;
|
|
43240
|
+
}
|
|
43241
|
+
if (capable) return;
|
|
43242
|
+
const path = join19(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
|
|
43243
|
+
if (!existsSync5(path)) return;
|
|
43244
|
+
let doc;
|
|
43245
|
+
try {
|
|
43246
|
+
doc = JSON.parse(readFileSync11(path, "utf8"));
|
|
43247
|
+
} catch {
|
|
43248
|
+
return;
|
|
43249
|
+
}
|
|
43250
|
+
const tools = doc?.manifest_tools;
|
|
43251
|
+
if (!Array.isArray(tools) || !tools.includes("web_extract")) return;
|
|
43252
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
|
|
43253
|
+
v: 1,
|
|
43254
|
+
manifest_tools: tools.filter((t) => typeof t === "string" && t !== "web_extract"),
|
|
43255
|
+
secret_path_mode: "enforce"
|
|
43256
|
+
});
|
|
43257
|
+
} catch (e) {
|
|
43258
|
+
writeServiceLog("warn", "authz.search_capability", { state: ServiceLogEnum.of("policy_write_failed", ["policy_write_failed"]) }, e);
|
|
43259
|
+
}
|
|
43260
|
+
}
|
|
42811
43261
|
noteConsumerCapability(state) {
|
|
42812
43262
|
this.lastConsumerCapability = state;
|
|
42813
43263
|
if (state !== "ok") {
|
|
@@ -42999,6 +43449,8 @@ ${channelId}`;
|
|
|
42999
43449
|
this.supersedeRecheckTimers.clear();
|
|
43000
43450
|
if (this.redeliveryTimer !== null) clearTimeout(this.redeliveryTimer);
|
|
43001
43451
|
this.redeliveryTimer = null;
|
|
43452
|
+
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
43453
|
+
this.searchCapabilityRefetchTimer = null;
|
|
43002
43454
|
this.pingHandle?.close();
|
|
43003
43455
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
43004
43456
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
@@ -43109,11 +43561,11 @@ ${channelId}`;
|
|
|
43109
43561
|
|
|
43110
43562
|
// src/auth.ts
|
|
43111
43563
|
import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
43112
|
-
import { existsSync as existsSync6, readFileSync as
|
|
43564
|
+
import { existsSync as existsSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
|
|
43113
43565
|
function resolveToken(opts) {
|
|
43114
43566
|
if (opts.token) return opts.token;
|
|
43115
43567
|
const f = opts.tokenFile;
|
|
43116
|
-
if (f && existsSync6(f)) return
|
|
43568
|
+
if (f && existsSync6(f)) return readFileSync12(f, "utf8").trim();
|
|
43117
43569
|
const tok = randomBytes11(32).toString("base64url");
|
|
43118
43570
|
if (f) writeFileSync2(f, tok, { mode: 384 });
|
|
43119
43571
|
return tok;
|
|
@@ -43214,6 +43666,7 @@ function withConsumerGrants(m, perToolConsumer) {
|
|
|
43214
43666
|
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
43215
43667
|
}
|
|
43216
43668
|
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
43669
|
+
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
43217
43670
|
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
43218
43671
|
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
43219
43672
|
team_room: Object.freeze(["current_time"])
|
|
@@ -43236,6 +43689,15 @@ function withHarnessGrants(m, withheld) {
|
|
|
43236
43689
|
if (!a || a.tool_grants.length === 0) return m;
|
|
43237
43690
|
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
43238
43691
|
}
|
|
43692
|
+
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
43693
|
+
if (consumerHasFeature) return m;
|
|
43694
|
+
const a = m.authorization;
|
|
43695
|
+
if (!a) return m;
|
|
43696
|
+
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
43697
|
+
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
43698
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
43699
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
43700
|
+
}
|
|
43239
43701
|
|
|
43240
43702
|
// src/http-server.ts
|
|
43241
43703
|
function writeSse(res, msgs, onWriteError) {
|
|
@@ -43258,7 +43720,8 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
43258
43720
|
}
|
|
43259
43721
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
43260
43722
|
const withheld = agent.harnessGrantsWithheld();
|
|
43261
|
-
const
|
|
43723
|
+
const provider = agent.signedSearchProvider();
|
|
43724
|
+
const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
|
|
43262
43725
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
43263
43726
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
43264
43727
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -43529,7 +43992,10 @@ function createHttpServer(agent, opts) {
|
|
|
43529
43992
|
if (method === "GET" && path === "/v1/messages") {
|
|
43530
43993
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
43531
43994
|
const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
|
|
43532
|
-
|
|
43995
|
+
const drained = await agent.drain(channel, wait);
|
|
43996
|
+
const withheld = agent.harnessGrantsWithheld();
|
|
43997
|
+
const provider = agent.signedSearchProvider();
|
|
43998
|
+
return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
|
|
43533
43999
|
}
|
|
43534
44000
|
if (method === "GET" && path === "/v1/events") {
|
|
43535
44001
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -43703,6 +44169,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43703
44169
|
consumer: "unknown",
|
|
43704
44170
|
perToolGrants: false,
|
|
43705
44171
|
manifestGrants: false,
|
|
44172
|
+
searchProviders: false,
|
|
43706
44173
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
43707
44174
|
deliveryOpen: helloWaitMs <= 0,
|
|
43708
44175
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -43756,9 +44223,13 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43756
44223
|
continue;
|
|
43757
44224
|
}
|
|
43758
44225
|
const out = withConsumerNotes(
|
|
43759
|
-
|
|
43760
|
-
|
|
43761
|
-
|
|
44226
|
+
withSearchCapability(
|
|
44227
|
+
withManifestCapability(
|
|
44228
|
+
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
44229
|
+
session.manifestGrants
|
|
44230
|
+
),
|
|
44231
|
+
session.searchProviders,
|
|
44232
|
+
agent.signedSearchProvider()
|
|
43762
44233
|
),
|
|
43763
44234
|
batch[i],
|
|
43764
44235
|
session,
|
|
@@ -43794,6 +44265,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43794
44265
|
const cleanup = () => {
|
|
43795
44266
|
if (cleaned) return;
|
|
43796
44267
|
cleaned = true;
|
|
44268
|
+
session.closed = true;
|
|
43797
44269
|
if (helloTimer) clearTimeout(helloTimer);
|
|
43798
44270
|
agent.noteSessionClose();
|
|
43799
44271
|
unsubscribe();
|
|
@@ -43814,6 +44286,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
43814
44286
|
if (session.harness) agent.clearHarnessReport(session.harness);
|
|
43815
44287
|
if (session.perToolGrants) agent.clearPerToolConsumer(session);
|
|
43816
44288
|
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
44289
|
+
agent.clearSearchProvidersSocket(session);
|
|
43817
44290
|
};
|
|
43818
44291
|
current = { sock, cleanup, sweepStale };
|
|
43819
44292
|
sock.on("close", cleanup);
|
|
@@ -43889,6 +44362,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43889
44362
|
return;
|
|
43890
44363
|
}
|
|
43891
44364
|
if (m.type === "hello") {
|
|
44365
|
+
if (session.closed) return;
|
|
43892
44366
|
const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
|
|
43893
44367
|
if (report) {
|
|
43894
44368
|
session.harness = report;
|
|
@@ -43912,6 +44386,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43912
44386
|
agent.clearManifestConsumer(session);
|
|
43913
44387
|
}
|
|
43914
44388
|
agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
|
|
44389
|
+
session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
|
|
44390
|
+
agent.noteSearchProvidersSocket(session, session.searchProviders);
|
|
43915
44391
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
43916
44392
|
agent.noteNotesConsumer();
|
|
43917
44393
|
}
|
|
@@ -43919,7 +44395,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43919
44395
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
43920
44396
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
43921
44397
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
43922
|
-
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
|
|
44398
|
+
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
|
|
43923
44399
|
if (!session.deliveryOpen) {
|
|
43924
44400
|
session.deliveryOpen = true;
|
|
43925
44401
|
flush();
|
|
@@ -43971,10 +44447,13 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
43971
44447
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
43972
44448
|
return;
|
|
43973
44449
|
}
|
|
44450
|
+
const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
|
|
44451
|
+
const callBufferReply = session.consumer === "runtime" && session.perToolGrants === true && session.harnessClaimed !== true && typeof m.in_reply_to === "string" && record !== void 0 && session.deliveryToken.get(m.in_reply_to) !== void 0 && record.channel_id === m.channel_id && !agent.isRuntimeMessageRetired(m.in_reply_to);
|
|
43974
44452
|
const sendOpts = {
|
|
43975
44453
|
agentTurn: isAgentTurn(agent, m),
|
|
43976
44454
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
43977
|
-
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {}
|
|
44455
|
+
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
44456
|
+
...callBufferReply ? { callBufferReply: true } : {}
|
|
43978
44457
|
};
|
|
43979
44458
|
if (m.a2a && typeof m.a2a === "object") {
|
|
43980
44459
|
const a = m.a2a;
|
|
@@ -44145,7 +44624,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
44145
44624
|
var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
44146
44625
|
|
|
44147
44626
|
// src/bin.ts
|
|
44148
|
-
import { readFileSync as
|
|
44627
|
+
import { readFileSync as readFileSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, rmSync as rmSync8, existsSync as existsSync7, realpathSync as realpathSync3, readdirSync as readdirSync4, statSync as statSync8, chmodSync as chmodSync6 } from "node:fs";
|
|
44149
44628
|
import { join as join25, dirname as dirname13, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
|
|
44150
44629
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
44151
44630
|
import { homedir as homedir5, userInfo } from "node:os";
|
|
@@ -44358,7 +44837,8 @@ function installService(input, deps) {
|
|
|
44358
44837
|
deps.writeFile(cfg.unitPath, renderSystemdUnit(cfg));
|
|
44359
44838
|
deps.run("systemctl", ["--user", "daemon-reload"]);
|
|
44360
44839
|
deps.run("loginctl", ["enable-linger", deps.user]);
|
|
44361
|
-
requireOk(deps.run("systemctl", ["--user", "enable",
|
|
44840
|
+
requireOk(deps.run("systemctl", ["--user", "enable", cfg.unitName]), "systemctl --user enable");
|
|
44841
|
+
requireOk(deps.run("systemctl", ["--user", "restart", cfg.unitName]), "systemctl --user restart");
|
|
44362
44842
|
return { label: cfg.unitName, filePath: cfg.unitPath, logHint: `journalctl --user -u ${cfg.unitName} -f`, tokenHint: tokenHint(input) };
|
|
44363
44843
|
}
|
|
44364
44844
|
throw new ServiceUnsupportedError(`Service install isn't supported on ${deps.platform} yet. Run the agent in a terminal, or use a process manager.`);
|
|
@@ -45107,7 +45587,7 @@ function renderStatusTable(rows) {
|
|
|
45107
45587
|
import {
|
|
45108
45588
|
chmodSync as chmodSync5,
|
|
45109
45589
|
mkdirSync as mkdirSync6,
|
|
45110
|
-
readFileSync as
|
|
45590
|
+
readFileSync as readFileSync13,
|
|
45111
45591
|
renameSync as renameSync4,
|
|
45112
45592
|
unlinkSync as unlinkSync2,
|
|
45113
45593
|
writeFileSync as writeFileSync3
|
|
@@ -45119,7 +45599,7 @@ var REAL_FS2 = {
|
|
|
45119
45599
|
chmod: (p, mode) => chmodSync5(p, mode),
|
|
45120
45600
|
writeFile: (p, data, mode) => writeFileSync3(p, data, { mode }),
|
|
45121
45601
|
rename: (a, b) => renameSync4(a, b),
|
|
45122
|
-
readFile: (p) =>
|
|
45602
|
+
readFile: (p) => readFileSync13(p, "utf8"),
|
|
45123
45603
|
unlink: (p) => unlinkSync2(p)
|
|
45124
45604
|
};
|
|
45125
45605
|
function securecomsDir(home) {
|
|
@@ -45168,7 +45648,7 @@ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_F
|
|
|
45168
45648
|
|
|
45169
45649
|
// src/service/instance-lock.ts
|
|
45170
45650
|
import { execFileSync } from "node:child_process";
|
|
45171
|
-
import { closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync7, openSync as openSync4, readFileSync as
|
|
45651
|
+
import { closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync7, openSync as openSync4, readFileSync as readFileSync14, statSync as statSync6, unlinkSync as unlinkSync3, utimesSync, writeSync as writeSync2 } from "node:fs";
|
|
45172
45652
|
import { hostname, uptime as uptime2 } from "node:os";
|
|
45173
45653
|
import { dirname as dirname11 } from "node:path";
|
|
45174
45654
|
var LOCK_PARTIAL_WRITE_GRACE_MS = 1e4;
|
|
@@ -45182,7 +45662,7 @@ function errno(error) {
|
|
|
45182
45662
|
}
|
|
45183
45663
|
function readTrimmed(path) {
|
|
45184
45664
|
try {
|
|
45185
|
-
const v =
|
|
45665
|
+
const v = readFileSync14(path, "utf8").trim();
|
|
45186
45666
|
return v === "" ? void 0 : v;
|
|
45187
45667
|
} catch {
|
|
45188
45668
|
return void 0;
|
|
@@ -45297,7 +45777,7 @@ var InstanceLock = class {
|
|
|
45297
45777
|
if (!this.held) return false;
|
|
45298
45778
|
let current;
|
|
45299
45779
|
try {
|
|
45300
|
-
current =
|
|
45780
|
+
current = readFileSync14(this.path, "utf8");
|
|
45301
45781
|
} catch {
|
|
45302
45782
|
current = void 0;
|
|
45303
45783
|
}
|
|
@@ -45315,7 +45795,7 @@ var InstanceLock = class {
|
|
|
45315
45795
|
let ageMs;
|
|
45316
45796
|
this.env.beforeLockRead?.();
|
|
45317
45797
|
try {
|
|
45318
|
-
raw =
|
|
45798
|
+
raw = readFileSync14(this.path, "utf8");
|
|
45319
45799
|
ageMs = Date.now() - statSync6(this.path).mtimeMs;
|
|
45320
45800
|
} catch (error) {
|
|
45321
45801
|
if (errno(error) === "ENOENT") {
|
|
@@ -45356,7 +45836,7 @@ var InstanceLock = class {
|
|
|
45356
45836
|
const written = this.written;
|
|
45357
45837
|
this.drop();
|
|
45358
45838
|
try {
|
|
45359
|
-
if (written !== void 0 &&
|
|
45839
|
+
if (written !== void 0 && readFileSync14(this.path, "utf8") === written) unlinkSync3(this.path);
|
|
45360
45840
|
} catch {
|
|
45361
45841
|
}
|
|
45362
45842
|
}
|
|
@@ -45370,7 +45850,7 @@ var InstanceLock = class {
|
|
|
45370
45850
|
took() {
|
|
45371
45851
|
this.held = true;
|
|
45372
45852
|
try {
|
|
45373
|
-
this.written =
|
|
45853
|
+
this.written = readFileSync14(this.path, "utf8");
|
|
45374
45854
|
} catch {
|
|
45375
45855
|
this.written = void 0;
|
|
45376
45856
|
}
|
|
@@ -45452,7 +45932,7 @@ var InstanceLock = class {
|
|
|
45452
45932
|
this.env.beforeTakeoverReread?.();
|
|
45453
45933
|
let current;
|
|
45454
45934
|
try {
|
|
45455
|
-
current =
|
|
45935
|
+
current = readFileSync14(this.path, "utf8");
|
|
45456
45936
|
} catch {
|
|
45457
45937
|
current = void 0;
|
|
45458
45938
|
}
|
|
@@ -45466,7 +45946,7 @@ var InstanceLock = class {
|
|
|
45466
45946
|
if (created !== "created") {
|
|
45467
45947
|
if (created === "failed") return "failed";
|
|
45468
45948
|
try {
|
|
45469
|
-
this.holderPid = parseBody(
|
|
45949
|
+
this.holderPid = parseBody(readFileSync14(this.path, "utf8"))?.pid;
|
|
45470
45950
|
} catch {
|
|
45471
45951
|
}
|
|
45472
45952
|
return "held";
|
|
@@ -45492,7 +45972,7 @@ import {
|
|
|
45492
45972
|
fsyncSync as fsyncSync2,
|
|
45493
45973
|
lstatSync as lstatSync8,
|
|
45494
45974
|
openSync as openSync5,
|
|
45495
|
-
readFileSync as
|
|
45975
|
+
readFileSync as readFileSync15,
|
|
45496
45976
|
realpathSync as realpathSync2,
|
|
45497
45977
|
renameSync as renameSync5,
|
|
45498
45978
|
unlinkSync as unlinkSync4,
|
|
@@ -45529,7 +46009,7 @@ function isSymlink(path) {
|
|
|
45529
46009
|
}
|
|
45530
46010
|
}
|
|
45531
46011
|
function readJson(path) {
|
|
45532
|
-
return JSON.parse(
|
|
46012
|
+
return JSON.parse(readFileSync15(path, "utf8"));
|
|
45533
46013
|
}
|
|
45534
46014
|
function publicKeyOf(v) {
|
|
45535
46015
|
const r = parsePinObject(v);
|
|
@@ -45773,7 +46253,7 @@ function realResolveDeps() {
|
|
|
45773
46253
|
}
|
|
45774
46254
|
function realPrepDeps() {
|
|
45775
46255
|
return {
|
|
45776
|
-
readFile: (p) =>
|
|
46256
|
+
readFile: (p) => readFileSync17(p, "utf8"),
|
|
45777
46257
|
writeFileMode: (p, data, mode) => writeFileSync5(p, data, { mode }),
|
|
45778
46258
|
mkdirp: (p, mode) => {
|
|
45779
46259
|
mkdirSync8(p, { recursive: true, mode: mode ?? 493 });
|
|
@@ -45826,7 +46306,7 @@ function realReadFleet() {
|
|
|
45826
46306
|
return [];
|
|
45827
46307
|
}
|
|
45828
46308
|
},
|
|
45829
|
-
readFile: (p) =>
|
|
46309
|
+
readFile: (p) => readFileSync17(p, "utf8")
|
|
45830
46310
|
});
|
|
45831
46311
|
}
|
|
45832
46312
|
function runStatus() {
|
|
@@ -45851,7 +46331,7 @@ function realCopyConnector(srcDir, dstDir, harness) {
|
|
|
45851
46331
|
}
|
|
45852
46332
|
if (name.includes(".test.")) continue;
|
|
45853
46333
|
if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
|
|
45854
|
-
writeFileSync5(join25(dstDir, name),
|
|
46334
|
+
writeFileSync5(join25(dstDir, name), readFileSync17(srcPath));
|
|
45855
46335
|
}
|
|
45856
46336
|
}
|
|
45857
46337
|
function realWriteBinding(dstDir, identityId) {
|
|
@@ -45860,7 +46340,7 @@ function realWriteBinding(dstDir, identityId) {
|
|
|
45860
46340
|
}
|
|
45861
46341
|
function realReadBinding(dstDir) {
|
|
45862
46342
|
try {
|
|
45863
|
-
const parsed = JSON.parse(
|
|
46343
|
+
const parsed = JSON.parse(readFileSync17(join25(dstDir, "securecoms.binding.json"), "utf8"));
|
|
45864
46344
|
return parsed.identity_id ?? null;
|
|
45865
46345
|
} catch {
|
|
45866
46346
|
return null;
|
|
@@ -45878,7 +46358,7 @@ function realStatDir(p) {
|
|
|
45878
46358
|
}
|
|
45879
46359
|
function realReadHermesConfig(p) {
|
|
45880
46360
|
try {
|
|
45881
|
-
return
|
|
46361
|
+
return readFileSync17(p, "utf8");
|
|
45882
46362
|
} catch {
|
|
45883
46363
|
return null;
|
|
45884
46364
|
}
|
|
@@ -46268,7 +46748,7 @@ async function runMode(deps) {
|
|
|
46268
46748
|
served = await deps.serve(agent, {
|
|
46269
46749
|
token,
|
|
46270
46750
|
allowOrigins: daemon.allowOrigins,
|
|
46271
|
-
readTls: () => daemon.tls ? { cert:
|
|
46751
|
+
readTls: () => daemon.tls ? { cert: readFileSync17(daemon.tlsCert, "utf8"), key: readFileSync17(daemon.tlsKey, "utf8") } : void 0
|
|
46272
46752
|
});
|
|
46273
46753
|
} catch (e) {
|
|
46274
46754
|
if (!(e instanceof SessionSupersededError)) throw e;
|