@securecoms/agent-bridge 0.33.1 → 0.34.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 +31 -0
- package/dist/bin.js +301 -76
- package/dist/bridge-agent.d.ts +37 -6
- package/dist/consumer-grants.d.ts +9 -0
- package/dist/errors.d.ts +1 -1
- package/dist/grants/binding.d.ts +18 -5
- package/dist/http-server.d.ts +10 -36
- package/dist/index.js +301 -76
- package/dist/reply-credit.d.ts +22 -0
- package/dist/reply-intent.d.ts +6 -1
- package/dist/service/runtime-config-dir.d.ts +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -18585,6 +18585,8 @@ function isEpochConflict(e) {
|
|
|
18585
18585
|
var LABEL = "agentvault-connect-v1";
|
|
18586
18586
|
var DEFAULT_STUCK_THRESHOLD = 2;
|
|
18587
18587
|
var DEFAULT_QUARANTINE_THRESHOLD = 8;
|
|
18588
|
+
var KEPT_WELCOME_COMMIT_DEFER_MS = 24 * 60 * 60 * 1e3;
|
|
18589
|
+
var KEPT_WELCOME_COMMIT_DEFER_MAX_ROWS = 32;
|
|
18588
18590
|
var DELIVERY_PULL_PAGE = 100;
|
|
18589
18591
|
var DELIVERY_MAX_RETRIES = 5;
|
|
18590
18592
|
var DrainPageFullError = class extends Error {
|
|
@@ -18789,6 +18791,15 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
18789
18791
|
* {@link errorStats}; mutated only through {@link recordDecryptError} /
|
|
18790
18792
|
* {@link recordDecryptSuccess}.
|
|
18791
18793
|
*/
|
|
18794
|
+
/** {@link KEPT_WELCOME_COMMIT_DEFER_MS}: channel -> when its first commit was
|
|
18795
|
+
* deferred behind the kept Welcome `welcomeId`. Restarted for a different
|
|
18796
|
+
* Welcome; cleared when the channel decrypts, or when a pass that saw the
|
|
18797
|
+
* whole queue kept no Welcome for it (fix round 1, L2: the Welcome
|
|
18798
|
+
* resolved, so a later re-Welcome gets a fresh, bounded window). An entry
|
|
18799
|
+
* past its bound stays (expired) until then, so the bound cannot restart
|
|
18800
|
+
* itself pass after pass. In memory: a reload restarts it, and the
|
|
18801
|
+
* per-pass row cap is what bounds the page cost across reloads. */
|
|
18802
|
+
keptWelcomeDeferSince = /* @__PURE__ */ new Map();
|
|
18792
18803
|
errors = /* @__PURE__ */ new Map();
|
|
18793
18804
|
stuckThreshold;
|
|
18794
18805
|
/** Consecutive-error threshold at which a wedged channel's row is quarantined
|
|
@@ -19007,8 +19018,33 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
19007
19018
|
* channel resumes normal processing and can quarantine again later if it
|
|
19008
19019
|
* re-wedges (the onWedged guard is reset so the hook can fire again).
|
|
19009
19020
|
*/
|
|
19021
|
+
/** See {@link MlsGroupKeyProviderOpts.deferCommitsWhileWelcomePending}. The
|
|
19022
|
+
* caller has already established that this pass KEPT a no-state Welcome
|
|
19023
|
+
* for `channelId`. True: defer the commit row. Never throws. */
|
|
19024
|
+
deferKeptWelcomeCommit(channelId, welcomeId) {
|
|
19025
|
+
const pred = this.o.deferCommitsWhileWelcomePending;
|
|
19026
|
+
if (!pred)
|
|
19027
|
+
return false;
|
|
19028
|
+
let boundMs = 0;
|
|
19029
|
+
try {
|
|
19030
|
+
const v = pred(channelId);
|
|
19031
|
+
boundMs = v === true ? KEPT_WELCOME_COMMIT_DEFER_MS : typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.min(v, KEPT_WELCOME_COMMIT_DEFER_MS) : 0;
|
|
19032
|
+
} catch {
|
|
19033
|
+
boundMs = 0;
|
|
19034
|
+
}
|
|
19035
|
+
if (boundMs <= 0)
|
|
19036
|
+
return false;
|
|
19037
|
+
const now = this.nowMs();
|
|
19038
|
+
const entry = this.keptWelcomeDeferSince.get(channelId);
|
|
19039
|
+
if (entry === void 0 || entry.welcomeId !== welcomeId) {
|
|
19040
|
+
this.keptWelcomeDeferSince.set(channelId, { since: now, welcomeId });
|
|
19041
|
+
return true;
|
|
19042
|
+
}
|
|
19043
|
+
return now - entry.since < boundMs;
|
|
19044
|
+
}
|
|
19010
19045
|
recordDecryptSuccess(channelId) {
|
|
19011
19046
|
this.lastDecryptAt.set(channelId, Date.now());
|
|
19047
|
+
this.keptWelcomeDeferSince.delete(channelId);
|
|
19012
19048
|
const e = this.errors.get(channelId);
|
|
19013
19049
|
if (!e)
|
|
19014
19050
|
return;
|
|
@@ -19404,6 +19440,12 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
19404
19440
|
* server's REJOIN_THRESHOLD, there was nothing to apply, or a commit would
|
|
19405
19441
|
* not apply (which means the state is genuinely FORKED, not merely behind,
|
|
19406
19442
|
* and replay cannot fix a fork).
|
|
19443
|
+
*
|
|
19444
|
+
* THROWS (retryable, never `false`, #1290): CustodyPersistError when the
|
|
19445
|
+
* channel is under the custody hold (nothing applied), and the C2 classes
|
|
19446
|
+
* (PendingCommitUnresolvedError from the guarded MAIN write) after evicting
|
|
19447
|
+
* the advanced cache, so the persisted state is unchanged. A /sync request
|
|
19448
|
+
* that fails also throws, before anything is applied.
|
|
19407
19449
|
*/
|
|
19408
19450
|
async resyncByReplay(channelId, groupId) {
|
|
19409
19451
|
if (!await this.hasPersistedChannelState(channelId))
|
|
@@ -19423,12 +19465,26 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
19423
19465
|
let applied = 0;
|
|
19424
19466
|
try {
|
|
19425
19467
|
await this.locks.run(channelId, async () => {
|
|
19468
|
+
if (await this.custodyHeld(channelId))
|
|
19469
|
+
throw new CustodyPersistError(channelId);
|
|
19426
19470
|
const mgr = await this.manager(channelId);
|
|
19427
19471
|
try {
|
|
19428
19472
|
for (const c of commits) {
|
|
19429
19473
|
if (c.epoch <= Number(mgr.epoch))
|
|
19430
19474
|
continue;
|
|
19431
|
-
|
|
19475
|
+
const commitBytes = hexToBytes22(c.payload);
|
|
19476
|
+
try {
|
|
19477
|
+
await mgr.decrypt(commitBytes);
|
|
19478
|
+
} catch (e) {
|
|
19479
|
+
if (await probeOwnRemoval(mgr, commitBytes) !== null) {
|
|
19480
|
+
this.noteOwnLeafRemoved(channelId, {
|
|
19481
|
+
group_id: groupId,
|
|
19482
|
+
epoch: c.epoch,
|
|
19483
|
+
message_id: c.id
|
|
19484
|
+
});
|
|
19485
|
+
}
|
|
19486
|
+
throw e;
|
|
19487
|
+
}
|
|
19432
19488
|
applied++;
|
|
19433
19489
|
}
|
|
19434
19490
|
if (applied > 0)
|
|
@@ -19440,6 +19496,10 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
19440
19496
|
}
|
|
19441
19497
|
});
|
|
19442
19498
|
} catch (e) {
|
|
19499
|
+
if (isSdkError(e, "CustodyPersistError") || e instanceof CustodyPersistError || e instanceof PendingCommitUnresolvedError || e instanceof GroupSupersededError || e instanceof AttestedOrphanRefusedError) {
|
|
19500
|
+
writeServiceLog("warn", "resync.replay_held", { applied }, e);
|
|
19501
|
+
throw e;
|
|
19502
|
+
}
|
|
19443
19503
|
writeServiceLog("warn", "resync.replay_commit_failed", { applied }, e);
|
|
19444
19504
|
return false;
|
|
19445
19505
|
}
|
|
@@ -22140,6 +22200,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
22140
22200
|
out.push({
|
|
22141
22201
|
queueId: `replay-${m.id}`,
|
|
22142
22202
|
messageId: envelope.message_id,
|
|
22203
|
+
// #1246 C-1: the group-messages row id is the same mls_messages key.
|
|
22204
|
+
...typeof m.id === "string" && m.id ? { serverMessageId: m.id } : {},
|
|
22143
22205
|
channelId,
|
|
22144
22206
|
plaintext,
|
|
22145
22207
|
envelope,
|
|
@@ -23040,6 +23102,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23040
23102
|
const heldBundleRefs = () => heldBundleRefsMemo ??= this.readHeldBundleRefs();
|
|
23041
23103
|
const w2Acked = /* @__PURE__ */ new Map();
|
|
23042
23104
|
const w2Kept = /* @__PURE__ */ new Set();
|
|
23105
|
+
const keptWelcomeId = /* @__PURE__ */ new Map();
|
|
23106
|
+
const deferredThisPass = /* @__PURE__ */ new Map();
|
|
23043
23107
|
const unpinnedRefusals = /* @__PURE__ */ new Map();
|
|
23044
23108
|
const welcomes = rows.filter((r) => r.message_type === "welcome");
|
|
23045
23109
|
const nonWelcomes = rows.filter((r) => r.message_type !== "welcome");
|
|
@@ -23198,6 +23262,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23198
23262
|
writeServiceLog("warn", "welcome.unopenable");
|
|
23199
23263
|
} else {
|
|
23200
23264
|
w2Kept.add(wcid);
|
|
23265
|
+
if (!keptWelcomeId.has(wcid))
|
|
23266
|
+
keptWelcomeId.set(wcid, row.queue_id);
|
|
23201
23267
|
if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
|
|
23202
23268
|
writeServiceLog("debug", "delivery.welcome_no_local_state");
|
|
23203
23269
|
}
|
|
@@ -23208,6 +23274,10 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23208
23274
|
const cid = row.connect_channel_id;
|
|
23209
23275
|
if (!cid)
|
|
23210
23276
|
continue;
|
|
23277
|
+
if (w2Kept.has(cid) && (deferredThisPass.get(cid) ?? 0) < KEPT_WELCOME_COMMIT_DEFER_MAX_ROWS && this.deferKeptWelcomeCommit(cid, keptWelcomeId.get(cid) ?? "")) {
|
|
23278
|
+
deferredThisPass.set(cid, (deferredThisPass.get(cid) ?? 0) + 1);
|
|
23279
|
+
continue;
|
|
23280
|
+
}
|
|
23211
23281
|
let advancedEpoch;
|
|
23212
23282
|
let custodyHeld = false;
|
|
23213
23283
|
await this.locks.run(cid, async () => {
|
|
@@ -23364,6 +23434,12 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23364
23434
|
this.leftStreak.set(cid, { pulls: 1, since: pullNow });
|
|
23365
23435
|
this.scheduleAutoResolve(cid);
|
|
23366
23436
|
}
|
|
23437
|
+
if (rows.length < DELIVERY_PULL_PAGE) {
|
|
23438
|
+
for (const cid of [...this.keptWelcomeDeferSince.keys()]) {
|
|
23439
|
+
if (!w2Kept.has(cid))
|
|
23440
|
+
this.keptWelcomeDeferSince.delete(cid);
|
|
23441
|
+
}
|
|
23442
|
+
}
|
|
23367
23443
|
for (const [cid, gid] of w2Acked) {
|
|
23368
23444
|
if (w2Kept.has(cid))
|
|
23369
23445
|
continue;
|
|
@@ -23643,7 +23719,9 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23643
23719
|
voiceDefaultResponder: row.voice_default_responder ?? void 0,
|
|
23644
23720
|
...typeof row.identity_signature === "string" && row.identity_signature ? { identitySignature: row.identity_signature } : {},
|
|
23645
23721
|
payloadHex: row.payload,
|
|
23646
|
-
openedAt
|
|
23722
|
+
openedAt,
|
|
23723
|
+
// #1246 C-1: the server row id, beside the sender-chosen envelope id.
|
|
23724
|
+
...typeof row.message_id === "string" && row.message_id ? { serverMessageId: row.message_id } : {}
|
|
23647
23725
|
};
|
|
23648
23726
|
const custodiedAt = this.nowMs();
|
|
23649
23727
|
this.custody.set(row.queue_id, entry);
|
|
@@ -26165,6 +26243,11 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
26165
26243
|
// Owner-signed tool grants (spec §7.0): like Plan 0's backend, each row carries
|
|
26166
26244
|
// the envelope's `identity_proof.signature` as `identity_signature` beside the
|
|
26167
26245
|
// unsigned canonical payload. `omitSignature` models a pre-Plan-0 row.
|
|
26246
|
+
/** `rowMessageId` (#1246 C-1): the server row id to deliver, when it is NOT
|
|
26247
|
+
* the envelope's id. The REST send path keys the row by the envelope id
|
|
26248
|
+
* (connect_messages.py, `MLSMessage(id=message_id)`); the WS `message_mls`
|
|
26249
|
+
* path stores through `store_mls_message`, whose row id is server-generated,
|
|
26250
|
+
* so there the two differ and the envelope id is the sender's free choice. */
|
|
26168
26251
|
async ingestApplication(envelope, opts) {
|
|
26169
26252
|
const channelId = envelope.channel_id;
|
|
26170
26253
|
const canonical = await canonicalEnvelopeBytes(envelope);
|
|
@@ -26176,7 +26259,7 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
26176
26259
|
continue;
|
|
26177
26260
|
this.q(dev).push({
|
|
26178
26261
|
queue_id: "q-" + this._seq++,
|
|
26179
|
-
message_id: envelope.message_id,
|
|
26262
|
+
message_id: opts?.rowMessageId ?? envelope.message_id,
|
|
26180
26263
|
group_id: groupId,
|
|
26181
26264
|
message_type: "application",
|
|
26182
26265
|
sender_device_id: senderDeviceId ?? "",
|
|
@@ -26560,6 +26643,7 @@ function openSchema(db) {
|
|
|
26560
26643
|
ensureRuntimeInboxDeliveryAttempts(db);
|
|
26561
26644
|
ensureRuntimeReplyIntent(db);
|
|
26562
26645
|
ensureRuntimeReplyIntentEnvelopeGroup(db);
|
|
26646
|
+
ensureRuntimeReplyIntentCredit(db);
|
|
26563
26647
|
ensureGroupLink(db);
|
|
26564
26648
|
ensureMlsCustody(db);
|
|
26565
26649
|
ensureToolGrantTables(db);
|
|
@@ -26640,6 +26724,19 @@ function ensureRuntimeReplyIntentEnvelopeGroup(db) {
|
|
|
26640
26724
|
throw e;
|
|
26641
26725
|
}
|
|
26642
26726
|
}
|
|
26727
|
+
function ensureRuntimeReplyIntentCredit(db) {
|
|
26728
|
+
const hasColumn = () => db.prepare("PRAGMA table_info(runtime_reply_intent)").all().some((c) => c.name === "reply_credit");
|
|
26729
|
+
const cols = db.prepare("PRAGMA table_info(runtime_reply_intent)").all();
|
|
26730
|
+
if (cols.length === 0 || cols.some((c) => c.name === "reply_credit"))
|
|
26731
|
+
return;
|
|
26732
|
+
try {
|
|
26733
|
+
db.exec("ALTER TABLE runtime_reply_intent ADD COLUMN reply_credit TEXT");
|
|
26734
|
+
} catch (e) {
|
|
26735
|
+
if (hasColumn())
|
|
26736
|
+
return;
|
|
26737
|
+
throw e;
|
|
26738
|
+
}
|
|
26739
|
+
}
|
|
26643
26740
|
var RECALL_STATE_COLUMNS = [
|
|
26644
26741
|
["recall_enabled", "INTEGER NOT NULL DEFAULT 0"],
|
|
26645
26742
|
["recall_mode_epoch", "INTEGER NOT NULL DEFAULT 0"],
|
|
@@ -27691,8 +27788,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27691
27788
|
this.db.prepare("DELETE FROM runtime_reply_intent WHERE identity_id=? AND updated_at < ?").run(this.id, now - RUNTIME_REPLY_INTENT_RETENTION_MS);
|
|
27692
27789
|
const info = this.db.prepare(`INSERT OR IGNORE INTO runtime_reply_intent
|
|
27693
27790
|
(identity_id, scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27694
|
-
sealed_body, sealed_envelope, state, created_at, updated_at)
|
|
27695
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now);
|
|
27791
|
+
sealed_body, sealed_envelope, state, created_at, updated_at, reply_credit)
|
|
27792
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now, input.replyCredit ?? null);
|
|
27696
27793
|
const count = this.db.prepare("SELECT COUNT(*) AS n FROM runtime_reply_intent WHERE identity_id=?").get(this.id);
|
|
27697
27794
|
if (count.n > RUNTIME_REPLY_INTENT_MAX) {
|
|
27698
27795
|
this.db.prepare(`DELETE FROM runtime_reply_intent
|
|
@@ -27702,7 +27799,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27702
27799
|
)`).run(this.id, this.id, RUNTIME_REPLY_INTENT_MAX);
|
|
27703
27800
|
}
|
|
27704
27801
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27705
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
|
|
27802
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
27706
27803
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, input.scope, input.part);
|
|
27707
27804
|
if (!row)
|
|
27708
27805
|
throw new Error("reply intent vanished after claim");
|
|
@@ -27711,7 +27808,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27711
27808
|
/** SEC-06 #18: every intent answering `inReplyTo`, in claim order. */
|
|
27712
27809
|
async listReplyIntents(inReplyTo) {
|
|
27713
27810
|
const rows = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27714
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
|
|
27811
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
27715
27812
|
FROM runtime_reply_intent WHERE identity_id=? AND in_reply_to=?
|
|
27716
27813
|
ORDER BY created_at ASC, rowid ASC`).all(this.id, inReplyTo);
|
|
27717
27814
|
const out = [];
|
|
@@ -27722,7 +27819,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27722
27819
|
/** SEC-06 #18: re-read one intent by its key (null when absent). */
|
|
27723
27820
|
async getReplyIntent(scope, part) {
|
|
27724
27821
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27725
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
|
|
27822
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
27726
27823
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, scope, part);
|
|
27727
27824
|
return row ? this.openReplyIntent(row) : null;
|
|
27728
27825
|
}
|
|
@@ -27796,7 +27893,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27796
27893
|
envelope,
|
|
27797
27894
|
envelopeGroupId: envelopeGroupFor(envelope, await openOrNull(row.sealed_envelope_group)),
|
|
27798
27895
|
state: row.state === "sent" ? "sent" : row.state === "skipped" ? "skipped" : "pending",
|
|
27799
|
-
createdAt: Number(row.created_at)
|
|
27896
|
+
createdAt: Number(row.created_at),
|
|
27897
|
+
replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null
|
|
27800
27898
|
};
|
|
27801
27899
|
}
|
|
27802
27900
|
async recordMemory(input) {
|
|
@@ -29566,8 +29664,8 @@ function decodeReplyBody(bytes) {
|
|
|
29566
29664
|
if (b?.kind === "a2a" && b.a2a && typeof b.a2a === "object") return b;
|
|
29567
29665
|
throw new Error("reply intent body is malformed");
|
|
29568
29666
|
}
|
|
29569
|
-
function replyBodyMac(key, replyId, channelId, body) {
|
|
29570
|
-
return createHmac("sha256", key).update(JSON.stringify(["reply-body", replyId, channelId])).update("\0").update(body).digest("hex");
|
|
29667
|
+
function replyBodyMac(key, replyId, channelId, body, credit) {
|
|
29668
|
+
return createHmac("sha256", key).update(JSON.stringify(credit === void 0 ? ["reply-body", replyId, channelId] : ["reply-body", replyId, channelId, credit])).update("\0").update(body).digest("hex");
|
|
29571
29669
|
}
|
|
29572
29670
|
function partOrdinal(part) {
|
|
29573
29671
|
const m = /^p:(\d+)$/.exec(part);
|
|
@@ -34789,6 +34887,8 @@ function checkConfiguredPaths(skillsJson, statFn = (p) => {
|
|
|
34789
34887
|
}
|
|
34790
34888
|
const search = skillsJson.search;
|
|
34791
34889
|
if (search?.keyFile) paths.push(search.keyFile);
|
|
34890
|
+
const weather = skillsJson.weather;
|
|
34891
|
+
if (weather?.keyFile) paths.push(weather.keyFile);
|
|
34792
34892
|
return paths.map((path) => {
|
|
34793
34893
|
let st;
|
|
34794
34894
|
try {
|
|
@@ -34814,7 +34914,7 @@ function parseSecretPathsReport(raw) {
|
|
|
34814
34914
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
34815
34915
|
const r = raw;
|
|
34816
34916
|
const f = r.outside_by_field;
|
|
34817
|
-
if (r.mode !== "report" && r.mode !== "enforce" || !isCount(r.inside) || !isCount(r.allowlisted) || !isCount(r.outside) || !f || typeof f !== "object" || Array.isArray(f) || !isCount(f.headers_file) || !isCount(f.postgres_url_file) || !isCount(f.search_key_file) || f.sqlite !== void 0 && !isCount(f.sqlite)) return void 0;
|
|
34917
|
+
if (r.mode !== "report" && r.mode !== "enforce" || !isCount(r.inside) || !isCount(r.allowlisted) || !isCount(r.outside) || !f || typeof f !== "object" || Array.isArray(f) || !isCount(f.headers_file) || !isCount(f.postgres_url_file) || !isCount(f.search_key_file) || f.sqlite !== void 0 && !isCount(f.sqlite) || f.weather_key_file !== void 0 && !isCount(f.weather_key_file)) return void 0;
|
|
34818
34918
|
return {
|
|
34819
34919
|
mode: r.mode,
|
|
34820
34920
|
inside: r.inside,
|
|
@@ -34824,7 +34924,8 @@ function parseSecretPathsReport(raw) {
|
|
|
34824
34924
|
headers_file: f.headers_file,
|
|
34825
34925
|
postgres_url_file: f.postgres_url_file,
|
|
34826
34926
|
search_key_file: f.search_key_file,
|
|
34827
|
-
...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {}
|
|
34927
|
+
...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {},
|
|
34928
|
+
...f.weather_key_file !== void 0 ? { weather_key_file: f.weather_key_file } : {}
|
|
34828
34929
|
}
|
|
34829
34930
|
};
|
|
34830
34931
|
}
|
|
@@ -34904,6 +35005,7 @@ function readLocalReport(home, identityId) {
|
|
|
34904
35005
|
has_search_key: hasSearchKey,
|
|
34905
35006
|
has_llm_key: hasLlmKey,
|
|
34906
35007
|
has_extract_provider: doc.has_extract_provider === true,
|
|
35008
|
+
has_weather_key: doc.has_weather_key === true,
|
|
34907
35009
|
...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
|
|
34908
35010
|
legacy_memory: localLegacySources(doc).map(({ path: _path, content: _content, ...source }) => source),
|
|
34909
35011
|
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
@@ -35005,6 +35107,19 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
35005
35107
|
return removed;
|
|
35006
35108
|
}
|
|
35007
35109
|
|
|
35110
|
+
// src/reply-credit.ts
|
|
35111
|
+
var REPLY_CREDIT_FEATURE = "reply_credit_v1";
|
|
35112
|
+
var WEATHER_CREDIT_LINE = "Weather data: WeatherAPI.com. For general information only.";
|
|
35113
|
+
var WEATHER_CREDIT_SUFFIX = `
|
|
35114
|
+
|
|
35115
|
+
${WEATHER_CREDIT_LINE}`;
|
|
35116
|
+
function isReplyCredit(v) {
|
|
35117
|
+
return v === "weatherapi";
|
|
35118
|
+
}
|
|
35119
|
+
function stripCreditSuffix(text) {
|
|
35120
|
+
return text.endsWith(WEATHER_CREDIT_SUFFIX) ? text.slice(0, text.length - WEATHER_CREDIT_SUFFIX.length) : text;
|
|
35121
|
+
}
|
|
35122
|
+
|
|
35008
35123
|
// src/grants/binding.ts
|
|
35009
35124
|
import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
35010
35125
|
|
|
@@ -35093,9 +35208,10 @@ function loadOwnerGrantPin(opts) {
|
|
|
35093
35208
|
// src/grants/manifest.ts
|
|
35094
35209
|
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
35095
35210
|
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
35096
|
-
owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
|
|
35211
|
+
owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query", "weather_forecast", "currency_convert"]),
|
|
35097
35212
|
// web_extract: owner DM only, never team_room (spec §5.4, D5)
|
|
35098
|
-
|
|
35213
|
+
// Weather/currency spec §6.3 (U5): both utility tools in both audiences, each held behind utility_tools_v1 (TOOL_FEATURE).
|
|
35214
|
+
team_room: Object.freeze(["web_search", "weather_forecast", "currency_convert"])
|
|
35099
35215
|
});
|
|
35100
35216
|
function manifestSignedBytes(manifest) {
|
|
35101
35217
|
const domain = new TextEncoder().encode(TOOL_GRANTS_DOMAIN);
|
|
@@ -35419,20 +35535,30 @@ function bindTargets(raw, signed, equals) {
|
|
|
35419
35535
|
}
|
|
35420
35536
|
return { written, mismatches };
|
|
35421
35537
|
}
|
|
35422
|
-
|
|
35423
|
-
|
|
35538
|
+
var NO_FEATURES = Object.freeze(/* @__PURE__ */ new Set());
|
|
35539
|
+
var TOOL_FEATURE = Object.freeze({
|
|
35540
|
+
web_extract: "search_providers_v2",
|
|
35541
|
+
weather_forecast: "utility_tools_v1",
|
|
35542
|
+
currency_convert: "utility_tools_v1"
|
|
35543
|
+
});
|
|
35544
|
+
function toolAllowedByFeatures(tool, features) {
|
|
35545
|
+
if (!Object.hasOwn(TOOL_FEATURE, tool)) return true;
|
|
35546
|
+
return features.has(TOOL_FEATURE[tool]);
|
|
35547
|
+
}
|
|
35548
|
+
function signedManifestTools(manifest, audience, features) {
|
|
35549
|
+
return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && toolAllowedByFeatures(t, features));
|
|
35424
35550
|
}
|
|
35425
|
-
function manifestPolicyFor(manifest, switches,
|
|
35551
|
+
function manifestPolicyFor(manifest, switches, features) {
|
|
35426
35552
|
return {
|
|
35427
35553
|
v: 1,
|
|
35428
35554
|
manifest_tools: [.../* @__PURE__ */ new Set([
|
|
35429
|
-
...switches.ownerDm ? signedManifestTools(manifest, "owner_dm",
|
|
35430
|
-
...switches.room ? signedManifestTools(manifest, "team_room",
|
|
35555
|
+
...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", features) : [],
|
|
35556
|
+
...switches.room ? signedManifestTools(manifest, "team_room", features) : []
|
|
35431
35557
|
])].sort(),
|
|
35432
35558
|
secret_path_mode: "enforce"
|
|
35433
35559
|
};
|
|
35434
35560
|
}
|
|
35435
|
-
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true },
|
|
35561
|
+
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, features = NO_FEATURES) {
|
|
35436
35562
|
const mismatches = /* @__PURE__ */ new Set();
|
|
35437
35563
|
const cfg = manifest.config;
|
|
35438
35564
|
const out = {};
|
|
@@ -35461,7 +35587,8 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35461
35587
|
for (const name of Object.keys(files)) {
|
|
35462
35588
|
if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
|
|
35463
35589
|
}
|
|
35464
|
-
const ownerDmTools = signedManifestTools(manifest, "owner_dm",
|
|
35590
|
+
const ownerDmTools = signedManifestTools(manifest, "owner_dm", features);
|
|
35591
|
+
const ownerDmSearchTools = ownerDmTools.filter((t) => t === "web_search" || t === "web_extract");
|
|
35465
35592
|
const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
|
|
35466
35593
|
const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
|
|
35467
35594
|
const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
|
|
@@ -35482,17 +35609,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35482
35609
|
const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
|
|
35483
35610
|
searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
|
|
35484
35611
|
}
|
|
35612
|
+
const pw = isObj2(sj?.weather) ? sj.weather : void 0;
|
|
35613
|
+
const weatherBlock = typeof pw?.keyFile === "string" && pw.keyFile !== "" ? { keyFile: pw.keyFile } : void 0;
|
|
35485
35614
|
const skillsDoc = {
|
|
35486
35615
|
endpoints: eps.written,
|
|
35487
35616
|
databases: dbs.written,
|
|
35488
35617
|
flags: [...cfg.flags],
|
|
35489
35618
|
...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
|
|
35490
35619
|
...searchBlock ? { search: searchBlock } : {},
|
|
35620
|
+
...weatherBlock ? { weather: weatherBlock } : {},
|
|
35491
35621
|
// D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
|
|
35492
35622
|
search_signed: cfg.search !== void 0,
|
|
35493
35623
|
// D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
|
|
35494
35624
|
// otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
|
|
35495
|
-
...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm &&
|
|
35625
|
+
...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmSearchTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
|
|
35496
35626
|
};
|
|
35497
35627
|
out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
|
|
35498
35628
|
const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
|
|
@@ -35523,7 +35653,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35523
35653
|
mismatches.add("legacy_assign");
|
|
35524
35654
|
legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
|
|
35525
35655
|
}
|
|
35526
|
-
const policy = manifestPolicyFor(manifest, switches,
|
|
35656
|
+
const policy = manifestPolicyFor(manifest, switches, features);
|
|
35527
35657
|
return {
|
|
35528
35658
|
mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
|
|
35529
35659
|
targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
|
|
@@ -35666,7 +35796,11 @@ var TOOL_ROOM_CLASSES = Object.freeze({
|
|
|
35666
35796
|
// D5: spends the owner's key and sends URLs out
|
|
35667
35797
|
fetch_url: Object.freeze(["egress"]),
|
|
35668
35798
|
sql_query: Object.freeze(["owner_credential"]),
|
|
35669
|
-
http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
|
|
35799
|
+
http_request: Object.freeze(["owner_credential", "egress", "side_effect"]),
|
|
35800
|
+
// Weather/currency (plan audit r1; supersedes spec §8's values): both send room words to an outside host, and weather
|
|
35801
|
+
// spends the owner's key.
|
|
35802
|
+
weather_forecast: Object.freeze(["owner_credential", "egress"]),
|
|
35803
|
+
currency_convert: Object.freeze(["egress"])
|
|
35670
35804
|
});
|
|
35671
35805
|
var LIVE_MEMBER_STATUSES = /* @__PURE__ */ new Set(["active", "add_pending"]);
|
|
35672
35806
|
function isLiveRow(r) {
|
|
@@ -37480,7 +37614,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37480
37614
|
}
|
|
37481
37615
|
let bound;
|
|
37482
37616
|
try {
|
|
37483
|
-
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.
|
|
37617
|
+
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.capabilityFeatures());
|
|
37484
37618
|
} catch (e) {
|
|
37485
37619
|
bound = null;
|
|
37486
37620
|
return this.bindFailed(manifest, payload.memory, e);
|
|
@@ -37501,7 +37635,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37501
37635
|
try {
|
|
37502
37636
|
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
|
|
37503
37637
|
if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
|
|
37504
|
-
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.
|
|
37638
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.capabilityFeatures()));
|
|
37505
37639
|
} catch (e) {
|
|
37506
37640
|
this.manifestBinding = null;
|
|
37507
37641
|
throw e;
|
|
@@ -37555,6 +37689,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37555
37689
|
async postRuntimeConfigReport(base, version, skillsJson) {
|
|
37556
37690
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
37557
37691
|
const reportedSearchProviders = this.searchProvidersCapable();
|
|
37692
|
+
const reportedUtilityTools = this.utilityToolsCapable();
|
|
37558
37693
|
const report = {
|
|
37559
37694
|
applied_version: version,
|
|
37560
37695
|
paths: checkConfiguredPaths(skillsJson),
|
|
@@ -37572,6 +37707,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37572
37707
|
// Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
|
|
37573
37708
|
// the bridge withholds web_extract from the runtime.
|
|
37574
37709
|
has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
|
|
37710
|
+
// Weather/currency L-1, §8 item 5: AND the current utility_tools_v1, so the web never shows a usable weather key
|
|
37711
|
+
// while the bridge withholds weather_forecast from the runtime.
|
|
37712
|
+
has_weather_key: (local?.has_weather_key ?? false) && reportedUtilityTools,
|
|
37575
37713
|
// D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
|
|
37576
37714
|
...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
|
|
37577
37715
|
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
@@ -37619,7 +37757,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37619
37757
|
// D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
|
|
37620
37758
|
search_providers_v2: reportedSearchProviders,
|
|
37621
37759
|
// D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
|
|
37622
|
-
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
|
|
37760
|
+
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders),
|
|
37761
|
+
// Weather/currency §8 item 5: a separate key, false with no runtime attached.
|
|
37762
|
+
utility_tools_v1: reportedUtilityTools
|
|
37623
37763
|
}
|
|
37624
37764
|
};
|
|
37625
37765
|
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
@@ -37641,6 +37781,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37641
37781
|
}
|
|
37642
37782
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
37643
37783
|
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
37784
|
+
this.lastReportedUtilityTools = reportedUtilityTools;
|
|
37644
37785
|
if (version) writeServiceLog2("info", "runtime_config.applied", { version });
|
|
37645
37786
|
else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
|
|
37646
37787
|
}
|
|
@@ -37710,7 +37851,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37710
37851
|
this.startedAt = Date.now();
|
|
37711
37852
|
await this.restoreRuntimeInbox();
|
|
37712
37853
|
await this.loadToolGrantState();
|
|
37713
|
-
this.recomputeManifestPolicy(
|
|
37854
|
+
this.recomputeManifestPolicy(NO_FEATURES);
|
|
37714
37855
|
try {
|
|
37715
37856
|
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
37716
37857
|
} catch (e) {
|
|
@@ -38693,13 +38834,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38693
38834
|
* absent, because the id is derived.
|
|
38694
38835
|
*/
|
|
38695
38836
|
async send(channelId, text, messageId, opts = {}) {
|
|
38696
|
-
const body = { kind: "text", text };
|
|
38837
|
+
const body = { kind: "text", text: opts.credit !== void 0 ? text + WEATHER_CREDIT_SUFFIX : text };
|
|
38697
38838
|
if (this.isIdempotentSend(messageId, opts)) return this.trackReplyWork(this.sendIdempotent(channelId, body, opts));
|
|
38698
38839
|
const id = resolveMessageId(messageId);
|
|
38699
38840
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
|
|
38700
38841
|
const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
|
|
38701
38842
|
await this.publishBody(channelId, body, id);
|
|
38702
|
-
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
|
|
38843
|
+
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture, text);
|
|
38703
38844
|
return { message_id: id, status: "sent" };
|
|
38704
38845
|
}
|
|
38705
38846
|
/** PR #716 review MEDIUM-5: register reply work so close() can wait for it. */
|
|
@@ -38718,7 +38859,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38718
38859
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
|
|
38719
38860
|
const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
|
|
38720
38861
|
await this.publishBody(channelId, body, id);
|
|
38721
|
-
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
|
|
38862
|
+
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture, null);
|
|
38722
38863
|
return { message_id: id, status: "sent" };
|
|
38723
38864
|
}
|
|
38724
38865
|
/**
|
|
@@ -38851,18 +38992,24 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38851
38992
|
const meta = this.freshMeta(capChannel);
|
|
38852
38993
|
if (meta && !meta.is_dm) this.enforceRateFloor("room", capChannel);
|
|
38853
38994
|
}
|
|
38854
|
-
/**
|
|
38855
|
-
|
|
38995
|
+
/**
|
|
38996
|
+
* Bookkeeping after a CONFIRMED publish of `body` under `id`. `recordText` (weather/currency Task 17) is what a text
|
|
38997
|
+
* reply is RECORDED as: the published text minus any credit line. Every record site below reads `recorded`, never
|
|
38998
|
+
* `body.text`. Every production caller passes it (pinned by reply-credit.test.ts: text, or null for an a2a body);
|
|
38999
|
+
* undefined is left only for direct test calls of an uncredited body, where the published and record texts are equal.
|
|
39000
|
+
*/
|
|
39001
|
+
afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture, recordText) {
|
|
39002
|
+
const recorded = recordText ?? (recordText === void 0 && body.kind === "text" ? body.text : "");
|
|
38856
39003
|
if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
|
|
38857
39004
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
38858
|
-
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(
|
|
39005
|
+
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(recorded);
|
|
38859
39006
|
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
38860
|
-
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(
|
|
38861
|
-
this.callBuffer.appendAgent(channelId,
|
|
39007
|
+
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(recorded) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
|
|
39008
|
+
this.callBuffer.appendAgent(channelId, recorded, "reply");
|
|
38862
39009
|
}
|
|
38863
39010
|
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";
|
|
38864
|
-
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId,
|
|
38865
|
-
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId,
|
|
39011
|
+
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, recorded, id, modality, purgeEpoch, origin);
|
|
39012
|
+
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, recorded, id, replyCapture ?? null, origin);
|
|
38866
39013
|
}
|
|
38867
39014
|
/**
|
|
38868
39015
|
* Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
|
|
@@ -38940,14 +39087,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38940
39087
|
const k = replyIntentKey({ inReplyTo: opts.inReplyTo, idempotencyKey: opts.idempotencyKey, ordinal: () => 0 });
|
|
38941
39088
|
if (!k) throw new Error("idempotent send without a key");
|
|
38942
39089
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
38943
|
-
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes);
|
|
38944
|
-
const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes });
|
|
39090
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit);
|
|
39091
|
+
const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null });
|
|
38945
39092
|
if (intent.bodyMac !== mac2 || intent.channelId !== channelId || intent.state === "skipped") {
|
|
38946
39093
|
throw new IdempotencyConflictError();
|
|
38947
39094
|
}
|
|
38948
39095
|
return k;
|
|
38949
39096
|
}
|
|
38950
|
-
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true);
|
|
39097
|
+
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null);
|
|
38951
39098
|
});
|
|
38952
39099
|
return this.publishIntent(key, opts);
|
|
38953
39100
|
}
|
|
@@ -38956,22 +39103,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38956
39103
|
* the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
|
|
38957
39104
|
* after a slot collision: the body must go to a NEW part.
|
|
38958
39105
|
*/
|
|
38959
|
-
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry) {
|
|
39106
|
+
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit) {
|
|
39107
|
+
const credit = replyCredit ?? void 0;
|
|
38960
39108
|
const scope = `reply:${inReplyTo}`;
|
|
38961
39109
|
let latest = null;
|
|
38962
39110
|
for (const intent of await this.store.listReplyIntents(inReplyTo)) {
|
|
38963
39111
|
const n = intent.scope === scope ? partOrdinal(intent.part) : null;
|
|
38964
39112
|
if (n !== null && (latest === null || n > latest.ordinal)) latest = { ordinal: n, intent };
|
|
38965
39113
|
}
|
|
38966
|
-
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes)) {
|
|
39114
|
+
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit)) {
|
|
38967
39115
|
return { scope, part: latest.intent.part, inReplyTo };
|
|
38968
39116
|
}
|
|
38969
39117
|
let ordinal = latest ? latest.ordinal + 1 : 0;
|
|
38970
39118
|
for (let i = 0; i < REPLY_MAX_PART_ADVANCES; i++, ordinal++) {
|
|
38971
39119
|
const k = { scope, part: `p:${ordinal}`, inReplyTo };
|
|
38972
39120
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
38973
|
-
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes);
|
|
38974
|
-
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes });
|
|
39121
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit);
|
|
39122
|
+
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit });
|
|
38975
39123
|
if (created) return k;
|
|
38976
39124
|
if (matchRetry && intent.state !== "skipped" && intent.bodyMac === mac2 && intent.channelId === channelId) return k;
|
|
38977
39125
|
writeServiceLog2("info", "reply.part_slot_taken", { attempt: i + 1 });
|
|
@@ -39016,6 +39164,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39016
39164
|
}
|
|
39017
39165
|
const bytes = intent.body;
|
|
39018
39166
|
const body = decodeReplyBody(bytes);
|
|
39167
|
+
const recordText = this.recordTextOf(intent.replyCredit, body);
|
|
39168
|
+
if (recordText === void 0) {
|
|
39169
|
+
writeServiceLog2("error", "reply.intent_integrity_refused");
|
|
39170
|
+
this.store.deletePendingReplyIntent(key.scope, key.part);
|
|
39171
|
+
throw new ReplyIntentIntegrityError();
|
|
39172
|
+
}
|
|
39019
39173
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
|
|
39020
39174
|
const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
|
|
39021
39175
|
try {
|
|
@@ -39044,14 +39198,17 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39044
39198
|
const inReplyTo = intent.inReplyTo;
|
|
39045
39199
|
const next = await this.withReplyLock(
|
|
39046
39200
|
JSON.stringify(["scope", key.scope]),
|
|
39047
|
-
() =>
|
|
39201
|
+
() => (
|
|
39202
|
+
// r2 BLOCKER 1: the colliding row's OWN credit (verified above), never `opts`.
|
|
39203
|
+
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null)
|
|
39204
|
+
)
|
|
39048
39205
|
);
|
|
39049
39206
|
return this.publishIntent(next, opts, advances + 1);
|
|
39050
39207
|
}
|
|
39051
39208
|
throw e;
|
|
39052
39209
|
}
|
|
39053
39210
|
this.store.markReplyIntentSent(key.scope, key.part);
|
|
39054
|
-
this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish, replyCapture);
|
|
39211
|
+
this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish, replyCapture, recordText);
|
|
39055
39212
|
return { message_id: intent.replyMessageId, status: "sent" };
|
|
39056
39213
|
});
|
|
39057
39214
|
}
|
|
@@ -39061,9 +39218,21 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39061
39218
|
*/
|
|
39062
39219
|
intentVerifies(intent) {
|
|
39063
39220
|
if (!intent.body) return false;
|
|
39221
|
+
const credit = intent.replyCredit ?? null;
|
|
39222
|
+
if (credit !== null && !isReplyCredit(credit)) return false;
|
|
39064
39223
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, intent.channelId, intent);
|
|
39065
39224
|
if (id !== intent.replyMessageId) return false;
|
|
39066
|
-
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body) === intent.bodyMac;
|
|
39225
|
+
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0) === intent.bodyMac;
|
|
39226
|
+
}
|
|
39227
|
+
/**
|
|
39228
|
+
* Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
|
|
39229
|
+
* for an a2a body). "weatherapi": the text minus the exact credit suffix. Undefined: the row is refused (an unknown
|
|
39230
|
+
* credit, a credited a2a body, or a credited text without the exact suffix).
|
|
39231
|
+
*/
|
|
39232
|
+
recordTextOf(credit, body) {
|
|
39233
|
+
if (credit === null) return body.kind === "text" ? body.text : null;
|
|
39234
|
+
if (!isReplyCredit(credit) || body.kind !== "text" || !body.text.endsWith(WEATHER_CREDIT_SUFFIX)) return void 0;
|
|
39235
|
+
return body.text.slice(0, body.text.length - WEATHER_CREDIT_SUFFIX.length);
|
|
39067
39236
|
}
|
|
39068
39237
|
/**
|
|
39069
39238
|
* SEC-06 #18 item 3: a rate-limited reply retries the PUBLISH in-process
|
|
@@ -43187,7 +43356,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43187
43356
|
this.memory,
|
|
43188
43357
|
m.channelId,
|
|
43189
43358
|
m.envelope.sender_id,
|
|
43190
|
-
parsed.text,
|
|
43359
|
+
m.envelope.sender_id === this.cfg.identityId ? stripCreditSuffix(parsed.text) : parsed.text,
|
|
43191
43360
|
m.messageId,
|
|
43192
43361
|
m.voiceMode ? "voice" : "text",
|
|
43193
43362
|
purgeEpochAtTap
|
|
@@ -43985,21 +44154,49 @@ ${channelId}`;
|
|
|
43985
44154
|
*/
|
|
43986
44155
|
searchProvidersSockets = /* @__PURE__ */ new Map();
|
|
43987
44156
|
/**
|
|
43988
|
-
*
|
|
43989
|
-
*
|
|
44157
|
+
* Weather/currency U6 (H-1): the same AND-over-sockets state for utility_tools_v1, kept separately per socket, so a
|
|
44158
|
+
* runtime that declares search_providers_v2 but not utility_tools_v1 (0.14.0, 0.14.1) keeps web_extract and never
|
|
44159
|
+
* sees a utility name.
|
|
43990
44160
|
*/
|
|
43991
|
-
|
|
44161
|
+
utilityToolsSockets = /* @__PURE__ */ new Map();
|
|
44162
|
+
/**
|
|
44163
|
+
* The (search, utility) tuple the files were last recomputed for; null until the first socket observation, so that
|
|
44164
|
+
* observation always recomputes (final review I-1), even when it is non-capable. One trigger keyed on BOTH values.
|
|
44165
|
+
*/
|
|
44166
|
+
capabilityState = null;
|
|
43992
44167
|
searchCapabilityRefetchTimer = null;
|
|
43993
44168
|
/** When the current burst's first change happened (valid while the timer is set). */
|
|
43994
44169
|
searchCapabilityBurstStart = 0;
|
|
43995
44170
|
/** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
|
|
43996
44171
|
lastReportedSearchProviders = null;
|
|
44172
|
+
/** U6: the utility_tools_v1 value in the last report posted (null until one is). */
|
|
44173
|
+
lastReportedUtilityTools = null;
|
|
43997
44174
|
noteSearchProvidersSocket(token, declared) {
|
|
43998
44175
|
this.searchProvidersSockets.set(token, declared);
|
|
43999
|
-
this.
|
|
44176
|
+
this.onCapabilityMaybeChanged();
|
|
44000
44177
|
}
|
|
44001
44178
|
clearSearchProvidersSocket(token) {
|
|
44002
|
-
if (this.searchProvidersSockets.delete(token)) this.
|
|
44179
|
+
if (this.searchProvidersSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
44180
|
+
}
|
|
44181
|
+
noteUtilityToolsSocket(token, declared) {
|
|
44182
|
+
this.utilityToolsSockets.set(token, declared);
|
|
44183
|
+
this.onCapabilityMaybeChanged();
|
|
44184
|
+
}
|
|
44185
|
+
clearUtilityToolsSocket(token) {
|
|
44186
|
+
if (this.utilityToolsSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
44187
|
+
}
|
|
44188
|
+
/** U6: every attached runtime socket declared utility_tools_v1; no socket attached means absent (false). */
|
|
44189
|
+
utilityToolsCapable() {
|
|
44190
|
+
if (this.utilityToolsSockets.size === 0) return false;
|
|
44191
|
+
for (const v of this.utilityToolsSockets.values()) if (!v) return false;
|
|
44192
|
+
return true;
|
|
44193
|
+
}
|
|
44194
|
+
/** U6: the features every attached runtime socket declared, for the policy functions. */
|
|
44195
|
+
capabilityFeatures() {
|
|
44196
|
+
const out = /* @__PURE__ */ new Set();
|
|
44197
|
+
if (this.searchProvidersCapable()) out.add("search_providers_v2");
|
|
44198
|
+
if (this.utilityToolsCapable()) out.add("utility_tools_v1");
|
|
44199
|
+
return out;
|
|
44003
44200
|
}
|
|
44004
44201
|
/** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
|
|
44005
44202
|
searchProvidersCapable() {
|
|
@@ -44026,14 +44223,14 @@ ${channelId}`;
|
|
|
44026
44223
|
* D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
|
|
44027
44224
|
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
44028
44225
|
*/
|
|
44029
|
-
|
|
44030
|
-
const
|
|
44031
|
-
const prev = this.
|
|
44032
|
-
if (
|
|
44033
|
-
this.
|
|
44034
|
-
this.recomputeManifestPolicy(
|
|
44226
|
+
onCapabilityMaybeChanged() {
|
|
44227
|
+
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable() };
|
|
44228
|
+
const prev = this.capabilityState;
|
|
44229
|
+
if (prev !== null && prev.search === next.search && prev.utility === next.utility) return;
|
|
44230
|
+
this.capabilityState = next;
|
|
44231
|
+
this.recomputeManifestPolicy(this.capabilityFeatures());
|
|
44035
44232
|
if (this.closingDown) return;
|
|
44036
|
-
if (prev === null && !
|
|
44233
|
+
if (prev === null && !next.search && !next.utility) return;
|
|
44037
44234
|
const now = Date.now();
|
|
44038
44235
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
44039
44236
|
else this.searchCapabilityBurstStart = now;
|
|
@@ -44051,7 +44248,7 @@ ${channelId}`;
|
|
|
44051
44248
|
searchCapabilityRefetchNeeded() {
|
|
44052
44249
|
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
44053
44250
|
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
44054
|
-
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
|
|
44251
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable();
|
|
44055
44252
|
}
|
|
44056
44253
|
/**
|
|
44057
44254
|
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
@@ -44059,15 +44256,16 @@ ${channelId}`;
|
|
|
44059
44256
|
* lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
|
|
44060
44257
|
* Content-free; a write failure is logged and the debounced refetch tries again.
|
|
44061
44258
|
*/
|
|
44062
|
-
recomputeManifestPolicy(
|
|
44259
|
+
recomputeManifestPolicy(features) {
|
|
44063
44260
|
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
44064
44261
|
const manifest = pin !== null ? this.toolGrantManifest : null;
|
|
44065
44262
|
try {
|
|
44066
44263
|
if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
|
|
44067
|
-
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants },
|
|
44264
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, features));
|
|
44068
44265
|
return;
|
|
44069
44266
|
}
|
|
44070
|
-
|
|
44267
|
+
const withheld = new Set(Object.keys(TOOL_FEATURE).filter((t) => !toolAllowedByFeatures(t, features)));
|
|
44268
|
+
if (withheld.size === 0) return;
|
|
44071
44269
|
const path = join17(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
|
|
44072
44270
|
if (!existsSync5(path)) return;
|
|
44073
44271
|
let doc;
|
|
@@ -44077,10 +44275,10 @@ ${channelId}`;
|
|
|
44077
44275
|
return;
|
|
44078
44276
|
}
|
|
44079
44277
|
const tools = doc?.manifest_tools;
|
|
44080
|
-
if (!Array.isArray(tools) || !tools.
|
|
44278
|
+
if (!Array.isArray(tools) || !tools.some((t) => typeof t === "string" && withheld.has(t))) return;
|
|
44081
44279
|
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
|
|
44082
44280
|
v: 1,
|
|
44083
|
-
manifest_tools: tools.filter((t) => typeof t === "string" && t
|
|
44281
|
+
manifest_tools: tools.filter((t) => typeof t === "string" && !withheld.has(t)),
|
|
44084
44282
|
secret_path_mode: "enforce"
|
|
44085
44283
|
});
|
|
44086
44284
|
} catch (e) {
|
|
@@ -44617,6 +44815,9 @@ var BRIDGE_ERRORS = {
|
|
|
44617
44815
|
// C9 (spec §4.1.6 step 1): another copy of this agent took its SecureComs
|
|
44618
44816
|
// session (WS 4409), so this copy stopped. Every local route but /healthz.
|
|
44619
44817
|
session_superseded: { http: 503, message: "another copy of this agent took its session; this copy stopped" },
|
|
44818
|
+
// Weather/currency Task 17 (OQ-P1): a send frame's credit is not the closed value, or rides an a2a frame. TERMINAL:
|
|
44819
|
+
// the inbound is retired, so the model (and the weather call) never runs again for it.
|
|
44820
|
+
reply_credit_unknown: { http: 400, message: "reply credit not recognised; not sent" },
|
|
44620
44821
|
internal: { http: 500, message: "internal error" }
|
|
44621
44822
|
};
|
|
44622
44823
|
function errorBody(code, detail) {
|
|
@@ -44646,6 +44847,7 @@ function withConsumerGrants(m, perToolConsumer) {
|
|
|
44646
44847
|
}
|
|
44647
44848
|
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
44648
44849
|
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
44850
|
+
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
44649
44851
|
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
44650
44852
|
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
44651
44853
|
team_room: Object.freeze(["current_time"])
|
|
@@ -44677,6 +44879,15 @@ function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
|
44677
44879
|
if (kept.length === a.tool_grants.length) return m;
|
|
44678
44880
|
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
44679
44881
|
}
|
|
44882
|
+
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
44883
|
+
function withUtilityCapability(m, consumerHasFeature) {
|
|
44884
|
+
if (consumerHasFeature) return m;
|
|
44885
|
+
const a = m.authorization;
|
|
44886
|
+
if (!a) return m;
|
|
44887
|
+
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
44888
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
44889
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
44890
|
+
}
|
|
44680
44891
|
|
|
44681
44892
|
// src/http-server.ts
|
|
44682
44893
|
function writeSse(res, msgs, onWriteError) {
|
|
@@ -44700,7 +44911,7 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
44700
44911
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
44701
44912
|
const withheld = agent.harnessGrantsWithheld();
|
|
44702
44913
|
const provider = agent.signedSearchProvider();
|
|
44703
|
-
const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
|
|
44914
|
+
const left3 = writeSse(res, batch.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)), onWriteError);
|
|
44704
44915
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
44705
44916
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
44706
44917
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -44732,7 +44943,7 @@ var OTHER_CAP = 8 * 1024;
|
|
|
44732
44943
|
var a2aSchema = z3.object({
|
|
44733
44944
|
a2a_version: z3.string().min(1),
|
|
44734
44945
|
kind: z3.enum(["message", "task", "artifact"]),
|
|
44735
|
-
object: z3.record(z3.unknown())
|
|
44946
|
+
object: z3.record(z3.string(), z3.unknown())
|
|
44736
44947
|
});
|
|
44737
44948
|
var sendAttachmentSchema = z3.object({
|
|
44738
44949
|
channel_id: z3.string().min(1),
|
|
@@ -44974,7 +45185,7 @@ function createHttpServer(agent, opts) {
|
|
|
44974
45185
|
const drained = await agent.drain(channel, wait);
|
|
44975
45186
|
const withheld = agent.harnessGrantsWithheld();
|
|
44976
45187
|
const provider = agent.signedSearchProvider();
|
|
44977
|
-
return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
|
|
45188
|
+
return json(res, 200, drained.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)));
|
|
44978
45189
|
}
|
|
44979
45190
|
if (method === "GET" && path === "/v1/events") {
|
|
44980
45191
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -45149,6 +45360,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45149
45360
|
perToolGrants: false,
|
|
45150
45361
|
manifestGrants: false,
|
|
45151
45362
|
searchProviders: false,
|
|
45363
|
+
utilityTools: false,
|
|
45152
45364
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
45153
45365
|
deliveryOpen: helloWaitMs <= 0,
|
|
45154
45366
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -45202,14 +45414,14 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45202
45414
|
continue;
|
|
45203
45415
|
}
|
|
45204
45416
|
const out = withConsumerNotes(
|
|
45205
|
-
withSearchCapability(
|
|
45417
|
+
withUtilityCapability(withSearchCapability(
|
|
45206
45418
|
withManifestCapability(
|
|
45207
45419
|
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
45208
45420
|
session.manifestGrants
|
|
45209
45421
|
),
|
|
45210
45422
|
session.searchProviders,
|
|
45211
45423
|
agent.signedSearchProvider()
|
|
45212
|
-
),
|
|
45424
|
+
), session.utilityTools),
|
|
45213
45425
|
batch[i],
|
|
45214
45426
|
session,
|
|
45215
45427
|
agent
|
|
@@ -45266,6 +45478,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45266
45478
|
if (session.perToolGrants) agent.clearPerToolConsumer(session);
|
|
45267
45479
|
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
45268
45480
|
agent.clearSearchProvidersSocket(session);
|
|
45481
|
+
agent.clearUtilityToolsSocket(session);
|
|
45269
45482
|
};
|
|
45270
45483
|
current = { sock, cleanup, sweepStale };
|
|
45271
45484
|
sock.on("close", cleanup);
|
|
@@ -45306,7 +45519,10 @@ function sendJson(sock, body) {
|
|
|
45306
45519
|
} catch {
|
|
45307
45520
|
}
|
|
45308
45521
|
}
|
|
45522
|
+
var ReplyCreditUnknownError = class extends Error {
|
|
45523
|
+
};
|
|
45309
45524
|
function sendErrorCode(e) {
|
|
45525
|
+
if (e instanceof ReplyCreditUnknownError) return "reply_credit_unknown";
|
|
45310
45526
|
if (e instanceof ChannelPendingError) return "channel_pending";
|
|
45311
45527
|
if (e instanceof RateLimitedError) return "rate_limited";
|
|
45312
45528
|
if (e instanceof AgentPausedSendError) return "agent_paused";
|
|
@@ -45367,6 +45583,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45367
45583
|
agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
|
|
45368
45584
|
session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
|
|
45369
45585
|
agent.noteSearchProvidersSocket(session, session.searchProviders);
|
|
45586
|
+
session.utilityTools = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(UTILITY_TOOLS_FEATURE);
|
|
45587
|
+
agent.noteUtilityToolsSocket(session, session.utilityTools);
|
|
45370
45588
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
45371
45589
|
agent.noteNotesConsumer();
|
|
45372
45590
|
}
|
|
@@ -45374,7 +45592,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45374
45592
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
45375
45593
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
45376
45594
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
45377
|
-
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
|
|
45595
|
+
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE, REPLY_CREDIT_FEATURE] });
|
|
45378
45596
|
if (!session.deliveryOpen) {
|
|
45379
45597
|
session.deliveryOpen = true;
|
|
45380
45598
|
flush();
|
|
@@ -45403,7 +45621,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45403
45621
|
}
|
|
45404
45622
|
const replyFailed = (e, inReplyTo) => {
|
|
45405
45623
|
const code = sendErrorCode(e);
|
|
45406
|
-
|
|
45624
|
+
const terminal = code === "agent_paused" || code === "send_forbidden" || code === "reply_credit_unknown";
|
|
45625
|
+
if (terminal && typeof inReplyTo === "string") {
|
|
45407
45626
|
inflight.delete(inReplyTo);
|
|
45408
45627
|
session.deliveryToken.delete(inReplyTo);
|
|
45409
45628
|
session.shownRecall.delete(inReplyTo);
|
|
@@ -45411,7 +45630,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45411
45630
|
agent.ackRuntimeMessages([inReplyTo]);
|
|
45412
45631
|
}
|
|
45413
45632
|
sendJson(sock, { ...wsErrorFrame(code), ...typeof inReplyTo === "string" ? { in_reply_to: inReplyTo } : {} });
|
|
45414
|
-
if (
|
|
45633
|
+
if (terminal) flush();
|
|
45415
45634
|
};
|
|
45416
45635
|
if (m.type === "send") {
|
|
45417
45636
|
if (typeof m.channel_id !== "string" || !m.channel_id) {
|
|
@@ -45428,8 +45647,14 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45428
45647
|
}
|
|
45429
45648
|
const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
|
|
45430
45649
|
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);
|
|
45650
|
+
if (m.credit !== void 0 && (m.a2a !== void 0 || !isReplyCredit(m.credit))) {
|
|
45651
|
+
writeServiceLog2("error", "reply.credit_unknown");
|
|
45652
|
+
replyFailed(new ReplyCreditUnknownError(), m.in_reply_to);
|
|
45653
|
+
return;
|
|
45654
|
+
}
|
|
45431
45655
|
const sendOpts = {
|
|
45432
45656
|
agentTurn: isAgentTurn(agent, m),
|
|
45657
|
+
...isReplyCredit(m.credit) ? { credit: m.credit } : {},
|
|
45433
45658
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
45434
45659
|
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
45435
45660
|
...callBufferReply ? { callBufferReply: true } : {}
|