@securecoms/agent-bridge 0.35.0 → 0.37.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 +33 -0
- package/dist/bin.js +1779 -145
- package/dist/bridge-agent.d.ts +286 -8
- package/dist/consumer-grants.d.ts +28 -0
- package/dist/grants/binding.d.ts +2 -1
- package/dist/grants/effective.d.ts +8 -0
- package/dist/grants/manifest.d.ts +14 -0
- package/dist/grants/mcp-catalog.d.ts +23 -0
- package/dist/grants/mcp-issuance.d.ts +34 -0
- package/dist/grants/mcp-policy.d.ts +82 -0
- package/dist/grants/mcp-section.d.ts +94 -0
- package/dist/grants/owner-turn.d.ts +40 -0
- package/dist/grants/tool-status.d.ts +79 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/index.js +1774 -142
- package/dist/mcp-catalog-message.d.ts +79 -0
- package/dist/mcp-catalog-rate.d.ts +35 -0
- package/dist/reply-intent.d.ts +1 -1
- package/dist/service/runtime-config-dir.d.ts +55 -0
- package/dist/tool-status-sender.d.ts +71 -0
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +1 -1
- package/package.json +2 -1
package/dist/bin.js
CHANGED
|
@@ -3084,7 +3084,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3084
3084
|
if (outputLen !== expectedLen)
|
|
3085
3085
|
throw new Error(`hash.outputLen must be ${expectedLen}, got ${outputLen}`);
|
|
3086
3086
|
}
|
|
3087
|
-
const
|
|
3087
|
+
const randomBytes14 = opts.randomBytes === void 0 ? randomBytes4 : opts.randomBytes;
|
|
3088
3088
|
const adjustScalarBytes3 = opts.adjustScalarBytes === void 0 ? (bytes) => bytes : opts.adjustScalarBytes;
|
|
3089
3089
|
const domain = opts.domain === void 0 ? (data, ctx, phflag) => {
|
|
3090
3090
|
abool2(phflag, "phflag");
|
|
@@ -3170,7 +3170,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3170
3170
|
seed: _size
|
|
3171
3171
|
};
|
|
3172
3172
|
function randomSecretKey(seed) {
|
|
3173
|
-
seed = seed === void 0 ?
|
|
3173
|
+
seed = seed === void 0 ? randomBytes14(lengths.seed) : seed;
|
|
3174
3174
|
return abytes3(seed, lengths.seed, "seed");
|
|
3175
3175
|
}
|
|
3176
3176
|
function isValidSecretKey(key) {
|
|
@@ -6419,7 +6419,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
6419
6419
|
bits2int_modN: "function"
|
|
6420
6420
|
});
|
|
6421
6421
|
ecdsaOpts = Object.assign({}, ecdsaOpts);
|
|
6422
|
-
const
|
|
6422
|
+
const randomBytes14 = ecdsaOpts.randomBytes === void 0 ? randomBytes4 : ecdsaOpts.randomBytes;
|
|
6423
6423
|
const hmac5 = ecdsaOpts.hmac === void 0 ? (key, msg) => hmac(hash_, key, msg) : ecdsaOpts.hmac;
|
|
6424
6424
|
const { Fp: Fp3, Fn: Fn3 } = Point;
|
|
6425
6425
|
const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn3;
|
|
@@ -6565,7 +6565,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
6565
6565
|
throw new Error("invalid private key");
|
|
6566
6566
|
const seedArgs = [int2octets(d), int2octets(h1int)];
|
|
6567
6567
|
if (extraEntropy != null && extraEntropy !== false) {
|
|
6568
|
-
const e = extraEntropy === true ?
|
|
6568
|
+
const e = extraEntropy === true ? randomBytes14(lengths.secretKey) : extraEntropy;
|
|
6569
6569
|
seedArgs.push(abytes3(e, void 0, "extraEntropy"));
|
|
6570
6570
|
}
|
|
6571
6571
|
const seed = concatBytes4(...seedArgs);
|
|
@@ -7555,7 +7555,7 @@ __export(import_bundle_exports, {
|
|
|
7555
7555
|
ModelImportError: () => ModelImportError,
|
|
7556
7556
|
importModelBundle: () => importModelBundle
|
|
7557
7557
|
});
|
|
7558
|
-
import { createHash as
|
|
7558
|
+
import { createHash as createHash7, randomBytes as randomBytes13 } from "node:crypto";
|
|
7559
7559
|
import { readFileSync as readFileSync16, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7560
7560
|
import { join as join25 } from "node:path";
|
|
7561
7561
|
async function importModelBundle(srcDir, root3, m) {
|
|
@@ -7578,14 +7578,14 @@ async function importModelBundle(srcDir, root3, m) {
|
|
|
7578
7578
|
} catch {
|
|
7579
7579
|
throw new ModelImportError(`${f.name} is missing from the import directory`);
|
|
7580
7580
|
}
|
|
7581
|
-
if (bytes.length !== f.size ||
|
|
7581
|
+
if (bytes.length !== f.size || createHash7("sha256").update(bytes).digest("hex") !== f.sha256) throw mismatch;
|
|
7582
7582
|
verified.push({ f, bytes });
|
|
7583
7583
|
}
|
|
7584
7584
|
const imported = [];
|
|
7585
7585
|
for (const { f, bytes } of verified) {
|
|
7586
7586
|
try {
|
|
7587
7587
|
const pdir = plainDirChain(root3, [".partial"], { create: true, fromModelsDir: true, mode: 448 });
|
|
7588
|
-
const partial = join25(pdir,
|
|
7588
|
+
const partial = join25(pdir, randomBytes13(12).toString("hex"));
|
|
7589
7589
|
writeFileSync4(partial, bytes, { mode: 384, flag: "wx" });
|
|
7590
7590
|
installVerified(root3, m.modelId, f, partial, { replace: true });
|
|
7591
7591
|
} catch (e) {
|
|
@@ -19161,8 +19161,8 @@ function sameRef(a, b) {
|
|
|
19161
19161
|
// ../connect-sdk-ts/dist/groupkey/own-removal.js
|
|
19162
19162
|
async function probeOwnRemoval(mgr, commitBytes) {
|
|
19163
19163
|
try {
|
|
19164
|
-
const
|
|
19165
|
-
if (typeof
|
|
19164
|
+
const own2 = mgr.state?.privatePath?.leafIndex;
|
|
19165
|
+
if (typeof own2 !== "number")
|
|
19166
19166
|
return null;
|
|
19167
19167
|
if (!hoistedTsMlsMatchesCrypto(mgr))
|
|
19168
19168
|
return null;
|
|
@@ -19187,7 +19187,7 @@ async function probeOwnRemoval(mgr, commitBytes) {
|
|
|
19187
19187
|
committer = typeof incoming.senderLeafIndex === "number" ? Number(incoming.senderLeafIndex) : null;
|
|
19188
19188
|
for (const p of incoming.proposals) {
|
|
19189
19189
|
const prop = p.proposal;
|
|
19190
|
-
if (prop.proposalType === "remove" && Number(prop.remove?.removed) ===
|
|
19190
|
+
if (prop.proposalType === "remove" && Number(prop.remove?.removed) === own2)
|
|
19191
19191
|
removesOwn = true;
|
|
19192
19192
|
}
|
|
19193
19193
|
}
|
|
@@ -19195,7 +19195,7 @@ async function probeOwnRemoval(mgr, commitBytes) {
|
|
|
19195
19195
|
}, cs);
|
|
19196
19196
|
if (!sawCommit || !removesOwn)
|
|
19197
19197
|
return null;
|
|
19198
|
-
return { ownLeafIndex:
|
|
19198
|
+
return { ownLeafIndex: own2, committerLeafIndex: committer };
|
|
19199
19199
|
} catch {
|
|
19200
19200
|
return null;
|
|
19201
19201
|
}
|
|
@@ -20057,6 +20057,33 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
20057
20057
|
}
|
|
20058
20058
|
return { ownLeafIndex: this.ownLeafIndex(mgr), leaves };
|
|
20059
20059
|
}
|
|
20060
|
+
/**
|
|
20061
|
+
* MCP client (bridge Task 16; audit H-2, round 2 H-D): a SYNCHRONOUS, read-only snapshot of the WARM in-memory
|
|
20062
|
+
* manager for `channelId`: its group id, its epoch and EVERY non-blank leaf, reported exactly as
|
|
20063
|
+
* {@link leafTreeSummary} reports them (non-basic credentials included, identity null, #771 HIGH-B). All three are
|
|
20064
|
+
* read from ONE `state` object (MLSGroupManager replaces `state` whole on every commit), with no await, so they belong
|
|
20065
|
+
* to the same epoch and no commit can land between them.
|
|
20066
|
+
*
|
|
20067
|
+
* `null` when no manager is warm: it never loads from the store, never takes the channel lock, never touches the
|
|
20068
|
+
* network and never mutates. A caller treating `null` as "cannot tell" fails closed.
|
|
20069
|
+
*/
|
|
20070
|
+
leafSnapshotCached(channelId) {
|
|
20071
|
+
const state = this.managers.get(channelId)?.state;
|
|
20072
|
+
const tree = state?.ratchetTree;
|
|
20073
|
+
if (!state || !tree)
|
|
20074
|
+
return null;
|
|
20075
|
+
const leaves = [];
|
|
20076
|
+
const numLeaves = Math.floor((tree.length + 1) / 2);
|
|
20077
|
+
for (let i = 0; i < numLeaves; i++) {
|
|
20078
|
+
const node = tree[i * 2];
|
|
20079
|
+
if (!node || node.nodeType !== "leaf")
|
|
20080
|
+
continue;
|
|
20081
|
+
const cred = getCredentialFromLeafIndex(tree, toLeafIndex2(i));
|
|
20082
|
+
const basic = cred.credentialType === "basic";
|
|
20083
|
+
leaves.push({ leafIndex: i, basic, identity: basic ? new TextDecoder().decode(cred.identity) : null });
|
|
20084
|
+
}
|
|
20085
|
+
return { groupId: new TextDecoder().decode(state.groupContext.groupId), epoch: Number(state.groupContext.epoch), leaves };
|
|
20086
|
+
}
|
|
20060
20087
|
async fetchDeviceLeafCredential(deviceId) {
|
|
20061
20088
|
const kps = await this.o.backend.fetchKeyPackages([deviceId]);
|
|
20062
20089
|
const hex = kps[deviceId];
|
|
@@ -27390,10 +27417,12 @@ function openSchema(db) {
|
|
|
27390
27417
|
ensureRuntimeReplyIntent(db);
|
|
27391
27418
|
ensureRuntimeReplyIntentEnvelopeGroup(db);
|
|
27392
27419
|
ensureRuntimeReplyIntentCredit(db);
|
|
27420
|
+
ensureRuntimeReplyIntentMcpTurn(db);
|
|
27393
27421
|
ensureGroupLink(db);
|
|
27394
27422
|
ensureMlsCustody(db);
|
|
27395
27423
|
ensureToolGrantTables(db);
|
|
27396
27424
|
ensureToolGrantAppliedTable(db);
|
|
27425
|
+
ensureToolGrantMcpSeenTable(db);
|
|
27397
27426
|
ensureRoomSearchCapTable(db);
|
|
27398
27427
|
ensureVoiceChannelParentTable(db);
|
|
27399
27428
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
@@ -27483,6 +27512,19 @@ function ensureRuntimeReplyIntentCredit(db) {
|
|
|
27483
27512
|
throw e;
|
|
27484
27513
|
}
|
|
27485
27514
|
}
|
|
27515
|
+
function ensureRuntimeReplyIntentMcpTurn(db) {
|
|
27516
|
+
const hasColumn = () => db.prepare("PRAGMA table_info(runtime_reply_intent)").all().some((c) => c.name === "mcp_turn");
|
|
27517
|
+
const cols = db.prepare("PRAGMA table_info(runtime_reply_intent)").all();
|
|
27518
|
+
if (cols.length === 0 || cols.some((c) => c.name === "mcp_turn"))
|
|
27519
|
+
return;
|
|
27520
|
+
try {
|
|
27521
|
+
db.exec("ALTER TABLE runtime_reply_intent ADD COLUMN mcp_turn INTEGER NOT NULL DEFAULT 0");
|
|
27522
|
+
} catch (e) {
|
|
27523
|
+
if (hasColumn())
|
|
27524
|
+
return;
|
|
27525
|
+
throw e;
|
|
27526
|
+
}
|
|
27527
|
+
}
|
|
27486
27528
|
var RECALL_STATE_COLUMNS = [
|
|
27487
27529
|
["recall_enabled", "INTEGER NOT NULL DEFAULT 0"],
|
|
27488
27530
|
["recall_mode_epoch", "INTEGER NOT NULL DEFAULT 0"],
|
|
@@ -27594,6 +27636,15 @@ function ensureToolGrantAppliedTable(db) {
|
|
|
27594
27636
|
);
|
|
27595
27637
|
`);
|
|
27596
27638
|
}
|
|
27639
|
+
function ensureToolGrantMcpSeenTable(db) {
|
|
27640
|
+
db.exec(`
|
|
27641
|
+
CREATE TABLE IF NOT EXISTS tool_grant_mcp_seen (
|
|
27642
|
+
identity_id TEXT PRIMARY KEY,
|
|
27643
|
+
sealed_state BLOB NOT NULL,
|
|
27644
|
+
updated_at INTEGER NOT NULL
|
|
27645
|
+
);
|
|
27646
|
+
`);
|
|
27647
|
+
}
|
|
27597
27648
|
function ensureVoiceChannelParentTable(db) {
|
|
27598
27649
|
db.exec(`
|
|
27599
27650
|
CREATE TABLE IF NOT EXISTS voice_channel_parent (
|
|
@@ -28534,8 +28585,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28534
28585
|
this.db.prepare("DELETE FROM runtime_reply_intent WHERE identity_id=? AND updated_at < ?").run(this.id, now - RUNTIME_REPLY_INTENT_RETENTION_MS);
|
|
28535
28586
|
const info = this.db.prepare(`INSERT OR IGNORE INTO runtime_reply_intent
|
|
28536
28587
|
(identity_id, scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
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);
|
|
28588
|
+
sealed_body, sealed_envelope, state, created_at, updated_at, reply_credit, mcp_turn)
|
|
28589
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?, ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now, input.replyCredit ?? null, input.mcpTurn === true ? 1 : 0);
|
|
28539
28590
|
const count = this.db.prepare("SELECT COUNT(*) AS n FROM runtime_reply_intent WHERE identity_id=?").get(this.id);
|
|
28540
28591
|
if (count.n > RUNTIME_REPLY_INTENT_MAX) {
|
|
28541
28592
|
this.db.prepare(`DELETE FROM runtime_reply_intent
|
|
@@ -28545,7 +28596,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28545
28596
|
)`).run(this.id, this.id, RUNTIME_REPLY_INTENT_MAX);
|
|
28546
28597
|
}
|
|
28547
28598
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
28548
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
28599
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
|
|
28549
28600
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, input.scope, input.part);
|
|
28550
28601
|
if (!row)
|
|
28551
28602
|
throw new Error("reply intent vanished after claim");
|
|
@@ -28554,7 +28605,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28554
28605
|
/** SEC-06 #18: every intent answering `inReplyTo`, in claim order. */
|
|
28555
28606
|
async listReplyIntents(inReplyTo) {
|
|
28556
28607
|
const rows = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
28557
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
28608
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
|
|
28558
28609
|
FROM runtime_reply_intent WHERE identity_id=? AND in_reply_to=?
|
|
28559
28610
|
ORDER BY created_at ASC, rowid ASC`).all(this.id, inReplyTo);
|
|
28560
28611
|
const out = [];
|
|
@@ -28565,7 +28616,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28565
28616
|
/** SEC-06 #18: re-read one intent by its key (null when absent). */
|
|
28566
28617
|
async getReplyIntent(scope, part) {
|
|
28567
28618
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
28568
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
28619
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
|
|
28569
28620
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, scope, part);
|
|
28570
28621
|
return row ? this.openReplyIntent(row) : null;
|
|
28571
28622
|
}
|
|
@@ -28640,7 +28691,9 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28640
28691
|
envelopeGroupId: envelopeGroupFor(envelope, await openOrNull(row.sealed_envelope_group)),
|
|
28641
28692
|
state: row.state === "sent" ? "sent" : row.state === "skipped" ? "skipped" : "pending",
|
|
28642
28693
|
createdAt: Number(row.created_at),
|
|
28643
|
-
replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null
|
|
28694
|
+
replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null,
|
|
28695
|
+
// Any value other than exactly 1 (0, NULL, a missing column) is false; the bridge's MAC decides what a 1 means.
|
|
28696
|
+
mcpTurn: Number(row.mcp_turn ?? 0) === 1
|
|
28644
28697
|
};
|
|
28645
28698
|
}
|
|
28646
28699
|
async recordMemory(input) {
|
|
@@ -29212,10 +29265,10 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
29212
29265
|
const r = this.db.prepare("SELECT owner_size_bytes AS b, owner_item_count AS n FROM agent_memory_state WHERE identity_id=?").get(this.id);
|
|
29213
29266
|
if (r && Number(r.b) >= 0 && Number(r.n) >= 0)
|
|
29214
29267
|
return { items: Number(r.n), bytes: Number(r.b) };
|
|
29215
|
-
const
|
|
29268
|
+
const own2 = this.scopeLoad("i.scope_kind='owner_dm'");
|
|
29216
29269
|
this.ensureStateRow();
|
|
29217
|
-
this.db.prepare("UPDATE agent_memory_state SET owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(
|
|
29218
|
-
return
|
|
29270
|
+
this.db.prepare("UPDATE agent_memory_state SET owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(own2.bytes, own2.items, this.id);
|
|
29271
|
+
return own2;
|
|
29219
29272
|
}
|
|
29220
29273
|
/** Inside a transaction: delete one origin group (the row, or the parent and its chunks) and its token rows; returns the ids and the accounts it held. */
|
|
29221
29274
|
deleteItemGroup(firstItemId) {
|
|
@@ -29389,8 +29442,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
29389
29442
|
const o2 = this.overCap();
|
|
29390
29443
|
if (!o2.overItems && !o2.overBytes)
|
|
29391
29444
|
break;
|
|
29392
|
-
const
|
|
29393
|
-
const protectOwner = (!o2.overItems ||
|
|
29445
|
+
const own2 = this.ownerLoad();
|
|
29446
|
+
const protectOwner = (!o2.overItems || own2.items < this.itemCap * OWNER_DM_FLOOR_SHARE) && (!o2.overBytes || own2.bytes < this.byteCap * OWNER_DM_FLOOR_SHARE);
|
|
29394
29447
|
const v = (protectOwner ? this.oldestAutoGroup({ notOwnerDm: true }) : void 0) ?? this.oldestAutoGroup({});
|
|
29395
29448
|
if (v === void 0 || !drop(v))
|
|
29396
29449
|
break;
|
|
@@ -29417,8 +29470,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
29417
29470
|
FROM agent_memory_item i WHERE identity_id=?`).get(this.id);
|
|
29418
29471
|
const tk = this.db.prepare("SELECT COUNT(*) AS n FROM agent_memory_token WHERE identity_id=?").get(this.id);
|
|
29419
29472
|
const size = Number(it.b) + Number(it.rows) * ITEM_ROW_OVERHEAD_BYTES + Number(tk.n) * TOKEN_ROW_OVERHEAD_BYTES;
|
|
29420
|
-
const
|
|
29421
|
-
this.db.prepare("UPDATE agent_memory_state SET size_bytes=?, item_count=?, owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(size, Number(it.n),
|
|
29473
|
+
const own2 = this.scopeLoad("i.scope_kind='owner_dm'");
|
|
29474
|
+
this.db.prepare("UPDATE agent_memory_state SET size_bytes=?, item_count=?, owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(size, Number(it.n), own2.bytes, own2.items, this.id);
|
|
29422
29475
|
}
|
|
29423
29476
|
/** Inside a transaction: delete every unified row of the identity and move each channel's content epoch. */
|
|
29424
29477
|
deleteAllItems() {
|
|
@@ -30234,9 +30287,40 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
30234
30287
|
dropToolGrantAppliedExcept(pinKeyB64) {
|
|
30235
30288
|
return Number(this.db.prepare("DELETE FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
|
|
30236
30289
|
}
|
|
30290
|
+
// MCP client Phase 1, Task 14 (OQ-3 ruling item 3): the sticky R-3 flag, per agent identity.
|
|
30291
|
+
/**
|
|
30292
|
+
* False when never written. A row that does not open, or opens to anything but this identity's own record, THROWS:
|
|
30293
|
+
* the bridge then holds R-3 (fail closed). Never returns false for an unreadable row.
|
|
30294
|
+
*/
|
|
30295
|
+
async loadMcpSeen() {
|
|
30296
|
+
const row = this.db.prepare("SELECT sealed_state FROM tool_grant_mcp_seen WHERE identity_id=?").get(this.id);
|
|
30297
|
+
if (!row)
|
|
30298
|
+
return false;
|
|
30299
|
+
const v = await this.unsealJson(row.sealed_state);
|
|
30300
|
+
if (v === null || typeof v !== "object" || v.identity_id !== this.id || typeof v.seen !== "boolean") {
|
|
30301
|
+
throw new MlsStoreSealError("tool_grant_mcp_seen: unexpected record");
|
|
30302
|
+
}
|
|
30303
|
+
return v.seen;
|
|
30304
|
+
}
|
|
30305
|
+
/** The sealed value names its identity, so a blob copied to another agent's row does not read as that agent's. */
|
|
30306
|
+
async saveMcpSeen(seen) {
|
|
30307
|
+
const sealed = await this.sealJson({ identity_id: this.id, seen });
|
|
30308
|
+
this.db.prepare(`INSERT INTO tool_grant_mcp_seen (identity_id, sealed_state, updated_at) VALUES (?, ?, ?)
|
|
30309
|
+
ON CONFLICT(identity_id) DO UPDATE SET sealed_state = excluded.sealed_state, updated_at = excluded.updated_at`).run(this.id, sealed, Date.now());
|
|
30310
|
+
}
|
|
30237
30311
|
hasOwnerMessage(channelId, messageId) {
|
|
30238
30312
|
return this.db.prepare("SELECT 1 FROM owner_replay WHERE identity_id=? AND channel_id=? AND message_id=?").get(this.id, channelId, messageId) !== void 0;
|
|
30239
30313
|
}
|
|
30314
|
+
/**
|
|
30315
|
+
* MCP client Phase 1, Task 18 (audit M-3): the owner replay row for a verified owner message that is NEVER staged for
|
|
30316
|
+
* the runtime (an `mcp_catalog_request`), so a redelivery of it is classed `replay`, after a restart too. The same row
|
|
30317
|
+
* stageRuntimeInbox writes in its staging transaction; insert-if-absent; pruned by pruneOwnerReplay like any other.
|
|
30318
|
+
*/
|
|
30319
|
+
recordOwnerMessage(channelId, messageId, groupId, epoch, processedAt = Date.now()) {
|
|
30320
|
+
this.db.prepare(`INSERT OR IGNORE INTO owner_replay
|
|
30321
|
+
(identity_id, channel_id, message_id, group_id, epoch, processed_at)
|
|
30322
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, channelId, messageId, groupId, epoch, processedAt);
|
|
30323
|
+
}
|
|
30240
30324
|
isOwnerEpochClosed(channelId, groupId, epoch) {
|
|
30241
30325
|
return this.db.prepare("SELECT 1 FROM owner_replay_closed_epoch WHERE identity_id=? AND channel_id=? AND group_id=? AND epoch=?").get(this.id, channelId, groupId, epoch) !== void 0;
|
|
30242
30326
|
}
|
|
@@ -30381,6 +30465,7 @@ function checkGroupLink(v) {
|
|
|
30381
30465
|
}
|
|
30382
30466
|
|
|
30383
30467
|
// src/bridge-agent.ts
|
|
30468
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
30384
30469
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
30385
30470
|
|
|
30386
30471
|
// src/reply-intent.ts
|
|
@@ -30410,8 +30495,9 @@ function decodeReplyBody(bytes) {
|
|
|
30410
30495
|
if (b?.kind === "a2a" && b.a2a && typeof b.a2a === "object") return b;
|
|
30411
30496
|
throw new Error("reply intent body is malformed");
|
|
30412
30497
|
}
|
|
30413
|
-
function replyBodyMac(key, replyId, channelId, body, credit) {
|
|
30414
|
-
|
|
30498
|
+
function replyBodyMac(key, replyId, channelId, body, credit, mcpTurn) {
|
|
30499
|
+
const label = mcpTurn === true ? ["reply-body", replyId, channelId, credit ?? null, "mcp_turn"] : credit === void 0 ? ["reply-body", replyId, channelId] : ["reply-body", replyId, channelId, credit];
|
|
30500
|
+
return createHmac("sha256", key).update(JSON.stringify(label)).update("\0").update(body).digest("hex");
|
|
30415
30501
|
}
|
|
30416
30502
|
function partOrdinal(part) {
|
|
30417
30503
|
const m = /^p:(\d+)$/.exec(part);
|
|
@@ -32117,8 +32203,8 @@ var MembershipPurgeSweeper = class {
|
|
|
32117
32203
|
const dms = /* @__PURE__ */ new Set();
|
|
32118
32204
|
for (const c of this.d.conversations()) {
|
|
32119
32205
|
if (stopping()) return { purged: [], held: 0, absent: 0 };
|
|
32120
|
-
const
|
|
32121
|
-
const channels =
|
|
32206
|
+
const own2 = this.d.enclaveOf(c);
|
|
32207
|
+
const channels = own2 === void 0 ? void 0 : listed.get(own2);
|
|
32122
32208
|
if (channels === void 0) continue;
|
|
32123
32209
|
if (channels.has(c)) {
|
|
32124
32210
|
this.d.clearAbsence(c);
|
|
@@ -35293,7 +35379,7 @@ function assertManagedName(name) {
|
|
|
35293
35379
|
}
|
|
35294
35380
|
function isLocalOnlyName(name) {
|
|
35295
35381
|
const n = name.toLowerCase().replace(/[. ]+$/, "");
|
|
35296
|
-
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE;
|
|
35382
|
+
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE || n === MCP_POLICY_FILE;
|
|
35297
35383
|
}
|
|
35298
35384
|
var MANIFEST_POLICY_FILE = "manifest-policy.json";
|
|
35299
35385
|
function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
|
|
@@ -35312,6 +35398,28 @@ function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
|
|
|
35312
35398
|
}
|
|
35313
35399
|
atomicWrite(join19(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
|
|
35314
35400
|
}
|
|
35401
|
+
var MCP_POLICY_FILE = "mcp-policy.json";
|
|
35402
|
+
var MCP_POLICY_TOP_STATES = Object.freeze([
|
|
35403
|
+
"ok",
|
|
35404
|
+
"not_entitled",
|
|
35405
|
+
"kill_switch",
|
|
35406
|
+
"suspended",
|
|
35407
|
+
"no_config",
|
|
35408
|
+
"switch_off",
|
|
35409
|
+
"no_manifest",
|
|
35410
|
+
"malformed",
|
|
35411
|
+
"no_capability",
|
|
35412
|
+
"section_absent"
|
|
35413
|
+
]);
|
|
35414
|
+
function writeMcpPolicy(home, identityId, doc, deps = REAL_FS) {
|
|
35415
|
+
const parent3 = join19(home, ".securecoms", "runtime-config");
|
|
35416
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
35417
|
+
for (const d of [join19(home, ".securecoms"), parent3, dir]) {
|
|
35418
|
+
deps.mkdir(d, 448);
|
|
35419
|
+
deps.chmod(d, 448);
|
|
35420
|
+
}
|
|
35421
|
+
atomicWrite(join19(dir, MCP_POLICY_FILE), JSON.stringify(doc) + "\n", deps);
|
|
35422
|
+
}
|
|
35315
35423
|
function atomicWrite(path, body, deps) {
|
|
35316
35424
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
35317
35425
|
deps.writeFile(tmp, body, 384);
|
|
@@ -35589,6 +35697,195 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
|
35589
35697
|
|
|
35590
35698
|
// src/grants/manifest.ts
|
|
35591
35699
|
import { ed25519 as ed255195 } from "@noble/curves/ed25519";
|
|
35700
|
+
|
|
35701
|
+
// src/grants/mcp-section.ts
|
|
35702
|
+
var MCP_MAX_SERVERS = 8;
|
|
35703
|
+
var MCP_MAX_TOOLS_PER_SERVER = 64;
|
|
35704
|
+
var MCP_MAX_SIGNED_TOOLS = 160;
|
|
35705
|
+
var MCP_MAX_REMOVED = 8;
|
|
35706
|
+
var SECURECOMS_CIMD_CLIENT_ID = "https://app.securecoms.ai/oauth/securecoms-agent-client.json";
|
|
35707
|
+
var MCP_SERVER_ID = /^[a-z0-9-]{1,24}$/;
|
|
35708
|
+
var MCP_TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
35709
|
+
var VENDOR_KEY = /^[a-z0-9_]{1,32}$/;
|
|
35710
|
+
var MCP_RESERVED_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
35711
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
35712
|
+
var PRINTABLE_ASCII = /^[\x21-\x7e]{1,256}$/;
|
|
35713
|
+
var MCP_GRANT = /^mcp:([a-z0-9-]{1,24})\/([A-Za-z0-9_.-]{1,64})$/;
|
|
35714
|
+
var isBag = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35715
|
+
var keysExactly = (o, keys) => {
|
|
35716
|
+
const have = Object.keys(o);
|
|
35717
|
+
return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(o, k));
|
|
35718
|
+
};
|
|
35719
|
+
function canonicalHttpsUrl(v) {
|
|
35720
|
+
if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
|
|
35721
|
+
const u = new URL(v);
|
|
35722
|
+
return u.protocol === "https:" && u.username === "" && u.password === "" && u.href === v;
|
|
35723
|
+
}
|
|
35724
|
+
function httpsIssuer(v) {
|
|
35725
|
+
if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
|
|
35726
|
+
const u = new URL(v);
|
|
35727
|
+
return u.protocol === "https:" && u.username === "" && u.password === "" && u.search === "" && u.hash === "" && (u.href === v || u.href === `${v}/`);
|
|
35728
|
+
}
|
|
35729
|
+
var MCP_AUTH_KEYS = Object.freeze({
|
|
35730
|
+
oauth_securecoms_cimd: Object.freeze(["mode", "issuer", "client"]),
|
|
35731
|
+
oauth_customer: Object.freeze(["mode", "issuer", "client", "client_id"]),
|
|
35732
|
+
key: Object.freeze(["mode"])
|
|
35733
|
+
});
|
|
35734
|
+
function parseAuth(v) {
|
|
35735
|
+
if (!isBag(v)) return null;
|
|
35736
|
+
if (v.mode === "key") return keysExactly(v, MCP_AUTH_KEYS.key) ? Object.freeze({ mode: "key" }) : null;
|
|
35737
|
+
if (v.mode !== "oauth" || !httpsIssuer(v.issuer)) return null;
|
|
35738
|
+
if (v.client === "securecoms_cimd") {
|
|
35739
|
+
if (!keysExactly(v, MCP_AUTH_KEYS.oauth_securecoms_cimd)) return null;
|
|
35740
|
+
return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "securecoms_cimd" });
|
|
35741
|
+
}
|
|
35742
|
+
if (v.client === "customer") {
|
|
35743
|
+
if (!keysExactly(v, MCP_AUTH_KEYS.oauth_customer)) return null;
|
|
35744
|
+
if (typeof v.client_id !== "string" || !PRINTABLE_ASCII.test(v.client_id)) return null;
|
|
35745
|
+
return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "customer", client_id: v.client_id });
|
|
35746
|
+
}
|
|
35747
|
+
return null;
|
|
35748
|
+
}
|
|
35749
|
+
function parseTools(v) {
|
|
35750
|
+
if (!isBag(v)) return null;
|
|
35751
|
+
const entries = Object.entries(v);
|
|
35752
|
+
if (entries.length > MCP_MAX_TOOLS_PER_SERVER) return null;
|
|
35753
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
35754
|
+
for (const [name, t] of entries) {
|
|
35755
|
+
if (!MCP_TOOL_NAME.test(name) || MCP_RESERVED_NAMES.has(name) || !isBag(t) || !keysExactly(t, ["sha256", "class"])) return null;
|
|
35756
|
+
if (typeof t.sha256 !== "string" || !HEX64.test(t.sha256) || t.class !== "read") return null;
|
|
35757
|
+
out[name] = Object.freeze({ sha256: t.sha256, class: "read" });
|
|
35758
|
+
}
|
|
35759
|
+
return out;
|
|
35760
|
+
}
|
|
35761
|
+
function parseServer(v) {
|
|
35762
|
+
if (!isBag(v) || !keysExactly(v, ["vendor", "url", "auth", "tools"])) return null;
|
|
35763
|
+
if (typeof v.vendor !== "string" || !VENDOR_KEY.test(v.vendor) || !canonicalHttpsUrl(v.url)) return null;
|
|
35764
|
+
const auth = parseAuth(v.auth);
|
|
35765
|
+
const tools = parseTools(v.tools);
|
|
35766
|
+
if (auth === null || tools === null) return null;
|
|
35767
|
+
return Object.freeze({ vendor: v.vendor, url: v.url, auth, tools: Object.freeze(tools) });
|
|
35768
|
+
}
|
|
35769
|
+
function parseRemoved(v, servers) {
|
|
35770
|
+
if (v === "*") return Object.keys(servers).length === 0 ? "*" : null;
|
|
35771
|
+
if (!Array.isArray(v) || v.length < 1 || v.length > MCP_MAX_REMOVED) return null;
|
|
35772
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35773
|
+
for (const id of v) {
|
|
35774
|
+
if (typeof id !== "string" || !MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
|
|
35775
|
+
if (seen.has(id) || Object.prototype.hasOwnProperty.call(servers, id)) return null;
|
|
35776
|
+
seen.add(id);
|
|
35777
|
+
}
|
|
35778
|
+
return Object.freeze([...seen]);
|
|
35779
|
+
}
|
|
35780
|
+
var NOT_REMOVED = Object.freeze([]);
|
|
35781
|
+
function parseMcp(v) {
|
|
35782
|
+
if (!isBag(v) || !(keysExactly(v, ["servers"]) || keysExactly(v, ["servers", "removed"])) || !isBag(v.servers)) return null;
|
|
35783
|
+
const entries = Object.entries(v.servers);
|
|
35784
|
+
if (entries.length > MCP_MAX_SERVERS) return null;
|
|
35785
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
35786
|
+
let signedTools = 0;
|
|
35787
|
+
for (const [id, raw] of entries) {
|
|
35788
|
+
if (!MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
|
|
35789
|
+
const s = parseServer(raw);
|
|
35790
|
+
if (s === null) return null;
|
|
35791
|
+
signedTools += Object.keys(s.tools).length;
|
|
35792
|
+
if (signedTools > MCP_MAX_SIGNED_TOOLS) return null;
|
|
35793
|
+
servers[id] = s;
|
|
35794
|
+
}
|
|
35795
|
+
let removed = NOT_REMOVED;
|
|
35796
|
+
if (Object.prototype.hasOwnProperty.call(v, "removed")) {
|
|
35797
|
+
const r = parseRemoved(v.removed, servers);
|
|
35798
|
+
if (r === null) return null;
|
|
35799
|
+
removed = r;
|
|
35800
|
+
}
|
|
35801
|
+
return Object.freeze({ servers: Object.freeze(servers), removed });
|
|
35802
|
+
}
|
|
35803
|
+
function mcpServerCatalogRow(s, catalog) {
|
|
35804
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
|
|
35805
|
+
const row = catalog[s.vendor];
|
|
35806
|
+
if (s.url !== row.readOnlyUrl) return null;
|
|
35807
|
+
if (s.auth.mode === "oauth" && s.auth.issuer !== row.issuer) return null;
|
|
35808
|
+
return row;
|
|
35809
|
+
}
|
|
35810
|
+
function parseMcpGrant(name) {
|
|
35811
|
+
const g = MCP_GRANT.exec(name);
|
|
35812
|
+
return g === null ? null : { serverId: g[1], tool: g[2] };
|
|
35813
|
+
}
|
|
35814
|
+
function mcpGrantWithinCeiling(name, mcp, catalog) {
|
|
35815
|
+
const g = parseMcpGrant(name);
|
|
35816
|
+
if (g === null || mcp === null || !Object.prototype.hasOwnProperty.call(mcp.servers, g.serverId)) return false;
|
|
35817
|
+
const s = mcp.servers[g.serverId];
|
|
35818
|
+
if (!Object.prototype.hasOwnProperty.call(s.tools, g.tool)) return false;
|
|
35819
|
+
const row = mcpServerCatalogRow(s, catalog);
|
|
35820
|
+
return row !== null && row.readTools.includes(g.tool);
|
|
35821
|
+
}
|
|
35822
|
+
|
|
35823
|
+
// src/grants/mcp-catalog.ts
|
|
35824
|
+
var LINEAR = Object.freeze({
|
|
35825
|
+
displayName: "Linear",
|
|
35826
|
+
host: "mcp.linear.app",
|
|
35827
|
+
readOnlyUrl: "https://mcp.linear.app/mcp/readonly",
|
|
35828
|
+
readTools: Object.freeze([
|
|
35829
|
+
"get_attachment",
|
|
35830
|
+
"list_agent_skills",
|
|
35831
|
+
"get_agent_skill",
|
|
35832
|
+
"list_comments",
|
|
35833
|
+
"list_cycles",
|
|
35834
|
+
"list_custom_views",
|
|
35835
|
+
"get_document",
|
|
35836
|
+
"list_documents",
|
|
35837
|
+
"extract_images",
|
|
35838
|
+
"get_issue",
|
|
35839
|
+
"list_issue_statuses",
|
|
35840
|
+
"get_issue_status",
|
|
35841
|
+
"list_issue_labels",
|
|
35842
|
+
"list_projects",
|
|
35843
|
+
"get_project",
|
|
35844
|
+
"list_project_labels",
|
|
35845
|
+
"list_release_pipelines",
|
|
35846
|
+
"list_releases",
|
|
35847
|
+
"get_release",
|
|
35848
|
+
"list_release_notes",
|
|
35849
|
+
"get_release_note",
|
|
35850
|
+
"list_milestones",
|
|
35851
|
+
"get_milestone",
|
|
35852
|
+
"get_notifications",
|
|
35853
|
+
"list_teams",
|
|
35854
|
+
"get_team",
|
|
35855
|
+
"get_triage_responsibility",
|
|
35856
|
+
"list_templates",
|
|
35857
|
+
"get_template",
|
|
35858
|
+
"list_users",
|
|
35859
|
+
"get_user",
|
|
35860
|
+
"get_workspace",
|
|
35861
|
+
"search_documentation",
|
|
35862
|
+
"get_diff",
|
|
35863
|
+
"list_diffs",
|
|
35864
|
+
"get_diff_threads",
|
|
35865
|
+
"list_initiatives",
|
|
35866
|
+
"get_initiative",
|
|
35867
|
+
"list_initiative_labels",
|
|
35868
|
+
"get_status_updates"
|
|
35869
|
+
]),
|
|
35870
|
+
scopes: Object.freeze(["read"]),
|
|
35871
|
+
issuer: "https://mcp.linear.app",
|
|
35872
|
+
authorizationEndpoint: "https://mcp.linear.app/authorize",
|
|
35873
|
+
tokenEndpoint: "https://mcp.linear.app/token",
|
|
35874
|
+
revocationEndpoint: "https://mcp.linear.app/token",
|
|
35875
|
+
docs: Object.freeze({
|
|
35876
|
+
host: "https://linear.app/docs/mcp",
|
|
35877
|
+
readOnlyUrl: "https://linear.app/docs/mcp",
|
|
35878
|
+
readTools: "https://linear.app/docs/mcp",
|
|
35879
|
+
scopes: "https://linear.app/docs/mcp",
|
|
35880
|
+
issuer: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35881
|
+
authorizationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35882
|
+
tokenEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35883
|
+
revocationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server"
|
|
35884
|
+
})
|
|
35885
|
+
});
|
|
35886
|
+
var MCP_CATALOG = Object.freeze({ linear: LINEAR });
|
|
35887
|
+
|
|
35888
|
+
// src/grants/manifest.ts
|
|
35592
35889
|
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
35593
35890
|
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
35594
35891
|
owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query", "weather_forecast", "currency_convert"]),
|
|
@@ -35613,9 +35910,9 @@ function wellTyped(v) {
|
|
|
35613
35910
|
return false;
|
|
35614
35911
|
}
|
|
35615
35912
|
var WRAPPER_KEYS = ["algo", "manifest", "signature"];
|
|
35616
|
-
var
|
|
35617
|
-
var
|
|
35618
|
-
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") &&
|
|
35913
|
+
var HEX642 = /^[0-9a-f]{64}$/;
|
|
35914
|
+
var PRINTABLE_ASCII2 = /^[\x21-\x7e]+$/;
|
|
35915
|
+
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII2.test(v);
|
|
35619
35916
|
var exactKeys = (o, required, optional = []) => required.every((k) => k in o) && Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
|
|
35620
35917
|
var isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
35621
35918
|
function parseEndpoints(v) {
|
|
@@ -35627,7 +35924,7 @@ function parseEndpoints(v) {
|
|
|
35627
35924
|
const e = { ...raw };
|
|
35628
35925
|
if (e.headersFile === "") delete e.headersFile;
|
|
35629
35926
|
if (!exactKeys(e, ["baseUrl"], ["headersFile"])) return null;
|
|
35630
|
-
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !
|
|
35927
|
+
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII2.test(e.baseUrl)) return null;
|
|
35631
35928
|
let url;
|
|
35632
35929
|
try {
|
|
35633
35930
|
url = new URL(e.baseUrl);
|
|
@@ -35658,8 +35955,8 @@ function parseDatabases(v) {
|
|
|
35658
35955
|
function parseConfig(v) {
|
|
35659
35956
|
if (!isPlainObject(v)) return null;
|
|
35660
35957
|
if (!exactKeys(v, ["persona_sha256", "flags", "memory", "legacy_assign_sha256"], ["llm", "search", "tools_allow"])) return null;
|
|
35661
|
-
if (typeof v.persona_sha256 !== "string" || !
|
|
35662
|
-
if (typeof v.legacy_assign_sha256 !== "string" || !
|
|
35958
|
+
if (typeof v.persona_sha256 !== "string" || !HEX642.test(v.persona_sha256)) return null;
|
|
35959
|
+
if (typeof v.legacy_assign_sha256 !== "string" || !HEX642.test(v.legacy_assign_sha256)) return null;
|
|
35663
35960
|
if (!isStringArray(v.flags)) return null;
|
|
35664
35961
|
if ("tools_allow" in v && !isStringArray(v.tools_allow)) return null;
|
|
35665
35962
|
if ("llm" in v) {
|
|
@@ -35703,7 +36000,7 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35703
36000
|
const m = raw.manifest;
|
|
35704
36001
|
if (!isPlainObject(m)) return { state: "malformed" };
|
|
35705
36002
|
if (m.type !== "securecoms.tool-grants" || m.v !== 1) return { state: "malformed" };
|
|
35706
|
-
if (!wellTyped(m)) return { state: "malformed" };
|
|
36003
|
+
if (!wellTyped(Object.fromEntries(Object.entries(m).filter(([k]) => k !== "mcp")))) return { state: "malformed" };
|
|
35707
36004
|
if (typeof m.agent_identity_id !== "string" || typeof m.owner_identity_id !== "string") return { state: "malformed" };
|
|
35708
36005
|
for (const k of ["owner_dm_channel_id", "owner_dm_relay_identity_id"]) {
|
|
35709
36006
|
if (k in m && typeof m[k] !== "string") return { state: "malformed" };
|
|
@@ -35733,6 +36030,11 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35733
36030
|
if (counter === ctx.storedCounter) return { state: "unchanged" };
|
|
35734
36031
|
if (counter < ctx.storedCounter) return { state: "rollback" };
|
|
35735
36032
|
}
|
|
36033
|
+
const rawOwnerDm = m.grants.owner_dm;
|
|
36034
|
+
const signedMcpPresent = Array.isArray(rawOwnerDm) && rawOwnerDm.some((t) => t.startsWith("mcp:"));
|
|
36035
|
+
const mcpKey = "mcp" in m ? "present" : "absent";
|
|
36036
|
+
const mcp = mcpKey === "present" ? parseMcp(m.mcp) : null;
|
|
36037
|
+
const catalog = ctx.mcpCatalog ?? MCP_CATALOG;
|
|
35736
36038
|
let droppedUnknown = false;
|
|
35737
36039
|
const grants = {};
|
|
35738
36040
|
for (const [audience, list] of Object.entries(m.grants)) {
|
|
@@ -35741,7 +36043,7 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35741
36043
|
continue;
|
|
35742
36044
|
}
|
|
35743
36045
|
const known = MANIFEST_KNOWN_TOOLS[audience];
|
|
35744
|
-
const kept = list.filter((t) => known.includes(t));
|
|
36046
|
+
const kept = list.filter((t) => known.includes(t) || audience === "owner_dm" && mcpGrantWithinCeiling(t, mcp, catalog));
|
|
35745
36047
|
if (kept.length !== list.length) droppedUnknown = true;
|
|
35746
36048
|
grants[audience] = Object.freeze(kept);
|
|
35747
36049
|
}
|
|
@@ -35756,7 +36058,10 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35756
36058
|
grants: Object.freeze(grants),
|
|
35757
36059
|
endpoints: Object.freeze(endpoints),
|
|
35758
36060
|
databases: Object.freeze(databases),
|
|
35759
|
-
config
|
|
36061
|
+
config,
|
|
36062
|
+
mcp,
|
|
36063
|
+
mcpKey,
|
|
36064
|
+
signedMcpPresent
|
|
35760
36065
|
});
|
|
35761
36066
|
return { state: "ok", manifest, droppedUnknown, wrapper: raw };
|
|
35762
36067
|
}
|
|
@@ -36077,21 +36382,32 @@ var AGENT_TOTAL_KEY = "agent-total";
|
|
|
36077
36382
|
// src/grants/effective.ts
|
|
36078
36383
|
var TOOL_GRANT_CODE_CEILING = MANIFEST_KNOWN_TOOLS;
|
|
36079
36384
|
var CURRENT_TIME = Object.freeze({ tool: "current_time", mode: "read" });
|
|
36385
|
+
var NO_WITHHELD = Object.freeze([]);
|
|
36080
36386
|
function effectiveToolGrants(i) {
|
|
36081
|
-
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false };
|
|
36082
|
-
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false };
|
|
36387
|
+
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
36388
|
+
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
36083
36389
|
const ceiling = TOOL_GRANT_CODE_CEILING[i.audience];
|
|
36084
36390
|
const listed = new Set(i.manifest.grants[i.audience] ?? []);
|
|
36085
36391
|
const signed = ceiling.filter((t) => listed.has(t));
|
|
36086
36392
|
if (!i.eligible || !i.manifestSwitchOn) {
|
|
36087
36393
|
const fallback = i.base.filter((g) => g.tool !== "current_time" && signed.includes(g.tool));
|
|
36088
|
-
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false };
|
|
36394
|
+
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
36089
36395
|
}
|
|
36090
36396
|
let withheld = false;
|
|
36397
|
+
const dropped = [];
|
|
36091
36398
|
const tools = signed.filter((t) => {
|
|
36092
|
-
if (i.disabled.has(t))
|
|
36093
|
-
|
|
36094
|
-
|
|
36399
|
+
if (i.disabled.has(t)) {
|
|
36400
|
+
dropped.push(Object.freeze({ tool: t, why: "disabled_by_owner" }));
|
|
36401
|
+
return false;
|
|
36402
|
+
}
|
|
36403
|
+
if (t === "http_request" && i.signedTargets.endpoints === 0) {
|
|
36404
|
+
dropped.push(Object.freeze({ tool: t, why: "no_endpoint" }));
|
|
36405
|
+
return false;
|
|
36406
|
+
}
|
|
36407
|
+
if (t === "sql_query" && i.signedTargets.databases === 0) {
|
|
36408
|
+
dropped.push(Object.freeze({ tool: t, why: "no_database" }));
|
|
36409
|
+
return false;
|
|
36410
|
+
}
|
|
36095
36411
|
if (i.audience === "team_room" && t === "web_search" && i.roomSearchCapped) {
|
|
36096
36412
|
withheld = true;
|
|
36097
36413
|
return false;
|
|
@@ -36101,7 +36417,8 @@ function effectiveToolGrants(i) {
|
|
|
36101
36417
|
return {
|
|
36102
36418
|
grants: Object.freeze([CURRENT_TIME, ...tools.map((tool) => Object.freeze({ tool, mode: "read", source: "manifest" }))]),
|
|
36103
36419
|
source: "manifest",
|
|
36104
|
-
withheldBySearchCap: withheld
|
|
36420
|
+
withheldBySearchCap: withheld,
|
|
36421
|
+
withheld: Object.freeze(dropped)
|
|
36105
36422
|
};
|
|
36106
36423
|
}
|
|
36107
36424
|
|
|
@@ -36406,6 +36723,200 @@ function classifyVoiceFrame(f) {
|
|
|
36406
36723
|
return b.text.trim() === VOICE_CALL_START_MARKER ? "greeting" : "drop";
|
|
36407
36724
|
}
|
|
36408
36725
|
|
|
36726
|
+
// src/grants/tool-status.ts
|
|
36727
|
+
var TOOL_STATUS_ALLOWLIST = [
|
|
36728
|
+
"current_time",
|
|
36729
|
+
"web_search",
|
|
36730
|
+
"web_extract",
|
|
36731
|
+
"fetch_url",
|
|
36732
|
+
"http_request",
|
|
36733
|
+
"sql_query",
|
|
36734
|
+
"weather_forecast",
|
|
36735
|
+
"currency_convert"
|
|
36736
|
+
];
|
|
36737
|
+
var ALLOW2 = new Set(TOOL_STATUS_ALLOWLIST);
|
|
36738
|
+
var TOOL_STATUS_BUCKET_MS = 9e5;
|
|
36739
|
+
function bucketStartAt(ms) {
|
|
36740
|
+
return new Date(Math.floor(ms / TOOL_STATUS_BUCKET_MS) * TOOL_STATUS_BUCKET_MS).toISOString().slice(0, 16) + "Z";
|
|
36741
|
+
}
|
|
36742
|
+
function bucketEndMs(at) {
|
|
36743
|
+
return Date.parse(at) + TOOL_STATUS_BUCKET_MS;
|
|
36744
|
+
}
|
|
36745
|
+
var SIGNATURE_FAILURES = /* @__PURE__ */ new Set(["bad_signature", "malformed", "unknown_algo", "no_signature"]);
|
|
36746
|
+
function ownerDmCause(i) {
|
|
36747
|
+
try {
|
|
36748
|
+
const v = i.verdict;
|
|
36749
|
+
if (v.kind === "failed" && v.reason === "replay") return null;
|
|
36750
|
+
if (v.kind !== "verified" && v.kind !== "constants_only" && v.kind !== "failed") return null;
|
|
36751
|
+
const failedReason = v.kind === "failed" ? v.reason : void 0;
|
|
36752
|
+
const blocked = i.killSwitchOff || i.pinMissingOrAnomalous || i.senderNotOwner || i.stale || v.kind === "failed";
|
|
36753
|
+
const eligible = v.kind === "verified" && i.signedDmChannel === i.channelId && i.boundNow && i.consumerAttached;
|
|
36754
|
+
const source = blocked ? "none" : !i.manifestPresent ? "constant" : !eligible || i.manifestSwitchOff ? "fallback" : "manifest";
|
|
36755
|
+
const at = (cause, sub) => sub === void 0 ? { source, cause } : { source, cause, sub };
|
|
36756
|
+
if (i.killSwitchOff) return at("owner_dm_unavailable", "kill_switch");
|
|
36757
|
+
if (i.manifestSwitchOff) return at("manifest_switch_off");
|
|
36758
|
+
if (i.pinMissingOrAnomalous) return at("owner_dm_unavailable", "pin");
|
|
36759
|
+
if (i.senderNotOwner) return at("owner_dm_unavailable", "sender_not_owner");
|
|
36760
|
+
if (i.stale) return at("owner_dm_unavailable", "stale");
|
|
36761
|
+
if (failedReason === "not_owner_dm_leaves") return at("unexpected_member_in_dm");
|
|
36762
|
+
if (failedReason !== void 0 && SIGNATURE_FAILURES.has(failedReason)) return at("owner_turn_failed");
|
|
36763
|
+
if (failedReason === "verify_error") return at("agent_state_error");
|
|
36764
|
+
if (failedReason === "wrong_dm_channel" || v.kind === "verified" && i.manifestPresent && i.signedDmChannel !== void 0 && i.signedDmChannel !== i.channelId) {
|
|
36765
|
+
return at("dm_channel_mismatch");
|
|
36766
|
+
}
|
|
36767
|
+
if (failedReason !== void 0) return null;
|
|
36768
|
+
if (i.manifestPresent && i.signedDmChannel === void 0) return at("dm_not_linked");
|
|
36769
|
+
if (v.kind === "constants_only") return at("unsigned_relay_in_dm");
|
|
36770
|
+
if (!i.manifestPresent || !i.boundNow) return at("manifest_not_bound");
|
|
36771
|
+
if (!i.consumerAttached) return at("no_runtime_consumer");
|
|
36772
|
+
return source === "manifest" ? { source } : null;
|
|
36773
|
+
} catch {
|
|
36774
|
+
return null;
|
|
36775
|
+
}
|
|
36776
|
+
}
|
|
36777
|
+
function sameTuple(a, b) {
|
|
36778
|
+
if (!a || !b) return false;
|
|
36779
|
+
return a.source === b.source && a.cause === b.cause && a.sub === b.sub && a.relay === b.relay && a.mcp_issued === b.mcp_issued && a.counter === b.counter && a.issued.length === b.issued.length && a.issued.every((t, k) => t === b.issued[k]) && a.withheld.length === b.withheld.length && a.withheld.every((w, k) => w.tool === b.withheld[k].tool && w.why === b.withheld[k].why);
|
|
36780
|
+
}
|
|
36781
|
+
function sanitizeIssued(names) {
|
|
36782
|
+
return [...new Set(names.filter((n) => ALLOW2.has(n)))].sort();
|
|
36783
|
+
}
|
|
36784
|
+
|
|
36785
|
+
// src/tool-status-sender.ts
|
|
36786
|
+
var TOOL_STATUS_JITTER_MS = 6e4;
|
|
36787
|
+
var RETRY_AFTER_FLOOR_MS = 6e4;
|
|
36788
|
+
function defaultToolStatusSetTimer(fn, ms) {
|
|
36789
|
+
const t = setTimeout(fn, ms);
|
|
36790
|
+
t.unref?.();
|
|
36791
|
+
return t;
|
|
36792
|
+
}
|
|
36793
|
+
function defaultToolStatusClearTimer(h) {
|
|
36794
|
+
clearTimeout(h);
|
|
36795
|
+
}
|
|
36796
|
+
function retryAfterMs(raw) {
|
|
36797
|
+
if (typeof raw !== "string" || !/^\d{1,9}$/.test(raw.trim())) return RETRY_AFTER_FLOOR_MS;
|
|
36798
|
+
return Math.max(RETRY_AFTER_FLOOR_MS, Number(raw.trim()) * 1e3);
|
|
36799
|
+
}
|
|
36800
|
+
var ToolStatusSender = class {
|
|
36801
|
+
postFn;
|
|
36802
|
+
now;
|
|
36803
|
+
setTimer;
|
|
36804
|
+
clearTimer;
|
|
36805
|
+
random;
|
|
36806
|
+
onFailure;
|
|
36807
|
+
/** The latest noted record while its bucket is still open. */
|
|
36808
|
+
open = null;
|
|
36809
|
+
/** The latest noted record whose bucket has closed: the only record an attempt or carry() may send. */
|
|
36810
|
+
closed = null;
|
|
36811
|
+
lastPosted = null;
|
|
36812
|
+
terminalFor = null;
|
|
36813
|
+
notBefore = Number.NEGATIVE_INFINITY;
|
|
36814
|
+
timer = null;
|
|
36815
|
+
inFlight = false;
|
|
36816
|
+
shut = false;
|
|
36817
|
+
constructor(o) {
|
|
36818
|
+
this.postFn = o.post;
|
|
36819
|
+
this.now = o.now ?? (() => Date.now());
|
|
36820
|
+
this.setTimer = o.setTimer ?? defaultToolStatusSetTimer;
|
|
36821
|
+
this.clearTimer = o.clearTimer ?? defaultToolStatusClearTimer;
|
|
36822
|
+
this.random = o.random ?? Math.random;
|
|
36823
|
+
this.onFailure = o.onFailure ?? (() => {
|
|
36824
|
+
});
|
|
36825
|
+
}
|
|
36826
|
+
note(rec) {
|
|
36827
|
+
if (this.shut) return;
|
|
36828
|
+
try {
|
|
36829
|
+
this.promote(this.now());
|
|
36830
|
+
this.open = rec;
|
|
36831
|
+
this.schedule();
|
|
36832
|
+
} catch {
|
|
36833
|
+
}
|
|
36834
|
+
}
|
|
36835
|
+
/** Ruling M-2: what the apply-time report carries. The closed record only; an open bucket is never revealed early. */
|
|
36836
|
+
carry() {
|
|
36837
|
+
try {
|
|
36838
|
+
this.promote(this.now());
|
|
36839
|
+
} catch {
|
|
36840
|
+
}
|
|
36841
|
+
return this.closed;
|
|
36842
|
+
}
|
|
36843
|
+
/** R2-M1: the record belongs to an owner who is no longer the manifest's; forget it and what was posted. */
|
|
36844
|
+
drop() {
|
|
36845
|
+
this.open = null;
|
|
36846
|
+
this.closed = null;
|
|
36847
|
+
this.lastPosted = null;
|
|
36848
|
+
this.terminalFor = null;
|
|
36849
|
+
this.cancelTimer();
|
|
36850
|
+
}
|
|
36851
|
+
close() {
|
|
36852
|
+
this.shut = true;
|
|
36853
|
+
this.cancelTimer();
|
|
36854
|
+
}
|
|
36855
|
+
cancelTimer() {
|
|
36856
|
+
if (this.timer !== null) this.clearTimer(this.timer);
|
|
36857
|
+
this.timer = null;
|
|
36858
|
+
}
|
|
36859
|
+
promote(t) {
|
|
36860
|
+
if (this.open !== null && bucketEndMs(this.open.at) <= t) {
|
|
36861
|
+
this.closed = this.open;
|
|
36862
|
+
this.open = null;
|
|
36863
|
+
}
|
|
36864
|
+
}
|
|
36865
|
+
pending() {
|
|
36866
|
+
return this.closed !== null && !sameTuple(this.closed, this.lastPosted) && !sameTuple(this.closed, this.terminalFor);
|
|
36867
|
+
}
|
|
36868
|
+
schedule() {
|
|
36869
|
+
if (this.shut || this.inFlight || this.timer !== null) return;
|
|
36870
|
+
if (this.open === null && !this.pending()) return;
|
|
36871
|
+
const t = this.now();
|
|
36872
|
+
let b = (Math.floor(t / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
|
|
36873
|
+
while (b < this.notBefore) b += TOOL_STATUS_BUCKET_MS;
|
|
36874
|
+
const jitter = Math.floor(this.random() * TOOL_STATUS_JITTER_MS);
|
|
36875
|
+
this.timer = this.setTimer(() => {
|
|
36876
|
+
this.timer = null;
|
|
36877
|
+
this.fire();
|
|
36878
|
+
}, b - t + jitter);
|
|
36879
|
+
}
|
|
36880
|
+
fire() {
|
|
36881
|
+
try {
|
|
36882
|
+
if (this.shut) return;
|
|
36883
|
+
this.promote(this.now());
|
|
36884
|
+
if (this.pending()) {
|
|
36885
|
+
void this.attempt(this.closed);
|
|
36886
|
+
return;
|
|
36887
|
+
}
|
|
36888
|
+
this.schedule();
|
|
36889
|
+
} catch {
|
|
36890
|
+
}
|
|
36891
|
+
}
|
|
36892
|
+
async attempt(rec) {
|
|
36893
|
+
this.inFlight = true;
|
|
36894
|
+
const attemptAt = this.now();
|
|
36895
|
+
let result;
|
|
36896
|
+
try {
|
|
36897
|
+
result = await this.postFn(rec);
|
|
36898
|
+
} catch {
|
|
36899
|
+
result = "network";
|
|
36900
|
+
}
|
|
36901
|
+
this.inFlight = false;
|
|
36902
|
+
try {
|
|
36903
|
+
if (typeof result === "object" && result.status >= 200 && result.status < 300) {
|
|
36904
|
+
this.lastPosted = rec;
|
|
36905
|
+
} else if (typeof result === "object" && result.status === 429) {
|
|
36906
|
+
this.notBefore = attemptAt + retryAfterMs(result.retryAfter);
|
|
36907
|
+
this.onFailure("http_error");
|
|
36908
|
+
} else if (typeof result === "object" && result.status >= 400 && result.status < 500) {
|
|
36909
|
+
this.terminalFor = rec;
|
|
36910
|
+
this.onFailure("http_error");
|
|
36911
|
+
} else {
|
|
36912
|
+
this.onFailure(typeof result === "object" ? "http_error" : result);
|
|
36913
|
+
}
|
|
36914
|
+
this.schedule();
|
|
36915
|
+
} catch {
|
|
36916
|
+
}
|
|
36917
|
+
}
|
|
36918
|
+
};
|
|
36919
|
+
|
|
36409
36920
|
// src/grants/owner-turn.ts
|
|
36410
36921
|
import { ed25519 as ed255196 } from "@noble/curves/ed25519";
|
|
36411
36922
|
var HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
@@ -36449,6 +36960,312 @@ function classifyOwnerDmLeaves(leaves, allowed) {
|
|
|
36449
36960
|
}
|
|
36450
36961
|
return "foreign";
|
|
36451
36962
|
}
|
|
36963
|
+
function ownerDmLeavesRelayFree(leaves, ids) {
|
|
36964
|
+
let owner = false;
|
|
36965
|
+
let agent = false;
|
|
36966
|
+
for (const l of leaves) {
|
|
36967
|
+
if (!l.basic || l.identity === null) return false;
|
|
36968
|
+
if (l.identity === ids.ownerIdentityId) owner = true;
|
|
36969
|
+
else if (l.identity === ids.agentIdentityId) agent = true;
|
|
36970
|
+
else return false;
|
|
36971
|
+
}
|
|
36972
|
+
return owner && agent;
|
|
36973
|
+
}
|
|
36974
|
+
function relayLeafFromPhase0Relay(relay) {
|
|
36975
|
+
return relay !== "none";
|
|
36976
|
+
}
|
|
36977
|
+
function ownerDmRelayOf(leafClass, leaves, ids) {
|
|
36978
|
+
if (leafClass === "unsigned_relay") return "unsigned";
|
|
36979
|
+
if (ownerDmLeavesRelayFree(leaves, ids)) return "none";
|
|
36980
|
+
if (ids.relayIdentityId !== void 0 && leaves.some((l) => l.identity === ids.relayIdentityId)) return "signed";
|
|
36981
|
+
return void 0;
|
|
36982
|
+
}
|
|
36983
|
+
|
|
36984
|
+
// src/grants/mcp-policy.ts
|
|
36985
|
+
var MCP_POLICY_WRITER_FEATURE = "mcp_policy_writer_v1";
|
|
36986
|
+
var MCP_DELETION_ACK_FEATURE = "mcp_deletion_ack_v1";
|
|
36987
|
+
var MCP_ACK_STATES = /* @__PURE__ */ new Set(["ok", "not_entitled", "kill_switch"]);
|
|
36988
|
+
function mcpEntitlementReading(mcpEntitled, mcpCause) {
|
|
36989
|
+
if (mcpEntitled === true && mcpCause === "entitled") return "entitled";
|
|
36990
|
+
if (mcpEntitled === false && mcpCause === "not_entitled") return "not_entitled";
|
|
36991
|
+
return "suspended";
|
|
36992
|
+
}
|
|
36993
|
+
function mcpGrantsHostSwitch(env, log = writeServiceLog) {
|
|
36994
|
+
const v = env.SECURECOMS_MCP_GRANTS;
|
|
36995
|
+
if (v === void 0 || v === "on") return "on";
|
|
36996
|
+
if (v === "off") return "off";
|
|
36997
|
+
log("warn", "config.mcp_grants_switch_suspended", { state: ServiceLogEnum.of("suspended", ["suspended"]) });
|
|
36998
|
+
return "suspended";
|
|
36999
|
+
}
|
|
37000
|
+
var empty = (state, r3Hold) => Object.freeze({ v: 1, state, r3_hold: r3Hold, servers: Object.freeze({}), remove: NO_REMOVE });
|
|
37001
|
+
var NO_REMOVE = Object.freeze([]);
|
|
37002
|
+
function policyAuth(s) {
|
|
37003
|
+
if (s.auth.mode === "key") return Object.freeze({ mode: "key" });
|
|
37004
|
+
const clientId = s.auth.client === "customer" ? s.auth.client_id : SECURECOMS_CIMD_CLIENT_ID;
|
|
37005
|
+
return Object.freeze({ mode: "oauth", issuer: s.auth.issuer, client: s.auth.client, client_id: clientId });
|
|
37006
|
+
}
|
|
37007
|
+
function mcpPolicyFor(i) {
|
|
37008
|
+
const m = i.manifest;
|
|
37009
|
+
const r3Hold = m?.signedMcpPresent === true || i.mcpSeen;
|
|
37010
|
+
if (i.hostSwitch === "off") return empty("kill_switch", r3Hold);
|
|
37011
|
+
if (i.hostSwitch !== "on") return empty("suspended", r3Hold);
|
|
37012
|
+
if (!i.config.fetched) return empty("no_config", r3Hold);
|
|
37013
|
+
const ent = mcpEntitlementReading(i.config.mcp_entitled, i.config.mcp_cause);
|
|
37014
|
+
if (ent !== "entitled") return empty(ent, r3Hold);
|
|
37015
|
+
if (!i.ownerDmManifestSwitchOn) return empty("switch_off", r3Hold);
|
|
37016
|
+
if (m === null) return empty("no_manifest", r3Hold);
|
|
37017
|
+
if (m.mcpKey === "absent") return empty("section_absent", r3Hold);
|
|
37018
|
+
if (m.mcp === null) return empty("malformed", r3Hold);
|
|
37019
|
+
if (!i.capability) return empty("no_capability", r3Hold);
|
|
37020
|
+
const catalog = i.catalog ?? MCP_CATALOG;
|
|
37021
|
+
const granted = /* @__PURE__ */ new Map();
|
|
37022
|
+
for (const name of m.grants.owner_dm ?? []) {
|
|
37023
|
+
const g = parseMcpGrant(name);
|
|
37024
|
+
if (g === null) continue;
|
|
37025
|
+
if (!granted.has(g.serverId)) granted.set(g.serverId, /* @__PURE__ */ new Set());
|
|
37026
|
+
granted.get(g.serverId).add(g.tool);
|
|
37027
|
+
}
|
|
37028
|
+
const servers = {};
|
|
37029
|
+
for (const id of Object.keys(m.mcp.servers).sort()) {
|
|
37030
|
+
const s = m.mcp.servers[id];
|
|
37031
|
+
const row = mcpServerCatalogRow(s, catalog);
|
|
37032
|
+
if (row === null) {
|
|
37033
|
+
servers[id] = Object.freeze({ state: "refused" });
|
|
37034
|
+
continue;
|
|
37035
|
+
}
|
|
37036
|
+
const tools = {};
|
|
37037
|
+
for (const t of Object.keys(s.tools).sort()) {
|
|
37038
|
+
if (granted.get(id)?.has(t) === true && row.readTools.includes(t)) tools[t] = Object.freeze({ sha256: s.tools[t].sha256, class: "read" });
|
|
37039
|
+
}
|
|
37040
|
+
servers[id] = Object.freeze({ state: "ok", vendor: s.vendor, url: row.readOnlyUrl, auth: policyAuth(s), tools: Object.freeze(tools) });
|
|
37041
|
+
}
|
|
37042
|
+
const remove = m.mcp.removed === "*" ? "*" : Object.freeze([...m.mcp.removed]);
|
|
37043
|
+
return Object.freeze({ v: 1, state: "ok", r3_hold: r3Hold, servers: Object.freeze(servers), remove });
|
|
37044
|
+
}
|
|
37045
|
+
|
|
37046
|
+
// src/grants/mcp-issuance.ts
|
|
37047
|
+
var NONE = Object.freeze([]);
|
|
37048
|
+
var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
37049
|
+
function mcpPolicyAllows(policy, serverId, tool) {
|
|
37050
|
+
if (policy === null || policy.state !== "ok" || !own(policy.servers, serverId)) return false;
|
|
37051
|
+
const s = policy.servers[serverId];
|
|
37052
|
+
return s.state === "ok" && own(s.tools, tool);
|
|
37053
|
+
}
|
|
37054
|
+
function mcpIssuance(i) {
|
|
37055
|
+
if (i.source !== "manifest") return NONE;
|
|
37056
|
+
if (i.verdict?.kind !== "verified" || i.verdict.relayLeaf !== false) return NONE;
|
|
37057
|
+
if (i.entitlement !== "entitled" || i.hostSwitch !== "on" || !i.ownerDmManifestSwitchOn) return NONE;
|
|
37058
|
+
if (i.manifest === null || i.policy === null || i.policy.state !== "ok") return NONE;
|
|
37059
|
+
const out = [];
|
|
37060
|
+
for (const name of i.manifest.grants.owner_dm ?? []) {
|
|
37061
|
+
const g = parseMcpGrant(name);
|
|
37062
|
+
if (g === null || i.disabled.has(name) || !mcpPolicyAllows(i.policy, g.serverId, g.tool)) continue;
|
|
37063
|
+
out.push(Object.freeze({ tool: name, mode: "read", source: "manifest" }));
|
|
37064
|
+
}
|
|
37065
|
+
return out.length === 0 ? NONE : Object.freeze(out);
|
|
37066
|
+
}
|
|
37067
|
+
|
|
37068
|
+
// src/consumer-grants.ts
|
|
37069
|
+
var NO_GRANTS = Object.freeze([]);
|
|
37070
|
+
var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
|
|
37071
|
+
function withConsumerGrants(m, perToolConsumer) {
|
|
37072
|
+
if (perToolConsumer) return m;
|
|
37073
|
+
const a = m.authorization;
|
|
37074
|
+
if (!a) return m;
|
|
37075
|
+
const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
|
|
37076
|
+
const stripMemory = "memory_write" in a;
|
|
37077
|
+
if (!stripGrants && !stripMemory) return m;
|
|
37078
|
+
const { memory_write: _dropped, ...rest } = a;
|
|
37079
|
+
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
37080
|
+
}
|
|
37081
|
+
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
37082
|
+
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
37083
|
+
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
37084
|
+
var MCP_CLIENT_FEATURE = "mcp_client_v1";
|
|
37085
|
+
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
37086
|
+
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
37087
|
+
team_room: Object.freeze(["current_time"])
|
|
37088
|
+
});
|
|
37089
|
+
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
37090
|
+
function withManifestCapability(m, manifestConsumer) {
|
|
37091
|
+
if (manifestConsumer) return m;
|
|
37092
|
+
const a = m.authorization;
|
|
37093
|
+
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
37094
|
+
const constants5 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
37095
|
+
const narrowed = a.tool_grants.flatMap((g) => {
|
|
37096
|
+
if (!isManifestGrant(g)) return [g];
|
|
37097
|
+
return constants5.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
37098
|
+
});
|
|
37099
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
37100
|
+
}
|
|
37101
|
+
function withHarnessGrants(m, withheld) {
|
|
37102
|
+
if (!withheld) return m;
|
|
37103
|
+
const a = m.authorization;
|
|
37104
|
+
if (!a || a.tool_grants.length === 0) return m;
|
|
37105
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
37106
|
+
}
|
|
37107
|
+
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
37108
|
+
if (consumerHasFeature) return m;
|
|
37109
|
+
const a = m.authorization;
|
|
37110
|
+
if (!a) return m;
|
|
37111
|
+
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
37112
|
+
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
37113
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
37114
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
37115
|
+
}
|
|
37116
|
+
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
37117
|
+
function withUtilityCapability(m, consumerHasFeature) {
|
|
37118
|
+
if (consumerHasFeature) return m;
|
|
37119
|
+
const a = m.authorization;
|
|
37120
|
+
if (!a) return m;
|
|
37121
|
+
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
37122
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
37123
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
37124
|
+
}
|
|
37125
|
+
var MCP_GRANT_PREFIX = "mcp:";
|
|
37126
|
+
var isMcpGrant = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
|
|
37127
|
+
function withMcpCapability(m, consumerHasFeature) {
|
|
37128
|
+
const a = m.authorization;
|
|
37129
|
+
if (!a) return m;
|
|
37130
|
+
const { relay_free: _relayFree, ...rest } = a;
|
|
37131
|
+
if (!a.tool_grants.some(isMcpGrant)) return "relay_free" in a ? { ...m, authorization: Object.freeze(rest) } : m;
|
|
37132
|
+
if (consumerHasFeature && a.audience === "owner_dm" && a.relay_speaker === void 0) return m;
|
|
37133
|
+
const kept = a.tool_grants.filter((g) => !isMcpGrant(g));
|
|
37134
|
+
return { ...m, authorization: Object.freeze({ ...rest, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
37135
|
+
}
|
|
37136
|
+
var R3_KEEP_TOOLS = /* @__PURE__ */ new Set(["current_time", "web_search", "weather_forecast", "currency_convert"]);
|
|
37137
|
+
function withR3Holds(m, hold) {
|
|
37138
|
+
if (!hold) return m;
|
|
37139
|
+
const a = m.authorization;
|
|
37140
|
+
if (!a || !(a.audience === "owner_dm" || a.relay_speaker === "owner")) return m;
|
|
37141
|
+
const kept = a.tool_grants.filter((g) => R3_KEEP_TOOLS.has(g.tool) || isMcpGrant(g));
|
|
37142
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
37143
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
37144
|
+
}
|
|
37145
|
+
|
|
37146
|
+
// src/mcp-catalog-message.ts
|
|
37147
|
+
var MCP_CATALOG_MAX_BYTES = 384 * 1024;
|
|
37148
|
+
var MCP_CATALOG_MAX_TOOLS = 64;
|
|
37149
|
+
var MCP_CATALOG_LIST_TIMEOUT_MS = 5e4;
|
|
37150
|
+
var REQUEST_ID = /^[0-9a-f]{32}$/;
|
|
37151
|
+
var SERVER_ID = /^[a-z0-9-]{1,24}$/;
|
|
37152
|
+
var TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
37153
|
+
var RESERVED = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
37154
|
+
var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
37155
|
+
var MCP_CATALOG_REASONS = /* @__PURE__ */ new Set(["catalog", "write_name", "hint", "schema", "header_param", "too_large"]);
|
|
37156
|
+
var isBag2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
37157
|
+
var keysExactly2 = (o, keys) => {
|
|
37158
|
+
const k = Object.keys(o);
|
|
37159
|
+
return k.length === keys.length && keys.every((x) => Object.prototype.hasOwnProperty.call(o, x));
|
|
37160
|
+
};
|
|
37161
|
+
function parseMcpCatalogRequest(body) {
|
|
37162
|
+
if (!isBag2(body) || !keysExactly2(body, ["v", "type", "server_id", "request_id"])) return null;
|
|
37163
|
+
if (body.v !== 1 || body.type !== "mcp_catalog_request") return null;
|
|
37164
|
+
const { server_id: serverId, request_id: requestId } = body;
|
|
37165
|
+
if (typeof serverId !== "string" || !SERVER_ID.test(serverId) || RESERVED.has(serverId)) return null;
|
|
37166
|
+
if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) return null;
|
|
37167
|
+
return Object.freeze({ serverId, requestId });
|
|
37168
|
+
}
|
|
37169
|
+
var PROJECTION_KEYS = /* @__PURE__ */ new Set(["name", "title", "description", "inputSchema", "annotations"]);
|
|
37170
|
+
function validProjection(p, toolName) {
|
|
37171
|
+
if (!Object.keys(p).every((k) => PROJECTION_KEYS.has(k))) return false;
|
|
37172
|
+
if (p.name !== toolName || typeof p.description !== "string" || !isBag2(p.inputSchema)) return false;
|
|
37173
|
+
if (Object.prototype.hasOwnProperty.call(p, "title") && typeof p.title !== "string") return false;
|
|
37174
|
+
if (Object.prototype.hasOwnProperty.call(p, "annotations") && !isBag2(p.annotations)) return false;
|
|
37175
|
+
return true;
|
|
37176
|
+
}
|
|
37177
|
+
function validTool(t) {
|
|
37178
|
+
if (!isBag2(t) || typeof t.name !== "string" || typeof t.grantable !== "boolean") return null;
|
|
37179
|
+
if (t.grantable) {
|
|
37180
|
+
if (!TOOL_NAME.test(t.name) || t.reason !== null || !isBag2(t.projection) || !validProjection(t.projection, t.name)) return null;
|
|
37181
|
+
return { name: t.name, projection: t.projection, grantable: true, reason: null };
|
|
37182
|
+
}
|
|
37183
|
+
if (t.projection !== void 0 || typeof t.reason !== "string" || !MCP_CATALOG_REASONS.has(t.reason)) return null;
|
|
37184
|
+
if (t.name.length < 1 || t.name.length > 64 || !/^[\x20-\x7e]+$/.test(t.name)) return null;
|
|
37185
|
+
return { name: t.name, grantable: false, reason: t.reason };
|
|
37186
|
+
}
|
|
37187
|
+
function validateCatalogListReply(frame, serverId, vendor) {
|
|
37188
|
+
if (!isBag2(frame) || frame.ok !== true || frame.server_id !== serverId || frame.vendor !== vendor) return null;
|
|
37189
|
+
if (typeof frame.listed_at !== "string" || !ISO_TIME.test(frame.listed_at) || typeof frame.truncated !== "boolean") return null;
|
|
37190
|
+
if (!Array.isArray(frame.tools) || frame.tools.length > MCP_CATALOG_MAX_TOOLS) return null;
|
|
37191
|
+
const tools = [];
|
|
37192
|
+
const names = /* @__PURE__ */ new Set();
|
|
37193
|
+
for (const t of frame.tools) {
|
|
37194
|
+
const v = validTool(t);
|
|
37195
|
+
if (v === null) return null;
|
|
37196
|
+
if (names.has(v.name)) return null;
|
|
37197
|
+
names.add(v.name);
|
|
37198
|
+
tools.push(v);
|
|
37199
|
+
}
|
|
37200
|
+
return { listedAt: frame.listed_at, truncated: frame.truncated, tools };
|
|
37201
|
+
}
|
|
37202
|
+
var utf8 = (v) => Buffer.byteLength(JSON.stringify(v), "utf8");
|
|
37203
|
+
var mcpCatalogBodyBytes = (b) => utf8(b);
|
|
37204
|
+
function buildMcpCatalogBody(i) {
|
|
37205
|
+
const head = { v: 1, type: "mcp_catalog", server_id: i.serverId, request_id: i.requestId, vendor: i.vendor, issuer: i.issuer, listed_at: i.reply.listedAt };
|
|
37206
|
+
const full = { ...head, tools: [...i.reply.tools], truncated: i.reply.truncated };
|
|
37207
|
+
if (mcpCatalogBodyBytes(full) <= MCP_CATALOG_MAX_BYTES) return full;
|
|
37208
|
+
const stub = (t) => ({ name: t.name, grantable: false, reason: "too_large" });
|
|
37209
|
+
const tools = i.reply.tools.map(stub);
|
|
37210
|
+
const body = { ...head, tools, truncated: true };
|
|
37211
|
+
let size = mcpCatalogBodyBytes(body);
|
|
37212
|
+
for (let k = 0; k < tools.length; k++) {
|
|
37213
|
+
const want = i.reply.tools[k];
|
|
37214
|
+
const delta = utf8(want) - utf8(tools[k]);
|
|
37215
|
+
if (size + delta <= MCP_CATALOG_MAX_BYTES) {
|
|
37216
|
+
tools[k] = want;
|
|
37217
|
+
size += delta;
|
|
37218
|
+
}
|
|
37219
|
+
}
|
|
37220
|
+
return body;
|
|
37221
|
+
}
|
|
37222
|
+
|
|
37223
|
+
// src/mcp-catalog-rate.ts
|
|
37224
|
+
var MINUTE_MS = 6e4;
|
|
37225
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
37226
|
+
var MCP_CATALOG_PER_SERVER_MINUTE = 3;
|
|
37227
|
+
var MCP_CATALOG_PER_SERVER_HOUR = 20;
|
|
37228
|
+
var MCP_CATALOG_PER_AGENT_HOUR = 40;
|
|
37229
|
+
var McpCatalogRateLimiter = class {
|
|
37230
|
+
now;
|
|
37231
|
+
onRateLimited;
|
|
37232
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
37233
|
+
perServer = /* @__PURE__ */ new Map();
|
|
37234
|
+
agent = [];
|
|
37235
|
+
/** Per scope key: no further log until this time (the end of the window that was full when it last logged). */
|
|
37236
|
+
quietUntil = /* @__PURE__ */ new Map();
|
|
37237
|
+
constructor(deps = {}) {
|
|
37238
|
+
this.now = deps.now ?? Date.now;
|
|
37239
|
+
this.onRateLimited = deps.onRateLimited ?? (() => {
|
|
37240
|
+
});
|
|
37241
|
+
}
|
|
37242
|
+
/** "ok" admits (and counts) the request and marks the server in flight; the caller MUST call release(serverId). */
|
|
37243
|
+
admit(serverId) {
|
|
37244
|
+
if (this.inFlight.has(serverId)) return "in_flight";
|
|
37245
|
+
const t = this.now();
|
|
37246
|
+
const server = (this.perServer.get(serverId) ?? []).filter((x) => t - x < HOUR_MS);
|
|
37247
|
+
this.perServer.set(serverId, server);
|
|
37248
|
+
this.agent = this.agent.filter((x) => t - x < HOUR_MS);
|
|
37249
|
+
const lastMinute = server.filter((x) => t - x < MINUTE_MS);
|
|
37250
|
+
if (lastMinute.length >= MCP_CATALOG_PER_SERVER_MINUTE) return this.limited(`m:${serverId}`, "server_minute", lastMinute[0] + MINUTE_MS, t);
|
|
37251
|
+
if (server.length >= MCP_CATALOG_PER_SERVER_HOUR) return this.limited(`h:${serverId}`, "server_hour", server[0] + HOUR_MS, t);
|
|
37252
|
+
if (this.agent.length >= MCP_CATALOG_PER_AGENT_HOUR) return this.limited("a", "agent_hour", this.agent[0] + HOUR_MS, t);
|
|
37253
|
+
server.push(t);
|
|
37254
|
+
this.agent.push(t);
|
|
37255
|
+
this.inFlight.add(serverId);
|
|
37256
|
+
return "ok";
|
|
37257
|
+
}
|
|
37258
|
+
release(serverId) {
|
|
37259
|
+
this.inFlight.delete(serverId);
|
|
37260
|
+
}
|
|
37261
|
+
limited(key, scope, windowEnd, t) {
|
|
37262
|
+
if (t >= (this.quietUntil.get(key) ?? -Infinity)) {
|
|
37263
|
+
this.quietUntil.set(key, windowEnd);
|
|
37264
|
+
this.onRateLimited(scope);
|
|
37265
|
+
}
|
|
37266
|
+
return "rate_limited";
|
|
37267
|
+
}
|
|
37268
|
+
};
|
|
36452
37269
|
|
|
36453
37270
|
// src/bridge-agent.ts
|
|
36454
37271
|
function logSendSupersededAdoption(kind2) {
|
|
@@ -36588,6 +37405,7 @@ function selectVoiceGuidance(isDm, voiceDefaultResponder) {
|
|
|
36588
37405
|
}
|
|
36589
37406
|
return VOICE_MODE_GUIDANCE;
|
|
36590
37407
|
}
|
|
37408
|
+
var MCP_INERT_STATES = /* @__PURE__ */ new Set(["suspended", "no_config", "switch_off", "no_manifest", "section_absent", "malformed", "no_capability"]);
|
|
36591
37409
|
var VOICE_COALESCE_WINDOW_MS = 0;
|
|
36592
37410
|
var VOICE_COALESCE_WINDOW_MAX_MS = 5e3;
|
|
36593
37411
|
function voiceCoalesceWindowFromEnv(env = process.env) {
|
|
@@ -36638,10 +37456,10 @@ var ChannelPendingError = class extends Error {
|
|
|
36638
37456
|
}
|
|
36639
37457
|
};
|
|
36640
37458
|
var RateLimitedError = class extends Error {
|
|
36641
|
-
constructor(channelId,
|
|
36642
|
-
super(`channel ${channelId} send rate limit exceeded (retry in ${
|
|
37459
|
+
constructor(channelId, retryAfterMs2) {
|
|
37460
|
+
super(`channel ${channelId} send rate limit exceeded (retry in ${retryAfterMs2}ms)`);
|
|
36643
37461
|
this.channelId = channelId;
|
|
36644
|
-
this.retryAfterMs =
|
|
37462
|
+
this.retryAfterMs = retryAfterMs2;
|
|
36645
37463
|
this.name = "RateLimitedError";
|
|
36646
37464
|
}
|
|
36647
37465
|
};
|
|
@@ -36769,6 +37587,31 @@ var SendForbiddenError = class extends Error {
|
|
|
36769
37587
|
this.name = "SendForbiddenError";
|
|
36770
37588
|
}
|
|
36771
37589
|
};
|
|
37590
|
+
var MCP_POLICY_RETRY_DELAYS_MS = Object.freeze([1e3, 5e3, 3e4, 12e4, 3e5]);
|
|
37591
|
+
function logMcpCatalogRequest(state) {
|
|
37592
|
+
writeServiceLog(state === "sent" ? "info" : "warn", "mcp.catalog_request", { state: ServiceLogEnum.of(state, [
|
|
37593
|
+
"malformed",
|
|
37594
|
+
"not_owner_dm",
|
|
37595
|
+
"not_verified",
|
|
37596
|
+
"replay",
|
|
37597
|
+
"relay_leaf",
|
|
37598
|
+
"not_ready",
|
|
37599
|
+
"in_flight",
|
|
37600
|
+
"runtime_timeout",
|
|
37601
|
+
"runtime_refused",
|
|
37602
|
+
"withheld",
|
|
37603
|
+
"send_failed",
|
|
37604
|
+
"failed",
|
|
37605
|
+
"sent"
|
|
37606
|
+
]) });
|
|
37607
|
+
}
|
|
37608
|
+
var McpReplyWithheldError = class extends SendForbiddenError {
|
|
37609
|
+
constructor(channelId) {
|
|
37610
|
+
super(channelId);
|
|
37611
|
+
this.name = "McpReplyWithheldError";
|
|
37612
|
+
this.message = "reply withheld: the chat is no longer private to the owner and the agent; no retry";
|
|
37613
|
+
}
|
|
37614
|
+
};
|
|
36772
37615
|
var RoomClassChangedSendError = class extends SendForbiddenError {
|
|
36773
37616
|
constructor(channelId) {
|
|
36774
37617
|
super(channelId);
|
|
@@ -36788,6 +37631,7 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
|
|
|
36788
37631
|
var RoomGrantEpochMovedError = class extends Error {
|
|
36789
37632
|
};
|
|
36790
37633
|
var ROOM_GRANT_TRACK_MAX = 4096;
|
|
37634
|
+
var MCP_DELIVERED_TRACK_MAX = 4096;
|
|
36791
37635
|
var VOICE_FRAME_PARENT_MAX = 4096;
|
|
36792
37636
|
var SessionSupersededError = class extends Error {
|
|
36793
37637
|
constructor() {
|
|
@@ -36939,8 +37783,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36939
37783
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
|
|
36940
37784
|
});
|
|
36941
37785
|
this.home = deps.home ?? homedir4();
|
|
37786
|
+
this.mcpCatalog = deps.mcpCatalog;
|
|
37787
|
+
this.mcpDeliveredTrackMax = deps.mcpDeliveredTrackMax ?? MCP_DELIVERED_TRACK_MAX;
|
|
37788
|
+
this.mcpPolicyRetryDelaysMs = deps.mcpPolicyRetryDelaysMs?.length ? deps.mcpPolicyRetryDelaysMs : MCP_POLICY_RETRY_DELAYS_MS;
|
|
36942
37789
|
this.grantWriters = {
|
|
36943
37790
|
writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
|
|
37791
|
+
writeMcpPolicy: deps.grantWriters?.writeMcpPolicy ?? writeMcpPolicy,
|
|
36944
37792
|
writeManagedFiles: deps.grantWriters?.writeManagedFiles ?? writeManagedFiles,
|
|
36945
37793
|
bindRuntimeConfig: deps.grantWriters?.bindRuntimeConfig ?? bindRuntimeConfig,
|
|
36946
37794
|
removeManagedLlmSource: deps.grantWriters?.removeManagedLlmSource ?? removeManagedLlmSource
|
|
@@ -37426,6 +38274,62 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37426
38274
|
roomDefaultGrants = roomDefaultGrantsEnabled();
|
|
37427
38275
|
/** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
|
|
37428
38276
|
ownerDmManifestGrants = ownerDmManifestGrantsEnabled();
|
|
38277
|
+
/**
|
|
38278
|
+
* MCP client Task 16 (OQ-16, coordinator ruling D-7): SECURECOMS_MCP_GRANTS, read ONCE at construction. "off" writes
|
|
38279
|
+
* kill_switch (deleting); any value other than unset/"on"/"off" is suspended (non-deleting, one content-free warn).
|
|
38280
|
+
*/
|
|
38281
|
+
mcpHostSwitch = mcpGrantsHostSwitch(process.env);
|
|
38282
|
+
/** MCP Task 16 test seam (BridgeAgentDeps.mcpCatalog); undefined in production, so MCP_CATALOG decides. */
|
|
38283
|
+
mcpCatalog;
|
|
38284
|
+
/**
|
|
38285
|
+
* MCP Task 16 (OQ-1; PR-B1 audit M-2): the two raw /files fields from the LAST fetch. Any failed or rejected fetch
|
|
38286
|
+
* sets `fetched: false` (no_config, non-deleting): the last values are never kept to keep an `ok` document alive.
|
|
38287
|
+
*/
|
|
38288
|
+
mcpConfig = Object.freeze({ fetched: false });
|
|
38289
|
+
/**
|
|
38290
|
+
* MCP Task 16: the mcp-policy.json document this process last WROTE (the file the runtime reads), or null before the
|
|
38291
|
+
* first write and after a failed one (fail closed: issuance reads only a document known to be on disk).
|
|
38292
|
+
*/
|
|
38293
|
+
mcpPolicyWritten = null;
|
|
38294
|
+
mcpPolicyWrittenJson = null;
|
|
38295
|
+
/** Deletion-ack ruling §3: the manifest counter in force when `mcpPolicyWrittenJson` was written (never read live). */
|
|
38296
|
+
mcpPolicyWrittenCounter = null;
|
|
38297
|
+
/** Ruling §3: newest counter whose written policy the runtime fully acted on. Memory only, monotonic, starts at 0. */
|
|
38298
|
+
mcpDeletionsThrough = 0;
|
|
38299
|
+
/** Ruling §3: the last `credentials_present` an accepted frame carried; null until one arrives. */
|
|
38300
|
+
mcpCredentialsPresentValue = null;
|
|
38301
|
+
/** Ruling §3: per attached socket, whether its latest hello declared mcp_deletion_ack_v1 (harness noted false). */
|
|
38302
|
+
mcpDeletionAckSockets = /* @__PURE__ */ new Map();
|
|
38303
|
+
/** Ruling §3 rate: the ack-derived report values in the last report posted (null until one is). */
|
|
38304
|
+
lastReportedMcpAck = null;
|
|
38305
|
+
/** #1351 round 2 M-1: bumped on every byte-changing policy write (and on the inert document being put in force). */
|
|
38306
|
+
policyWriteSeq = 0;
|
|
38307
|
+
/** M-1: per ack-capable socket, the policyWriteSeq at the dispatch of each owner-DM turn, by message id (bounded). */
|
|
38308
|
+
mcpTurnSeq = /* @__PURE__ */ new Map();
|
|
38309
|
+
/**
|
|
38310
|
+
* #1351 round 2 H-1: the ack values the REPORT may carry. An ack lands seconds after an owner-DM turn and a report
|
|
38311
|
+
* posted then would time the turn (record_report sets applied_at). So, as the tool-status sender: `open` holds a
|
|
38312
|
+
* change while its 15-minute UTC bucket is open, `closed` what the report carries, and the only post is from one
|
|
38313
|
+
* timer at the next bucket boundary plus jitter.
|
|
38314
|
+
*/
|
|
38315
|
+
mcpAckOpen = null;
|
|
38316
|
+
mcpAckClosed = { through: 0, credentials: null };
|
|
38317
|
+
mcpAckTimer = null;
|
|
38318
|
+
/** PR-B2 follow-up: a failed write is retried on this backoff (the last value repeats) until one lands or close(). */
|
|
38319
|
+
mcpPolicyRetryDelaysMs;
|
|
38320
|
+
mcpPolicyRetryTimer = null;
|
|
38321
|
+
mcpPolicyRetryAttempt = 0;
|
|
38322
|
+
/** MCP Task 18: the loopback catalog listers of the attached runtime sockets that declared mcp_client_v1 (ws-server.ts). */
|
|
38323
|
+
mcpCatalogListers = /* @__PURE__ */ new Map();
|
|
38324
|
+
/** MCP Task 18 (OQ-15): per server 1 in flight, 3 / 60 s, 20 / hour; per agent 40 / hour. Memory only. */
|
|
38325
|
+
mcpCatalogRate = new McpCatalogRateLimiter({
|
|
38326
|
+
onRateLimited: (scope) => writeServiceLog("warn", "mcp.catalog_request", {
|
|
38327
|
+
state: ServiceLogEnum.of("catalog_rate_limited", ["catalog_rate_limited"]),
|
|
38328
|
+
scope: ServiceLogEnum.of(scope, ["server_minute", "server_hour", "agent_hour"])
|
|
38329
|
+
})
|
|
38330
|
+
});
|
|
38331
|
+
/** MCP Task 18: answers in progress (outside the ingest cycle). Observable by tests; close() does not wait on them. */
|
|
38332
|
+
mcpCatalogWork = /* @__PURE__ */ new Set();
|
|
37429
38333
|
roomManifestGrants = roomManifestGrantsEnabled();
|
|
37430
38334
|
/**
|
|
37431
38335
|
* Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
|
|
@@ -37436,6 +38340,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37436
38340
|
// RECORDED here and in the sealed store; no grant code reads it in step 1.
|
|
37437
38341
|
toolGrantManifest = null;
|
|
37438
38342
|
toolGrantCounter = null;
|
|
38343
|
+
/**
|
|
38344
|
+
* Tool status Phase 0 (spec §3.1, §3.2; plan Task 8): the latest owner-DM last-turn record, in memory only (no store
|
|
38345
|
+
* table: after a restart there is none until the next typed owner-DM turn). ADVISORY: nothing reads it to decide a
|
|
38346
|
+
* grant. Content-free: enums, booleans, one 15-minute bucket `at` (ruling M-2), one counter, built-in names from the
|
|
38347
|
+
* allowlist. This is the latest record, open bucket included; what leaves the host is the sender's closed record.
|
|
38348
|
+
*/
|
|
38349
|
+
ownerDmLastTurn = null;
|
|
38350
|
+
/**
|
|
38351
|
+
* Test seams (Task 8; ruling M-2): the cause function, the bridge clock the record and the sender read, and the
|
|
38352
|
+
* sender's timer and jitter source.
|
|
38353
|
+
*/
|
|
38354
|
+
toolStatusCause = ownerDmCause;
|
|
38355
|
+
toolStatusNow = () => Date.now();
|
|
38356
|
+
toolStatusSetTimer = defaultToolStatusSetTimer;
|
|
38357
|
+
toolStatusClearTimer = defaultToolStatusClearTimer;
|
|
38358
|
+
toolStatusRandom = Math.random;
|
|
38359
|
+
/**
|
|
38360
|
+
* Phase 0 Task 9 (spec §3.5), ruling M-2: the post of the record to the narrow route, only just after a 15-minute
|
|
38361
|
+
* UTC bucket boundary, and only a record whose bucket has closed.
|
|
38362
|
+
*/
|
|
38363
|
+
toolStatusSender = new ToolStatusSender({
|
|
38364
|
+
post: (rec) => this.postToolStatus(rec),
|
|
38365
|
+
now: () => this.toolStatusNow(),
|
|
38366
|
+
setTimer: (fn, ms) => this.toolStatusSetTimer(fn, ms),
|
|
38367
|
+
clearTimer: (h) => this.toolStatusClearTimer(h),
|
|
38368
|
+
random: () => this.toolStatusRandom(),
|
|
38369
|
+
onFailure: (kind2) => writeServiceLog("warn", "tool_status.post_failed", { status: ServiceLogEnum.of(kind2, ["http_error", "timeout", "network"]) })
|
|
38370
|
+
});
|
|
38371
|
+
/**
|
|
38372
|
+
* MCP Phase 1 Task 14 (OQ-3 ruling item 3): the sticky R-3 flag, mirrored from the sealed store. Starts false and is
|
|
38373
|
+
* set true at start() by the store (or by a failed read, fail closed). Only applyToolGrantManifest changes it after.
|
|
38374
|
+
*/
|
|
38375
|
+
mcpSeen = false;
|
|
37439
38376
|
manifestState = "no_manifest";
|
|
37440
38377
|
/** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
|
|
37441
38378
|
ownerVerifyFailures = 0;
|
|
@@ -37901,10 +38838,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37901
38838
|
const payload = await res.json();
|
|
37902
38839
|
if (!payload) {
|
|
37903
38840
|
writeServiceLog("warn", "runtime_config.empty_response");
|
|
38841
|
+
this.noteMcpConfigUnfetched();
|
|
37904
38842
|
return;
|
|
37905
38843
|
}
|
|
37906
38844
|
if (typeof payload.version !== "number") {
|
|
37907
38845
|
writeServiceLog("warn", "runtime_config.invalid_version");
|
|
38846
|
+
this.noteMcpConfigUnfetched();
|
|
37908
38847
|
return;
|
|
37909
38848
|
}
|
|
37910
38849
|
const version = payload.version;
|
|
@@ -37918,6 +38857,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37918
38857
|
manifestStepThrew = true;
|
|
37919
38858
|
writeServiceLog("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
|
|
37920
38859
|
}
|
|
38860
|
+
const raw = payload;
|
|
38861
|
+
this.mcpConfig = Object.freeze({ fetched: true, mcp_entitled: raw.mcp_entitled, mcp_cause: raw.mcp_cause });
|
|
38862
|
+
this.recomputeMcpPolicy();
|
|
37921
38863
|
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
37922
38864
|
const inForce = pin !== null ? this.toolGrantManifest : null;
|
|
37923
38865
|
if (inForce !== null && pin !== null) {
|
|
@@ -37984,7 +38926,88 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37984
38926
|
await this.postRuntimeConfigReport(base, version, skillsJson);
|
|
37985
38927
|
} catch (e) {
|
|
37986
38928
|
writeServiceLog("warn", "runtime_config.refetch_failed", {}, e);
|
|
38929
|
+
this.noteMcpConfigUnfetched();
|
|
38930
|
+
}
|
|
38931
|
+
}
|
|
38932
|
+
/** MCP Task 16 (OQ-1): the bridge's in-force entitlement reading; not fetched (or failed) is suspended. */
|
|
38933
|
+
mcpEntitlementInForce() {
|
|
38934
|
+
return this.mcpConfig.fetched ? mcpEntitlementReading(this.mcpConfig.mcp_entitled, this.mcpConfig.mcp_cause) : "suspended";
|
|
38935
|
+
}
|
|
38936
|
+
/** PR-B1 audit M-2: forget the last /files fields and rewrite mcp-policy.json (no_config: no grants, no deletion). */
|
|
38937
|
+
noteMcpConfigUnfetched() {
|
|
38938
|
+
this.mcpConfig = Object.freeze({ fetched: false });
|
|
38939
|
+
this.recomputeMcpPolicy();
|
|
38940
|
+
}
|
|
38941
|
+
/** MCP Task 16: the live inputs of mcp-policy.json (the pinned manifest in force only). */
|
|
38942
|
+
mcpPolicyInput() {
|
|
38943
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
38944
|
+
return {
|
|
38945
|
+
hostSwitch: this.mcpHostSwitch,
|
|
38946
|
+
config: this.mcpConfig,
|
|
38947
|
+
ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
|
|
38948
|
+
manifest: pin !== null ? this.toolGrantManifest : null,
|
|
38949
|
+
capability: this.mcpClientCapable(),
|
|
38950
|
+
mcpSeen: this.mcpSeen,
|
|
38951
|
+
...this.mcpCatalog ? { catalog: this.mcpCatalog } : {}
|
|
38952
|
+
};
|
|
38953
|
+
}
|
|
38954
|
+
/**
|
|
38955
|
+
* MCP Task 16 (spec §12.1): recompute mcp-policy.json and write it when it changed. The written document is what MCP
|
|
38956
|
+
* issuance and release-time re-narrowing read, so it is cleared BEFORE the write and set only after it lands: a failed
|
|
38957
|
+
* write leaves no document in force here (no MCP grant), and the next trigger tries again. Never throws.
|
|
38958
|
+
*/
|
|
38959
|
+
recomputeMcpPolicy() {
|
|
38960
|
+
const doc = mcpPolicyFor(this.mcpPolicyInput());
|
|
38961
|
+
const json2 = JSON.stringify(doc);
|
|
38962
|
+
if (json2 === this.mcpPolicyWrittenJson) {
|
|
38963
|
+
if (this.mcpPolicyWritten !== null && this.mcpPolicyWrittenCounter !== null) this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38964
|
+
return;
|
|
37987
38965
|
}
|
|
38966
|
+
if (MCP_INERT_STATES.has(doc.state) && !doc.r3_hold && Object.keys(doc.servers).length === 0 && !existsSync5(join20(runtimeConfigDir(this.home, this.cfg.identityId), MCP_POLICY_FILE))) {
|
|
38967
|
+
this.mcpPolicyWritten = doc;
|
|
38968
|
+
this.mcpPolicyWrittenJson = json2;
|
|
38969
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38970
|
+
this.policyWriteSeq++;
|
|
38971
|
+
this.clearMcpPolicyRetry();
|
|
38972
|
+
return;
|
|
38973
|
+
}
|
|
38974
|
+
this.mcpPolicyWritten = null;
|
|
38975
|
+
this.mcpPolicyWrittenJson = null;
|
|
38976
|
+
this.mcpPolicyWrittenCounter = null;
|
|
38977
|
+
this.policyWriteSeq++;
|
|
38978
|
+
try {
|
|
38979
|
+
this.grantWriters.writeMcpPolicy(this.home, this.cfg.identityId, doc);
|
|
38980
|
+
} catch (e) {
|
|
38981
|
+
writeServiceLog("warn", "authz.mcp_policy", { state: ServiceLogEnum.of("write_failed", ["write_failed"]) }, e);
|
|
38982
|
+
this.scheduleMcpPolicyRetry();
|
|
38983
|
+
return;
|
|
38984
|
+
}
|
|
38985
|
+
this.mcpPolicyWritten = doc;
|
|
38986
|
+
this.mcpPolicyWrittenJson = json2;
|
|
38987
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38988
|
+
this.clearMcpPolicyRetry();
|
|
38989
|
+
}
|
|
38990
|
+
/** A document is in force: no pending retry, and the next failure starts the backoff from the first delay. */
|
|
38991
|
+
clearMcpPolicyRetry() {
|
|
38992
|
+
this.mcpPolicyRetryAttempt = 0;
|
|
38993
|
+
if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
|
|
38994
|
+
this.mcpPolicyRetryTimer = null;
|
|
38995
|
+
}
|
|
38996
|
+
/**
|
|
38997
|
+
* PR-B2 follow-up: one pending retry at a time. The retry recomputes from the state THEN (never re-writes a stale
|
|
38998
|
+
* document); any other trigger that writes successfully meanwhile cancels it. Nothing is in force on the bridge side
|
|
38999
|
+
* until a write lands (mcpPolicyWritten stays null).
|
|
39000
|
+
*/
|
|
39001
|
+
scheduleMcpPolicyRetry() {
|
|
39002
|
+
if (this.closingDown || this.mcpPolicyRetryTimer !== null) return;
|
|
39003
|
+
const d = this.mcpPolicyRetryDelaysMs;
|
|
39004
|
+
const delay = d[Math.min(this.mcpPolicyRetryAttempt, d.length - 1)];
|
|
39005
|
+
this.mcpPolicyRetryAttempt++;
|
|
39006
|
+
this.mcpPolicyRetryTimer = setTimeout(() => {
|
|
39007
|
+
this.mcpPolicyRetryTimer = null;
|
|
39008
|
+
if (!this.closingDown) this.recomputeMcpPolicy();
|
|
39009
|
+
}, delay);
|
|
39010
|
+
this.mcpPolicyRetryTimer.unref?.();
|
|
37988
39011
|
}
|
|
37989
39012
|
/**
|
|
37990
39013
|
* Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
|
|
@@ -38077,6 +39100,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38077
39100
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
38078
39101
|
const reportedSearchProviders = this.searchProvidersCapable();
|
|
38079
39102
|
const reportedUtilityTools = this.utilityToolsCapable();
|
|
39103
|
+
const reportedMcpAck = this.mcpAckSignature();
|
|
39104
|
+
const carriedOwnerDmTurn = this.toolStatusSender.carry();
|
|
38080
39105
|
const report = {
|
|
38081
39106
|
applied_version: version,
|
|
38082
39107
|
paths: checkConfiguredPaths(skillsJson),
|
|
@@ -38146,8 +39171,20 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38146
39171
|
// D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
|
|
38147
39172
|
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders),
|
|
38148
39173
|
// Weather/currency §8 item 5: a separate key, false with no runtime attached.
|
|
38149
|
-
utility_tools_v1: reportedUtilityTools
|
|
38150
|
-
|
|
39174
|
+
utility_tools_v1: reportedUtilityTools,
|
|
39175
|
+
// MCP Task 19 (spec §12.3): separate from `capability`; content-free enums and pattern tokens only.
|
|
39176
|
+
mcp_capability: this.reportedMcpCapability(),
|
|
39177
|
+
mcp_servers: this.reportedMcpServers(),
|
|
39178
|
+
// Deletion-ack ruling §5.3: content-free scalars. `mcp_deletions_through` only while EVERY attached runtime socket
|
|
39179
|
+
// declared mcp_deletion_ack_v1 (absent = cannot acknowledge); `mcp_credentials_present` only once one reported.
|
|
39180
|
+
...this.reportedMcpAck(),
|
|
39181
|
+
// Tool status Phase 0 (spec §3.5, round 2 H-5): an updated bridge, with or without a record yet.
|
|
39182
|
+
tool_status_v1: true
|
|
39183
|
+
},
|
|
39184
|
+
// Tool status Phase 0 (spec §3.5): the record rides every apply-time report, so record_report's wholesale replace
|
|
39185
|
+
// never erases it. Ruling M-2: the sender's CLOSED record, never the latest one, so a report sent inside a bucket
|
|
39186
|
+
// never reveals that bucket's change early. Advisory and content-free.
|
|
39187
|
+
...carriedOwnerDmTurn ? { owner_dm_last_turn: carriedOwnerDmTurn } : {}
|
|
38151
39188
|
};
|
|
38152
39189
|
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
38153
39190
|
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
@@ -38169,6 +39206,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38169
39206
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
38170
39207
|
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
38171
39208
|
this.lastReportedUtilityTools = reportedUtilityTools;
|
|
39209
|
+
this.lastReportedMcpAck = reportedMcpAck;
|
|
38172
39210
|
if (version) writeServiceLog("info", "runtime_config.applied", { version });
|
|
38173
39211
|
else writeServiceLog("info", "runtime_config.legacy_memory_reported");
|
|
38174
39212
|
}
|
|
@@ -38245,6 +39283,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38245
39283
|
}
|
|
38246
39284
|
await this.restoreRuntimeInbox();
|
|
38247
39285
|
await this.loadToolGrantState();
|
|
39286
|
+
this.recomputeMcpPolicy();
|
|
38248
39287
|
this.recomputeManifestPolicy(NO_FEATURES);
|
|
38249
39288
|
try {
|
|
38250
39289
|
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
@@ -39262,7 +40301,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39262
40301
|
* for every envelope built, BEFORE it is published (SEC-06 #18 persists it
|
|
39263
40302
|
* there so a retry can resend the exact bytes).
|
|
39264
40303
|
*/
|
|
39265
|
-
async publishBody(channelId, body, id, onEnvelope, sealCheck) {
|
|
40304
|
+
async publishBody(channelId, body, id, onEnvelope, sealCheck, inReplyTo, mcpTurn) {
|
|
40305
|
+
const mcpGuard = mcpTurn === true || this.mcpReplyGuarded(channelId, inReplyTo ?? void 0);
|
|
40306
|
+
let attempt = 0;
|
|
39266
40307
|
this.enforceBodyRateFloor(channelId, body);
|
|
39267
40308
|
this.statusGate.replyStarted(channelId);
|
|
39268
40309
|
const joined = await this.reconcileJoin(channelId);
|
|
@@ -39302,6 +40343,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39302
40343
|
);
|
|
39303
40344
|
},
|
|
39304
40345
|
async (envelope) => {
|
|
40346
|
+
attempt++;
|
|
40347
|
+
if (mcpGuard) this.assertMcpReplyRelayFree(channelId, envelope, attempt);
|
|
39305
40348
|
if (onEnvelope) await onEnvelope(envelope);
|
|
39306
40349
|
return this.publishEnvelope(
|
|
39307
40350
|
channelId,
|
|
@@ -39311,6 +40354,77 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39311
40354
|
);
|
|
39312
40355
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
39313
40356
|
}
|
|
40357
|
+
/**
|
|
40358
|
+
* MCP Task 16 (round 2 H-D): message ids whose DELIVERED authorization (after release-time re-narrowing) carried an
|
|
40359
|
+
* `mcp:` grant, with their channel and whether the runtime acked them. Recorded at the buffer exit, the one place
|
|
40360
|
+
* every consumer's copy leaves from (over-recording a turn a non-capable consumer then received without the grant only
|
|
40361
|
+
* adds a check). Kept after the ack, so a late frame citing the turn is still checked. Bounded: the oldest ACKED entry
|
|
40362
|
+
* goes first.
|
|
40363
|
+
*/
|
|
40364
|
+
mcpDeliveredTurns = /* @__PURE__ */ new Map();
|
|
40365
|
+
mcpDeliveredTrackMax;
|
|
40366
|
+
/**
|
|
40367
|
+
* Record `m` when its authorization carries an `mcp:` grant. Returns false ONLY when `m` is a new turn, the record is
|
|
40368
|
+
* at its bound and no entry is acked: the caller (the buffer exit) must then issue no mcp: grant (audit M-2, fail
|
|
40369
|
+
* closed). `rebuild` (PR-B2 audit C-1 (b): a restored runtime-inbox row, or a replayed turn) records unconditionally:
|
|
40370
|
+
* those grants were issued before, so the record may exceed its bound by at most the runtime inbox's size.
|
|
40371
|
+
*/
|
|
40372
|
+
noteMcpDelivered(m, rebuild = false) {
|
|
40373
|
+
if (!m.authorization?.tool_grants.some((g) => g.tool.startsWith(MCP_GRANT_PREFIX))) return true;
|
|
40374
|
+
if (!this.mcpDeliveredTurns.has(m.message_id) && this.mcpDeliveredTurns.size >= this.mcpDeliveredTrackMax) {
|
|
40375
|
+
let victim;
|
|
40376
|
+
for (const [id, r] of this.mcpDeliveredTurns) if (r.acked) {
|
|
40377
|
+
victim = id;
|
|
40378
|
+
break;
|
|
40379
|
+
}
|
|
40380
|
+
if (victim !== void 0) this.mcpDeliveredTurns.delete(victim);
|
|
40381
|
+
else if (!rebuild) return false;
|
|
40382
|
+
}
|
|
40383
|
+
this.mcpDeliveredTurns.set(m.message_id, { channelId: m.channel_id, acked: false });
|
|
40384
|
+
return true;
|
|
40385
|
+
}
|
|
40386
|
+
/**
|
|
40387
|
+
* H-D: whether a frame to `channelId` must pass the relay-free check: it answers a recorded MCP turn (any channel:
|
|
40388
|
+
* a frame of that turn sent elsewhere is checked against ITS channel), or it cites no turn while an MCP turn is
|
|
40389
|
+
* unacked in the same channel.
|
|
40390
|
+
*/
|
|
40391
|
+
mcpReplyGuarded(channelId, inReplyTo) {
|
|
40392
|
+
if (inReplyTo !== void 0 && this.mcpDeliveredTurns.has(inReplyTo)) return true;
|
|
40393
|
+
if (inReplyTo !== void 0) return false;
|
|
40394
|
+
for (const r of this.mcpDeliveredTurns.values()) if (!r.acked && r.channelId === channelId) return true;
|
|
40395
|
+
return false;
|
|
40396
|
+
}
|
|
40397
|
+
/**
|
|
40398
|
+
* H-D, synchronous: read the channel's local leaves from the warm manager in one step and tie the read to the
|
|
40399
|
+
* envelope's SEALED group and epoch, so the leaf set is exactly the membership that can open these bytes. Owner and
|
|
40400
|
+
* agent only: pass. Anyone else: withhold. The tree moved past the seal on the first attempt: StaleEpochError, which
|
|
40401
|
+
* sendWithSelfHeal answers with its one converge-and-re-seal (the existing semantics); still unprovable on the retry,
|
|
40402
|
+
* or no warm manager, no pin, or any throw: withhold (fail closed). Read-only; never touches the group.
|
|
40403
|
+
*
|
|
40404
|
+
* Scope (PR-B2 audit item 5): this guards the `send` path (publishBody) only. `status` detail, the `action_*` run
|
|
40405
|
+
* frames, `/v1/send-attachment` and summary frames are NOT checked; they are safe for MCP turns only because the
|
|
40406
|
+
* runtime emits none of them (pinned by connect-agent-runtime/src/outbound-frames.arch.test.ts). A runtime that starts
|
|
40407
|
+
* emitting any of them needs this guard extended to that frame first.
|
|
40408
|
+
*/
|
|
40409
|
+
assertMcpReplyRelayFree(channelId, envelope, attempt) {
|
|
40410
|
+
let verdict = "unknown";
|
|
40411
|
+
try {
|
|
40412
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
40413
|
+
const snap = this.provider.leafSnapshotCached(channelId);
|
|
40414
|
+
const sealedGroup = sealedGroupIdOf(envelope);
|
|
40415
|
+
if (pin !== null && snap !== null && sealedGroup !== void 0 && snap.groupId === sealedGroup) {
|
|
40416
|
+
if (snap.epoch === envelope.mls_group_epoch) {
|
|
40417
|
+
verdict = ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId }) ? "free" : "relay";
|
|
40418
|
+
} else if (snap.epoch > envelope.mls_group_epoch) verdict = "moved";
|
|
40419
|
+
}
|
|
40420
|
+
} catch {
|
|
40421
|
+
verdict = "unknown";
|
|
40422
|
+
}
|
|
40423
|
+
if (verdict === "free") return;
|
|
40424
|
+
if (verdict === "moved" && attempt === 1) throw new StaleEpochError(envelope.mls_group_epoch, envelope.mls_group_epoch + 1);
|
|
40425
|
+
writeServiceLog("warn", "mcp.reply_withheld", { state: ServiceLogEnum.of(verdict, ["relay", "moved", "unknown"]) });
|
|
40426
|
+
throw new McpReplyWithheldError(channelId);
|
|
40427
|
+
}
|
|
39314
40428
|
/**
|
|
39315
40429
|
* Audit M-B1 (controller ruling): a reply to a room-granted turn is sealed
|
|
39316
40430
|
* only at the epoch the room was classified at. On a move, the room is
|
|
@@ -39322,9 +40436,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39322
40436
|
* the redelivery replay (replayReplyIntents, which has no in_reply_to in its
|
|
39323
40437
|
* options) is checked the same as a live send.
|
|
39324
40438
|
*/
|
|
39325
|
-
async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope) {
|
|
40439
|
+
async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope, mcpTurn) {
|
|
39326
40440
|
const grant = inReplyTo !== null ? this.roomGrantEpochs.get(inReplyTo) : void 0;
|
|
39327
|
-
if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope);
|
|
40441
|
+
if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope, void 0, inReplyTo, mcpTurn);
|
|
39328
40442
|
if (channelId !== grant.channelId) {
|
|
39329
40443
|
writeServiceLog("warn", "reply.room_class_changed");
|
|
39330
40444
|
throw new RoomClassChangedSendError(channelId);
|
|
@@ -39346,7 +40460,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39346
40460
|
},
|
|
39347
40461
|
(epoch) => {
|
|
39348
40462
|
if (epoch !== grant.epoch) throw new RoomGrantEpochMovedError();
|
|
39349
|
-
}
|
|
40463
|
+
},
|
|
40464
|
+
inReplyTo,
|
|
40465
|
+
mcpTurn
|
|
39350
40466
|
);
|
|
39351
40467
|
} catch (e) {
|
|
39352
40468
|
if (!(e instanceof RoomGrantEpochMovedError)) throw e;
|
|
@@ -39481,14 +40597,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39481
40597
|
const k = replyIntentKey({ inReplyTo: opts.inReplyTo, idempotencyKey: opts.idempotencyKey, ordinal: () => 0 });
|
|
39482
40598
|
if (!k) throw new Error("idempotent send without a key");
|
|
39483
40599
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39484
|
-
const
|
|
39485
|
-
const
|
|
39486
|
-
|
|
40600
|
+
const mcpTurn = this.mcpReplyGuarded(channelId, opts.inReplyTo);
|
|
40601
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, mcpTurn);
|
|
40602
|
+
const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null, mcpTurn });
|
|
40603
|
+
if (intent.bodyMac !== replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, intent.mcpTurn) || intent.channelId !== channelId || intent.state === "skipped") {
|
|
39487
40604
|
throw new IdempotencyConflictError();
|
|
39488
40605
|
}
|
|
39489
40606
|
return k;
|
|
39490
40607
|
}
|
|
39491
|
-
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null);
|
|
40608
|
+
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null, false);
|
|
39492
40609
|
});
|
|
39493
40610
|
return this.publishIntent(key, opts);
|
|
39494
40611
|
}
|
|
@@ -39497,25 +40614,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39497
40614
|
* the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
|
|
39498
40615
|
* after a slot collision: the body must go to a NEW part.
|
|
39499
40616
|
*/
|
|
39500
|
-
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit) {
|
|
40617
|
+
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit, inheritedMcpTurn) {
|
|
39501
40618
|
const credit = replyCredit ?? void 0;
|
|
40619
|
+
const mcpTurn = inheritedMcpTurn || this.mcpReplyGuarded(channelId, inReplyTo);
|
|
39502
40620
|
const scope = `reply:${inReplyTo}`;
|
|
39503
40621
|
let latest = null;
|
|
39504
40622
|
for (const intent of await this.store.listReplyIntents(inReplyTo)) {
|
|
39505
40623
|
const n = intent.scope === scope ? partOrdinal(intent.part) : null;
|
|
39506
40624
|
if (n !== null && (latest === null || n > latest.ordinal)) latest = { ordinal: n, intent };
|
|
39507
40625
|
}
|
|
39508
|
-
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit)) {
|
|
40626
|
+
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit, latest.intent.mcpTurn)) {
|
|
39509
40627
|
return { scope, part: latest.intent.part, inReplyTo };
|
|
39510
40628
|
}
|
|
39511
40629
|
let ordinal = latest ? latest.ordinal + 1 : 0;
|
|
39512
40630
|
for (let i = 0; i < REPLY_MAX_PART_ADVANCES; i++, ordinal++) {
|
|
39513
40631
|
const k = { scope, part: `p:${ordinal}`, inReplyTo };
|
|
39514
40632
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39515
|
-
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit);
|
|
39516
|
-
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit });
|
|
40633
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, mcpTurn);
|
|
40634
|
+
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit, mcpTurn });
|
|
39517
40635
|
if (created) return k;
|
|
39518
|
-
if (matchRetry && intent.state !== "skipped" && intent.
|
|
40636
|
+
if (matchRetry && intent.state !== "skipped" && intent.channelId === channelId && intent.bodyMac === replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, intent.mcpTurn)) return k;
|
|
39519
40637
|
writeServiceLog("info", "reply.part_slot_taken", { attempt: i + 1 });
|
|
39520
40638
|
}
|
|
39521
40639
|
throw new Error("could not claim a reply part");
|
|
@@ -39567,7 +40685,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39567
40685
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
|
|
39568
40686
|
const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
|
|
39569
40687
|
try {
|
|
39570
|
-
await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo);
|
|
40688
|
+
await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo, intent.mcpTurn);
|
|
39571
40689
|
} catch (e) {
|
|
39572
40690
|
if (e instanceof AgentPausedSendError || e instanceof SendForbiddenError) {
|
|
39573
40691
|
if (intent.inReplyTo !== null) this.store.deletePendingReplyIntents(intent.inReplyTo);
|
|
@@ -39594,7 +40712,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39594
40712
|
JSON.stringify(["scope", key.scope]),
|
|
39595
40713
|
() => (
|
|
39596
40714
|
// r2 BLOCKER 1: the colliding row's OWN credit (verified above), never `opts`.
|
|
39597
|
-
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null)
|
|
40715
|
+
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null, intent.mcpTurn)
|
|
39598
40716
|
)
|
|
39599
40717
|
);
|
|
39600
40718
|
return this.publishIntent(next, opts, advances + 1);
|
|
@@ -39616,7 +40734,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39616
40734
|
if (credit !== null && !isReplyCredit(credit)) return false;
|
|
39617
40735
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, intent.channelId, intent);
|
|
39618
40736
|
if (id !== intent.replyMessageId) return false;
|
|
39619
|
-
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0) === intent.bodyMac;
|
|
40737
|
+
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0, intent.mcpTurn === true) === intent.bodyMac;
|
|
39620
40738
|
}
|
|
39621
40739
|
/**
|
|
39622
40740
|
* Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
|
|
@@ -39637,12 +40755,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39637
40755
|
* failure. Either way the intent stays pending, so the redelivery
|
|
39638
40756
|
* republishes the same reply.
|
|
39639
40757
|
*/
|
|
39640
|
-
async publishIntentWithRetry(channelId, body, id, key, inReplyTo) {
|
|
40758
|
+
async publishIntentWithRetry(channelId, body, id, key, inReplyTo, mcpTurn) {
|
|
39641
40759
|
let rateLimited = 0;
|
|
39642
40760
|
let inFlight = 0;
|
|
39643
40761
|
for (; ; ) {
|
|
39644
40762
|
try {
|
|
39645
|
-
await this.publishIntentOnce(channelId, body, id, key, inReplyTo);
|
|
40763
|
+
await this.publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn);
|
|
39646
40764
|
return;
|
|
39647
40765
|
} catch (e) {
|
|
39648
40766
|
if (e instanceof ReplyInFlightError) {
|
|
@@ -39713,8 +40831,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39713
40831
|
* original ack; a conflict can only mean the slot belongs to another
|
|
39714
40832
|
* message (for example an old reply whose intent was pruned).
|
|
39715
40833
|
*/
|
|
39716
|
-
async publishIntentOnce(channelId, body, id, key, inReplyTo) {
|
|
40834
|
+
async publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn) {
|
|
39717
40835
|
const current = await this.store.getReplyIntent(key.scope, key.part);
|
|
40836
|
+
const guardMcp = mcpTurn || current?.mcpTurn === true;
|
|
39718
40837
|
try {
|
|
39719
40838
|
if (current?.envelope) {
|
|
39720
40839
|
const cached = JSON.parse(new TextDecoder().decode(current.envelope));
|
|
@@ -39764,7 +40883,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39764
40883
|
key.part,
|
|
39765
40884
|
new TextEncoder().encode(JSON.stringify(envelope)),
|
|
39766
40885
|
sealedGroupIdOf(envelope)
|
|
39767
|
-
)
|
|
40886
|
+
),
|
|
40887
|
+
guardMcp
|
|
39768
40888
|
);
|
|
39769
40889
|
} catch (e) {
|
|
39770
40890
|
if (publishConflictCode(e) === "message_id_conflict") throw new ReplyIdCollisionError();
|
|
@@ -42275,16 +43395,20 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42275
43395
|
}
|
|
42276
43396
|
if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
|
|
42277
43397
|
let leafClass = "ok";
|
|
43398
|
+
let relay = void 0;
|
|
42278
43399
|
if (this.freshMeta(keyChannel)?.is_dm === true) {
|
|
42279
43400
|
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
42280
43401
|
if (signedDm !== void 0 && signedDm !== keyChannel) return this.ownerTurnFailed(m, "wrong_dm_channel");
|
|
42281
|
-
|
|
43402
|
+
const leaves = (await this.provider.leafTreeSummary(m.channelId)).leaves;
|
|
43403
|
+
const relayIdentityId = this.toolGrantManifest?.owner_dm_relay_identity_id;
|
|
43404
|
+
leafClass = classifyOwnerDmLeaves(leaves, {
|
|
42282
43405
|
ownerIdentityId: pin.ownerIdentityId,
|
|
42283
43406
|
agentIdentityId: this.cfg.identityId,
|
|
42284
|
-
relayIdentityId
|
|
43407
|
+
relayIdentityId,
|
|
42285
43408
|
isServerRelay: (id) => this.freshRoster(m.channelId)?.get(id)?.is_relay === true
|
|
42286
43409
|
});
|
|
42287
43410
|
if (leafClass === "foreign") return this.ownerTurnFailed(m, "not_owner_dm_leaves");
|
|
43411
|
+
relay = ownerDmRelayOf(leafClass, leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId, relayIdentityId });
|
|
42288
43412
|
}
|
|
42289
43413
|
const held = await this.heldPosition(m.channelId);
|
|
42290
43414
|
if (held.groupId === null) return this.ownerTurnFailed(m, "verify_error");
|
|
@@ -42293,7 +43417,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42293
43417
|
}
|
|
42294
43418
|
const replay = Object.freeze({ channelId: m.channelId, groupId: held.groupId, epoch: v.mls_group_epoch });
|
|
42295
43419
|
this.ownerReplayPending.set(m.messageId, replay);
|
|
42296
|
-
|
|
43420
|
+
const relayField = relay !== void 0 ? { relay } : {};
|
|
43421
|
+
if (leafClass === "unsigned_relay") return Object.freeze({ kind: "constants_only", replay, ...relayField });
|
|
43422
|
+
return Object.freeze({ kind: "verified", replay, ...relayField, relayLeaf: relayLeafFromPhase0Relay(relay) });
|
|
42297
43423
|
} catch {
|
|
42298
43424
|
return this.ownerTurnFailed(m, "verify_error");
|
|
42299
43425
|
}
|
|
@@ -42369,6 +43495,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42369
43495
|
}
|
|
42370
43496
|
/** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
|
|
42371
43497
|
async loadToolGrantState() {
|
|
43498
|
+
try {
|
|
43499
|
+
this.mcpSeen = await this.store.loadMcpSeen();
|
|
43500
|
+
} catch (e) {
|
|
43501
|
+
this.mcpSeen = true;
|
|
43502
|
+
writeServiceLog("warn", "authz.mcp_seen_load_failed", {}, e);
|
|
43503
|
+
}
|
|
42372
43504
|
const pin = this.cfg.ownerGrantPin?.pin;
|
|
42373
43505
|
if (!pin) return;
|
|
42374
43506
|
try {
|
|
@@ -42376,9 +43508,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42376
43508
|
this.store.dropToolGrantAppliedExcept(pin.publicKeyB64);
|
|
42377
43509
|
const saved = await this.store.loadToolGrantManifest(pin.publicKeyB64);
|
|
42378
43510
|
if (saved) {
|
|
42379
|
-
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null });
|
|
43511
|
+
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
|
|
42380
43512
|
if (v.state === "ok") {
|
|
42381
43513
|
this.toolGrantManifest = v.manifest;
|
|
43514
|
+
if (v.manifest.signedMcpPresent && !this.mcpSeen) await this.setMcpSeen(true);
|
|
42382
43515
|
this.toolGrantCounter = v.manifest.counter;
|
|
42383
43516
|
} else {
|
|
42384
43517
|
this.logManifest("malformed");
|
|
@@ -42403,8 +43536,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42403
43536
|
this.logManifest("no_manifest");
|
|
42404
43537
|
return;
|
|
42405
43538
|
}
|
|
42406
|
-
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter });
|
|
43539
|
+
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
|
|
42407
43540
|
if (v.state === "ok") {
|
|
43541
|
+
if (v.manifest.signedMcpPresent) await this.setMcpSeen(true);
|
|
42408
43542
|
try {
|
|
42409
43543
|
await this.store.saveToolGrantManifest(pin.publicKeyB64, v.manifest.counter, v.wrapper);
|
|
42410
43544
|
} catch {
|
|
@@ -42413,13 +43547,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42413
43547
|
}
|
|
42414
43548
|
this.toolGrantManifest = v.manifest;
|
|
42415
43549
|
this.toolGrantCounter = v.manifest.counter;
|
|
43550
|
+
this.recomputeMcpPolicy();
|
|
43551
|
+
if (v.manifest.mcpKey === "present" && !v.manifest.signedMcpPresent && this.mcpSeen) await this.setMcpSeen(false);
|
|
42416
43552
|
this.logManifest("ok");
|
|
42417
43553
|
if (v.droppedUnknown) this.logManifest("unknown_entry");
|
|
42418
43554
|
return;
|
|
42419
43555
|
}
|
|
42420
43556
|
this.logManifest(v.state);
|
|
43557
|
+
if (v.state === "wrong_owner") {
|
|
43558
|
+
this.dropOwnerDmLastTurn();
|
|
43559
|
+
this.mcpDeletionsThrough = 0;
|
|
43560
|
+
this.mcpPolicyWrittenCounter = null;
|
|
43561
|
+
this.noteMcpAckChanged();
|
|
43562
|
+
}
|
|
42421
43563
|
if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
|
|
42422
43564
|
}
|
|
43565
|
+
/**
|
|
43566
|
+
* MCP Task 14: the R-3 hold Task 17 filters on and Task 15 writes as `r3_hold`. True while the manifest in force signs
|
|
43567
|
+
* an owner_dm `mcp:` string, or while the sticky flag is set (an old client's re-sign cannot lift it, audit C-1).
|
|
43568
|
+
*/
|
|
43569
|
+
r3Hold() {
|
|
43570
|
+
return this.toolGrantManifest?.signedMcpPresent === true || this.mcpSeen;
|
|
43571
|
+
}
|
|
43572
|
+
/** Persist then mirror. Setting mirrors even on a failed write; clearing mirrors only after the write (fail closed). */
|
|
43573
|
+
async setMcpSeen(seen) {
|
|
43574
|
+
if (seen) this.mcpSeen = true;
|
|
43575
|
+
try {
|
|
43576
|
+
await this.store.saveMcpSeen(seen);
|
|
43577
|
+
} catch (e) {
|
|
43578
|
+
writeServiceLog("warn", "authz.mcp_seen_save_failed", {}, e);
|
|
43579
|
+
return;
|
|
43580
|
+
}
|
|
43581
|
+
this.mcpSeen = seen;
|
|
43582
|
+
}
|
|
42423
43583
|
logManifest(state) {
|
|
42424
43584
|
this.manifestState = state;
|
|
42425
43585
|
const quiet = state === "ok" || state === "unchanged" || state === "no_pin" || state === "no_manifest";
|
|
@@ -42516,6 +43676,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42516
43676
|
let tool_grants = NO_TOOL_GRANTS;
|
|
42517
43677
|
let relaySpeaker;
|
|
42518
43678
|
let memoryWrite = false;
|
|
43679
|
+
let relayFree = false;
|
|
42519
43680
|
const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId) && (opts.voice === void 0 || rosterFreshAt(vEntry, opts.voice.heldEpoch, opts.voice.heldGroupId));
|
|
42520
43681
|
const ownerDmFresh = epochFresh || opts.voice === void 0 && this.ownerDmCycleFresh.has(channelId);
|
|
42521
43682
|
if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
|
|
@@ -42542,6 +43703,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42542
43703
|
tool_grants = ROOM_DEFAULT_READ_GRANTS;
|
|
42543
43704
|
}
|
|
42544
43705
|
let grantSource = tool_grants.length ? "constant" : "none";
|
|
43706
|
+
let ownerDmEffective = null;
|
|
42545
43707
|
const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
|
|
42546
43708
|
const binding = this.manifestBinding;
|
|
42547
43709
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
@@ -42560,6 +43722,21 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42560
43722
|
});
|
|
42561
43723
|
tool_grants = r.grants;
|
|
42562
43724
|
grantSource = r.source;
|
|
43725
|
+
ownerDmEffective = r;
|
|
43726
|
+
const mcp = mcpIssuance({
|
|
43727
|
+
source: r.source,
|
|
43728
|
+
verdict: opts.ownerVerdict,
|
|
43729
|
+
entitlement: this.mcpEntitlementInForce(),
|
|
43730
|
+
hostSwitch: this.mcpHostSwitch,
|
|
43731
|
+
ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
|
|
43732
|
+
disabled: this.payloadToolDeny,
|
|
43733
|
+
manifest,
|
|
43734
|
+
policy: this.mcpPolicyWritten
|
|
43735
|
+
});
|
|
43736
|
+
if (mcp.length > 0) {
|
|
43737
|
+
tool_grants = Object.freeze([...tool_grants, ...mcp]);
|
|
43738
|
+
relayFree = true;
|
|
43739
|
+
}
|
|
42563
43740
|
} else if (tool_grants === ROOM_DEFAULT_READ_GRANTS) {
|
|
42564
43741
|
const r = effectiveToolGrants({
|
|
42565
43742
|
audience: "team_room",
|
|
@@ -42577,12 +43754,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42577
43754
|
if (opts.ownerVerdict?.kind === "failed") {
|
|
42578
43755
|
tool_grants = NO_TOOL_GRANTS;
|
|
42579
43756
|
memoryWrite = false;
|
|
43757
|
+
relayFree = false;
|
|
42580
43758
|
relaySpeaker = void 0;
|
|
42581
43759
|
grantSource = "none";
|
|
42582
43760
|
}
|
|
42583
43761
|
const roomClass = meta?.is_dm !== false ? "na" : entry?.crossOrg === false ? "internal" : entry?.crossOrg === true ? "cross_org" : "unknown";
|
|
42584
43762
|
if (opts.log !== false) {
|
|
42585
43763
|
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite, grantSource);
|
|
43764
|
+
const pinOwner = this.cfg.ownerGrantPin?.pin?.ownerIdentityId;
|
|
43765
|
+
const verdict = opts.ownerVerdict;
|
|
43766
|
+
if (pinOwner !== void 0 && senderId === pinOwner && meta?.is_dm === true && relaySpeakerId === void 0 && opts.voiceScoped !== true && opts.voice === void 0 && verdict !== void 0 && verdict.kind !== "unchecked" && !(verdict.kind === "failed" && verdict.reason === "replay")) {
|
|
43767
|
+
const pinned = this.channelOwner.get(channelId);
|
|
43768
|
+
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
43769
|
+
const ownerDirect = ownerId !== void 0 && audience === "owner_dm" && senderId === ownerId;
|
|
43770
|
+
this.noteOwnerDmTurn({
|
|
43771
|
+
cause: {
|
|
43772
|
+
killSwitchOff: !this.ownerDmDefaultGrants,
|
|
43773
|
+
manifestSwitchOff: !this.ownerDmManifestGrants,
|
|
43774
|
+
pinMissingOrAnomalous: ownerId === void 0,
|
|
43775
|
+
// OQ-5: only from a PRESENT roster entry; a missing (TTL-expired) entry is `stale` below.
|
|
43776
|
+
senderNotOwner: entry !== void 0 && (sender === void 0 || !ownerDirect),
|
|
43777
|
+
stale: !ownerDmFresh || entry === void 0,
|
|
43778
|
+
verdict,
|
|
43779
|
+
manifestPresent: manifest !== null,
|
|
43780
|
+
signedDmChannel: manifest?.owner_dm_channel_id,
|
|
43781
|
+
channelId,
|
|
43782
|
+
boundNow,
|
|
43783
|
+
consumerAttached: this.manifestConsumer !== null
|
|
43784
|
+
},
|
|
43785
|
+
grantSource,
|
|
43786
|
+
toolGrants: tool_grants,
|
|
43787
|
+
withheld: grantSource === "manifest" && ownerDmEffective !== null ? ownerDmEffective.withheld : []
|
|
43788
|
+
});
|
|
43789
|
+
}
|
|
42586
43790
|
}
|
|
42587
43791
|
return Object.freeze({
|
|
42588
43792
|
version: 1,
|
|
@@ -42590,9 +43794,78 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42590
43794
|
sender_membership: sender ? "active" : "unknown",
|
|
42591
43795
|
tool_grants,
|
|
42592
43796
|
...relaySpeaker ? { relay_speaker: relaySpeaker } : {},
|
|
42593
|
-
...memoryWrite ? { memory_write: true } : {}
|
|
43797
|
+
...memoryWrite ? { memory_write: true } : {},
|
|
43798
|
+
...relayFree ? { relay_free: true } : {}
|
|
42594
43799
|
});
|
|
42595
43800
|
}
|
|
43801
|
+
/**
|
|
43802
|
+
* Tool status Phase 0 (spec §3.2, §3.5; plan Task 8; round 1 H-2, M-4; round 2 H-4): build the candidate record and
|
|
43803
|
+
* keep the previous one (and its `at`) when the change tuple is the same, so `at` means "last changed". Never throws
|
|
43804
|
+
* into the authorization path. Never logs a channel id, sender id or tool name.
|
|
43805
|
+
*/
|
|
43806
|
+
noteOwnerDmTurn(t) {
|
|
43807
|
+
try {
|
|
43808
|
+
const v = t.cause.verdict;
|
|
43809
|
+
const relay = (v.kind === "verified" || v.kind === "constants_only") && v.relay !== void 0 ? v.relay : "unknown";
|
|
43810
|
+
let reason = {};
|
|
43811
|
+
if (t.grantSource !== "manifest") {
|
|
43812
|
+
const c = this.toolStatusCause(t.cause);
|
|
43813
|
+
if (c !== null && c.source === t.grantSource && c.cause !== void 0) {
|
|
43814
|
+
reason = c.sub !== void 0 ? { cause: c.cause, sub: c.sub } : { cause: c.cause };
|
|
43815
|
+
} else {
|
|
43816
|
+
writeServiceLog("warn", "tool_status.source_mismatch");
|
|
43817
|
+
}
|
|
43818
|
+
} else {
|
|
43819
|
+
const c = this.toolStatusCause(t.cause);
|
|
43820
|
+
if (c === null || c.source !== "manifest") writeServiceLog("warn", "tool_status.source_mismatch");
|
|
43821
|
+
}
|
|
43822
|
+
const names = t.toolGrants.map((g) => g.tool);
|
|
43823
|
+
const candidate = Object.freeze({
|
|
43824
|
+
at: bucketStartAt(this.toolStatusNow()),
|
|
43825
|
+
// ruling M-2: the 15-minute UTC bucket, never the minute
|
|
43826
|
+
counter: this.toolGrantCounter,
|
|
43827
|
+
source: t.grantSource,
|
|
43828
|
+
...reason,
|
|
43829
|
+
relay,
|
|
43830
|
+
issued: Object.freeze(sanitizeIssued(names)),
|
|
43831
|
+
withheld: Object.freeze(t.withheld.map((w) => Object.freeze({ tool: w.tool, why: w.why }))),
|
|
43832
|
+
// A count only, never a name (content-free). 0 unless the MCP issuance above added mcp: grants.
|
|
43833
|
+
mcp_issued: Math.min(160, names.filter((n) => n.startsWith("mcp:")).length)
|
|
43834
|
+
});
|
|
43835
|
+
if (sameTuple(this.ownerDmLastTurn, candidate)) return;
|
|
43836
|
+
this.ownerDmLastTurn = candidate;
|
|
43837
|
+
this.toolStatusSender.note(candidate);
|
|
43838
|
+
} catch (e) {
|
|
43839
|
+
writeServiceLog("warn", "tool_status.record_failed", {}, e);
|
|
43840
|
+
}
|
|
43841
|
+
}
|
|
43842
|
+
/** R2-M1: the served manifest names another owner than the pin; the held record belongs to the old owner. */
|
|
43843
|
+
dropOwnerDmLastTurn() {
|
|
43844
|
+
this.ownerDmLastTurn = null;
|
|
43845
|
+
this.toolStatusSender.drop();
|
|
43846
|
+
}
|
|
43847
|
+
/**
|
|
43848
|
+
* Phase 0 Task 9 (spec §3.5): POST the record, body = the record only, to the narrow agent-key route. The backend
|
|
43849
|
+
* resolves the identity from the device credential; no identity, channel or sender id is in the URL or the body.
|
|
43850
|
+
* Returns the status class only; never throws.
|
|
43851
|
+
*/
|
|
43852
|
+
async postToolStatus(rec) {
|
|
43853
|
+
try {
|
|
43854
|
+
const res = await this.fetchBounded("runtime_config_tool_status", "control", `${this.cfg.apiBase}/api/v1/agents/me/runtime-config/tool-status`, {
|
|
43855
|
+
method: "POST",
|
|
43856
|
+
headers: { authorization: `Bearer ${this.cfg.apiKey}`, "content-type": "application/json" },
|
|
43857
|
+
body: JSON.stringify(rec)
|
|
43858
|
+
});
|
|
43859
|
+
const retryAfter = res.headers.get("retry-after");
|
|
43860
|
+
try {
|
|
43861
|
+
await res.body?.cancel();
|
|
43862
|
+
} catch {
|
|
43863
|
+
}
|
|
43864
|
+
return { status: res.status, retryAfter };
|
|
43865
|
+
} catch (e) {
|
|
43866
|
+
return e?.name === "FetchDeadlineError" ? "timeout" : "network";
|
|
43867
|
+
}
|
|
43868
|
+
}
|
|
42596
43869
|
/**
|
|
42597
43870
|
* One content-free INFO line per authorization decision (every call site of
|
|
42598
43871
|
* authorizationFor stages exactly one runtime turn). It says WHY a turn got
|
|
@@ -42816,6 +44089,111 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42816
44089
|
* client's own wait already renders "Summary unavailable" — so there is
|
|
42817
44090
|
* nothing to catch here besides the send itself.
|
|
42818
44091
|
*/
|
|
44092
|
+
/**
|
|
44093
|
+
* MCP client Task 18 (spec §8 item 2; audit M-3, H-3; OQ-2, OQ-15). Accept an `mcp_catalog_request` only when: its
|
|
44094
|
+
* body is exactly the §8 shape; it is a direct frame (never a voice channel's) in a channel the server marks as a DM
|
|
44095
|
+
* that IS the signed owner DM; verifyOwnerTurn says `verified` (the pinned owner's signature, the signed DM, the leaf
|
|
44096
|
+
* check, not a replay) with `relayLeaf === false`; mcp-policy.json as written is `ok` with this server `ok`; a runtime
|
|
44097
|
+
* socket that declared mcp_client_v1 is attached; and the rate limits admit it. The replay row is written HERE
|
|
44098
|
+
* (M-3: the request never reaches stageRuntimeInbox, where it is otherwise written), for every verified request, before
|
|
44099
|
+
* anything is answered; the ownerReplayPending entry is removed in `finally`. A failure to record answers nothing.
|
|
44100
|
+
* Never throws. The answer runs outside the ingest cycle, so the row is acked now.
|
|
44101
|
+
*/
|
|
44102
|
+
async acceptMcpCatalogRequest(m, body) {
|
|
44103
|
+
try {
|
|
44104
|
+
const req = parseMcpCatalogRequest(body);
|
|
44105
|
+
if (req === null) return logMcpCatalogRequest("malformed");
|
|
44106
|
+
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
44107
|
+
if (this.keyChannelForFrame(m) !== m.channelId || this.freshMeta(m.channelId)?.is_dm !== true || signedDm === void 0 || signedDm !== m.channelId) {
|
|
44108
|
+
return logMcpCatalogRequest("not_owner_dm");
|
|
44109
|
+
}
|
|
44110
|
+
const verdict = await this.verifyOwnerTurn(m);
|
|
44111
|
+
if (verdict.kind !== "verified") return logMcpCatalogRequest(verdict.kind === "failed" && verdict.reason === "replay" ? "replay" : "not_verified");
|
|
44112
|
+
this.store.recordOwnerMessage(verdict.replay.channelId, m.messageId, verdict.replay.groupId, verdict.replay.epoch, Date.now());
|
|
44113
|
+
if (verdict.relayLeaf !== false) return logMcpCatalogRequest("relay_leaf");
|
|
44114
|
+
const target = this.mcpCatalogTarget(req.serverId);
|
|
44115
|
+
const lister = this.mcpClientCapable() ? this.mcpCatalogListers.values().next().value : void 0;
|
|
44116
|
+
if (target === null || lister === void 0) return logMcpCatalogRequest("not_ready");
|
|
44117
|
+
const admitted = this.mcpCatalogRate.admit(req.serverId);
|
|
44118
|
+
if (admitted !== "ok") return admitted === "in_flight" ? logMcpCatalogRequest("in_flight") : void 0;
|
|
44119
|
+
const work = this.outsideIngestCycle(() => this.answerMcpCatalogRequest(m.channelId, req, lister)).finally(() => {
|
|
44120
|
+
this.mcpCatalogRate.release(req.serverId);
|
|
44121
|
+
this.mcpCatalogWork.delete(work);
|
|
44122
|
+
});
|
|
44123
|
+
this.mcpCatalogWork.add(work);
|
|
44124
|
+
} catch {
|
|
44125
|
+
logMcpCatalogRequest("failed");
|
|
44126
|
+
} finally {
|
|
44127
|
+
this.ownerReplayPending.delete(m.messageId);
|
|
44128
|
+
}
|
|
44129
|
+
}
|
|
44130
|
+
/** The signed server's vendor and the bridge catalog row's issuer, when mcp-policy.json as WRITTEN is `ok` with it `ok`. */
|
|
44131
|
+
mcpCatalogTarget(serverId) {
|
|
44132
|
+
const doc = this.mcpPolicyWritten;
|
|
44133
|
+
if (doc === null || doc.state !== "ok" || !Object.prototype.hasOwnProperty.call(doc.servers, serverId)) return null;
|
|
44134
|
+
const s = doc.servers[serverId];
|
|
44135
|
+
if (s.state !== "ok" || s.vendor === void 0) return null;
|
|
44136
|
+
const catalog = this.mcpCatalog ?? MCP_CATALOG;
|
|
44137
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
|
|
44138
|
+
return { vendor: s.vendor, issuer: catalog[s.vendor].issuer };
|
|
44139
|
+
}
|
|
44140
|
+
/** Ask the runtime, re-check, build (384 KB cap) and send. Never throws. */
|
|
44141
|
+
async answerMcpCatalogRequest(channelId, req, lister) {
|
|
44142
|
+
try {
|
|
44143
|
+
const target = this.mcpCatalogTarget(req.serverId);
|
|
44144
|
+
if (target === null) return logMcpCatalogRequest("not_ready");
|
|
44145
|
+
const frame = await lister(req.serverId, MCP_CATALOG_LIST_TIMEOUT_MS);
|
|
44146
|
+
if (frame === null || frame === void 0) return logMcpCatalogRequest("runtime_timeout");
|
|
44147
|
+
const listed = validateCatalogListReply(frame, req.serverId, target.vendor);
|
|
44148
|
+
if (listed === null) return logMcpCatalogRequest("runtime_refused");
|
|
44149
|
+
const still = this.mcpCatalogTarget(req.serverId);
|
|
44150
|
+
if (still === null || still.vendor !== target.vendor) return logMcpCatalogRequest("not_ready");
|
|
44151
|
+
await this.sendMcpCatalog(channelId, buildMcpCatalogBody({ serverId: req.serverId, requestId: req.requestId, vendor: still.vendor, issuer: still.issuer, reply: listed }));
|
|
44152
|
+
logMcpCatalogRequest("sent");
|
|
44153
|
+
} catch (e) {
|
|
44154
|
+
logMcpCatalogRequest(e instanceof McpReplyWithheldError ? "withheld" : "send_failed");
|
|
44155
|
+
}
|
|
44156
|
+
}
|
|
44157
|
+
/**
|
|
44158
|
+
* The `mcp_catalog` reply: a system_event envelope with a body-level type (the sendSummary shape), on the shared
|
|
44159
|
+
* self-heal seam and the one publish chokepoint. H-D applies to EVERY attempt (it carries vendor-supplied tool
|
|
44160
|
+
* descriptions): after the seal, before the POST, the leaves at the SEALED group and epoch must be exactly the owner
|
|
44161
|
+
* and the agent, or the frame is withheld. One message id across the retry.
|
|
44162
|
+
*/
|
|
44163
|
+
async sendMcpCatalog(channelId, body) {
|
|
44164
|
+
const joined = await this.reconcileJoin(channelId);
|
|
44165
|
+
if (!joined) throw new ChannelPendingError(channelId);
|
|
44166
|
+
const messageId = crypto.randomUUID();
|
|
44167
|
+
let attempt = 0;
|
|
44168
|
+
await this.sendWithSelfHealTerminal(
|
|
44169
|
+
this.provider,
|
|
44170
|
+
channelId,
|
|
44171
|
+
(epoch) => buildOutboundEnvelope({
|
|
44172
|
+
custody: this.custody,
|
|
44173
|
+
groupKey: this.provider,
|
|
44174
|
+
messageId,
|
|
44175
|
+
channelId,
|
|
44176
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
44177
|
+
senderId: this.cfg.identityId,
|
|
44178
|
+
senderType: "agent",
|
|
44179
|
+
type: "system_event",
|
|
44180
|
+
bodyPlaintext: JSON.stringify(body),
|
|
44181
|
+
bindingId: "",
|
|
44182
|
+
bindingVersion: "0.0.0",
|
|
44183
|
+
agentSkillId: null,
|
|
44184
|
+
epoch,
|
|
44185
|
+
traceparent: ZERO_TRACEPARENT,
|
|
44186
|
+
references: [],
|
|
44187
|
+
stage5Outcome: "allow",
|
|
44188
|
+
stage5Evaluators: []
|
|
44189
|
+
}),
|
|
44190
|
+
async (envelope) => {
|
|
44191
|
+
attempt++;
|
|
44192
|
+
this.assertMcpReplyRelayFree(channelId, envelope, attempt);
|
|
44193
|
+
return this.publishEnvelope(channelId, () => this.transport.publish(envelope));
|
|
44194
|
+
}
|
|
44195
|
+
);
|
|
44196
|
+
}
|
|
42819
44197
|
async handleSummarize(channelId, request) {
|
|
42820
44198
|
const body = await runSummarize({
|
|
42821
44199
|
request,
|
|
@@ -43326,6 +44704,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43326
44704
|
new TextDecoder().decode(m.plaintext)
|
|
43327
44705
|
);
|
|
43328
44706
|
if (parsed.type && parsed.type !== "text") {
|
|
44707
|
+
if (parsed.type === "mcp_catalog_request" || parsed.type === "mcp_catalog") {
|
|
44708
|
+
if (parsed.type === "mcp_catalog_request") await this.acceptMcpCatalogRequest(m, parsed);
|
|
44709
|
+
acked.push(m.queueId);
|
|
44710
|
+
continue;
|
|
44711
|
+
}
|
|
43329
44712
|
if (parsed.type === "summarize") {
|
|
43330
44713
|
const request = parseSummarizeBody(parsed, this.cfg.identityId, writeServiceLog);
|
|
43331
44714
|
if (request !== null) {
|
|
@@ -43834,7 +45217,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43834
45217
|
}
|
|
43835
45218
|
/** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
|
|
43836
45219
|
delivering(m) {
|
|
43837
|
-
|
|
45220
|
+
let out = this.renarrowManifestGrants(m);
|
|
45221
|
+
if (!this.noteMcpDelivered(out)) {
|
|
45222
|
+
out = withMcpCapability(out, false);
|
|
45223
|
+
writeServiceLog("warn", "mcp.delivered_record_full");
|
|
45224
|
+
}
|
|
43838
45225
|
this.noteDeliveredFrame(out);
|
|
43839
45226
|
return out;
|
|
43840
45227
|
}
|
|
@@ -43845,6 +45232,50 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43845
45232
|
* audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
|
|
43846
45233
|
*/
|
|
43847
45234
|
renarrowManifestGrants(m) {
|
|
45235
|
+
const narrowed = this.renarrowManifestGrantsBase(m);
|
|
45236
|
+
return this.renarrowMcpGrants(narrowed);
|
|
45237
|
+
}
|
|
45238
|
+
/**
|
|
45239
|
+
* MCP Task 16 (audit H-2; round 2 LOW), after the base re-narrowing: a buffered `mcp:` grant survives release only
|
|
45240
|
+
* when, NOW, the turn is still a plain owner-DM turn in the signed DM channel, the host switch reads "on", the
|
|
45241
|
+
* entitlement reading is in force, the name is not in skills.disabled, the mcp-policy.json document last written
|
|
45242
|
+
* lists the server ok with the tool, and a FRESH synchronous read of the DM's local leaves holds exactly the pinned
|
|
45243
|
+
* owner and the agent (MlsGroupKeyProvider.leafSnapshotCached: one `state` object, no await; null fails closed). Any
|
|
45244
|
+
* other mcp: grant is dropped, never kept as a fallback. relay_free is dropped whenever no mcp: grant survives.
|
|
45245
|
+
* Read-only: never touches the MLS group. Only narrows; returns `m` by reference when nothing changes.
|
|
45246
|
+
*/
|
|
45247
|
+
renarrowMcpGrants(m) {
|
|
45248
|
+
const a = m.authorization;
|
|
45249
|
+
if (!a) return m;
|
|
45250
|
+
const isMcp = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
|
|
45251
|
+
if (!a.tool_grants.some(isMcp) && a.relay_free === void 0) return m;
|
|
45252
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
45253
|
+
const manifest = pin !== null ? this.toolGrantManifest : null;
|
|
45254
|
+
const turnOk = pin !== null && manifest !== null && a.audience === "owner_dm" && a.relay_speaker === void 0 && manifest.owner_dm_channel_id === m.channel_id && this.mcpHostSwitch === "on" && this.ownerDmManifestGrants && this.mcpEntitlementInForce() === "entitled" && this.ownerDmRelayFreeNow(m.channel_id, pin.ownerIdentityId);
|
|
45255
|
+
const kept = a.tool_grants.filter((g) => {
|
|
45256
|
+
if (!isMcp(g)) return true;
|
|
45257
|
+
const parsed = parseMcpGrant(g.tool);
|
|
45258
|
+
return turnOk && g.mode === "read" && g.source === "manifest" && parsed !== null && !this.payloadToolDeny.has(g.tool) && mcpPolicyAllows(this.mcpPolicyWritten, parsed.serverId, parsed.tool);
|
|
45259
|
+
});
|
|
45260
|
+
const keepRelayFree = a.relay_free === true && kept.some(isMcp);
|
|
45261
|
+
if (kept.length === a.tool_grants.length && keepRelayFree === (a.relay_free === true)) return m;
|
|
45262
|
+
const { relay_free: _relayFree, ...rest } = a;
|
|
45263
|
+
return { ...m, authorization: Object.freeze({ ...rest, tool_grants: Object.freeze(kept), ...keepRelayFree ? { relay_free: true } : {} }) };
|
|
45264
|
+
}
|
|
45265
|
+
/**
|
|
45266
|
+
* MCP Task 16 (OQ-2; audit H-2, round 2 H-D): a FRESH read of the channel's local leaf set holds exactly the pinned
|
|
45267
|
+
* owner and this agent. Synchronous (one manager `state`, so the leaves belong to one epoch); a cold manager or any
|
|
45268
|
+
* throw reads as NOT relay-free.
|
|
45269
|
+
*/
|
|
45270
|
+
ownerDmRelayFreeNow(channelId, ownerIdentityId) {
|
|
45271
|
+
try {
|
|
45272
|
+
const snap = this.provider.leafSnapshotCached(channelId);
|
|
45273
|
+
return snap !== null && ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId, agentIdentityId: this.cfg.identityId });
|
|
45274
|
+
} catch {
|
|
45275
|
+
return false;
|
|
45276
|
+
}
|
|
45277
|
+
}
|
|
45278
|
+
renarrowManifestGrantsBase(m) {
|
|
43848
45279
|
const a = m.authorization;
|
|
43849
45280
|
if (!a || !a.tool_grants.some((g) => g.mode === "read" && g.source === "manifest")) return m;
|
|
43850
45281
|
const audience = a.audience === "owner_dm" || a.audience === "team_room" ? a.audience : null;
|
|
@@ -43940,6 +45371,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43940
45371
|
return;
|
|
43941
45372
|
}
|
|
43942
45373
|
if (!this.admitRuntimeDelivery(m)) return;
|
|
45374
|
+
this.noteMcpDelivered(m, true);
|
|
43943
45375
|
const ch = m.channel_id;
|
|
43944
45376
|
this.replayingChannels.add(ch);
|
|
43945
45377
|
this.replyIntentReplays++;
|
|
@@ -44290,11 +45722,11 @@ ${channelId}`;
|
|
|
44290
45722
|
if (!res.ok) return void 0;
|
|
44291
45723
|
const rows = await res.json();
|
|
44292
45724
|
if (!Array.isArray(rows)) return void 0;
|
|
44293
|
-
const
|
|
45725
|
+
const own2 = rows.find(
|
|
44294
45726
|
(r) => r?.identity_id === this.cfg.identityId
|
|
44295
45727
|
);
|
|
44296
|
-
if (!
|
|
44297
|
-
return typeof
|
|
45728
|
+
if (!own2) return void 0;
|
|
45729
|
+
return typeof own2.paused_at === "string" && own2.paused_at.length > 0;
|
|
44298
45730
|
} catch (e) {
|
|
44299
45731
|
writeServiceLog("warn", "pause.probe_failed", {}, e);
|
|
44300
45732
|
return void 0;
|
|
@@ -44327,6 +45759,10 @@ ${channelId}`;
|
|
|
44327
45759
|
/** Complete the durable handoff only after the runtime explicitly acks. */
|
|
44328
45760
|
ackRuntimeMessages(messageIds) {
|
|
44329
45761
|
for (const id of messageIds) this.roomGrantEpochs.delete(id);
|
|
45762
|
+
for (const id of messageIds) {
|
|
45763
|
+
const r = this.mcpDeliveredTurns.get(id);
|
|
45764
|
+
if (r) r.acked = true;
|
|
45765
|
+
}
|
|
44330
45766
|
for (const messageId of messageIds) {
|
|
44331
45767
|
this.store.ackRuntimeInbox(messageId);
|
|
44332
45768
|
this.runtimePendingIds.delete(messageId);
|
|
@@ -44408,6 +45844,7 @@ ${channelId}`;
|
|
|
44408
45844
|
const message = rest;
|
|
44409
45845
|
this.applyStagedMarks(message.message_id, marks);
|
|
44410
45846
|
this.runtimePendingIds.add(message.message_id);
|
|
45847
|
+
this.noteMcpDelivered(message, true);
|
|
44411
45848
|
if (this.cfg.ownerGrantPin?.pin && this.isOwnerVerifyFailed(message.message_id)) {
|
|
44412
45849
|
this.ownerVerifyFailedIds.set(message.message_id, message.channel_id);
|
|
44413
45850
|
}
|
|
@@ -44588,6 +46025,179 @@ ${channelId}`;
|
|
|
44588
46025
|
clearSearchProvidersSocket(token) {
|
|
44589
46026
|
if (this.searchProvidersSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
44590
46027
|
}
|
|
46028
|
+
/** MCP Task 16 (spec §15): the same AND-over-sockets state for mcp_client_v1, kept per socket. */
|
|
46029
|
+
mcpClientSockets = /* @__PURE__ */ new Map();
|
|
46030
|
+
noteMcpClientSocket(token, declared) {
|
|
46031
|
+
this.mcpClientSockets.set(token, declared);
|
|
46032
|
+
this.onCapabilityMaybeChanged();
|
|
46033
|
+
}
|
|
46034
|
+
clearMcpClientSocket(token) {
|
|
46035
|
+
if (this.mcpClientSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
46036
|
+
}
|
|
46037
|
+
/** Deletion-ack ruling §3: ws-server notes every runtime hello's mcp_deletion_ack_v1 declaration (harness: false). */
|
|
46038
|
+
noteMcpDeletionAckSocket(token, declared) {
|
|
46039
|
+
this.mcpDeletionAckSockets.set(token, declared);
|
|
46040
|
+
if (!declared) this.mcpTurnSeq.delete(token);
|
|
46041
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
46042
|
+
this.maybeReportMcpAck();
|
|
46043
|
+
}
|
|
46044
|
+
/** #1351 round 2 observation: with no ack-capable socket left (detached, or re-hello'd without the feature), the last
|
|
46045
|
+
* credentials value is no longer known: forget it, so a later socket does not inherit a stale value. */
|
|
46046
|
+
forgetCredentialsIfNoAckSocket() {
|
|
46047
|
+
if ([...this.mcpDeletionAckSockets.values()].some(Boolean)) return;
|
|
46048
|
+
this.mcpCredentialsPresentValue = null;
|
|
46049
|
+
this.mcpAckClosed = { ...this.mcpAckClosed, credentials: null };
|
|
46050
|
+
if (this.mcpAckOpen !== null) this.mcpAckOpen = { ...this.mcpAckOpen, credentials: null };
|
|
46051
|
+
}
|
|
46052
|
+
clearMcpDeletionAckSocket(token) {
|
|
46053
|
+
this.mcpTurnSeq.delete(token);
|
|
46054
|
+
if (!this.mcpDeletionAckSockets.delete(token)) return;
|
|
46055
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
46056
|
+
this.maybeReportMcpAck();
|
|
46057
|
+
}
|
|
46058
|
+
/** #1351 round 2 M-1: ws-server calls this when it sends an owner-DM turn to an ack-capable socket. Bounded. */
|
|
46059
|
+
noteMcpTurnDispatch(token, messageId) {
|
|
46060
|
+
let m = this.mcpTurnSeq.get(token);
|
|
46061
|
+
if (m === void 0) this.mcpTurnSeq.set(token, m = /* @__PURE__ */ new Map());
|
|
46062
|
+
m.set(messageId, this.policyWriteSeq);
|
|
46063
|
+
while (m.size > 64) m.delete(m.keys().next().value);
|
|
46064
|
+
}
|
|
46065
|
+
/** Ruling §3 / §5.3: the AND over attached sockets, false with none attached (as mcpClientCapable). */
|
|
46066
|
+
mcpDeletionAckCapable() {
|
|
46067
|
+
if (this.mcpDeletionAckSockets.size === 0) return false;
|
|
46068
|
+
for (const v of this.mcpDeletionAckSockets.values()) if (!v) return false;
|
|
46069
|
+
return true;
|
|
46070
|
+
}
|
|
46071
|
+
/**
|
|
46072
|
+
* Deletion-ack ruling (2026-10-31) §3 acceptance. ws-server has validated the frame shape and the socket (declared
|
|
46073
|
+
* mcp_deletion_ack_v1 and mcp_client_v1, no harness). `credentials_present` is taken as is. `applied_sha256` advances
|
|
46074
|
+
* `mcpDeletionsThrough` only when it is the SHA-256 of the bytes THIS bridge last wrote (JSON + newline) and still
|
|
46075
|
+
* holds in force, and that document's top state is one whose deletion set covers the signed removal; then to the
|
|
46076
|
+
* counter recorded at that write, never the live one. Anything else is ignored silently. Never throws.
|
|
46077
|
+
*/
|
|
46078
|
+
noteMcpDeletionAck(token, frame) {
|
|
46079
|
+
this.mcpCredentialsPresentValue = frame.credentials_present === true;
|
|
46080
|
+
const json2 = this.mcpPolicyWrittenJson;
|
|
46081
|
+
const doc = this.mcpPolicyWritten;
|
|
46082
|
+
const counter = this.mcpPolicyWrittenCounter;
|
|
46083
|
+
const turns = frame.message_id !== void 0 ? this.mcpTurnSeq.get(token) : void 0;
|
|
46084
|
+
const seqAtDispatch = frame.message_id !== void 0 ? turns?.get(frame.message_id) : void 0;
|
|
46085
|
+
if (frame.message_id !== void 0) turns?.delete(frame.message_id);
|
|
46086
|
+
if (frame.applied_sha256 !== void 0 && seqAtDispatch !== void 0 && seqAtDispatch === this.policyWriteSeq && json2 !== null && doc !== null && counter !== null && MCP_ACK_STATES.has(doc.state) && frame.applied_sha256 === createHash6("sha256").update(json2 + "\n", "utf8").digest("hex")) {
|
|
46087
|
+
this.mcpDeletionsThrough = Math.max(this.mcpDeletionsThrough, counter);
|
|
46088
|
+
}
|
|
46089
|
+
this.noteMcpAckChanged();
|
|
46090
|
+
}
|
|
46091
|
+
/** H-1: a change of the ack values goes into the open bucket; the report sees it only once the bucket has closed. */
|
|
46092
|
+
noteMcpAckChanged() {
|
|
46093
|
+
const now = this.toolStatusNow();
|
|
46094
|
+
this.promoteMcpAck(now);
|
|
46095
|
+
const live = { through: this.mcpDeletionsThrough, credentials: this.mcpCredentialsPresentValue };
|
|
46096
|
+
const base = this.mcpAckOpen ?? this.mcpAckClosed;
|
|
46097
|
+
if (base.through === live.through && base.credentials === live.credentials) return;
|
|
46098
|
+
this.mcpAckOpen = { ...live, end: bucketEndMs(bucketStartAt(now)) };
|
|
46099
|
+
this.scheduleMcpAckReport();
|
|
46100
|
+
}
|
|
46101
|
+
promoteMcpAck(now) {
|
|
46102
|
+
if (this.mcpAckOpen !== null && this.mcpAckOpen.end <= now) {
|
|
46103
|
+
this.mcpAckClosed = { through: this.mcpAckOpen.through, credentials: this.mcpAckOpen.credentials };
|
|
46104
|
+
this.mcpAckOpen = null;
|
|
46105
|
+
}
|
|
46106
|
+
}
|
|
46107
|
+
/** H-1: one timer, at the first bucket boundary strictly after now plus a jitter in [0, 60 s), as the tool-status sender. */
|
|
46108
|
+
scheduleMcpAckReport() {
|
|
46109
|
+
if (this.mcpAckTimer !== null || this.closingDown) return;
|
|
46110
|
+
const now = this.toolStatusNow();
|
|
46111
|
+
const boundary = (Math.floor(now / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
|
|
46112
|
+
const at = boundary + Math.floor(this.toolStatusRandom() * TOOL_STATUS_JITTER_MS);
|
|
46113
|
+
this.mcpAckTimer = this.toolStatusSetTimer(() => {
|
|
46114
|
+
this.mcpAckTimer = null;
|
|
46115
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
46116
|
+
if (this.lastReportedMcpAck !== this.mcpAckSignature() && this.bootConfigSettled && !this.closingDown) void this.refetchRuntimeConfig();
|
|
46117
|
+
if (this.mcpAckOpen !== null) this.scheduleMcpAckReport();
|
|
46118
|
+
}, Math.max(0, at - now));
|
|
46119
|
+
}
|
|
46120
|
+
/**
|
|
46121
|
+
* Ruling §5.3: the three keys. The two ack values come from the CLOSED bucket only (H-1), so no report, whatever
|
|
46122
|
+
* posts it, reveals an owner-DM turn before its 15-minute bucket has closed. `mcp_r3_hold` is not DM timing: live.
|
|
46123
|
+
*/
|
|
46124
|
+
reportedMcpAck() {
|
|
46125
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
46126
|
+
const ackCapable = this.mcpDeletionAckCapable();
|
|
46127
|
+
const anyAck = [...this.mcpDeletionAckSockets.values()].some(Boolean);
|
|
46128
|
+
const closed = this.mcpAckClosed;
|
|
46129
|
+
return {
|
|
46130
|
+
mcp_r3_hold: this.r3Hold(),
|
|
46131
|
+
...ackCapable ? { mcp_deletions_through: closed.through } : {},
|
|
46132
|
+
// `anyAck` is an equivalent mutant today (forgetCredentialsIfNoAckSocket nulls the value whenever no ack socket is
|
|
46133
|
+
// left), kept as defense in depth so the report never depends on that reset alone.
|
|
46134
|
+
...anyAck && closed.credentials !== null ? { mcp_credentials_present: closed.credentials } : {}
|
|
46135
|
+
};
|
|
46136
|
+
}
|
|
46137
|
+
mcpAckSignature() {
|
|
46138
|
+
return JSON.stringify(this.reportedMcpAck());
|
|
46139
|
+
}
|
|
46140
|
+
/**
|
|
46141
|
+
* A SOCKET change (attach, detach, a hello) can change which keys the report carries; their values are the closed
|
|
46142
|
+
* ones, so this is not owner-DM timing and may use the capability refetch. An ACK never comes here (H-1): it goes
|
|
46143
|
+
* through noteMcpAckChanged and the bucket timer.
|
|
46144
|
+
*/
|
|
46145
|
+
maybeReportMcpAck() {
|
|
46146
|
+
if (this.lastReportedMcpAck === this.mcpAckSignature()) return;
|
|
46147
|
+
this.scheduleCapabilityRefetch();
|
|
46148
|
+
}
|
|
46149
|
+
/** MCP Task 18: ws-server registers (or, with null, removes) a socket's loopback catalog lister. */
|
|
46150
|
+
noteMcpCatalogLister(token, lister) {
|
|
46151
|
+
if (lister === null) this.mcpCatalogListers.delete(token);
|
|
46152
|
+
else this.mcpCatalogListers.set(token, lister);
|
|
46153
|
+
}
|
|
46154
|
+
/**
|
|
46155
|
+
* MCP Task 19 (spec §12.3): "ok" when every attached runtime socket declared mcp_client_v1 (the AND, which harness
|
|
46156
|
+
* sockets hold false: a harness socket counts in the AND and never declares it); else "harness_consumer" when the
|
|
46157
|
+
* latest hello was a harness; else "missing".
|
|
46158
|
+
*/
|
|
46159
|
+
reportedMcpCapability() {
|
|
46160
|
+
if (this.mcpClientCapable()) return "ok";
|
|
46161
|
+
return this.lastConsumerCapability === "harness_consumer" ? "harness_consumer" : "missing";
|
|
46162
|
+
}
|
|
46163
|
+
/**
|
|
46164
|
+
* MCP Task 19 (spec §12.3): at most 8 `{server_id, vendor, state}` for the signed servers of the manifest in force,
|
|
46165
|
+
* from mcp-policy.json as WRITTEN. Only the states the bridge itself knows: top `kill_switch` and `not_entitled` give
|
|
46166
|
+
* that state per server; top `ok` gives `ok` for a server the bridge lists `ok` and `malformed` for one it refused.
|
|
46167
|
+
* Every other top state (no capability, suspended, no config, switch off, section absent, malformed section) gives no
|
|
46168
|
+
* row. The runtime-only states (needs_login, login_expired, definition_changed, server_version_unsupported,
|
|
46169
|
+
* server_unreachable) have no runtime-to-bridge report path in Phase 1. `vendor` is reported only when it is a key of
|
|
46170
|
+
* the bridge's compiled catalog; a signed vendor outside it gives no row. Never a tool name, URL or issuer.
|
|
46171
|
+
*/
|
|
46172
|
+
reportedMcpServers() {
|
|
46173
|
+
const doc = this.mcpPolicyWritten;
|
|
46174
|
+
const signed = (this.cfg.ownerGrantPin?.pin ?? null) !== null ? this.toolGrantManifest?.mcp?.servers : void 0;
|
|
46175
|
+
if (doc === null || signed === void 0) return [];
|
|
46176
|
+
const catalog = this.mcpCatalog ?? MCP_CATALOG;
|
|
46177
|
+
const out = [];
|
|
46178
|
+
for (const id of Object.keys(signed).sort()) {
|
|
46179
|
+
if (out.length >= 8) break;
|
|
46180
|
+
const vendor = signed[id].vendor;
|
|
46181
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, vendor)) continue;
|
|
46182
|
+
let state;
|
|
46183
|
+
if (doc.state === "kill_switch" || doc.state === "not_entitled") state = doc.state;
|
|
46184
|
+
else if (doc.state === "ok") state = Object.prototype.hasOwnProperty.call(doc.servers, id) && doc.servers[id].state === "ok" ? "ok" : "malformed";
|
|
46185
|
+
else continue;
|
|
46186
|
+
out.push({ server_id: id, vendor, state });
|
|
46187
|
+
}
|
|
46188
|
+
return out;
|
|
46189
|
+
}
|
|
46190
|
+
/**
|
|
46191
|
+
* MCP Task 16: every attached runtime socket declared mcp_client_v1; no socket attached means absent (false).
|
|
46192
|
+
* PR-B2 follow-up (documented): a HARNESS socket counts in this AND. ws-server notes every socket that said hello,
|
|
46193
|
+
* and a harness hello is always noted `false` (harnessClaimed never resets), so one attached harness connector holds
|
|
46194
|
+
* the capability false for files, the report and mcp-policy.json (top `no_capability`) while it is attached.
|
|
46195
|
+
*/
|
|
46196
|
+
mcpClientCapable() {
|
|
46197
|
+
if (this.mcpClientSockets.size === 0) return false;
|
|
46198
|
+
for (const v of this.mcpClientSockets.values()) if (!v) return false;
|
|
46199
|
+
return true;
|
|
46200
|
+
}
|
|
44591
46201
|
noteUtilityToolsSocket(token, declared) {
|
|
44592
46202
|
this.utilityToolsSockets.set(token, declared);
|
|
44593
46203
|
this.onCapabilityMaybeChanged();
|
|
@@ -44606,6 +46216,7 @@ ${channelId}`;
|
|
|
44606
46216
|
const out = /* @__PURE__ */ new Set();
|
|
44607
46217
|
if (this.searchProvidersCapable()) out.add("search_providers_v2");
|
|
44608
46218
|
if (this.utilityToolsCapable()) out.add("utility_tools_v1");
|
|
46219
|
+
if (this.mcpClientCapable()) out.add("mcp_client_v1");
|
|
44609
46220
|
return out;
|
|
44610
46221
|
}
|
|
44611
46222
|
/** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
|
|
@@ -44634,13 +46245,19 @@ ${channelId}`;
|
|
|
44634
46245
|
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
44635
46246
|
*/
|
|
44636
46247
|
onCapabilityMaybeChanged() {
|
|
44637
|
-
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable() };
|
|
46248
|
+
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable(), mcp: this.mcpClientCapable() };
|
|
44638
46249
|
const prev = this.capabilityState;
|
|
44639
|
-
if (prev !== null && prev.search === next.search && prev.utility === next.utility) return;
|
|
46250
|
+
if (prev !== null && prev.search === next.search && prev.utility === next.utility && prev.mcp === next.mcp) return;
|
|
44640
46251
|
this.capabilityState = next;
|
|
44641
46252
|
this.recomputeManifestPolicy(this.capabilityFeatures());
|
|
46253
|
+
this.recomputeMcpPolicy();
|
|
44642
46254
|
if (this.closingDown) return;
|
|
44643
46255
|
if (prev === null && !next.search && !next.utility) return;
|
|
46256
|
+
this.scheduleCapabilityRefetch();
|
|
46257
|
+
}
|
|
46258
|
+
/** Debounced, with a max wait: a burst that never pauses still refetches at most maxWait after its first change. */
|
|
46259
|
+
scheduleCapabilityRefetch() {
|
|
46260
|
+
if (this.closingDown) return;
|
|
44644
46261
|
const now = Date.now();
|
|
44645
46262
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
44646
46263
|
else this.searchCapabilityBurstStart = now;
|
|
@@ -44658,7 +46275,7 @@ ${channelId}`;
|
|
|
44658
46275
|
searchCapabilityRefetchNeeded() {
|
|
44659
46276
|
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
44660
46277
|
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
44661
|
-
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable();
|
|
46278
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable() || this.lastReportedMcpAck !== this.mcpAckSignature();
|
|
44662
46279
|
}
|
|
44663
46280
|
/**
|
|
44664
46281
|
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
@@ -44884,6 +46501,11 @@ ${channelId}`;
|
|
|
44884
46501
|
async closeOnce() {
|
|
44885
46502
|
this.closingDown = true;
|
|
44886
46503
|
this.statusGate.close();
|
|
46504
|
+
this.toolStatusSender.close();
|
|
46505
|
+
if (this.mcpAckTimer !== null) {
|
|
46506
|
+
this.toolStatusClearTimer(this.mcpAckTimer);
|
|
46507
|
+
this.mcpAckTimer = null;
|
|
46508
|
+
}
|
|
44887
46509
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
44888
46510
|
this.pauseRecheckTimers.clear();
|
|
44889
46511
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -44892,6 +46514,8 @@ ${channelId}`;
|
|
|
44892
46514
|
this.redeliveryTimer = null;
|
|
44893
46515
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
44894
46516
|
this.searchCapabilityRefetchTimer = null;
|
|
46517
|
+
if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
|
|
46518
|
+
this.mcpPolicyRetryTimer = null;
|
|
44895
46519
|
this.pingHandle?.close();
|
|
44896
46520
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
44897
46521
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
@@ -45100,63 +46724,6 @@ function wsErrorFrame(code, detail) {
|
|
|
45100
46724
|
return { type: "error", ...errorBody(code, detail) };
|
|
45101
46725
|
}
|
|
45102
46726
|
|
|
45103
|
-
// src/consumer-grants.ts
|
|
45104
|
-
var NO_GRANTS = Object.freeze([]);
|
|
45105
|
-
var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
|
|
45106
|
-
function withConsumerGrants(m, perToolConsumer) {
|
|
45107
|
-
if (perToolConsumer) return m;
|
|
45108
|
-
const a = m.authorization;
|
|
45109
|
-
if (!a) return m;
|
|
45110
|
-
const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
|
|
45111
|
-
const stripMemory = "memory_write" in a;
|
|
45112
|
-
if (!stripGrants && !stripMemory) return m;
|
|
45113
|
-
const { memory_write: _dropped, ...rest } = a;
|
|
45114
|
-
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
45115
|
-
}
|
|
45116
|
-
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
45117
|
-
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
45118
|
-
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
45119
|
-
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
45120
|
-
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
45121
|
-
team_room: Object.freeze(["current_time"])
|
|
45122
|
-
});
|
|
45123
|
-
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
45124
|
-
function withManifestCapability(m, manifestConsumer) {
|
|
45125
|
-
if (manifestConsumer) return m;
|
|
45126
|
-
const a = m.authorization;
|
|
45127
|
-
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
45128
|
-
const constants5 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
45129
|
-
const narrowed = a.tool_grants.flatMap((g) => {
|
|
45130
|
-
if (!isManifestGrant(g)) return [g];
|
|
45131
|
-
return constants5.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
45132
|
-
});
|
|
45133
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
45134
|
-
}
|
|
45135
|
-
function withHarnessGrants(m, withheld) {
|
|
45136
|
-
if (!withheld) return m;
|
|
45137
|
-
const a = m.authorization;
|
|
45138
|
-
if (!a || a.tool_grants.length === 0) return m;
|
|
45139
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
45140
|
-
}
|
|
45141
|
-
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
45142
|
-
if (consumerHasFeature) return m;
|
|
45143
|
-
const a = m.authorization;
|
|
45144
|
-
if (!a) return m;
|
|
45145
|
-
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
45146
|
-
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
45147
|
-
if (kept.length === a.tool_grants.length) return m;
|
|
45148
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
45149
|
-
}
|
|
45150
|
-
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
45151
|
-
function withUtilityCapability(m, consumerHasFeature) {
|
|
45152
|
-
if (consumerHasFeature) return m;
|
|
45153
|
-
const a = m.authorization;
|
|
45154
|
-
if (!a) return m;
|
|
45155
|
-
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
45156
|
-
if (kept.length === a.tool_grants.length) return m;
|
|
45157
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
45158
|
-
}
|
|
45159
|
-
|
|
45160
46727
|
// src/http-server.ts
|
|
45161
46728
|
function writeSse(res, msgs, onWriteError) {
|
|
45162
46729
|
for (let i = 0; i < msgs.length; i++) {
|
|
@@ -45179,7 +46746,8 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
45179
46746
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
45180
46747
|
const withheld = agent.harnessGrantsWithheld();
|
|
45181
46748
|
const provider = agent.signedSearchProvider();
|
|
45182
|
-
const
|
|
46749
|
+
const hold = agent.r3Hold();
|
|
46750
|
+
const left3 = writeSse(res, batch.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)), onWriteError);
|
|
45183
46751
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
45184
46752
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
45185
46753
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -45456,7 +47024,8 @@ function createHttpServer(agent, opts) {
|
|
|
45456
47024
|
const drained = await agent.drain(channel, wait);
|
|
45457
47025
|
const withheld = agent.harnessGrantsWithheld();
|
|
45458
47026
|
const provider = agent.signedSearchProvider();
|
|
45459
|
-
|
|
47027
|
+
const hold = agent.r3Hold();
|
|
47028
|
+
return json(res, 200, drained.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)));
|
|
45460
47029
|
}
|
|
45461
47030
|
if (method === "GET" && path === "/v1/events") {
|
|
45462
47031
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -45526,6 +47095,7 @@ function parseHarnessReport(raw) {
|
|
|
45526
47095
|
}
|
|
45527
47096
|
|
|
45528
47097
|
// src/ws-server.ts
|
|
47098
|
+
import { randomBytes as randomBytes12 } from "node:crypto";
|
|
45529
47099
|
import { WebSocketServer, WebSocket } from "ws";
|
|
45530
47100
|
|
|
45531
47101
|
// src/memory/consumer-notes.ts
|
|
@@ -45636,6 +47206,8 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45636
47206
|
manifestGrants: false,
|
|
45637
47207
|
searchProviders: false,
|
|
45638
47208
|
utilityTools: false,
|
|
47209
|
+
mcpClient: false,
|
|
47210
|
+
pendingCatalog: /* @__PURE__ */ new Map(),
|
|
45639
47211
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
45640
47212
|
deliveryOpen: helloWaitMs <= 0,
|
|
45641
47213
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -45689,14 +47261,14 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45689
47261
|
continue;
|
|
45690
47262
|
}
|
|
45691
47263
|
const out = withConsumerNotes(
|
|
45692
|
-
withUtilityCapability(withSearchCapability(
|
|
47264
|
+
withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(
|
|
45693
47265
|
withManifestCapability(
|
|
45694
47266
|
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
45695
47267
|
session.manifestGrants
|
|
45696
47268
|
),
|
|
45697
47269
|
session.searchProviders,
|
|
45698
47270
|
agent.signedSearchProvider()
|
|
45699
|
-
), session.utilityTools),
|
|
47271
|
+
), session.utilityTools), session.mcpClient), agent.r3Hold()),
|
|
45700
47272
|
batch[i],
|
|
45701
47273
|
session,
|
|
45702
47274
|
agent
|
|
@@ -45707,6 +47279,10 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45707
47279
|
try {
|
|
45708
47280
|
sock.send(JSON.stringify(frame));
|
|
45709
47281
|
inflight.set(batch[i].message_id, batch[i]);
|
|
47282
|
+
const a = frame.authorization;
|
|
47283
|
+
if (session.mcpDeletionAck === true && (a?.audience === "owner_dm" || a?.relay_speaker === "owner")) {
|
|
47284
|
+
agent.noteMcpTurnDispatch(session, batch[i].message_id);
|
|
47285
|
+
}
|
|
45710
47286
|
session.deliveryToken.set(batch[i].message_id, ++session.deliverySeq);
|
|
45711
47287
|
session.shownRecall.set(batch[i].message_id, { token: session.deliverySeq, ids: recallOut.shown });
|
|
45712
47288
|
session.shownUnified.set(batch[i].message_id, { token: session.deliverySeq, ids: unifiedOut.shown });
|
|
@@ -45754,6 +47330,11 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45754
47330
|
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
45755
47331
|
agent.clearSearchProvidersSocket(session);
|
|
45756
47332
|
agent.clearUtilityToolsSocket(session);
|
|
47333
|
+
agent.clearMcpClientSocket(session);
|
|
47334
|
+
agent.clearMcpDeletionAckSocket(session);
|
|
47335
|
+
agent.noteMcpCatalogLister(session, null);
|
|
47336
|
+
for (const settle of session.pendingCatalog.values()) settle(null);
|
|
47337
|
+
session.pendingCatalog.clear();
|
|
45757
47338
|
};
|
|
45758
47339
|
current = { sock, cleanup, sweepStale };
|
|
45759
47340
|
sock.on("close", cleanup);
|
|
@@ -45787,6 +47368,32 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45787
47368
|
}
|
|
45788
47369
|
};
|
|
45789
47370
|
}
|
|
47371
|
+
var MCP_CATALOG_PENDING_MAX = 8;
|
|
47372
|
+
function mcpCatalogLister(sock, session) {
|
|
47373
|
+
return (serverId, timeoutMs) => new Promise((resolve5) => {
|
|
47374
|
+
if (session.closed || sock.readyState !== WebSocket.OPEN || session.pendingCatalog.size >= MCP_CATALOG_PENDING_MAX) {
|
|
47375
|
+
resolve5(null);
|
|
47376
|
+
return;
|
|
47377
|
+
}
|
|
47378
|
+
const requestId = randomBytes12(16).toString("hex");
|
|
47379
|
+
const timer = setTimeout(() => {
|
|
47380
|
+
session.pendingCatalog.delete(requestId);
|
|
47381
|
+
resolve5(null);
|
|
47382
|
+
}, timeoutMs);
|
|
47383
|
+
timer.unref?.();
|
|
47384
|
+
session.pendingCatalog.set(requestId, (frame) => {
|
|
47385
|
+
clearTimeout(timer);
|
|
47386
|
+
resolve5(frame);
|
|
47387
|
+
});
|
|
47388
|
+
try {
|
|
47389
|
+
sock.send(JSON.stringify({ type: "mcp_catalog_list", request_id: requestId, server_id: serverId }));
|
|
47390
|
+
} catch {
|
|
47391
|
+
session.pendingCatalog.delete(requestId);
|
|
47392
|
+
clearTimeout(timer);
|
|
47393
|
+
resolve5(null);
|
|
47394
|
+
}
|
|
47395
|
+
});
|
|
47396
|
+
}
|
|
45790
47397
|
function sendJson(sock, body) {
|
|
45791
47398
|
if (sock.readyState !== WebSocket.OPEN) return;
|
|
45792
47399
|
try {
|
|
@@ -45860,6 +47467,11 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45860
47467
|
agent.noteSearchProvidersSocket(session, session.searchProviders);
|
|
45861
47468
|
session.utilityTools = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(UTILITY_TOOLS_FEATURE);
|
|
45862
47469
|
agent.noteUtilityToolsSocket(session, session.utilityTools);
|
|
47470
|
+
session.mcpClient = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(MANIFEST_GRANTS_FEATURE) && m.features.includes(MCP_CLIENT_FEATURE);
|
|
47471
|
+
agent.noteMcpClientSocket(session, session.mcpClient);
|
|
47472
|
+
agent.noteMcpCatalogLister(session, session.mcpClient ? mcpCatalogLister(sock, session) : null);
|
|
47473
|
+
session.mcpDeletionAck = session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MCP_DELETION_ACK_FEATURE);
|
|
47474
|
+
agent.noteMcpDeletionAckSocket(session, session.mcpDeletionAck);
|
|
45863
47475
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
45864
47476
|
agent.noteNotesConsumer();
|
|
45865
47477
|
}
|
|
@@ -45867,13 +47479,21 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45867
47479
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
45868
47480
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
45869
47481
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
45870
|
-
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] });
|
|
47482
|
+
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, MCP_POLICY_WRITER_FEATURE, MCP_DELETION_ACK_FEATURE] });
|
|
45871
47483
|
if (!session.deliveryOpen) {
|
|
45872
47484
|
session.deliveryOpen = true;
|
|
45873
47485
|
flush();
|
|
45874
47486
|
}
|
|
45875
47487
|
return;
|
|
45876
47488
|
}
|
|
47489
|
+
if (m.type === "mcp_catalog_list_result") {
|
|
47490
|
+
const id = m.request_id;
|
|
47491
|
+
const settle = typeof id === "string" ? session.pendingCatalog.get(id) : void 0;
|
|
47492
|
+
if (settle === void 0) return;
|
|
47493
|
+
session.pendingCatalog.delete(id);
|
|
47494
|
+
settle(m);
|
|
47495
|
+
return;
|
|
47496
|
+
}
|
|
45877
47497
|
if (m.type === "nack") {
|
|
45878
47498
|
if (typeof m.message_id !== "string" || !m.message_id) {
|
|
45879
47499
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
@@ -46090,6 +47710,20 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
46090
47710
|
}
|
|
46091
47711
|
return;
|
|
46092
47712
|
}
|
|
47713
|
+
if (m.type === "mcp_deletion_ack") {
|
|
47714
|
+
const keys = Object.keys(m).sort().join(",");
|
|
47715
|
+
const valid = session.mcpDeletionAck === true && session.mcpClient === true && session.harnessClaimed !== true && (keys === "credentials_present,type" || keys === "credentials_present,message_id,type" || keys === "applied_sha256,credentials_present,message_id,type") && typeof m.credentials_present === "boolean" && (m.message_id === void 0 || typeof m.message_id === "string" && m.message_id.length > 0 && m.message_id.length <= 256) && (m.applied_sha256 === void 0 || typeof m.applied_sha256 === "string" && /^[0-9a-f]{64}$/.test(m.applied_sha256));
|
|
47716
|
+
if (!valid) {
|
|
47717
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
47718
|
+
return;
|
|
47719
|
+
}
|
|
47720
|
+
agent.noteMcpDeletionAck(session, {
|
|
47721
|
+
credentials_present: m.credentials_present,
|
|
47722
|
+
...typeof m.message_id === "string" ? { message_id: m.message_id } : {},
|
|
47723
|
+
...typeof m.applied_sha256 === "string" ? { applied_sha256: m.applied_sha256 } : {}
|
|
47724
|
+
});
|
|
47725
|
+
return;
|
|
47726
|
+
}
|
|
46093
47727
|
if (m.type === "room_cap_reached") {
|
|
46094
47728
|
if (!session.manifestGrants || typeof m.channel_id !== "string" || !LOWER_UUID.test(m.channel_id)) {
|
|
46095
47729
|
sendJson(sock, wsErrorFrame("invalid_request"));
|