@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/bin.js
CHANGED
|
@@ -19331,6 +19331,8 @@ function isEpochConflict(e) {
|
|
|
19331
19331
|
var LABEL = "agentvault-connect-v1";
|
|
19332
19332
|
var DEFAULT_STUCK_THRESHOLD = 2;
|
|
19333
19333
|
var DEFAULT_QUARANTINE_THRESHOLD = 8;
|
|
19334
|
+
var KEPT_WELCOME_COMMIT_DEFER_MS = 24 * 60 * 60 * 1e3;
|
|
19335
|
+
var KEPT_WELCOME_COMMIT_DEFER_MAX_ROWS = 32;
|
|
19334
19336
|
var DELIVERY_PULL_PAGE = 100;
|
|
19335
19337
|
var DELIVERY_MAX_RETRIES = 5;
|
|
19336
19338
|
var DrainPageFullError = class extends Error {
|
|
@@ -19535,6 +19537,15 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
19535
19537
|
* {@link errorStats}; mutated only through {@link recordDecryptError} /
|
|
19536
19538
|
* {@link recordDecryptSuccess}.
|
|
19537
19539
|
*/
|
|
19540
|
+
/** {@link KEPT_WELCOME_COMMIT_DEFER_MS}: channel -> when its first commit was
|
|
19541
|
+
* deferred behind the kept Welcome `welcomeId`. Restarted for a different
|
|
19542
|
+
* Welcome; cleared when the channel decrypts, or when a pass that saw the
|
|
19543
|
+
* whole queue kept no Welcome for it (fix round 1, L2: the Welcome
|
|
19544
|
+
* resolved, so a later re-Welcome gets a fresh, bounded window). An entry
|
|
19545
|
+
* past its bound stays (expired) until then, so the bound cannot restart
|
|
19546
|
+
* itself pass after pass. In memory: a reload restarts it, and the
|
|
19547
|
+
* per-pass row cap is what bounds the page cost across reloads. */
|
|
19548
|
+
keptWelcomeDeferSince = /* @__PURE__ */ new Map();
|
|
19538
19549
|
errors = /* @__PURE__ */ new Map();
|
|
19539
19550
|
stuckThreshold;
|
|
19540
19551
|
/** Consecutive-error threshold at which a wedged channel's row is quarantined
|
|
@@ -19753,8 +19764,33 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
19753
19764
|
* channel resumes normal processing and can quarantine again later if it
|
|
19754
19765
|
* re-wedges (the onWedged guard is reset so the hook can fire again).
|
|
19755
19766
|
*/
|
|
19767
|
+
/** See {@link MlsGroupKeyProviderOpts.deferCommitsWhileWelcomePending}. The
|
|
19768
|
+
* caller has already established that this pass KEPT a no-state Welcome
|
|
19769
|
+
* for `channelId`. True: defer the commit row. Never throws. */
|
|
19770
|
+
deferKeptWelcomeCommit(channelId, welcomeId) {
|
|
19771
|
+
const pred = this.o.deferCommitsWhileWelcomePending;
|
|
19772
|
+
if (!pred)
|
|
19773
|
+
return false;
|
|
19774
|
+
let boundMs = 0;
|
|
19775
|
+
try {
|
|
19776
|
+
const v = pred(channelId);
|
|
19777
|
+
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;
|
|
19778
|
+
} catch {
|
|
19779
|
+
boundMs = 0;
|
|
19780
|
+
}
|
|
19781
|
+
if (boundMs <= 0)
|
|
19782
|
+
return false;
|
|
19783
|
+
const now = this.nowMs();
|
|
19784
|
+
const entry = this.keptWelcomeDeferSince.get(channelId);
|
|
19785
|
+
if (entry === void 0 || entry.welcomeId !== welcomeId) {
|
|
19786
|
+
this.keptWelcomeDeferSince.set(channelId, { since: now, welcomeId });
|
|
19787
|
+
return true;
|
|
19788
|
+
}
|
|
19789
|
+
return now - entry.since < boundMs;
|
|
19790
|
+
}
|
|
19756
19791
|
recordDecryptSuccess(channelId) {
|
|
19757
19792
|
this.lastDecryptAt.set(channelId, Date.now());
|
|
19793
|
+
this.keptWelcomeDeferSince.delete(channelId);
|
|
19758
19794
|
const e = this.errors.get(channelId);
|
|
19759
19795
|
if (!e)
|
|
19760
19796
|
return;
|
|
@@ -20150,6 +20186,12 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
20150
20186
|
* server's REJOIN_THRESHOLD, there was nothing to apply, or a commit would
|
|
20151
20187
|
* not apply (which means the state is genuinely FORKED, not merely behind,
|
|
20152
20188
|
* and replay cannot fix a fork).
|
|
20189
|
+
*
|
|
20190
|
+
* THROWS (retryable, never `false`, #1290): CustodyPersistError when the
|
|
20191
|
+
* channel is under the custody hold (nothing applied), and the C2 classes
|
|
20192
|
+
* (PendingCommitUnresolvedError from the guarded MAIN write) after evicting
|
|
20193
|
+
* the advanced cache, so the persisted state is unchanged. A /sync request
|
|
20194
|
+
* that fails also throws, before anything is applied.
|
|
20153
20195
|
*/
|
|
20154
20196
|
async resyncByReplay(channelId, groupId) {
|
|
20155
20197
|
if (!await this.hasPersistedChannelState(channelId))
|
|
@@ -20169,12 +20211,26 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
20169
20211
|
let applied = 0;
|
|
20170
20212
|
try {
|
|
20171
20213
|
await this.locks.run(channelId, async () => {
|
|
20214
|
+
if (await this.custodyHeld(channelId))
|
|
20215
|
+
throw new CustodyPersistError(channelId);
|
|
20172
20216
|
const mgr = await this.manager(channelId);
|
|
20173
20217
|
try {
|
|
20174
20218
|
for (const c of commits) {
|
|
20175
20219
|
if (c.epoch <= Number(mgr.epoch))
|
|
20176
20220
|
continue;
|
|
20177
|
-
|
|
20221
|
+
const commitBytes = hexToBytes22(c.payload);
|
|
20222
|
+
try {
|
|
20223
|
+
await mgr.decrypt(commitBytes);
|
|
20224
|
+
} catch (e) {
|
|
20225
|
+
if (await probeOwnRemoval(mgr, commitBytes) !== null) {
|
|
20226
|
+
this.noteOwnLeafRemoved(channelId, {
|
|
20227
|
+
group_id: groupId,
|
|
20228
|
+
epoch: c.epoch,
|
|
20229
|
+
message_id: c.id
|
|
20230
|
+
});
|
|
20231
|
+
}
|
|
20232
|
+
throw e;
|
|
20233
|
+
}
|
|
20178
20234
|
applied++;
|
|
20179
20235
|
}
|
|
20180
20236
|
if (applied > 0)
|
|
@@ -20186,6 +20242,10 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
20186
20242
|
}
|
|
20187
20243
|
});
|
|
20188
20244
|
} catch (e) {
|
|
20245
|
+
if (isSdkError(e, "CustodyPersistError") || e instanceof CustodyPersistError || e instanceof PendingCommitUnresolvedError || e instanceof GroupSupersededError || e instanceof AttestedOrphanRefusedError) {
|
|
20246
|
+
writeServiceLog2("warn", "resync.replay_held", { applied }, e);
|
|
20247
|
+
throw e;
|
|
20248
|
+
}
|
|
20189
20249
|
writeServiceLog2("warn", "resync.replay_commit_failed", { applied }, e);
|
|
20190
20250
|
return false;
|
|
20191
20251
|
}
|
|
@@ -22886,6 +22946,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
22886
22946
|
out.push({
|
|
22887
22947
|
queueId: `replay-${m.id}`,
|
|
22888
22948
|
messageId: envelope.message_id,
|
|
22949
|
+
// #1246 C-1: the group-messages row id is the same mls_messages key.
|
|
22950
|
+
...typeof m.id === "string" && m.id ? { serverMessageId: m.id } : {},
|
|
22889
22951
|
channelId,
|
|
22890
22952
|
plaintext,
|
|
22891
22953
|
envelope,
|
|
@@ -23786,6 +23848,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23786
23848
|
const heldBundleRefs = () => heldBundleRefsMemo ??= this.readHeldBundleRefs();
|
|
23787
23849
|
const w2Acked = /* @__PURE__ */ new Map();
|
|
23788
23850
|
const w2Kept = /* @__PURE__ */ new Set();
|
|
23851
|
+
const keptWelcomeId = /* @__PURE__ */ new Map();
|
|
23852
|
+
const deferredThisPass = /* @__PURE__ */ new Map();
|
|
23789
23853
|
const unpinnedRefusals = /* @__PURE__ */ new Map();
|
|
23790
23854
|
const welcomes = rows.filter((r) => r.message_type === "welcome");
|
|
23791
23855
|
const nonWelcomes = rows.filter((r) => r.message_type !== "welcome");
|
|
@@ -23944,6 +24008,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23944
24008
|
writeServiceLog2("warn", "welcome.unopenable");
|
|
23945
24009
|
} else {
|
|
23946
24010
|
w2Kept.add(wcid);
|
|
24011
|
+
if (!keptWelcomeId.has(wcid))
|
|
24012
|
+
keptWelcomeId.set(wcid, row.queue_id);
|
|
23947
24013
|
if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
|
|
23948
24014
|
writeServiceLog2("debug", "delivery.welcome_no_local_state");
|
|
23949
24015
|
}
|
|
@@ -23954,6 +24020,10 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
23954
24020
|
const cid = row.connect_channel_id;
|
|
23955
24021
|
if (!cid)
|
|
23956
24022
|
continue;
|
|
24023
|
+
if (w2Kept.has(cid) && (deferredThisPass.get(cid) ?? 0) < KEPT_WELCOME_COMMIT_DEFER_MAX_ROWS && this.deferKeptWelcomeCommit(cid, keptWelcomeId.get(cid) ?? "")) {
|
|
24024
|
+
deferredThisPass.set(cid, (deferredThisPass.get(cid) ?? 0) + 1);
|
|
24025
|
+
continue;
|
|
24026
|
+
}
|
|
23957
24027
|
let advancedEpoch;
|
|
23958
24028
|
let custodyHeld = false;
|
|
23959
24029
|
await this.locks.run(cid, async () => {
|
|
@@ -24110,6 +24180,12 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
24110
24180
|
this.leftStreak.set(cid, { pulls: 1, since: pullNow });
|
|
24111
24181
|
this.scheduleAutoResolve(cid);
|
|
24112
24182
|
}
|
|
24183
|
+
if (rows.length < DELIVERY_PULL_PAGE) {
|
|
24184
|
+
for (const cid of [...this.keptWelcomeDeferSince.keys()]) {
|
|
24185
|
+
if (!w2Kept.has(cid))
|
|
24186
|
+
this.keptWelcomeDeferSince.delete(cid);
|
|
24187
|
+
}
|
|
24188
|
+
}
|
|
24113
24189
|
for (const [cid, gid] of w2Acked) {
|
|
24114
24190
|
if (w2Kept.has(cid))
|
|
24115
24191
|
continue;
|
|
@@ -24389,7 +24465,9 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
24389
24465
|
voiceDefaultResponder: row.voice_default_responder ?? void 0,
|
|
24390
24466
|
...typeof row.identity_signature === "string" && row.identity_signature ? { identitySignature: row.identity_signature } : {},
|
|
24391
24467
|
payloadHex: row.payload,
|
|
24392
|
-
openedAt
|
|
24468
|
+
openedAt,
|
|
24469
|
+
// #1246 C-1: the server row id, beside the sender-chosen envelope id.
|
|
24470
|
+
...typeof row.message_id === "string" && row.message_id ? { serverMessageId: row.message_id } : {}
|
|
24393
24471
|
};
|
|
24394
24472
|
const custodiedAt = this.nowMs();
|
|
24395
24473
|
this.custody.set(row.queue_id, entry);
|
|
@@ -26911,6 +26989,11 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
26911
26989
|
// Owner-signed tool grants (spec §7.0): like Plan 0's backend, each row carries
|
|
26912
26990
|
// the envelope's `identity_proof.signature` as `identity_signature` beside the
|
|
26913
26991
|
// unsigned canonical payload. `omitSignature` models a pre-Plan-0 row.
|
|
26992
|
+
/** `rowMessageId` (#1246 C-1): the server row id to deliver, when it is NOT
|
|
26993
|
+
* the envelope's id. The REST send path keys the row by the envelope id
|
|
26994
|
+
* (connect_messages.py, `MLSMessage(id=message_id)`); the WS `message_mls`
|
|
26995
|
+
* path stores through `store_mls_message`, whose row id is server-generated,
|
|
26996
|
+
* so there the two differ and the envelope id is the sender's free choice. */
|
|
26914
26997
|
async ingestApplication(envelope, opts) {
|
|
26915
26998
|
const channelId = envelope.channel_id;
|
|
26916
26999
|
const canonical = await canonicalEnvelopeBytes(envelope);
|
|
@@ -26922,7 +27005,7 @@ var FakeConnectBackend = class _FakeConnectBackend {
|
|
|
26922
27005
|
continue;
|
|
26923
27006
|
this.q(dev).push({
|
|
26924
27007
|
queue_id: "q-" + this._seq++,
|
|
26925
|
-
message_id: envelope.message_id,
|
|
27008
|
+
message_id: opts?.rowMessageId ?? envelope.message_id,
|
|
26926
27009
|
group_id: groupId,
|
|
26927
27010
|
message_type: "application",
|
|
26928
27011
|
sender_device_id: senderDeviceId ?? "",
|
|
@@ -27306,6 +27389,7 @@ function openSchema(db) {
|
|
|
27306
27389
|
ensureRuntimeInboxDeliveryAttempts(db);
|
|
27307
27390
|
ensureRuntimeReplyIntent(db);
|
|
27308
27391
|
ensureRuntimeReplyIntentEnvelopeGroup(db);
|
|
27392
|
+
ensureRuntimeReplyIntentCredit(db);
|
|
27309
27393
|
ensureGroupLink(db);
|
|
27310
27394
|
ensureMlsCustody(db);
|
|
27311
27395
|
ensureToolGrantTables(db);
|
|
@@ -27386,6 +27470,19 @@ function ensureRuntimeReplyIntentEnvelopeGroup(db) {
|
|
|
27386
27470
|
throw e;
|
|
27387
27471
|
}
|
|
27388
27472
|
}
|
|
27473
|
+
function ensureRuntimeReplyIntentCredit(db) {
|
|
27474
|
+
const hasColumn = () => db.prepare("PRAGMA table_info(runtime_reply_intent)").all().some((c) => c.name === "reply_credit");
|
|
27475
|
+
const cols = db.prepare("PRAGMA table_info(runtime_reply_intent)").all();
|
|
27476
|
+
if (cols.length === 0 || cols.some((c) => c.name === "reply_credit"))
|
|
27477
|
+
return;
|
|
27478
|
+
try {
|
|
27479
|
+
db.exec("ALTER TABLE runtime_reply_intent ADD COLUMN reply_credit TEXT");
|
|
27480
|
+
} catch (e) {
|
|
27481
|
+
if (hasColumn())
|
|
27482
|
+
return;
|
|
27483
|
+
throw e;
|
|
27484
|
+
}
|
|
27485
|
+
}
|
|
27389
27486
|
var RECALL_STATE_COLUMNS = [
|
|
27390
27487
|
["recall_enabled", "INTEGER NOT NULL DEFAULT 0"],
|
|
27391
27488
|
["recall_mode_epoch", "INTEGER NOT NULL DEFAULT 0"],
|
|
@@ -28437,8 +28534,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28437
28534
|
this.db.prepare("DELETE FROM runtime_reply_intent WHERE identity_id=? AND updated_at < ?").run(this.id, now - RUNTIME_REPLY_INTENT_RETENTION_MS);
|
|
28438
28535
|
const info = this.db.prepare(`INSERT OR IGNORE INTO runtime_reply_intent
|
|
28439
28536
|
(identity_id, scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
28440
|
-
sealed_body, sealed_envelope, state, created_at, updated_at)
|
|
28441
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now);
|
|
28537
|
+
sealed_body, sealed_envelope, state, created_at, updated_at, reply_credit)
|
|
28538
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now, input.replyCredit ?? null);
|
|
28442
28539
|
const count = this.db.prepare("SELECT COUNT(*) AS n FROM runtime_reply_intent WHERE identity_id=?").get(this.id);
|
|
28443
28540
|
if (count.n > RUNTIME_REPLY_INTENT_MAX) {
|
|
28444
28541
|
this.db.prepare(`DELETE FROM runtime_reply_intent
|
|
@@ -28448,7 +28545,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28448
28545
|
)`).run(this.id, this.id, RUNTIME_REPLY_INTENT_MAX);
|
|
28449
28546
|
}
|
|
28450
28547
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
28451
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
|
|
28548
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
28452
28549
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, input.scope, input.part);
|
|
28453
28550
|
if (!row)
|
|
28454
28551
|
throw new Error("reply intent vanished after claim");
|
|
@@ -28457,7 +28554,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28457
28554
|
/** SEC-06 #18: every intent answering `inReplyTo`, in claim order. */
|
|
28458
28555
|
async listReplyIntents(inReplyTo) {
|
|
28459
28556
|
const rows = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
28460
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
|
|
28557
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
28461
28558
|
FROM runtime_reply_intent WHERE identity_id=? AND in_reply_to=?
|
|
28462
28559
|
ORDER BY created_at ASC, rowid ASC`).all(this.id, inReplyTo);
|
|
28463
28560
|
const out = [];
|
|
@@ -28468,7 +28565,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28468
28565
|
/** SEC-06 #18: re-read one intent by its key (null when absent). */
|
|
28469
28566
|
async getReplyIntent(scope, part) {
|
|
28470
28567
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
28471
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
|
|
28568
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
28472
28569
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, scope, part);
|
|
28473
28570
|
return row ? this.openReplyIntent(row) : null;
|
|
28474
28571
|
}
|
|
@@ -28542,7 +28639,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28542
28639
|
envelope,
|
|
28543
28640
|
envelopeGroupId: envelopeGroupFor(envelope, await openOrNull(row.sealed_envelope_group)),
|
|
28544
28641
|
state: row.state === "sent" ? "sent" : row.state === "skipped" ? "skipped" : "pending",
|
|
28545
|
-
createdAt: Number(row.created_at)
|
|
28642
|
+
createdAt: Number(row.created_at),
|
|
28643
|
+
replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null
|
|
28546
28644
|
};
|
|
28547
28645
|
}
|
|
28548
28646
|
async recordMemory(input) {
|
|
@@ -30312,8 +30410,8 @@ function decodeReplyBody(bytes) {
|
|
|
30312
30410
|
if (b?.kind === "a2a" && b.a2a && typeof b.a2a === "object") return b;
|
|
30313
30411
|
throw new Error("reply intent body is malformed");
|
|
30314
30412
|
}
|
|
30315
|
-
function replyBodyMac(key, replyId, channelId, body) {
|
|
30316
|
-
return createHmac("sha256", key).update(JSON.stringify(["reply-body", replyId, channelId])).update("\0").update(body).digest("hex");
|
|
30413
|
+
function replyBodyMac(key, replyId, channelId, body, credit) {
|
|
30414
|
+
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");
|
|
30317
30415
|
}
|
|
30318
30416
|
function partOrdinal(part) {
|
|
30319
30417
|
const m = /^p:(\d+)$/.exec(part);
|
|
@@ -35154,6 +35252,8 @@ function checkConfiguredPaths(skillsJson, statFn = (p) => {
|
|
|
35154
35252
|
}
|
|
35155
35253
|
const search = skillsJson.search;
|
|
35156
35254
|
if (search?.keyFile) paths.push(search.keyFile);
|
|
35255
|
+
const weather = skillsJson.weather;
|
|
35256
|
+
if (weather?.keyFile) paths.push(weather.keyFile);
|
|
35157
35257
|
return paths.map((path) => {
|
|
35158
35258
|
let st;
|
|
35159
35259
|
try {
|
|
@@ -35179,7 +35279,7 @@ function parseSecretPathsReport(raw) {
|
|
|
35179
35279
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
35180
35280
|
const r = raw;
|
|
35181
35281
|
const f = r.outside_by_field;
|
|
35182
|
-
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;
|
|
35282
|
+
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;
|
|
35183
35283
|
return {
|
|
35184
35284
|
mode: r.mode,
|
|
35185
35285
|
inside: r.inside,
|
|
@@ -35189,7 +35289,8 @@ function parseSecretPathsReport(raw) {
|
|
|
35189
35289
|
headers_file: f.headers_file,
|
|
35190
35290
|
postgres_url_file: f.postgres_url_file,
|
|
35191
35291
|
search_key_file: f.search_key_file,
|
|
35192
|
-
...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {}
|
|
35292
|
+
...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {},
|
|
35293
|
+
...f.weather_key_file !== void 0 ? { weather_key_file: f.weather_key_file } : {}
|
|
35193
35294
|
}
|
|
35194
35295
|
};
|
|
35195
35296
|
}
|
|
@@ -35269,6 +35370,7 @@ function readLocalReport(home, identityId) {
|
|
|
35269
35370
|
has_search_key: hasSearchKey,
|
|
35270
35371
|
has_llm_key: hasLlmKey,
|
|
35271
35372
|
has_extract_provider: doc.has_extract_provider === true,
|
|
35373
|
+
has_weather_key: doc.has_weather_key === true,
|
|
35272
35374
|
...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
|
|
35273
35375
|
legacy_memory: localLegacySources(doc).map(({ path: _path, content: _content, ...source }) => source),
|
|
35274
35376
|
secret_paths: parseSecretPathsReport(doc.secret_paths)
|
|
@@ -35370,6 +35472,19 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
|
35370
35472
|
return removed;
|
|
35371
35473
|
}
|
|
35372
35474
|
|
|
35475
|
+
// src/reply-credit.ts
|
|
35476
|
+
var REPLY_CREDIT_FEATURE = "reply_credit_v1";
|
|
35477
|
+
var WEATHER_CREDIT_LINE = "Weather data: WeatherAPI.com. For general information only.";
|
|
35478
|
+
var WEATHER_CREDIT_SUFFIX = `
|
|
35479
|
+
|
|
35480
|
+
${WEATHER_CREDIT_LINE}`;
|
|
35481
|
+
function isReplyCredit(v) {
|
|
35482
|
+
return v === "weatherapi";
|
|
35483
|
+
}
|
|
35484
|
+
function stripCreditSuffix(text) {
|
|
35485
|
+
return text.endsWith(WEATHER_CREDIT_SUFFIX) ? text.slice(0, text.length - WEATHER_CREDIT_SUFFIX.length) : text;
|
|
35486
|
+
}
|
|
35487
|
+
|
|
35373
35488
|
// src/grants/binding.ts
|
|
35374
35489
|
import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
35375
35490
|
|
|
@@ -35377,9 +35492,10 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
|
35377
35492
|
import { ed25519 as ed255195 } from "@noble/curves/ed25519";
|
|
35378
35493
|
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
35379
35494
|
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
35380
|
-
owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
|
|
35495
|
+
owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query", "weather_forecast", "currency_convert"]),
|
|
35381
35496
|
// web_extract: owner DM only, never team_room (spec §5.4, D5)
|
|
35382
|
-
|
|
35497
|
+
// Weather/currency spec §6.3 (U5): both utility tools in both audiences, each held behind utility_tools_v1 (TOOL_FEATURE).
|
|
35498
|
+
team_room: Object.freeze(["web_search", "weather_forecast", "currency_convert"])
|
|
35383
35499
|
});
|
|
35384
35500
|
function manifestSignedBytes(manifest) {
|
|
35385
35501
|
const domain = new TextEncoder().encode(TOOL_GRANTS_DOMAIN);
|
|
@@ -35703,20 +35819,30 @@ function bindTargets(raw, signed, equals) {
|
|
|
35703
35819
|
}
|
|
35704
35820
|
return { written, mismatches };
|
|
35705
35821
|
}
|
|
35706
|
-
|
|
35707
|
-
|
|
35822
|
+
var NO_FEATURES = Object.freeze(/* @__PURE__ */ new Set());
|
|
35823
|
+
var TOOL_FEATURE = Object.freeze({
|
|
35824
|
+
web_extract: "search_providers_v2",
|
|
35825
|
+
weather_forecast: "utility_tools_v1",
|
|
35826
|
+
currency_convert: "utility_tools_v1"
|
|
35827
|
+
});
|
|
35828
|
+
function toolAllowedByFeatures(tool, features) {
|
|
35829
|
+
if (!Object.hasOwn(TOOL_FEATURE, tool)) return true;
|
|
35830
|
+
return features.has(TOOL_FEATURE[tool]);
|
|
35831
|
+
}
|
|
35832
|
+
function signedManifestTools(manifest, audience, features) {
|
|
35833
|
+
return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && toolAllowedByFeatures(t, features));
|
|
35708
35834
|
}
|
|
35709
|
-
function manifestPolicyFor(manifest, switches,
|
|
35835
|
+
function manifestPolicyFor(manifest, switches, features) {
|
|
35710
35836
|
return {
|
|
35711
35837
|
v: 1,
|
|
35712
35838
|
manifest_tools: [.../* @__PURE__ */ new Set([
|
|
35713
|
-
...switches.ownerDm ? signedManifestTools(manifest, "owner_dm",
|
|
35714
|
-
...switches.room ? signedManifestTools(manifest, "team_room",
|
|
35839
|
+
...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", features) : [],
|
|
35840
|
+
...switches.room ? signedManifestTools(manifest, "team_room", features) : []
|
|
35715
35841
|
])].sort(),
|
|
35716
35842
|
secret_path_mode: "enforce"
|
|
35717
35843
|
};
|
|
35718
35844
|
}
|
|
35719
|
-
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true },
|
|
35845
|
+
function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, features = NO_FEATURES) {
|
|
35720
35846
|
const mismatches = /* @__PURE__ */ new Set();
|
|
35721
35847
|
const cfg = manifest.config;
|
|
35722
35848
|
const out = {};
|
|
@@ -35745,7 +35871,8 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35745
35871
|
for (const name of Object.keys(files)) {
|
|
35746
35872
|
if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
|
|
35747
35873
|
}
|
|
35748
|
-
const ownerDmTools = signedManifestTools(manifest, "owner_dm",
|
|
35874
|
+
const ownerDmTools = signedManifestTools(manifest, "owner_dm", features);
|
|
35875
|
+
const ownerDmSearchTools = ownerDmTools.filter((t) => t === "web_search" || t === "web_extract");
|
|
35749
35876
|
const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
|
|
35750
35877
|
const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
|
|
35751
35878
|
const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
|
|
@@ -35766,17 +35893,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35766
35893
|
const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
|
|
35767
35894
|
searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
|
|
35768
35895
|
}
|
|
35896
|
+
const pw = isObj2(sj?.weather) ? sj.weather : void 0;
|
|
35897
|
+
const weatherBlock = typeof pw?.keyFile === "string" && pw.keyFile !== "" ? { keyFile: pw.keyFile } : void 0;
|
|
35769
35898
|
const skillsDoc = {
|
|
35770
35899
|
endpoints: eps.written,
|
|
35771
35900
|
databases: dbs.written,
|
|
35772
35901
|
flags: [...cfg.flags],
|
|
35773
35902
|
...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
|
|
35774
35903
|
...searchBlock ? { search: searchBlock } : {},
|
|
35904
|
+
...weatherBlock ? { weather: weatherBlock } : {},
|
|
35775
35905
|
// D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
|
|
35776
35906
|
search_signed: cfg.search !== void 0,
|
|
35777
35907
|
// D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
|
|
35778
35908
|
// otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
|
|
35779
|
-
...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm &&
|
|
35909
|
+
...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmSearchTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
|
|
35780
35910
|
};
|
|
35781
35911
|
out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
|
|
35782
35912
|
const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
|
|
@@ -35807,7 +35937,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
|
|
|
35807
35937
|
mismatches.add("legacy_assign");
|
|
35808
35938
|
legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
|
|
35809
35939
|
}
|
|
35810
|
-
const policy = manifestPolicyFor(manifest, switches,
|
|
35940
|
+
const policy = manifestPolicyFor(manifest, switches, features);
|
|
35811
35941
|
return {
|
|
35812
35942
|
mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
|
|
35813
35943
|
targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
|
|
@@ -35950,7 +36080,11 @@ var TOOL_ROOM_CLASSES = Object.freeze({
|
|
|
35950
36080
|
// D5: spends the owner's key and sends URLs out
|
|
35951
36081
|
fetch_url: Object.freeze(["egress"]),
|
|
35952
36082
|
sql_query: Object.freeze(["owner_credential"]),
|
|
35953
|
-
http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
|
|
36083
|
+
http_request: Object.freeze(["owner_credential", "egress", "side_effect"]),
|
|
36084
|
+
// Weather/currency (plan audit r1; supersedes spec §8's values): both send room words to an outside host, and weather
|
|
36085
|
+
// spends the owner's key.
|
|
36086
|
+
weather_forecast: Object.freeze(["owner_credential", "egress"]),
|
|
36087
|
+
currency_convert: Object.freeze(["egress"])
|
|
35954
36088
|
});
|
|
35955
36089
|
var LIVE_MEMBER_STATUSES = /* @__PURE__ */ new Set(["active", "add_pending"]);
|
|
35956
36090
|
function isLiveRow(r) {
|
|
@@ -37764,7 +37898,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37764
37898
|
}
|
|
37765
37899
|
let bound;
|
|
37766
37900
|
try {
|
|
37767
|
-
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.
|
|
37901
|
+
bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.capabilityFeatures());
|
|
37768
37902
|
} catch (e) {
|
|
37769
37903
|
bound = null;
|
|
37770
37904
|
return this.bindFailed(manifest, payload.memory, e);
|
|
@@ -37785,7 +37919,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37785
37919
|
try {
|
|
37786
37920
|
this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
|
|
37787
37921
|
if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
|
|
37788
|
-
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.
|
|
37922
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.capabilityFeatures()));
|
|
37789
37923
|
} catch (e) {
|
|
37790
37924
|
this.manifestBinding = null;
|
|
37791
37925
|
throw e;
|
|
@@ -37839,6 +37973,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37839
37973
|
async postRuntimeConfigReport(base, version, skillsJson) {
|
|
37840
37974
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
37841
37975
|
const reportedSearchProviders = this.searchProvidersCapable();
|
|
37976
|
+
const reportedUtilityTools = this.utilityToolsCapable();
|
|
37842
37977
|
const report = {
|
|
37843
37978
|
applied_version: version,
|
|
37844
37979
|
paths: checkConfiguredPaths(skillsJson),
|
|
@@ -37856,6 +37991,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37856
37991
|
// Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
|
|
37857
37992
|
// the bridge withholds web_extract from the runtime.
|
|
37858
37993
|
has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
|
|
37994
|
+
// Weather/currency L-1, §8 item 5: AND the current utility_tools_v1, so the web never shows a usable weather key
|
|
37995
|
+
// while the bridge withholds weather_forecast from the runtime.
|
|
37996
|
+
has_weather_key: (local?.has_weather_key ?? false) && reportedUtilityTools,
|
|
37859
37997
|
// D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
|
|
37860
37998
|
...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
|
|
37861
37999
|
// Same idea, for the BYOK LLM key file: only the runtime can see
|
|
@@ -37903,7 +38041,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37903
38041
|
// D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
|
|
37904
38042
|
search_providers_v2: reportedSearchProviders,
|
|
37905
38043
|
// D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
|
|
37906
|
-
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
|
|
38044
|
+
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders),
|
|
38045
|
+
// Weather/currency §8 item 5: a separate key, false with no runtime attached.
|
|
38046
|
+
utility_tools_v1: reportedUtilityTools
|
|
37907
38047
|
}
|
|
37908
38048
|
};
|
|
37909
38049
|
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
@@ -37925,6 +38065,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37925
38065
|
}
|
|
37926
38066
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
37927
38067
|
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
38068
|
+
this.lastReportedUtilityTools = reportedUtilityTools;
|
|
37928
38069
|
if (version) writeServiceLog("info", "runtime_config.applied", { version });
|
|
37929
38070
|
else writeServiceLog("info", "runtime_config.legacy_memory_reported");
|
|
37930
38071
|
}
|
|
@@ -37994,7 +38135,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37994
38135
|
this.startedAt = Date.now();
|
|
37995
38136
|
await this.restoreRuntimeInbox();
|
|
37996
38137
|
await this.loadToolGrantState();
|
|
37997
|
-
this.recomputeManifestPolicy(
|
|
38138
|
+
this.recomputeManifestPolicy(NO_FEATURES);
|
|
37998
38139
|
try {
|
|
37999
38140
|
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
38000
38141
|
} catch (e) {
|
|
@@ -38977,13 +39118,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38977
39118
|
* absent, because the id is derived.
|
|
38978
39119
|
*/
|
|
38979
39120
|
async send(channelId, text, messageId, opts = {}) {
|
|
38980
|
-
const body = { kind: "text", text };
|
|
39121
|
+
const body = { kind: "text", text: opts.credit !== void 0 ? text + WEATHER_CREDIT_SUFFIX : text };
|
|
38981
39122
|
if (this.isIdempotentSend(messageId, opts)) return this.trackReplyWork(this.sendIdempotent(channelId, body, opts));
|
|
38982
39123
|
const id = resolveMessageId(messageId);
|
|
38983
39124
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
|
|
38984
39125
|
const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
|
|
38985
39126
|
await this.publishBody(channelId, body, id);
|
|
38986
|
-
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
|
|
39127
|
+
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture, text);
|
|
38987
39128
|
return { message_id: id, status: "sent" };
|
|
38988
39129
|
}
|
|
38989
39130
|
/** PR #716 review MEDIUM-5: register reply work so close() can wait for it. */
|
|
@@ -39002,7 +39143,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39002
39143
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
|
|
39003
39144
|
const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
|
|
39004
39145
|
await this.publishBody(channelId, body, id);
|
|
39005
|
-
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
|
|
39146
|
+
this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture, null);
|
|
39006
39147
|
return { message_id: id, status: "sent" };
|
|
39007
39148
|
}
|
|
39008
39149
|
/**
|
|
@@ -39135,18 +39276,24 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39135
39276
|
const meta = this.freshMeta(capChannel);
|
|
39136
39277
|
if (meta && !meta.is_dm) this.enforceRateFloor("room", capChannel);
|
|
39137
39278
|
}
|
|
39138
|
-
/**
|
|
39139
|
-
|
|
39279
|
+
/**
|
|
39280
|
+
* Bookkeeping after a CONFIRMED publish of `body` under `id`. `recordText` (weather/currency Task 17) is what a text
|
|
39281
|
+
* reply is RECORDED as: the published text minus any credit line. Every record site below reads `recorded`, never
|
|
39282
|
+
* `body.text`. Every production caller passes it (pinned by reply-credit.test.ts: text, or null for an a2a body);
|
|
39283
|
+
* undefined is left only for direct test calls of an uncredited body, where the published and record texts are equal.
|
|
39284
|
+
*/
|
|
39285
|
+
afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture, recordText) {
|
|
39286
|
+
const recorded = recordText ?? (recordText === void 0 && body.kind === "text" ? body.text : "");
|
|
39140
39287
|
if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
|
|
39141
39288
|
const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
|
|
39142
|
-
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(
|
|
39289
|
+
const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(recorded);
|
|
39143
39290
|
if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
|
|
39144
|
-
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(
|
|
39145
|
-
this.callBuffer.appendAgent(channelId,
|
|
39291
|
+
if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(recorded) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
|
|
39292
|
+
this.callBuffer.appendAgent(channelId, recorded, "reply");
|
|
39146
39293
|
}
|
|
39147
39294
|
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";
|
|
39148
|
-
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId,
|
|
39149
|
-
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId,
|
|
39295
|
+
if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, recorded, id, modality, purgeEpoch, origin);
|
|
39296
|
+
if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, recorded, id, replyCapture ?? null, origin);
|
|
39150
39297
|
}
|
|
39151
39298
|
/**
|
|
39152
39299
|
* Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
|
|
@@ -39224,14 +39371,14 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39224
39371
|
const k = replyIntentKey({ inReplyTo: opts.inReplyTo, idempotencyKey: opts.idempotencyKey, ordinal: () => 0 });
|
|
39225
39372
|
if (!k) throw new Error("idempotent send without a key");
|
|
39226
39373
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39227
|
-
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes);
|
|
39228
|
-
const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes });
|
|
39374
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit);
|
|
39375
|
+
const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null });
|
|
39229
39376
|
if (intent.bodyMac !== mac2 || intent.channelId !== channelId || intent.state === "skipped") {
|
|
39230
39377
|
throw new IdempotencyConflictError();
|
|
39231
39378
|
}
|
|
39232
39379
|
return k;
|
|
39233
39380
|
}
|
|
39234
|
-
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true);
|
|
39381
|
+
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null);
|
|
39235
39382
|
});
|
|
39236
39383
|
return this.publishIntent(key, opts);
|
|
39237
39384
|
}
|
|
@@ -39240,22 +39387,23 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39240
39387
|
* the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
|
|
39241
39388
|
* after a slot collision: the body must go to a NEW part.
|
|
39242
39389
|
*/
|
|
39243
|
-
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry) {
|
|
39390
|
+
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit) {
|
|
39391
|
+
const credit = replyCredit ?? void 0;
|
|
39244
39392
|
const scope = `reply:${inReplyTo}`;
|
|
39245
39393
|
let latest = null;
|
|
39246
39394
|
for (const intent of await this.store.listReplyIntents(inReplyTo)) {
|
|
39247
39395
|
const n = intent.scope === scope ? partOrdinal(intent.part) : null;
|
|
39248
39396
|
if (n !== null && (latest === null || n > latest.ordinal)) latest = { ordinal: n, intent };
|
|
39249
39397
|
}
|
|
39250
|
-
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes)) {
|
|
39398
|
+
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit)) {
|
|
39251
39399
|
return { scope, part: latest.intent.part, inReplyTo };
|
|
39252
39400
|
}
|
|
39253
39401
|
let ordinal = latest ? latest.ordinal + 1 : 0;
|
|
39254
39402
|
for (let i = 0; i < REPLY_MAX_PART_ADVANCES; i++, ordinal++) {
|
|
39255
39403
|
const k = { scope, part: `p:${ordinal}`, inReplyTo };
|
|
39256
39404
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39257
|
-
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes);
|
|
39258
|
-
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes });
|
|
39405
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit);
|
|
39406
|
+
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit });
|
|
39259
39407
|
if (created) return k;
|
|
39260
39408
|
if (matchRetry && intent.state !== "skipped" && intent.bodyMac === mac2 && intent.channelId === channelId) return k;
|
|
39261
39409
|
writeServiceLog("info", "reply.part_slot_taken", { attempt: i + 1 });
|
|
@@ -39300,6 +39448,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39300
39448
|
}
|
|
39301
39449
|
const bytes = intent.body;
|
|
39302
39450
|
const body = decodeReplyBody(bytes);
|
|
39451
|
+
const recordText = this.recordTextOf(intent.replyCredit, body);
|
|
39452
|
+
if (recordText === void 0) {
|
|
39453
|
+
writeServiceLog("error", "reply.intent_integrity_refused");
|
|
39454
|
+
this.store.deletePendingReplyIntent(key.scope, key.part);
|
|
39455
|
+
throw new ReplyIntentIntegrityError();
|
|
39456
|
+
}
|
|
39303
39457
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
|
|
39304
39458
|
const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
|
|
39305
39459
|
try {
|
|
@@ -39328,14 +39482,17 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39328
39482
|
const inReplyTo = intent.inReplyTo;
|
|
39329
39483
|
const next = await this.withReplyLock(
|
|
39330
39484
|
JSON.stringify(["scope", key.scope]),
|
|
39331
|
-
() =>
|
|
39485
|
+
() => (
|
|
39486
|
+
// r2 BLOCKER 1: the colliding row's OWN credit (verified above), never `opts`.
|
|
39487
|
+
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null)
|
|
39488
|
+
)
|
|
39332
39489
|
);
|
|
39333
39490
|
return this.publishIntent(next, opts, advances + 1);
|
|
39334
39491
|
}
|
|
39335
39492
|
throw e;
|
|
39336
39493
|
}
|
|
39337
39494
|
this.store.markReplyIntentSent(key.scope, key.part);
|
|
39338
|
-
this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish, replyCapture);
|
|
39495
|
+
this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish, replyCapture, recordText);
|
|
39339
39496
|
return { message_id: intent.replyMessageId, status: "sent" };
|
|
39340
39497
|
});
|
|
39341
39498
|
}
|
|
@@ -39345,9 +39502,21 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39345
39502
|
*/
|
|
39346
39503
|
intentVerifies(intent) {
|
|
39347
39504
|
if (!intent.body) return false;
|
|
39505
|
+
const credit = intent.replyCredit ?? null;
|
|
39506
|
+
if (credit !== null && !isReplyCredit(credit)) return false;
|
|
39348
39507
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, intent.channelId, intent);
|
|
39349
39508
|
if (id !== intent.replyMessageId) return false;
|
|
39350
|
-
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body) === intent.bodyMac;
|
|
39509
|
+
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0) === intent.bodyMac;
|
|
39510
|
+
}
|
|
39511
|
+
/**
|
|
39512
|
+
* Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
|
|
39513
|
+
* for an a2a body). "weatherapi": the text minus the exact credit suffix. Undefined: the row is refused (an unknown
|
|
39514
|
+
* credit, a credited a2a body, or a credited text without the exact suffix).
|
|
39515
|
+
*/
|
|
39516
|
+
recordTextOf(credit, body) {
|
|
39517
|
+
if (credit === null) return body.kind === "text" ? body.text : null;
|
|
39518
|
+
if (!isReplyCredit(credit) || body.kind !== "text" || !body.text.endsWith(WEATHER_CREDIT_SUFFIX)) return void 0;
|
|
39519
|
+
return body.text.slice(0, body.text.length - WEATHER_CREDIT_SUFFIX.length);
|
|
39351
39520
|
}
|
|
39352
39521
|
/**
|
|
39353
39522
|
* SEC-06 #18 item 3: a rate-limited reply retries the PUBLISH in-process
|
|
@@ -43471,7 +43640,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43471
43640
|
this.memory,
|
|
43472
43641
|
m.channelId,
|
|
43473
43642
|
m.envelope.sender_id,
|
|
43474
|
-
parsed.text,
|
|
43643
|
+
m.envelope.sender_id === this.cfg.identityId ? stripCreditSuffix(parsed.text) : parsed.text,
|
|
43475
43644
|
m.messageId,
|
|
43476
43645
|
m.voiceMode ? "voice" : "text",
|
|
43477
43646
|
purgeEpochAtTap
|
|
@@ -44269,21 +44438,49 @@ ${channelId}`;
|
|
|
44269
44438
|
*/
|
|
44270
44439
|
searchProvidersSockets = /* @__PURE__ */ new Map();
|
|
44271
44440
|
/**
|
|
44272
|
-
*
|
|
44273
|
-
*
|
|
44441
|
+
* Weather/currency U6 (H-1): the same AND-over-sockets state for utility_tools_v1, kept separately per socket, so a
|
|
44442
|
+
* runtime that declares search_providers_v2 but not utility_tools_v1 (0.14.0, 0.14.1) keeps web_extract and never
|
|
44443
|
+
* sees a utility name.
|
|
44274
44444
|
*/
|
|
44275
|
-
|
|
44445
|
+
utilityToolsSockets = /* @__PURE__ */ new Map();
|
|
44446
|
+
/**
|
|
44447
|
+
* The (search, utility) tuple the files were last recomputed for; null until the first socket observation, so that
|
|
44448
|
+
* observation always recomputes (final review I-1), even when it is non-capable. One trigger keyed on BOTH values.
|
|
44449
|
+
*/
|
|
44450
|
+
capabilityState = null;
|
|
44276
44451
|
searchCapabilityRefetchTimer = null;
|
|
44277
44452
|
/** When the current burst's first change happened (valid while the timer is set). */
|
|
44278
44453
|
searchCapabilityBurstStart = 0;
|
|
44279
44454
|
/** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
|
|
44280
44455
|
lastReportedSearchProviders = null;
|
|
44456
|
+
/** U6: the utility_tools_v1 value in the last report posted (null until one is). */
|
|
44457
|
+
lastReportedUtilityTools = null;
|
|
44281
44458
|
noteSearchProvidersSocket(token, declared) {
|
|
44282
44459
|
this.searchProvidersSockets.set(token, declared);
|
|
44283
|
-
this.
|
|
44460
|
+
this.onCapabilityMaybeChanged();
|
|
44284
44461
|
}
|
|
44285
44462
|
clearSearchProvidersSocket(token) {
|
|
44286
|
-
if (this.searchProvidersSockets.delete(token)) this.
|
|
44463
|
+
if (this.searchProvidersSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
44464
|
+
}
|
|
44465
|
+
noteUtilityToolsSocket(token, declared) {
|
|
44466
|
+
this.utilityToolsSockets.set(token, declared);
|
|
44467
|
+
this.onCapabilityMaybeChanged();
|
|
44468
|
+
}
|
|
44469
|
+
clearUtilityToolsSocket(token) {
|
|
44470
|
+
if (this.utilityToolsSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
44471
|
+
}
|
|
44472
|
+
/** U6: every attached runtime socket declared utility_tools_v1; no socket attached means absent (false). */
|
|
44473
|
+
utilityToolsCapable() {
|
|
44474
|
+
if (this.utilityToolsSockets.size === 0) return false;
|
|
44475
|
+
for (const v of this.utilityToolsSockets.values()) if (!v) return false;
|
|
44476
|
+
return true;
|
|
44477
|
+
}
|
|
44478
|
+
/** U6: the features every attached runtime socket declared, for the policy functions. */
|
|
44479
|
+
capabilityFeatures() {
|
|
44480
|
+
const out = /* @__PURE__ */ new Set();
|
|
44481
|
+
if (this.searchProvidersCapable()) out.add("search_providers_v2");
|
|
44482
|
+
if (this.utilityToolsCapable()) out.add("utility_tools_v1");
|
|
44483
|
+
return out;
|
|
44287
44484
|
}
|
|
44288
44485
|
/** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
|
|
44289
44486
|
searchProvidersCapable() {
|
|
@@ -44310,14 +44507,14 @@ ${channelId}`;
|
|
|
44310
44507
|
* D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
|
|
44311
44508
|
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
44312
44509
|
*/
|
|
44313
|
-
|
|
44314
|
-
const
|
|
44315
|
-
const prev = this.
|
|
44316
|
-
if (
|
|
44317
|
-
this.
|
|
44318
|
-
this.recomputeManifestPolicy(
|
|
44510
|
+
onCapabilityMaybeChanged() {
|
|
44511
|
+
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable() };
|
|
44512
|
+
const prev = this.capabilityState;
|
|
44513
|
+
if (prev !== null && prev.search === next.search && prev.utility === next.utility) return;
|
|
44514
|
+
this.capabilityState = next;
|
|
44515
|
+
this.recomputeManifestPolicy(this.capabilityFeatures());
|
|
44319
44516
|
if (this.closingDown) return;
|
|
44320
|
-
if (prev === null && !
|
|
44517
|
+
if (prev === null && !next.search && !next.utility) return;
|
|
44321
44518
|
const now = Date.now();
|
|
44322
44519
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
44323
44520
|
else this.searchCapabilityBurstStart = now;
|
|
@@ -44335,7 +44532,7 @@ ${channelId}`;
|
|
|
44335
44532
|
searchCapabilityRefetchNeeded() {
|
|
44336
44533
|
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
44337
44534
|
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
44338
|
-
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
|
|
44535
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable();
|
|
44339
44536
|
}
|
|
44340
44537
|
/**
|
|
44341
44538
|
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
@@ -44343,15 +44540,16 @@ ${channelId}`;
|
|
|
44343
44540
|
* lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
|
|
44344
44541
|
* Content-free; a write failure is logged and the debounced refetch tries again.
|
|
44345
44542
|
*/
|
|
44346
|
-
recomputeManifestPolicy(
|
|
44543
|
+
recomputeManifestPolicy(features) {
|
|
44347
44544
|
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
44348
44545
|
const manifest = pin !== null ? this.toolGrantManifest : null;
|
|
44349
44546
|
try {
|
|
44350
44547
|
if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
|
|
44351
|
-
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants },
|
|
44548
|
+
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, features));
|
|
44352
44549
|
return;
|
|
44353
44550
|
}
|
|
44354
|
-
|
|
44551
|
+
const withheld = new Set(Object.keys(TOOL_FEATURE).filter((t) => !toolAllowedByFeatures(t, features)));
|
|
44552
|
+
if (withheld.size === 0) return;
|
|
44355
44553
|
const path = join19(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
|
|
44356
44554
|
if (!existsSync5(path)) return;
|
|
44357
44555
|
let doc;
|
|
@@ -44361,10 +44559,10 @@ ${channelId}`;
|
|
|
44361
44559
|
return;
|
|
44362
44560
|
}
|
|
44363
44561
|
const tools = doc?.manifest_tools;
|
|
44364
|
-
if (!Array.isArray(tools) || !tools.
|
|
44562
|
+
if (!Array.isArray(tools) || !tools.some((t) => typeof t === "string" && withheld.has(t))) return;
|
|
44365
44563
|
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
|
|
44366
44564
|
v: 1,
|
|
44367
|
-
manifest_tools: tools.filter((t) => typeof t === "string" && t
|
|
44565
|
+
manifest_tools: tools.filter((t) => typeof t === "string" && !withheld.has(t)),
|
|
44368
44566
|
secret_path_mode: "enforce"
|
|
44369
44567
|
});
|
|
44370
44568
|
} catch (e) {
|
|
@@ -44755,6 +44953,9 @@ var BRIDGE_ERRORS = {
|
|
|
44755
44953
|
// C9 (spec §4.1.6 step 1): another copy of this agent took its SecureComs
|
|
44756
44954
|
// session (WS 4409), so this copy stopped. Every local route but /healthz.
|
|
44757
44955
|
session_superseded: { http: 503, message: "another copy of this agent took its session; this copy stopped" },
|
|
44956
|
+
// Weather/currency Task 17 (OQ-P1): a send frame's credit is not the closed value, or rides an a2a frame. TERMINAL:
|
|
44957
|
+
// the inbound is retired, so the model (and the weather call) never runs again for it.
|
|
44958
|
+
reply_credit_unknown: { http: 400, message: "reply credit not recognised; not sent" },
|
|
44758
44959
|
internal: { http: 500, message: "internal error" }
|
|
44759
44960
|
};
|
|
44760
44961
|
function errorBody(code, detail) {
|
|
@@ -44784,6 +44985,7 @@ function withConsumerGrants(m, perToolConsumer) {
|
|
|
44784
44985
|
}
|
|
44785
44986
|
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
44786
44987
|
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
44988
|
+
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
44787
44989
|
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
44788
44990
|
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
44789
44991
|
team_room: Object.freeze(["current_time"])
|
|
@@ -44815,6 +45017,15 @@ function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
|
44815
45017
|
if (kept.length === a.tool_grants.length) return m;
|
|
44816
45018
|
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
44817
45019
|
}
|
|
45020
|
+
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
45021
|
+
function withUtilityCapability(m, consumerHasFeature) {
|
|
45022
|
+
if (consumerHasFeature) return m;
|
|
45023
|
+
const a = m.authorization;
|
|
45024
|
+
if (!a) return m;
|
|
45025
|
+
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
45026
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
45027
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
45028
|
+
}
|
|
44818
45029
|
|
|
44819
45030
|
// src/http-server.ts
|
|
44820
45031
|
function writeSse(res, msgs, onWriteError) {
|
|
@@ -44838,7 +45049,7 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
44838
45049
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
44839
45050
|
const withheld = agent.harnessGrantsWithheld();
|
|
44840
45051
|
const provider = agent.signedSearchProvider();
|
|
44841
|
-
const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
|
|
45052
|
+
const left3 = writeSse(res, batch.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)), onWriteError);
|
|
44842
45053
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
44843
45054
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
44844
45055
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -44870,7 +45081,7 @@ var OTHER_CAP = 8 * 1024;
|
|
|
44870
45081
|
var a2aSchema = z3.object({
|
|
44871
45082
|
a2a_version: z3.string().min(1),
|
|
44872
45083
|
kind: z3.enum(["message", "task", "artifact"]),
|
|
44873
|
-
object: z3.record(z3.unknown())
|
|
45084
|
+
object: z3.record(z3.string(), z3.unknown())
|
|
44874
45085
|
});
|
|
44875
45086
|
var sendAttachmentSchema = z3.object({
|
|
44876
45087
|
channel_id: z3.string().min(1),
|
|
@@ -45112,7 +45323,7 @@ function createHttpServer(agent, opts) {
|
|
|
45112
45323
|
const drained = await agent.drain(channel, wait);
|
|
45113
45324
|
const withheld = agent.harnessGrantsWithheld();
|
|
45114
45325
|
const provider = agent.signedSearchProvider();
|
|
45115
|
-
return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
|
|
45326
|
+
return json(res, 200, drained.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)));
|
|
45116
45327
|
}
|
|
45117
45328
|
if (method === "GET" && path === "/v1/events") {
|
|
45118
45329
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -45287,6 +45498,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45287
45498
|
perToolGrants: false,
|
|
45288
45499
|
manifestGrants: false,
|
|
45289
45500
|
searchProviders: false,
|
|
45501
|
+
utilityTools: false,
|
|
45290
45502
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
45291
45503
|
deliveryOpen: helloWaitMs <= 0,
|
|
45292
45504
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -45340,14 +45552,14 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45340
45552
|
continue;
|
|
45341
45553
|
}
|
|
45342
45554
|
const out = withConsumerNotes(
|
|
45343
|
-
withSearchCapability(
|
|
45555
|
+
withUtilityCapability(withSearchCapability(
|
|
45344
45556
|
withManifestCapability(
|
|
45345
45557
|
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
45346
45558
|
session.manifestGrants
|
|
45347
45559
|
),
|
|
45348
45560
|
session.searchProviders,
|
|
45349
45561
|
agent.signedSearchProvider()
|
|
45350
|
-
),
|
|
45562
|
+
), session.utilityTools),
|
|
45351
45563
|
batch[i],
|
|
45352
45564
|
session,
|
|
45353
45565
|
agent
|
|
@@ -45404,6 +45616,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45404
45616
|
if (session.perToolGrants) agent.clearPerToolConsumer(session);
|
|
45405
45617
|
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
45406
45618
|
agent.clearSearchProvidersSocket(session);
|
|
45619
|
+
agent.clearUtilityToolsSocket(session);
|
|
45407
45620
|
};
|
|
45408
45621
|
current = { sock, cleanup, sweepStale };
|
|
45409
45622
|
sock.on("close", cleanup);
|
|
@@ -45444,7 +45657,10 @@ function sendJson(sock, body) {
|
|
|
45444
45657
|
} catch {
|
|
45445
45658
|
}
|
|
45446
45659
|
}
|
|
45660
|
+
var ReplyCreditUnknownError = class extends Error {
|
|
45661
|
+
};
|
|
45447
45662
|
function sendErrorCode(e) {
|
|
45663
|
+
if (e instanceof ReplyCreditUnknownError) return "reply_credit_unknown";
|
|
45448
45664
|
if (e instanceof ChannelPendingError) return "channel_pending";
|
|
45449
45665
|
if (e instanceof RateLimitedError) return "rate_limited";
|
|
45450
45666
|
if (e instanceof AgentPausedSendError) return "agent_paused";
|
|
@@ -45505,6 +45721,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45505
45721
|
agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
|
|
45506
45722
|
session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
|
|
45507
45723
|
agent.noteSearchProvidersSocket(session, session.searchProviders);
|
|
45724
|
+
session.utilityTools = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(UTILITY_TOOLS_FEATURE);
|
|
45725
|
+
agent.noteUtilityToolsSocket(session, session.utilityTools);
|
|
45508
45726
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
45509
45727
|
agent.noteNotesConsumer();
|
|
45510
45728
|
}
|
|
@@ -45512,7 +45730,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45512
45730
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
45513
45731
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
45514
45732
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
45515
|
-
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
|
|
45733
|
+
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] });
|
|
45516
45734
|
if (!session.deliveryOpen) {
|
|
45517
45735
|
session.deliveryOpen = true;
|
|
45518
45736
|
flush();
|
|
@@ -45541,7 +45759,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45541
45759
|
}
|
|
45542
45760
|
const replyFailed = (e, inReplyTo) => {
|
|
45543
45761
|
const code = sendErrorCode(e);
|
|
45544
|
-
|
|
45762
|
+
const terminal = code === "agent_paused" || code === "send_forbidden" || code === "reply_credit_unknown";
|
|
45763
|
+
if (terminal && typeof inReplyTo === "string") {
|
|
45545
45764
|
inflight.delete(inReplyTo);
|
|
45546
45765
|
session.deliveryToken.delete(inReplyTo);
|
|
45547
45766
|
session.shownRecall.delete(inReplyTo);
|
|
@@ -45549,7 +45768,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45549
45768
|
agent.ackRuntimeMessages([inReplyTo]);
|
|
45550
45769
|
}
|
|
45551
45770
|
sendJson(sock, { ...wsErrorFrame(code), ...typeof inReplyTo === "string" ? { in_reply_to: inReplyTo } : {} });
|
|
45552
|
-
if (
|
|
45771
|
+
if (terminal) flush();
|
|
45553
45772
|
};
|
|
45554
45773
|
if (m.type === "send") {
|
|
45555
45774
|
if (typeof m.channel_id !== "string" || !m.channel_id) {
|
|
@@ -45566,8 +45785,14 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45566
45785
|
}
|
|
45567
45786
|
const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
|
|
45568
45787
|
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);
|
|
45788
|
+
if (m.credit !== void 0 && (m.a2a !== void 0 || !isReplyCredit(m.credit))) {
|
|
45789
|
+
writeServiceLog("error", "reply.credit_unknown");
|
|
45790
|
+
replyFailed(new ReplyCreditUnknownError(), m.in_reply_to);
|
|
45791
|
+
return;
|
|
45792
|
+
}
|
|
45569
45793
|
const sendOpts = {
|
|
45570
45794
|
agentTurn: isAgentTurn(agent, m),
|
|
45795
|
+
...isReplyCredit(m.credit) ? { credit: m.credit } : {},
|
|
45571
45796
|
...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
|
|
45572
45797
|
...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
|
|
45573
45798
|
...callBufferReply ? { callBufferReply: true } : {}
|