@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/index.js
CHANGED
|
@@ -3083,7 +3083,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3083
3083
|
if (outputLen !== expectedLen)
|
|
3084
3084
|
throw new Error(`hash.outputLen must be ${expectedLen}, got ${outputLen}`);
|
|
3085
3085
|
}
|
|
3086
|
-
const
|
|
3086
|
+
const randomBytes13 = opts.randomBytes === void 0 ? randomBytes4 : opts.randomBytes;
|
|
3087
3087
|
const adjustScalarBytes3 = opts.adjustScalarBytes === void 0 ? (bytes) => bytes : opts.adjustScalarBytes;
|
|
3088
3088
|
const domain = opts.domain === void 0 ? (data, ctx, phflag) => {
|
|
3089
3089
|
abool2(phflag, "phflag");
|
|
@@ -3169,7 +3169,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
3169
3169
|
seed: _size
|
|
3170
3170
|
};
|
|
3171
3171
|
function randomSecretKey(seed) {
|
|
3172
|
-
seed = seed === void 0 ?
|
|
3172
|
+
seed = seed === void 0 ? randomBytes13(lengths.seed) : seed;
|
|
3173
3173
|
return abytes3(seed, lengths.seed, "seed");
|
|
3174
3174
|
}
|
|
3175
3175
|
function isValidSecretKey(key) {
|
|
@@ -6418,7 +6418,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
6418
6418
|
bits2int_modN: "function"
|
|
6419
6419
|
});
|
|
6420
6420
|
ecdsaOpts = Object.assign({}, ecdsaOpts);
|
|
6421
|
-
const
|
|
6421
|
+
const randomBytes13 = ecdsaOpts.randomBytes === void 0 ? randomBytes4 : ecdsaOpts.randomBytes;
|
|
6422
6422
|
const hmac5 = ecdsaOpts.hmac === void 0 ? (key, msg) => hmac(hash_, key, msg) : ecdsaOpts.hmac;
|
|
6423
6423
|
const { Fp: Fp3, Fn: Fn3 } = Point;
|
|
6424
6424
|
const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn3;
|
|
@@ -6564,7 +6564,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
|
6564
6564
|
throw new Error("invalid private key");
|
|
6565
6565
|
const seedArgs = [int2octets(d), int2octets(h1int)];
|
|
6566
6566
|
if (extraEntropy != null && extraEntropy !== false) {
|
|
6567
|
-
const e = extraEntropy === true ?
|
|
6567
|
+
const e = extraEntropy === true ? randomBytes13(lengths.secretKey) : extraEntropy;
|
|
6568
6568
|
seedArgs.push(abytes3(e, void 0, "extraEntropy"));
|
|
6569
6569
|
}
|
|
6570
6570
|
const seed = concatBytes4(...seedArgs);
|
|
@@ -18415,8 +18415,8 @@ function sameRef(a, b) {
|
|
|
18415
18415
|
// ../connect-sdk-ts/dist/groupkey/own-removal.js
|
|
18416
18416
|
async function probeOwnRemoval(mgr, commitBytes) {
|
|
18417
18417
|
try {
|
|
18418
|
-
const
|
|
18419
|
-
if (typeof
|
|
18418
|
+
const own2 = mgr.state?.privatePath?.leafIndex;
|
|
18419
|
+
if (typeof own2 !== "number")
|
|
18420
18420
|
return null;
|
|
18421
18421
|
if (!hoistedTsMlsMatchesCrypto(mgr))
|
|
18422
18422
|
return null;
|
|
@@ -18441,7 +18441,7 @@ async function probeOwnRemoval(mgr, commitBytes) {
|
|
|
18441
18441
|
committer = typeof incoming.senderLeafIndex === "number" ? Number(incoming.senderLeafIndex) : null;
|
|
18442
18442
|
for (const p of incoming.proposals) {
|
|
18443
18443
|
const prop = p.proposal;
|
|
18444
|
-
if (prop.proposalType === "remove" && Number(prop.remove?.removed) ===
|
|
18444
|
+
if (prop.proposalType === "remove" && Number(prop.remove?.removed) === own2)
|
|
18445
18445
|
removesOwn = true;
|
|
18446
18446
|
}
|
|
18447
18447
|
}
|
|
@@ -18449,7 +18449,7 @@ async function probeOwnRemoval(mgr, commitBytes) {
|
|
|
18449
18449
|
}, cs);
|
|
18450
18450
|
if (!sawCommit || !removesOwn)
|
|
18451
18451
|
return null;
|
|
18452
|
-
return { ownLeafIndex:
|
|
18452
|
+
return { ownLeafIndex: own2, committerLeafIndex: committer };
|
|
18453
18453
|
} catch {
|
|
18454
18454
|
return null;
|
|
18455
18455
|
}
|
|
@@ -19311,6 +19311,33 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
|
19311
19311
|
}
|
|
19312
19312
|
return { ownLeafIndex: this.ownLeafIndex(mgr), leaves };
|
|
19313
19313
|
}
|
|
19314
|
+
/**
|
|
19315
|
+
* MCP client (bridge Task 16; audit H-2, round 2 H-D): a SYNCHRONOUS, read-only snapshot of the WARM in-memory
|
|
19316
|
+
* manager for `channelId`: its group id, its epoch and EVERY non-blank leaf, reported exactly as
|
|
19317
|
+
* {@link leafTreeSummary} reports them (non-basic credentials included, identity null, #771 HIGH-B). All three are
|
|
19318
|
+
* read from ONE `state` object (MLSGroupManager replaces `state` whole on every commit), with no await, so they belong
|
|
19319
|
+
* to the same epoch and no commit can land between them.
|
|
19320
|
+
*
|
|
19321
|
+
* `null` when no manager is warm: it never loads from the store, never takes the channel lock, never touches the
|
|
19322
|
+
* network and never mutates. A caller treating `null` as "cannot tell" fails closed.
|
|
19323
|
+
*/
|
|
19324
|
+
leafSnapshotCached(channelId) {
|
|
19325
|
+
const state = this.managers.get(channelId)?.state;
|
|
19326
|
+
const tree = state?.ratchetTree;
|
|
19327
|
+
if (!state || !tree)
|
|
19328
|
+
return null;
|
|
19329
|
+
const leaves = [];
|
|
19330
|
+
const numLeaves = Math.floor((tree.length + 1) / 2);
|
|
19331
|
+
for (let i = 0; i < numLeaves; i++) {
|
|
19332
|
+
const node = tree[i * 2];
|
|
19333
|
+
if (!node || node.nodeType !== "leaf")
|
|
19334
|
+
continue;
|
|
19335
|
+
const cred = getCredentialFromLeafIndex(tree, toLeafIndex2(i));
|
|
19336
|
+
const basic = cred.credentialType === "basic";
|
|
19337
|
+
leaves.push({ leafIndex: i, basic, identity: basic ? new TextDecoder().decode(cred.identity) : null });
|
|
19338
|
+
}
|
|
19339
|
+
return { groupId: new TextDecoder().decode(state.groupContext.groupId), epoch: Number(state.groupContext.epoch), leaves };
|
|
19340
|
+
}
|
|
19314
19341
|
async fetchDeviceLeafCredential(deviceId) {
|
|
19315
19342
|
const kps = await this.o.backend.fetchKeyPackages([deviceId]);
|
|
19316
19343
|
const hex = kps[deviceId];
|
|
@@ -26644,10 +26671,12 @@ function openSchema(db) {
|
|
|
26644
26671
|
ensureRuntimeReplyIntent(db);
|
|
26645
26672
|
ensureRuntimeReplyIntentEnvelopeGroup(db);
|
|
26646
26673
|
ensureRuntimeReplyIntentCredit(db);
|
|
26674
|
+
ensureRuntimeReplyIntentMcpTurn(db);
|
|
26647
26675
|
ensureGroupLink(db);
|
|
26648
26676
|
ensureMlsCustody(db);
|
|
26649
26677
|
ensureToolGrantTables(db);
|
|
26650
26678
|
ensureToolGrantAppliedTable(db);
|
|
26679
|
+
ensureToolGrantMcpSeenTable(db);
|
|
26651
26680
|
ensureRoomSearchCapTable(db);
|
|
26652
26681
|
ensureVoiceChannelParentTable(db);
|
|
26653
26682
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
@@ -26737,6 +26766,19 @@ function ensureRuntimeReplyIntentCredit(db) {
|
|
|
26737
26766
|
throw e;
|
|
26738
26767
|
}
|
|
26739
26768
|
}
|
|
26769
|
+
function ensureRuntimeReplyIntentMcpTurn(db) {
|
|
26770
|
+
const hasColumn = () => db.prepare("PRAGMA table_info(runtime_reply_intent)").all().some((c) => c.name === "mcp_turn");
|
|
26771
|
+
const cols = db.prepare("PRAGMA table_info(runtime_reply_intent)").all();
|
|
26772
|
+
if (cols.length === 0 || cols.some((c) => c.name === "mcp_turn"))
|
|
26773
|
+
return;
|
|
26774
|
+
try {
|
|
26775
|
+
db.exec("ALTER TABLE runtime_reply_intent ADD COLUMN mcp_turn INTEGER NOT NULL DEFAULT 0");
|
|
26776
|
+
} catch (e) {
|
|
26777
|
+
if (hasColumn())
|
|
26778
|
+
return;
|
|
26779
|
+
throw e;
|
|
26780
|
+
}
|
|
26781
|
+
}
|
|
26740
26782
|
var RECALL_STATE_COLUMNS = [
|
|
26741
26783
|
["recall_enabled", "INTEGER NOT NULL DEFAULT 0"],
|
|
26742
26784
|
["recall_mode_epoch", "INTEGER NOT NULL DEFAULT 0"],
|
|
@@ -26848,6 +26890,15 @@ function ensureToolGrantAppliedTable(db) {
|
|
|
26848
26890
|
);
|
|
26849
26891
|
`);
|
|
26850
26892
|
}
|
|
26893
|
+
function ensureToolGrantMcpSeenTable(db) {
|
|
26894
|
+
db.exec(`
|
|
26895
|
+
CREATE TABLE IF NOT EXISTS tool_grant_mcp_seen (
|
|
26896
|
+
identity_id TEXT PRIMARY KEY,
|
|
26897
|
+
sealed_state BLOB NOT NULL,
|
|
26898
|
+
updated_at INTEGER NOT NULL
|
|
26899
|
+
);
|
|
26900
|
+
`);
|
|
26901
|
+
}
|
|
26851
26902
|
function ensureVoiceChannelParentTable(db) {
|
|
26852
26903
|
db.exec(`
|
|
26853
26904
|
CREATE TABLE IF NOT EXISTS voice_channel_parent (
|
|
@@ -27788,8 +27839,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27788
27839
|
this.db.prepare("DELETE FROM runtime_reply_intent WHERE identity_id=? AND updated_at < ?").run(this.id, now - RUNTIME_REPLY_INTENT_RETENTION_MS);
|
|
27789
27840
|
const info = this.db.prepare(`INSERT OR IGNORE INTO runtime_reply_intent
|
|
27790
27841
|
(identity_id, scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27791
|
-
sealed_body, sealed_envelope, state, created_at, updated_at, reply_credit)
|
|
27792
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now, input.replyCredit ?? null);
|
|
27842
|
+
sealed_body, sealed_envelope, state, created_at, updated_at, reply_credit, mcp_turn)
|
|
27843
|
+
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);
|
|
27793
27844
|
const count = this.db.prepare("SELECT COUNT(*) AS n FROM runtime_reply_intent WHERE identity_id=?").get(this.id);
|
|
27794
27845
|
if (count.n > RUNTIME_REPLY_INTENT_MAX) {
|
|
27795
27846
|
this.db.prepare(`DELETE FROM runtime_reply_intent
|
|
@@ -27799,7 +27850,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27799
27850
|
)`).run(this.id, this.id, RUNTIME_REPLY_INTENT_MAX);
|
|
27800
27851
|
}
|
|
27801
27852
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27802
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
27853
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
|
|
27803
27854
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, input.scope, input.part);
|
|
27804
27855
|
if (!row)
|
|
27805
27856
|
throw new Error("reply intent vanished after claim");
|
|
@@ -27808,7 +27859,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27808
27859
|
/** SEC-06 #18: every intent answering `inReplyTo`, in claim order. */
|
|
27809
27860
|
async listReplyIntents(inReplyTo) {
|
|
27810
27861
|
const rows = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27811
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
27862
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
|
|
27812
27863
|
FROM runtime_reply_intent WHERE identity_id=? AND in_reply_to=?
|
|
27813
27864
|
ORDER BY created_at ASC, rowid ASC`).all(this.id, inReplyTo);
|
|
27814
27865
|
const out = [];
|
|
@@ -27819,7 +27870,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27819
27870
|
/** SEC-06 #18: re-read one intent by its key (null when absent). */
|
|
27820
27871
|
async getReplyIntent(scope, part) {
|
|
27821
27872
|
const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
|
|
27822
|
-
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
|
|
27873
|
+
sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
|
|
27823
27874
|
FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, scope, part);
|
|
27824
27875
|
return row ? this.openReplyIntent(row) : null;
|
|
27825
27876
|
}
|
|
@@ -27894,7 +27945,9 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
27894
27945
|
envelopeGroupId: envelopeGroupFor(envelope, await openOrNull(row.sealed_envelope_group)),
|
|
27895
27946
|
state: row.state === "sent" ? "sent" : row.state === "skipped" ? "skipped" : "pending",
|
|
27896
27947
|
createdAt: Number(row.created_at),
|
|
27897
|
-
replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null
|
|
27948
|
+
replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null,
|
|
27949
|
+
// Any value other than exactly 1 (0, NULL, a missing column) is false; the bridge's MAC decides what a 1 means.
|
|
27950
|
+
mcpTurn: Number(row.mcp_turn ?? 0) === 1
|
|
27898
27951
|
};
|
|
27899
27952
|
}
|
|
27900
27953
|
async recordMemory(input) {
|
|
@@ -28466,10 +28519,10 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28466
28519
|
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);
|
|
28467
28520
|
if (r && Number(r.b) >= 0 && Number(r.n) >= 0)
|
|
28468
28521
|
return { items: Number(r.n), bytes: Number(r.b) };
|
|
28469
|
-
const
|
|
28522
|
+
const own2 = this.scopeLoad("i.scope_kind='owner_dm'");
|
|
28470
28523
|
this.ensureStateRow();
|
|
28471
|
-
this.db.prepare("UPDATE agent_memory_state SET owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(
|
|
28472
|
-
return
|
|
28524
|
+
this.db.prepare("UPDATE agent_memory_state SET owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(own2.bytes, own2.items, this.id);
|
|
28525
|
+
return own2;
|
|
28473
28526
|
}
|
|
28474
28527
|
/** 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. */
|
|
28475
28528
|
deleteItemGroup(firstItemId) {
|
|
@@ -28643,8 +28696,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28643
28696
|
const o2 = this.overCap();
|
|
28644
28697
|
if (!o2.overItems && !o2.overBytes)
|
|
28645
28698
|
break;
|
|
28646
|
-
const
|
|
28647
|
-
const protectOwner = (!o2.overItems ||
|
|
28699
|
+
const own2 = this.ownerLoad();
|
|
28700
|
+
const protectOwner = (!o2.overItems || own2.items < this.itemCap * OWNER_DM_FLOOR_SHARE) && (!o2.overBytes || own2.bytes < this.byteCap * OWNER_DM_FLOOR_SHARE);
|
|
28648
28701
|
const v = (protectOwner ? this.oldestAutoGroup({ notOwnerDm: true }) : void 0) ?? this.oldestAutoGroup({});
|
|
28649
28702
|
if (v === void 0 || !drop(v))
|
|
28650
28703
|
break;
|
|
@@ -28671,8 +28724,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
28671
28724
|
FROM agent_memory_item i WHERE identity_id=?`).get(this.id);
|
|
28672
28725
|
const tk = this.db.prepare("SELECT COUNT(*) AS n FROM agent_memory_token WHERE identity_id=?").get(this.id);
|
|
28673
28726
|
const size = Number(it.b) + Number(it.rows) * ITEM_ROW_OVERHEAD_BYTES + Number(tk.n) * TOKEN_ROW_OVERHEAD_BYTES;
|
|
28674
|
-
const
|
|
28675
|
-
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),
|
|
28727
|
+
const own2 = this.scopeLoad("i.scope_kind='owner_dm'");
|
|
28728
|
+
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);
|
|
28676
28729
|
}
|
|
28677
28730
|
/** Inside a transaction: delete every unified row of the identity and move each channel's content epoch. */
|
|
28678
28731
|
deleteAllItems() {
|
|
@@ -29488,9 +29541,40 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
|
|
|
29488
29541
|
dropToolGrantAppliedExcept(pinKeyB64) {
|
|
29489
29542
|
return Number(this.db.prepare("DELETE FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
|
|
29490
29543
|
}
|
|
29544
|
+
// MCP client Phase 1, Task 14 (OQ-3 ruling item 3): the sticky R-3 flag, per agent identity.
|
|
29545
|
+
/**
|
|
29546
|
+
* False when never written. A row that does not open, or opens to anything but this identity's own record, THROWS:
|
|
29547
|
+
* the bridge then holds R-3 (fail closed). Never returns false for an unreadable row.
|
|
29548
|
+
*/
|
|
29549
|
+
async loadMcpSeen() {
|
|
29550
|
+
const row = this.db.prepare("SELECT sealed_state FROM tool_grant_mcp_seen WHERE identity_id=?").get(this.id);
|
|
29551
|
+
if (!row)
|
|
29552
|
+
return false;
|
|
29553
|
+
const v = await this.unsealJson(row.sealed_state);
|
|
29554
|
+
if (v === null || typeof v !== "object" || v.identity_id !== this.id || typeof v.seen !== "boolean") {
|
|
29555
|
+
throw new MlsStoreSealError("tool_grant_mcp_seen: unexpected record");
|
|
29556
|
+
}
|
|
29557
|
+
return v.seen;
|
|
29558
|
+
}
|
|
29559
|
+
/** The sealed value names its identity, so a blob copied to another agent's row does not read as that agent's. */
|
|
29560
|
+
async saveMcpSeen(seen) {
|
|
29561
|
+
const sealed = await this.sealJson({ identity_id: this.id, seen });
|
|
29562
|
+
this.db.prepare(`INSERT INTO tool_grant_mcp_seen (identity_id, sealed_state, updated_at) VALUES (?, ?, ?)
|
|
29563
|
+
ON CONFLICT(identity_id) DO UPDATE SET sealed_state = excluded.sealed_state, updated_at = excluded.updated_at`).run(this.id, sealed, Date.now());
|
|
29564
|
+
}
|
|
29491
29565
|
hasOwnerMessage(channelId, messageId) {
|
|
29492
29566
|
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;
|
|
29493
29567
|
}
|
|
29568
|
+
/**
|
|
29569
|
+
* MCP client Phase 1, Task 18 (audit M-3): the owner replay row for a verified owner message that is NEVER staged for
|
|
29570
|
+
* the runtime (an `mcp_catalog_request`), so a redelivery of it is classed `replay`, after a restart too. The same row
|
|
29571
|
+
* stageRuntimeInbox writes in its staging transaction; insert-if-absent; pruned by pruneOwnerReplay like any other.
|
|
29572
|
+
*/
|
|
29573
|
+
recordOwnerMessage(channelId, messageId, groupId, epoch, processedAt = Date.now()) {
|
|
29574
|
+
this.db.prepare(`INSERT OR IGNORE INTO owner_replay
|
|
29575
|
+
(identity_id, channel_id, message_id, group_id, epoch, processed_at)
|
|
29576
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, channelId, messageId, groupId, epoch, processedAt);
|
|
29577
|
+
}
|
|
29494
29578
|
isOwnerEpochClosed(channelId, groupId, epoch) {
|
|
29495
29579
|
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;
|
|
29496
29580
|
}
|
|
@@ -29635,6 +29719,7 @@ function checkGroupLink(v) {
|
|
|
29635
29719
|
}
|
|
29636
29720
|
|
|
29637
29721
|
// src/bridge-agent.ts
|
|
29722
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
29638
29723
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
29639
29724
|
|
|
29640
29725
|
// src/reply-intent.ts
|
|
@@ -29664,8 +29749,9 @@ function decodeReplyBody(bytes) {
|
|
|
29664
29749
|
if (b?.kind === "a2a" && b.a2a && typeof b.a2a === "object") return b;
|
|
29665
29750
|
throw new Error("reply intent body is malformed");
|
|
29666
29751
|
}
|
|
29667
|
-
function replyBodyMac(key, replyId, channelId, body, credit) {
|
|
29668
|
-
|
|
29752
|
+
function replyBodyMac(key, replyId, channelId, body, credit, mcpTurn) {
|
|
29753
|
+
const label = mcpTurn === true ? ["reply-body", replyId, channelId, credit ?? null, "mcp_turn"] : credit === void 0 ? ["reply-body", replyId, channelId] : ["reply-body", replyId, channelId, credit];
|
|
29754
|
+
return createHmac("sha256", key).update(JSON.stringify(label)).update("\0").update(body).digest("hex");
|
|
29669
29755
|
}
|
|
29670
29756
|
function partOrdinal(part) {
|
|
29671
29757
|
const m = /^p:(\d+)$/.exec(part);
|
|
@@ -31380,8 +31466,8 @@ var MembershipPurgeSweeper = class {
|
|
|
31380
31466
|
const dms = /* @__PURE__ */ new Set();
|
|
31381
31467
|
for (const c of this.d.conversations()) {
|
|
31382
31468
|
if (stopping()) return { purged: [], held: 0, absent: 0 };
|
|
31383
|
-
const
|
|
31384
|
-
const channels =
|
|
31469
|
+
const own2 = this.d.enclaveOf(c);
|
|
31470
|
+
const channels = own2 === void 0 ? void 0 : listed.get(own2);
|
|
31385
31471
|
if (channels === void 0) continue;
|
|
31386
31472
|
if (channels.has(c)) {
|
|
31387
31473
|
this.d.clearAbsence(c);
|
|
@@ -34928,7 +35014,7 @@ function assertManagedName(name) {
|
|
|
34928
35014
|
}
|
|
34929
35015
|
function isLocalOnlyName(name) {
|
|
34930
35016
|
const n = name.toLowerCase().replace(/[. ]+$/, "");
|
|
34931
|
-
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE;
|
|
35017
|
+
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE || n === MCP_POLICY_FILE;
|
|
34932
35018
|
}
|
|
34933
35019
|
var MANIFEST_POLICY_FILE = "manifest-policy.json";
|
|
34934
35020
|
function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
|
|
@@ -34947,6 +35033,28 @@ function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
|
|
|
34947
35033
|
}
|
|
34948
35034
|
atomicWrite(join16(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
|
|
34949
35035
|
}
|
|
35036
|
+
var MCP_POLICY_FILE = "mcp-policy.json";
|
|
35037
|
+
var MCP_POLICY_TOP_STATES = Object.freeze([
|
|
35038
|
+
"ok",
|
|
35039
|
+
"not_entitled",
|
|
35040
|
+
"kill_switch",
|
|
35041
|
+
"suspended",
|
|
35042
|
+
"no_config",
|
|
35043
|
+
"switch_off",
|
|
35044
|
+
"no_manifest",
|
|
35045
|
+
"malformed",
|
|
35046
|
+
"no_capability",
|
|
35047
|
+
"section_absent"
|
|
35048
|
+
]);
|
|
35049
|
+
function writeMcpPolicy(home, identityId, doc, deps = REAL_FS) {
|
|
35050
|
+
const parent3 = join16(home, ".securecoms", "runtime-config");
|
|
35051
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
35052
|
+
for (const d of [join16(home, ".securecoms"), parent3, dir]) {
|
|
35053
|
+
deps.mkdir(d, 448);
|
|
35054
|
+
deps.chmod(d, 448);
|
|
35055
|
+
}
|
|
35056
|
+
atomicWrite(join16(dir, MCP_POLICY_FILE), JSON.stringify(doc) + "\n", deps);
|
|
35057
|
+
}
|
|
34950
35058
|
function atomicWrite(path, body, deps) {
|
|
34951
35059
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
34952
35060
|
deps.writeFile(tmp, body, 384);
|
|
@@ -35304,6 +35412,193 @@ function loadOwnerGrantPin(opts) {
|
|
|
35304
35412
|
return { state: "ok", pin: one };
|
|
35305
35413
|
}
|
|
35306
35414
|
|
|
35415
|
+
// src/grants/mcp-section.ts
|
|
35416
|
+
var MCP_MAX_SERVERS = 8;
|
|
35417
|
+
var MCP_MAX_TOOLS_PER_SERVER = 64;
|
|
35418
|
+
var MCP_MAX_SIGNED_TOOLS = 160;
|
|
35419
|
+
var MCP_MAX_REMOVED = 8;
|
|
35420
|
+
var SECURECOMS_CIMD_CLIENT_ID = "https://app.securecoms.ai/oauth/securecoms-agent-client.json";
|
|
35421
|
+
var MCP_SERVER_ID = /^[a-z0-9-]{1,24}$/;
|
|
35422
|
+
var MCP_TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
35423
|
+
var VENDOR_KEY = /^[a-z0-9_]{1,32}$/;
|
|
35424
|
+
var MCP_RESERVED_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
35425
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
35426
|
+
var PRINTABLE_ASCII = /^[\x21-\x7e]{1,256}$/;
|
|
35427
|
+
var MCP_GRANT = /^mcp:([a-z0-9-]{1,24})\/([A-Za-z0-9_.-]{1,64})$/;
|
|
35428
|
+
var isBag = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35429
|
+
var keysExactly = (o, keys) => {
|
|
35430
|
+
const have = Object.keys(o);
|
|
35431
|
+
return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(o, k));
|
|
35432
|
+
};
|
|
35433
|
+
function canonicalHttpsUrl(v) {
|
|
35434
|
+
if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
|
|
35435
|
+
const u = new URL(v);
|
|
35436
|
+
return u.protocol === "https:" && u.username === "" && u.password === "" && u.href === v;
|
|
35437
|
+
}
|
|
35438
|
+
function httpsIssuer(v) {
|
|
35439
|
+
if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
|
|
35440
|
+
const u = new URL(v);
|
|
35441
|
+
return u.protocol === "https:" && u.username === "" && u.password === "" && u.search === "" && u.hash === "" && (u.href === v || u.href === `${v}/`);
|
|
35442
|
+
}
|
|
35443
|
+
var MCP_AUTH_KEYS = Object.freeze({
|
|
35444
|
+
oauth_securecoms_cimd: Object.freeze(["mode", "issuer", "client"]),
|
|
35445
|
+
oauth_customer: Object.freeze(["mode", "issuer", "client", "client_id"]),
|
|
35446
|
+
key: Object.freeze(["mode"])
|
|
35447
|
+
});
|
|
35448
|
+
function parseAuth(v) {
|
|
35449
|
+
if (!isBag(v)) return null;
|
|
35450
|
+
if (v.mode === "key") return keysExactly(v, MCP_AUTH_KEYS.key) ? Object.freeze({ mode: "key" }) : null;
|
|
35451
|
+
if (v.mode !== "oauth" || !httpsIssuer(v.issuer)) return null;
|
|
35452
|
+
if (v.client === "securecoms_cimd") {
|
|
35453
|
+
if (!keysExactly(v, MCP_AUTH_KEYS.oauth_securecoms_cimd)) return null;
|
|
35454
|
+
return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "securecoms_cimd" });
|
|
35455
|
+
}
|
|
35456
|
+
if (v.client === "customer") {
|
|
35457
|
+
if (!keysExactly(v, MCP_AUTH_KEYS.oauth_customer)) return null;
|
|
35458
|
+
if (typeof v.client_id !== "string" || !PRINTABLE_ASCII.test(v.client_id)) return null;
|
|
35459
|
+
return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "customer", client_id: v.client_id });
|
|
35460
|
+
}
|
|
35461
|
+
return null;
|
|
35462
|
+
}
|
|
35463
|
+
function parseTools(v) {
|
|
35464
|
+
if (!isBag(v)) return null;
|
|
35465
|
+
const entries = Object.entries(v);
|
|
35466
|
+
if (entries.length > MCP_MAX_TOOLS_PER_SERVER) return null;
|
|
35467
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
35468
|
+
for (const [name, t] of entries) {
|
|
35469
|
+
if (!MCP_TOOL_NAME.test(name) || MCP_RESERVED_NAMES.has(name) || !isBag(t) || !keysExactly(t, ["sha256", "class"])) return null;
|
|
35470
|
+
if (typeof t.sha256 !== "string" || !HEX64.test(t.sha256) || t.class !== "read") return null;
|
|
35471
|
+
out[name] = Object.freeze({ sha256: t.sha256, class: "read" });
|
|
35472
|
+
}
|
|
35473
|
+
return out;
|
|
35474
|
+
}
|
|
35475
|
+
function parseServer(v) {
|
|
35476
|
+
if (!isBag(v) || !keysExactly(v, ["vendor", "url", "auth", "tools"])) return null;
|
|
35477
|
+
if (typeof v.vendor !== "string" || !VENDOR_KEY.test(v.vendor) || !canonicalHttpsUrl(v.url)) return null;
|
|
35478
|
+
const auth = parseAuth(v.auth);
|
|
35479
|
+
const tools = parseTools(v.tools);
|
|
35480
|
+
if (auth === null || tools === null) return null;
|
|
35481
|
+
return Object.freeze({ vendor: v.vendor, url: v.url, auth, tools: Object.freeze(tools) });
|
|
35482
|
+
}
|
|
35483
|
+
function parseRemoved(v, servers) {
|
|
35484
|
+
if (v === "*") return Object.keys(servers).length === 0 ? "*" : null;
|
|
35485
|
+
if (!Array.isArray(v) || v.length < 1 || v.length > MCP_MAX_REMOVED) return null;
|
|
35486
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35487
|
+
for (const id of v) {
|
|
35488
|
+
if (typeof id !== "string" || !MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
|
|
35489
|
+
if (seen.has(id) || Object.prototype.hasOwnProperty.call(servers, id)) return null;
|
|
35490
|
+
seen.add(id);
|
|
35491
|
+
}
|
|
35492
|
+
return Object.freeze([...seen]);
|
|
35493
|
+
}
|
|
35494
|
+
var NOT_REMOVED = Object.freeze([]);
|
|
35495
|
+
function parseMcp(v) {
|
|
35496
|
+
if (!isBag(v) || !(keysExactly(v, ["servers"]) || keysExactly(v, ["servers", "removed"])) || !isBag(v.servers)) return null;
|
|
35497
|
+
const entries = Object.entries(v.servers);
|
|
35498
|
+
if (entries.length > MCP_MAX_SERVERS) return null;
|
|
35499
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
35500
|
+
let signedTools = 0;
|
|
35501
|
+
for (const [id, raw] of entries) {
|
|
35502
|
+
if (!MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
|
|
35503
|
+
const s = parseServer(raw);
|
|
35504
|
+
if (s === null) return null;
|
|
35505
|
+
signedTools += Object.keys(s.tools).length;
|
|
35506
|
+
if (signedTools > MCP_MAX_SIGNED_TOOLS) return null;
|
|
35507
|
+
servers[id] = s;
|
|
35508
|
+
}
|
|
35509
|
+
let removed = NOT_REMOVED;
|
|
35510
|
+
if (Object.prototype.hasOwnProperty.call(v, "removed")) {
|
|
35511
|
+
const r = parseRemoved(v.removed, servers);
|
|
35512
|
+
if (r === null) return null;
|
|
35513
|
+
removed = r;
|
|
35514
|
+
}
|
|
35515
|
+
return Object.freeze({ servers: Object.freeze(servers), removed });
|
|
35516
|
+
}
|
|
35517
|
+
function mcpServerCatalogRow(s, catalog) {
|
|
35518
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
|
|
35519
|
+
const row = catalog[s.vendor];
|
|
35520
|
+
if (s.url !== row.readOnlyUrl) return null;
|
|
35521
|
+
if (s.auth.mode === "oauth" && s.auth.issuer !== row.issuer) return null;
|
|
35522
|
+
return row;
|
|
35523
|
+
}
|
|
35524
|
+
function parseMcpGrant(name) {
|
|
35525
|
+
const g = MCP_GRANT.exec(name);
|
|
35526
|
+
return g === null ? null : { serverId: g[1], tool: g[2] };
|
|
35527
|
+
}
|
|
35528
|
+
function mcpGrantWithinCeiling(name, mcp, catalog) {
|
|
35529
|
+
const g = parseMcpGrant(name);
|
|
35530
|
+
if (g === null || mcp === null || !Object.prototype.hasOwnProperty.call(mcp.servers, g.serverId)) return false;
|
|
35531
|
+
const s = mcp.servers[g.serverId];
|
|
35532
|
+
if (!Object.prototype.hasOwnProperty.call(s.tools, g.tool)) return false;
|
|
35533
|
+
const row = mcpServerCatalogRow(s, catalog);
|
|
35534
|
+
return row !== null && row.readTools.includes(g.tool);
|
|
35535
|
+
}
|
|
35536
|
+
|
|
35537
|
+
// src/grants/mcp-catalog.ts
|
|
35538
|
+
var LINEAR = Object.freeze({
|
|
35539
|
+
displayName: "Linear",
|
|
35540
|
+
host: "mcp.linear.app",
|
|
35541
|
+
readOnlyUrl: "https://mcp.linear.app/mcp/readonly",
|
|
35542
|
+
readTools: Object.freeze([
|
|
35543
|
+
"get_attachment",
|
|
35544
|
+
"list_agent_skills",
|
|
35545
|
+
"get_agent_skill",
|
|
35546
|
+
"list_comments",
|
|
35547
|
+
"list_cycles",
|
|
35548
|
+
"list_custom_views",
|
|
35549
|
+
"get_document",
|
|
35550
|
+
"list_documents",
|
|
35551
|
+
"extract_images",
|
|
35552
|
+
"get_issue",
|
|
35553
|
+
"list_issue_statuses",
|
|
35554
|
+
"get_issue_status",
|
|
35555
|
+
"list_issue_labels",
|
|
35556
|
+
"list_projects",
|
|
35557
|
+
"get_project",
|
|
35558
|
+
"list_project_labels",
|
|
35559
|
+
"list_release_pipelines",
|
|
35560
|
+
"list_releases",
|
|
35561
|
+
"get_release",
|
|
35562
|
+
"list_release_notes",
|
|
35563
|
+
"get_release_note",
|
|
35564
|
+
"list_milestones",
|
|
35565
|
+
"get_milestone",
|
|
35566
|
+
"get_notifications",
|
|
35567
|
+
"list_teams",
|
|
35568
|
+
"get_team",
|
|
35569
|
+
"get_triage_responsibility",
|
|
35570
|
+
"list_templates",
|
|
35571
|
+
"get_template",
|
|
35572
|
+
"list_users",
|
|
35573
|
+
"get_user",
|
|
35574
|
+
"get_workspace",
|
|
35575
|
+
"search_documentation",
|
|
35576
|
+
"get_diff",
|
|
35577
|
+
"list_diffs",
|
|
35578
|
+
"get_diff_threads",
|
|
35579
|
+
"list_initiatives",
|
|
35580
|
+
"get_initiative",
|
|
35581
|
+
"list_initiative_labels",
|
|
35582
|
+
"get_status_updates"
|
|
35583
|
+
]),
|
|
35584
|
+
scopes: Object.freeze(["read"]),
|
|
35585
|
+
issuer: "https://mcp.linear.app",
|
|
35586
|
+
authorizationEndpoint: "https://mcp.linear.app/authorize",
|
|
35587
|
+
tokenEndpoint: "https://mcp.linear.app/token",
|
|
35588
|
+
revocationEndpoint: "https://mcp.linear.app/token",
|
|
35589
|
+
docs: Object.freeze({
|
|
35590
|
+
host: "https://linear.app/docs/mcp",
|
|
35591
|
+
readOnlyUrl: "https://linear.app/docs/mcp",
|
|
35592
|
+
readTools: "https://linear.app/docs/mcp",
|
|
35593
|
+
scopes: "https://linear.app/docs/mcp",
|
|
35594
|
+
issuer: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35595
|
+
authorizationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35596
|
+
tokenEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35597
|
+
revocationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server"
|
|
35598
|
+
})
|
|
35599
|
+
});
|
|
35600
|
+
var MCP_CATALOG = Object.freeze({ linear: LINEAR });
|
|
35601
|
+
|
|
35307
35602
|
// src/grants/manifest.ts
|
|
35308
35603
|
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
35309
35604
|
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
@@ -35329,9 +35624,9 @@ function wellTyped(v) {
|
|
|
35329
35624
|
return false;
|
|
35330
35625
|
}
|
|
35331
35626
|
var WRAPPER_KEYS = ["algo", "manifest", "signature"];
|
|
35332
|
-
var
|
|
35333
|
-
var
|
|
35334
|
-
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") &&
|
|
35627
|
+
var HEX642 = /^[0-9a-f]{64}$/;
|
|
35628
|
+
var PRINTABLE_ASCII2 = /^[\x21-\x7e]+$/;
|
|
35629
|
+
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII2.test(v);
|
|
35335
35630
|
var exactKeys = (o, required, optional = []) => required.every((k) => k in o) && Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
|
|
35336
35631
|
var isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
35337
35632
|
function parseEndpoints(v) {
|
|
@@ -35343,7 +35638,7 @@ function parseEndpoints(v) {
|
|
|
35343
35638
|
const e = { ...raw };
|
|
35344
35639
|
if (e.headersFile === "") delete e.headersFile;
|
|
35345
35640
|
if (!exactKeys(e, ["baseUrl"], ["headersFile"])) return null;
|
|
35346
|
-
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !
|
|
35641
|
+
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII2.test(e.baseUrl)) return null;
|
|
35347
35642
|
let url;
|
|
35348
35643
|
try {
|
|
35349
35644
|
url = new URL(e.baseUrl);
|
|
@@ -35374,8 +35669,8 @@ function parseDatabases(v) {
|
|
|
35374
35669
|
function parseConfig(v) {
|
|
35375
35670
|
if (!isPlainObject(v)) return null;
|
|
35376
35671
|
if (!exactKeys(v, ["persona_sha256", "flags", "memory", "legacy_assign_sha256"], ["llm", "search", "tools_allow"])) return null;
|
|
35377
|
-
if (typeof v.persona_sha256 !== "string" || !
|
|
35378
|
-
if (typeof v.legacy_assign_sha256 !== "string" || !
|
|
35672
|
+
if (typeof v.persona_sha256 !== "string" || !HEX642.test(v.persona_sha256)) return null;
|
|
35673
|
+
if (typeof v.legacy_assign_sha256 !== "string" || !HEX642.test(v.legacy_assign_sha256)) return null;
|
|
35379
35674
|
if (!isStringArray(v.flags)) return null;
|
|
35380
35675
|
if ("tools_allow" in v && !isStringArray(v.tools_allow)) return null;
|
|
35381
35676
|
if ("llm" in v) {
|
|
@@ -35419,7 +35714,7 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35419
35714
|
const m = raw.manifest;
|
|
35420
35715
|
if (!isPlainObject(m)) return { state: "malformed" };
|
|
35421
35716
|
if (m.type !== "securecoms.tool-grants" || m.v !== 1) return { state: "malformed" };
|
|
35422
|
-
if (!wellTyped(m)) return { state: "malformed" };
|
|
35717
|
+
if (!wellTyped(Object.fromEntries(Object.entries(m).filter(([k]) => k !== "mcp")))) return { state: "malformed" };
|
|
35423
35718
|
if (typeof m.agent_identity_id !== "string" || typeof m.owner_identity_id !== "string") return { state: "malformed" };
|
|
35424
35719
|
for (const k of ["owner_dm_channel_id", "owner_dm_relay_identity_id"]) {
|
|
35425
35720
|
if (k in m && typeof m[k] !== "string") return { state: "malformed" };
|
|
@@ -35449,6 +35744,11 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35449
35744
|
if (counter === ctx.storedCounter) return { state: "unchanged" };
|
|
35450
35745
|
if (counter < ctx.storedCounter) return { state: "rollback" };
|
|
35451
35746
|
}
|
|
35747
|
+
const rawOwnerDm = m.grants.owner_dm;
|
|
35748
|
+
const signedMcpPresent = Array.isArray(rawOwnerDm) && rawOwnerDm.some((t) => t.startsWith("mcp:"));
|
|
35749
|
+
const mcpKey = "mcp" in m ? "present" : "absent";
|
|
35750
|
+
const mcp = mcpKey === "present" ? parseMcp(m.mcp) : null;
|
|
35751
|
+
const catalog = ctx.mcpCatalog ?? MCP_CATALOG;
|
|
35452
35752
|
let droppedUnknown = false;
|
|
35453
35753
|
const grants = {};
|
|
35454
35754
|
for (const [audience, list] of Object.entries(m.grants)) {
|
|
@@ -35457,7 +35757,7 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35457
35757
|
continue;
|
|
35458
35758
|
}
|
|
35459
35759
|
const known = MANIFEST_KNOWN_TOOLS[audience];
|
|
35460
|
-
const kept = list.filter((t) => known.includes(t));
|
|
35760
|
+
const kept = list.filter((t) => known.includes(t) || audience === "owner_dm" && mcpGrantWithinCeiling(t, mcp, catalog));
|
|
35461
35761
|
if (kept.length !== list.length) droppedUnknown = true;
|
|
35462
35762
|
grants[audience] = Object.freeze(kept);
|
|
35463
35763
|
}
|
|
@@ -35472,7 +35772,10 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35472
35772
|
grants: Object.freeze(grants),
|
|
35473
35773
|
endpoints: Object.freeze(endpoints),
|
|
35474
35774
|
databases: Object.freeze(databases),
|
|
35475
|
-
config
|
|
35775
|
+
config,
|
|
35776
|
+
mcp,
|
|
35777
|
+
mcpKey,
|
|
35778
|
+
signedMcpPresent
|
|
35476
35779
|
});
|
|
35477
35780
|
return { state: "ok", manifest, droppedUnknown, wrapper: raw };
|
|
35478
35781
|
}
|
|
@@ -35793,21 +36096,32 @@ var AGENT_TOTAL_KEY = "agent-total";
|
|
|
35793
36096
|
// src/grants/effective.ts
|
|
35794
36097
|
var TOOL_GRANT_CODE_CEILING = MANIFEST_KNOWN_TOOLS;
|
|
35795
36098
|
var CURRENT_TIME = Object.freeze({ tool: "current_time", mode: "read" });
|
|
36099
|
+
var NO_WITHHELD = Object.freeze([]);
|
|
35796
36100
|
function effectiveToolGrants(i) {
|
|
35797
|
-
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false };
|
|
35798
|
-
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false };
|
|
36101
|
+
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
36102
|
+
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
35799
36103
|
const ceiling = TOOL_GRANT_CODE_CEILING[i.audience];
|
|
35800
36104
|
const listed = new Set(i.manifest.grants[i.audience] ?? []);
|
|
35801
36105
|
const signed = ceiling.filter((t) => listed.has(t));
|
|
35802
36106
|
if (!i.eligible || !i.manifestSwitchOn) {
|
|
35803
36107
|
const fallback = i.base.filter((g) => g.tool !== "current_time" && signed.includes(g.tool));
|
|
35804
|
-
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false };
|
|
36108
|
+
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
35805
36109
|
}
|
|
35806
36110
|
let withheld = false;
|
|
36111
|
+
const dropped = [];
|
|
35807
36112
|
const tools = signed.filter((t) => {
|
|
35808
|
-
if (i.disabled.has(t))
|
|
35809
|
-
|
|
35810
|
-
|
|
36113
|
+
if (i.disabled.has(t)) {
|
|
36114
|
+
dropped.push(Object.freeze({ tool: t, why: "disabled_by_owner" }));
|
|
36115
|
+
return false;
|
|
36116
|
+
}
|
|
36117
|
+
if (t === "http_request" && i.signedTargets.endpoints === 0) {
|
|
36118
|
+
dropped.push(Object.freeze({ tool: t, why: "no_endpoint" }));
|
|
36119
|
+
return false;
|
|
36120
|
+
}
|
|
36121
|
+
if (t === "sql_query" && i.signedTargets.databases === 0) {
|
|
36122
|
+
dropped.push(Object.freeze({ tool: t, why: "no_database" }));
|
|
36123
|
+
return false;
|
|
36124
|
+
}
|
|
35811
36125
|
if (i.audience === "team_room" && t === "web_search" && i.roomSearchCapped) {
|
|
35812
36126
|
withheld = true;
|
|
35813
36127
|
return false;
|
|
@@ -35817,7 +36131,8 @@ function effectiveToolGrants(i) {
|
|
|
35817
36131
|
return {
|
|
35818
36132
|
grants: Object.freeze([CURRENT_TIME, ...tools.map((tool) => Object.freeze({ tool, mode: "read", source: "manifest" }))]),
|
|
35819
36133
|
source: "manifest",
|
|
35820
|
-
withheldBySearchCap: withheld
|
|
36134
|
+
withheldBySearchCap: withheld,
|
|
36135
|
+
withheld: Object.freeze(dropped)
|
|
35821
36136
|
};
|
|
35822
36137
|
}
|
|
35823
36138
|
|
|
@@ -36122,6 +36437,200 @@ function classifyVoiceFrame(f) {
|
|
|
36122
36437
|
return b.text.trim() === VOICE_CALL_START_MARKER ? "greeting" : "drop";
|
|
36123
36438
|
}
|
|
36124
36439
|
|
|
36440
|
+
// src/grants/tool-status.ts
|
|
36441
|
+
var TOOL_STATUS_ALLOWLIST = [
|
|
36442
|
+
"current_time",
|
|
36443
|
+
"web_search",
|
|
36444
|
+
"web_extract",
|
|
36445
|
+
"fetch_url",
|
|
36446
|
+
"http_request",
|
|
36447
|
+
"sql_query",
|
|
36448
|
+
"weather_forecast",
|
|
36449
|
+
"currency_convert"
|
|
36450
|
+
];
|
|
36451
|
+
var ALLOW2 = new Set(TOOL_STATUS_ALLOWLIST);
|
|
36452
|
+
var TOOL_STATUS_BUCKET_MS = 9e5;
|
|
36453
|
+
function bucketStartAt(ms) {
|
|
36454
|
+
return new Date(Math.floor(ms / TOOL_STATUS_BUCKET_MS) * TOOL_STATUS_BUCKET_MS).toISOString().slice(0, 16) + "Z";
|
|
36455
|
+
}
|
|
36456
|
+
function bucketEndMs(at) {
|
|
36457
|
+
return Date.parse(at) + TOOL_STATUS_BUCKET_MS;
|
|
36458
|
+
}
|
|
36459
|
+
var SIGNATURE_FAILURES = /* @__PURE__ */ new Set(["bad_signature", "malformed", "unknown_algo", "no_signature"]);
|
|
36460
|
+
function ownerDmCause(i) {
|
|
36461
|
+
try {
|
|
36462
|
+
const v = i.verdict;
|
|
36463
|
+
if (v.kind === "failed" && v.reason === "replay") return null;
|
|
36464
|
+
if (v.kind !== "verified" && v.kind !== "constants_only" && v.kind !== "failed") return null;
|
|
36465
|
+
const failedReason = v.kind === "failed" ? v.reason : void 0;
|
|
36466
|
+
const blocked = i.killSwitchOff || i.pinMissingOrAnomalous || i.senderNotOwner || i.stale || v.kind === "failed";
|
|
36467
|
+
const eligible = v.kind === "verified" && i.signedDmChannel === i.channelId && i.boundNow && i.consumerAttached;
|
|
36468
|
+
const source = blocked ? "none" : !i.manifestPresent ? "constant" : !eligible || i.manifestSwitchOff ? "fallback" : "manifest";
|
|
36469
|
+
const at = (cause, sub) => sub === void 0 ? { source, cause } : { source, cause, sub };
|
|
36470
|
+
if (i.killSwitchOff) return at("owner_dm_unavailable", "kill_switch");
|
|
36471
|
+
if (i.manifestSwitchOff) return at("manifest_switch_off");
|
|
36472
|
+
if (i.pinMissingOrAnomalous) return at("owner_dm_unavailable", "pin");
|
|
36473
|
+
if (i.senderNotOwner) return at("owner_dm_unavailable", "sender_not_owner");
|
|
36474
|
+
if (i.stale) return at("owner_dm_unavailable", "stale");
|
|
36475
|
+
if (failedReason === "not_owner_dm_leaves") return at("unexpected_member_in_dm");
|
|
36476
|
+
if (failedReason !== void 0 && SIGNATURE_FAILURES.has(failedReason)) return at("owner_turn_failed");
|
|
36477
|
+
if (failedReason === "verify_error") return at("agent_state_error");
|
|
36478
|
+
if (failedReason === "wrong_dm_channel" || v.kind === "verified" && i.manifestPresent && i.signedDmChannel !== void 0 && i.signedDmChannel !== i.channelId) {
|
|
36479
|
+
return at("dm_channel_mismatch");
|
|
36480
|
+
}
|
|
36481
|
+
if (failedReason !== void 0) return null;
|
|
36482
|
+
if (i.manifestPresent && i.signedDmChannel === void 0) return at("dm_not_linked");
|
|
36483
|
+
if (v.kind === "constants_only") return at("unsigned_relay_in_dm");
|
|
36484
|
+
if (!i.manifestPresent || !i.boundNow) return at("manifest_not_bound");
|
|
36485
|
+
if (!i.consumerAttached) return at("no_runtime_consumer");
|
|
36486
|
+
return source === "manifest" ? { source } : null;
|
|
36487
|
+
} catch {
|
|
36488
|
+
return null;
|
|
36489
|
+
}
|
|
36490
|
+
}
|
|
36491
|
+
function sameTuple(a, b) {
|
|
36492
|
+
if (!a || !b) return false;
|
|
36493
|
+
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);
|
|
36494
|
+
}
|
|
36495
|
+
function sanitizeIssued(names) {
|
|
36496
|
+
return [...new Set(names.filter((n) => ALLOW2.has(n)))].sort();
|
|
36497
|
+
}
|
|
36498
|
+
|
|
36499
|
+
// src/tool-status-sender.ts
|
|
36500
|
+
var TOOL_STATUS_JITTER_MS = 6e4;
|
|
36501
|
+
var RETRY_AFTER_FLOOR_MS = 6e4;
|
|
36502
|
+
function defaultToolStatusSetTimer(fn, ms) {
|
|
36503
|
+
const t = setTimeout(fn, ms);
|
|
36504
|
+
t.unref?.();
|
|
36505
|
+
return t;
|
|
36506
|
+
}
|
|
36507
|
+
function defaultToolStatusClearTimer(h) {
|
|
36508
|
+
clearTimeout(h);
|
|
36509
|
+
}
|
|
36510
|
+
function retryAfterMs(raw) {
|
|
36511
|
+
if (typeof raw !== "string" || !/^\d{1,9}$/.test(raw.trim())) return RETRY_AFTER_FLOOR_MS;
|
|
36512
|
+
return Math.max(RETRY_AFTER_FLOOR_MS, Number(raw.trim()) * 1e3);
|
|
36513
|
+
}
|
|
36514
|
+
var ToolStatusSender = class {
|
|
36515
|
+
postFn;
|
|
36516
|
+
now;
|
|
36517
|
+
setTimer;
|
|
36518
|
+
clearTimer;
|
|
36519
|
+
random;
|
|
36520
|
+
onFailure;
|
|
36521
|
+
/** The latest noted record while its bucket is still open. */
|
|
36522
|
+
open = null;
|
|
36523
|
+
/** The latest noted record whose bucket has closed: the only record an attempt or carry() may send. */
|
|
36524
|
+
closed = null;
|
|
36525
|
+
lastPosted = null;
|
|
36526
|
+
terminalFor = null;
|
|
36527
|
+
notBefore = Number.NEGATIVE_INFINITY;
|
|
36528
|
+
timer = null;
|
|
36529
|
+
inFlight = false;
|
|
36530
|
+
shut = false;
|
|
36531
|
+
constructor(o) {
|
|
36532
|
+
this.postFn = o.post;
|
|
36533
|
+
this.now = o.now ?? (() => Date.now());
|
|
36534
|
+
this.setTimer = o.setTimer ?? defaultToolStatusSetTimer;
|
|
36535
|
+
this.clearTimer = o.clearTimer ?? defaultToolStatusClearTimer;
|
|
36536
|
+
this.random = o.random ?? Math.random;
|
|
36537
|
+
this.onFailure = o.onFailure ?? (() => {
|
|
36538
|
+
});
|
|
36539
|
+
}
|
|
36540
|
+
note(rec) {
|
|
36541
|
+
if (this.shut) return;
|
|
36542
|
+
try {
|
|
36543
|
+
this.promote(this.now());
|
|
36544
|
+
this.open = rec;
|
|
36545
|
+
this.schedule();
|
|
36546
|
+
} catch {
|
|
36547
|
+
}
|
|
36548
|
+
}
|
|
36549
|
+
/** Ruling M-2: what the apply-time report carries. The closed record only; an open bucket is never revealed early. */
|
|
36550
|
+
carry() {
|
|
36551
|
+
try {
|
|
36552
|
+
this.promote(this.now());
|
|
36553
|
+
} catch {
|
|
36554
|
+
}
|
|
36555
|
+
return this.closed;
|
|
36556
|
+
}
|
|
36557
|
+
/** R2-M1: the record belongs to an owner who is no longer the manifest's; forget it and what was posted. */
|
|
36558
|
+
drop() {
|
|
36559
|
+
this.open = null;
|
|
36560
|
+
this.closed = null;
|
|
36561
|
+
this.lastPosted = null;
|
|
36562
|
+
this.terminalFor = null;
|
|
36563
|
+
this.cancelTimer();
|
|
36564
|
+
}
|
|
36565
|
+
close() {
|
|
36566
|
+
this.shut = true;
|
|
36567
|
+
this.cancelTimer();
|
|
36568
|
+
}
|
|
36569
|
+
cancelTimer() {
|
|
36570
|
+
if (this.timer !== null) this.clearTimer(this.timer);
|
|
36571
|
+
this.timer = null;
|
|
36572
|
+
}
|
|
36573
|
+
promote(t) {
|
|
36574
|
+
if (this.open !== null && bucketEndMs(this.open.at) <= t) {
|
|
36575
|
+
this.closed = this.open;
|
|
36576
|
+
this.open = null;
|
|
36577
|
+
}
|
|
36578
|
+
}
|
|
36579
|
+
pending() {
|
|
36580
|
+
return this.closed !== null && !sameTuple(this.closed, this.lastPosted) && !sameTuple(this.closed, this.terminalFor);
|
|
36581
|
+
}
|
|
36582
|
+
schedule() {
|
|
36583
|
+
if (this.shut || this.inFlight || this.timer !== null) return;
|
|
36584
|
+
if (this.open === null && !this.pending()) return;
|
|
36585
|
+
const t = this.now();
|
|
36586
|
+
let b = (Math.floor(t / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
|
|
36587
|
+
while (b < this.notBefore) b += TOOL_STATUS_BUCKET_MS;
|
|
36588
|
+
const jitter = Math.floor(this.random() * TOOL_STATUS_JITTER_MS);
|
|
36589
|
+
this.timer = this.setTimer(() => {
|
|
36590
|
+
this.timer = null;
|
|
36591
|
+
this.fire();
|
|
36592
|
+
}, b - t + jitter);
|
|
36593
|
+
}
|
|
36594
|
+
fire() {
|
|
36595
|
+
try {
|
|
36596
|
+
if (this.shut) return;
|
|
36597
|
+
this.promote(this.now());
|
|
36598
|
+
if (this.pending()) {
|
|
36599
|
+
void this.attempt(this.closed);
|
|
36600
|
+
return;
|
|
36601
|
+
}
|
|
36602
|
+
this.schedule();
|
|
36603
|
+
} catch {
|
|
36604
|
+
}
|
|
36605
|
+
}
|
|
36606
|
+
async attempt(rec) {
|
|
36607
|
+
this.inFlight = true;
|
|
36608
|
+
const attemptAt = this.now();
|
|
36609
|
+
let result;
|
|
36610
|
+
try {
|
|
36611
|
+
result = await this.postFn(rec);
|
|
36612
|
+
} catch {
|
|
36613
|
+
result = "network";
|
|
36614
|
+
}
|
|
36615
|
+
this.inFlight = false;
|
|
36616
|
+
try {
|
|
36617
|
+
if (typeof result === "object" && result.status >= 200 && result.status < 300) {
|
|
36618
|
+
this.lastPosted = rec;
|
|
36619
|
+
} else if (typeof result === "object" && result.status === 429) {
|
|
36620
|
+
this.notBefore = attemptAt + retryAfterMs(result.retryAfter);
|
|
36621
|
+
this.onFailure("http_error");
|
|
36622
|
+
} else if (typeof result === "object" && result.status >= 400 && result.status < 500) {
|
|
36623
|
+
this.terminalFor = rec;
|
|
36624
|
+
this.onFailure("http_error");
|
|
36625
|
+
} else {
|
|
36626
|
+
this.onFailure(typeof result === "object" ? "http_error" : result);
|
|
36627
|
+
}
|
|
36628
|
+
this.schedule();
|
|
36629
|
+
} catch {
|
|
36630
|
+
}
|
|
36631
|
+
}
|
|
36632
|
+
};
|
|
36633
|
+
|
|
36125
36634
|
// src/grants/owner-turn.ts
|
|
36126
36635
|
import { ed25519 as ed255196 } from "@noble/curves/ed25519";
|
|
36127
36636
|
var HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
@@ -36165,6 +36674,312 @@ function classifyOwnerDmLeaves(leaves, allowed) {
|
|
|
36165
36674
|
}
|
|
36166
36675
|
return "foreign";
|
|
36167
36676
|
}
|
|
36677
|
+
function ownerDmLeavesRelayFree(leaves, ids) {
|
|
36678
|
+
let owner = false;
|
|
36679
|
+
let agent = false;
|
|
36680
|
+
for (const l of leaves) {
|
|
36681
|
+
if (!l.basic || l.identity === null) return false;
|
|
36682
|
+
if (l.identity === ids.ownerIdentityId) owner = true;
|
|
36683
|
+
else if (l.identity === ids.agentIdentityId) agent = true;
|
|
36684
|
+
else return false;
|
|
36685
|
+
}
|
|
36686
|
+
return owner && agent;
|
|
36687
|
+
}
|
|
36688
|
+
function relayLeafFromPhase0Relay(relay) {
|
|
36689
|
+
return relay !== "none";
|
|
36690
|
+
}
|
|
36691
|
+
function ownerDmRelayOf(leafClass, leaves, ids) {
|
|
36692
|
+
if (leafClass === "unsigned_relay") return "unsigned";
|
|
36693
|
+
if (ownerDmLeavesRelayFree(leaves, ids)) return "none";
|
|
36694
|
+
if (ids.relayIdentityId !== void 0 && leaves.some((l) => l.identity === ids.relayIdentityId)) return "signed";
|
|
36695
|
+
return void 0;
|
|
36696
|
+
}
|
|
36697
|
+
|
|
36698
|
+
// src/grants/mcp-policy.ts
|
|
36699
|
+
var MCP_POLICY_WRITER_FEATURE = "mcp_policy_writer_v1";
|
|
36700
|
+
var MCP_DELETION_ACK_FEATURE = "mcp_deletion_ack_v1";
|
|
36701
|
+
var MCP_ACK_STATES = /* @__PURE__ */ new Set(["ok", "not_entitled", "kill_switch"]);
|
|
36702
|
+
function mcpEntitlementReading(mcpEntitled, mcpCause) {
|
|
36703
|
+
if (mcpEntitled === true && mcpCause === "entitled") return "entitled";
|
|
36704
|
+
if (mcpEntitled === false && mcpCause === "not_entitled") return "not_entitled";
|
|
36705
|
+
return "suspended";
|
|
36706
|
+
}
|
|
36707
|
+
function mcpGrantsHostSwitch(env, log = writeServiceLog2) {
|
|
36708
|
+
const v = env.SECURECOMS_MCP_GRANTS;
|
|
36709
|
+
if (v === void 0 || v === "on") return "on";
|
|
36710
|
+
if (v === "off") return "off";
|
|
36711
|
+
log("warn", "config.mcp_grants_switch_suspended", { state: ServiceLogEnum.of("suspended", ["suspended"]) });
|
|
36712
|
+
return "suspended";
|
|
36713
|
+
}
|
|
36714
|
+
var empty = (state, r3Hold) => Object.freeze({ v: 1, state, r3_hold: r3Hold, servers: Object.freeze({}), remove: NO_REMOVE });
|
|
36715
|
+
var NO_REMOVE = Object.freeze([]);
|
|
36716
|
+
function policyAuth(s) {
|
|
36717
|
+
if (s.auth.mode === "key") return Object.freeze({ mode: "key" });
|
|
36718
|
+
const clientId = s.auth.client === "customer" ? s.auth.client_id : SECURECOMS_CIMD_CLIENT_ID;
|
|
36719
|
+
return Object.freeze({ mode: "oauth", issuer: s.auth.issuer, client: s.auth.client, client_id: clientId });
|
|
36720
|
+
}
|
|
36721
|
+
function mcpPolicyFor(i) {
|
|
36722
|
+
const m = i.manifest;
|
|
36723
|
+
const r3Hold = m?.signedMcpPresent === true || i.mcpSeen;
|
|
36724
|
+
if (i.hostSwitch === "off") return empty("kill_switch", r3Hold);
|
|
36725
|
+
if (i.hostSwitch !== "on") return empty("suspended", r3Hold);
|
|
36726
|
+
if (!i.config.fetched) return empty("no_config", r3Hold);
|
|
36727
|
+
const ent = mcpEntitlementReading(i.config.mcp_entitled, i.config.mcp_cause);
|
|
36728
|
+
if (ent !== "entitled") return empty(ent, r3Hold);
|
|
36729
|
+
if (!i.ownerDmManifestSwitchOn) return empty("switch_off", r3Hold);
|
|
36730
|
+
if (m === null) return empty("no_manifest", r3Hold);
|
|
36731
|
+
if (m.mcpKey === "absent") return empty("section_absent", r3Hold);
|
|
36732
|
+
if (m.mcp === null) return empty("malformed", r3Hold);
|
|
36733
|
+
if (!i.capability) return empty("no_capability", r3Hold);
|
|
36734
|
+
const catalog = i.catalog ?? MCP_CATALOG;
|
|
36735
|
+
const granted = /* @__PURE__ */ new Map();
|
|
36736
|
+
for (const name of m.grants.owner_dm ?? []) {
|
|
36737
|
+
const g = parseMcpGrant(name);
|
|
36738
|
+
if (g === null) continue;
|
|
36739
|
+
if (!granted.has(g.serverId)) granted.set(g.serverId, /* @__PURE__ */ new Set());
|
|
36740
|
+
granted.get(g.serverId).add(g.tool);
|
|
36741
|
+
}
|
|
36742
|
+
const servers = {};
|
|
36743
|
+
for (const id of Object.keys(m.mcp.servers).sort()) {
|
|
36744
|
+
const s = m.mcp.servers[id];
|
|
36745
|
+
const row = mcpServerCatalogRow(s, catalog);
|
|
36746
|
+
if (row === null) {
|
|
36747
|
+
servers[id] = Object.freeze({ state: "refused" });
|
|
36748
|
+
continue;
|
|
36749
|
+
}
|
|
36750
|
+
const tools = {};
|
|
36751
|
+
for (const t of Object.keys(s.tools).sort()) {
|
|
36752
|
+
if (granted.get(id)?.has(t) === true && row.readTools.includes(t)) tools[t] = Object.freeze({ sha256: s.tools[t].sha256, class: "read" });
|
|
36753
|
+
}
|
|
36754
|
+
servers[id] = Object.freeze({ state: "ok", vendor: s.vendor, url: row.readOnlyUrl, auth: policyAuth(s), tools: Object.freeze(tools) });
|
|
36755
|
+
}
|
|
36756
|
+
const remove = m.mcp.removed === "*" ? "*" : Object.freeze([...m.mcp.removed]);
|
|
36757
|
+
return Object.freeze({ v: 1, state: "ok", r3_hold: r3Hold, servers: Object.freeze(servers), remove });
|
|
36758
|
+
}
|
|
36759
|
+
|
|
36760
|
+
// src/grants/mcp-issuance.ts
|
|
36761
|
+
var NONE = Object.freeze([]);
|
|
36762
|
+
var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
36763
|
+
function mcpPolicyAllows(policy, serverId, tool) {
|
|
36764
|
+
if (policy === null || policy.state !== "ok" || !own(policy.servers, serverId)) return false;
|
|
36765
|
+
const s = policy.servers[serverId];
|
|
36766
|
+
return s.state === "ok" && own(s.tools, tool);
|
|
36767
|
+
}
|
|
36768
|
+
function mcpIssuance(i) {
|
|
36769
|
+
if (i.source !== "manifest") return NONE;
|
|
36770
|
+
if (i.verdict?.kind !== "verified" || i.verdict.relayLeaf !== false) return NONE;
|
|
36771
|
+
if (i.entitlement !== "entitled" || i.hostSwitch !== "on" || !i.ownerDmManifestSwitchOn) return NONE;
|
|
36772
|
+
if (i.manifest === null || i.policy === null || i.policy.state !== "ok") return NONE;
|
|
36773
|
+
const out = [];
|
|
36774
|
+
for (const name of i.manifest.grants.owner_dm ?? []) {
|
|
36775
|
+
const g = parseMcpGrant(name);
|
|
36776
|
+
if (g === null || i.disabled.has(name) || !mcpPolicyAllows(i.policy, g.serverId, g.tool)) continue;
|
|
36777
|
+
out.push(Object.freeze({ tool: name, mode: "read", source: "manifest" }));
|
|
36778
|
+
}
|
|
36779
|
+
return out.length === 0 ? NONE : Object.freeze(out);
|
|
36780
|
+
}
|
|
36781
|
+
|
|
36782
|
+
// src/consumer-grants.ts
|
|
36783
|
+
var NO_GRANTS = Object.freeze([]);
|
|
36784
|
+
var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
|
|
36785
|
+
function withConsumerGrants(m, perToolConsumer) {
|
|
36786
|
+
if (perToolConsumer) return m;
|
|
36787
|
+
const a = m.authorization;
|
|
36788
|
+
if (!a) return m;
|
|
36789
|
+
const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
|
|
36790
|
+
const stripMemory = "memory_write" in a;
|
|
36791
|
+
if (!stripGrants && !stripMemory) return m;
|
|
36792
|
+
const { memory_write: _dropped, ...rest } = a;
|
|
36793
|
+
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
36794
|
+
}
|
|
36795
|
+
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
36796
|
+
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
36797
|
+
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
36798
|
+
var MCP_CLIENT_FEATURE = "mcp_client_v1";
|
|
36799
|
+
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
36800
|
+
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
36801
|
+
team_room: Object.freeze(["current_time"])
|
|
36802
|
+
});
|
|
36803
|
+
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
36804
|
+
function withManifestCapability(m, manifestConsumer) {
|
|
36805
|
+
if (manifestConsumer) return m;
|
|
36806
|
+
const a = m.authorization;
|
|
36807
|
+
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
36808
|
+
const constants5 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
36809
|
+
const narrowed = a.tool_grants.flatMap((g) => {
|
|
36810
|
+
if (!isManifestGrant(g)) return [g];
|
|
36811
|
+
return constants5.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
36812
|
+
});
|
|
36813
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
36814
|
+
}
|
|
36815
|
+
function withHarnessGrants(m, withheld) {
|
|
36816
|
+
if (!withheld) return m;
|
|
36817
|
+
const a = m.authorization;
|
|
36818
|
+
if (!a || a.tool_grants.length === 0) return m;
|
|
36819
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
36820
|
+
}
|
|
36821
|
+
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
36822
|
+
if (consumerHasFeature) return m;
|
|
36823
|
+
const a = m.authorization;
|
|
36824
|
+
if (!a) return m;
|
|
36825
|
+
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
36826
|
+
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
36827
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
36828
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36829
|
+
}
|
|
36830
|
+
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
36831
|
+
function withUtilityCapability(m, consumerHasFeature) {
|
|
36832
|
+
if (consumerHasFeature) return m;
|
|
36833
|
+
const a = m.authorization;
|
|
36834
|
+
if (!a) return m;
|
|
36835
|
+
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
36836
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
36837
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36838
|
+
}
|
|
36839
|
+
var MCP_GRANT_PREFIX = "mcp:";
|
|
36840
|
+
var isMcpGrant = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
|
|
36841
|
+
function withMcpCapability(m, consumerHasFeature) {
|
|
36842
|
+
const a = m.authorization;
|
|
36843
|
+
if (!a) return m;
|
|
36844
|
+
const { relay_free: _relayFree, ...rest } = a;
|
|
36845
|
+
if (!a.tool_grants.some(isMcpGrant)) return "relay_free" in a ? { ...m, authorization: Object.freeze(rest) } : m;
|
|
36846
|
+
if (consumerHasFeature && a.audience === "owner_dm" && a.relay_speaker === void 0) return m;
|
|
36847
|
+
const kept = a.tool_grants.filter((g) => !isMcpGrant(g));
|
|
36848
|
+
return { ...m, authorization: Object.freeze({ ...rest, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36849
|
+
}
|
|
36850
|
+
var R3_KEEP_TOOLS = /* @__PURE__ */ new Set(["current_time", "web_search", "weather_forecast", "currency_convert"]);
|
|
36851
|
+
function withR3Holds(m, hold) {
|
|
36852
|
+
if (!hold) return m;
|
|
36853
|
+
const a = m.authorization;
|
|
36854
|
+
if (!a || !(a.audience === "owner_dm" || a.relay_speaker === "owner")) return m;
|
|
36855
|
+
const kept = a.tool_grants.filter((g) => R3_KEEP_TOOLS.has(g.tool) || isMcpGrant(g));
|
|
36856
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
36857
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36858
|
+
}
|
|
36859
|
+
|
|
36860
|
+
// src/mcp-catalog-message.ts
|
|
36861
|
+
var MCP_CATALOG_MAX_BYTES = 384 * 1024;
|
|
36862
|
+
var MCP_CATALOG_MAX_TOOLS = 64;
|
|
36863
|
+
var MCP_CATALOG_LIST_TIMEOUT_MS = 5e4;
|
|
36864
|
+
var REQUEST_ID = /^[0-9a-f]{32}$/;
|
|
36865
|
+
var SERVER_ID = /^[a-z0-9-]{1,24}$/;
|
|
36866
|
+
var TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
36867
|
+
var RESERVED = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
36868
|
+
var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
36869
|
+
var MCP_CATALOG_REASONS = /* @__PURE__ */ new Set(["catalog", "write_name", "hint", "schema", "header_param", "too_large"]);
|
|
36870
|
+
var isBag2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
36871
|
+
var keysExactly2 = (o, keys) => {
|
|
36872
|
+
const k = Object.keys(o);
|
|
36873
|
+
return k.length === keys.length && keys.every((x) => Object.prototype.hasOwnProperty.call(o, x));
|
|
36874
|
+
};
|
|
36875
|
+
function parseMcpCatalogRequest(body) {
|
|
36876
|
+
if (!isBag2(body) || !keysExactly2(body, ["v", "type", "server_id", "request_id"])) return null;
|
|
36877
|
+
if (body.v !== 1 || body.type !== "mcp_catalog_request") return null;
|
|
36878
|
+
const { server_id: serverId, request_id: requestId } = body;
|
|
36879
|
+
if (typeof serverId !== "string" || !SERVER_ID.test(serverId) || RESERVED.has(serverId)) return null;
|
|
36880
|
+
if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) return null;
|
|
36881
|
+
return Object.freeze({ serverId, requestId });
|
|
36882
|
+
}
|
|
36883
|
+
var PROJECTION_KEYS = /* @__PURE__ */ new Set(["name", "title", "description", "inputSchema", "annotations"]);
|
|
36884
|
+
function validProjection(p, toolName) {
|
|
36885
|
+
if (!Object.keys(p).every((k) => PROJECTION_KEYS.has(k))) return false;
|
|
36886
|
+
if (p.name !== toolName || typeof p.description !== "string" || !isBag2(p.inputSchema)) return false;
|
|
36887
|
+
if (Object.prototype.hasOwnProperty.call(p, "title") && typeof p.title !== "string") return false;
|
|
36888
|
+
if (Object.prototype.hasOwnProperty.call(p, "annotations") && !isBag2(p.annotations)) return false;
|
|
36889
|
+
return true;
|
|
36890
|
+
}
|
|
36891
|
+
function validTool(t) {
|
|
36892
|
+
if (!isBag2(t) || typeof t.name !== "string" || typeof t.grantable !== "boolean") return null;
|
|
36893
|
+
if (t.grantable) {
|
|
36894
|
+
if (!TOOL_NAME.test(t.name) || t.reason !== null || !isBag2(t.projection) || !validProjection(t.projection, t.name)) return null;
|
|
36895
|
+
return { name: t.name, projection: t.projection, grantable: true, reason: null };
|
|
36896
|
+
}
|
|
36897
|
+
if (t.projection !== void 0 || typeof t.reason !== "string" || !MCP_CATALOG_REASONS.has(t.reason)) return null;
|
|
36898
|
+
if (t.name.length < 1 || t.name.length > 64 || !/^[\x20-\x7e]+$/.test(t.name)) return null;
|
|
36899
|
+
return { name: t.name, grantable: false, reason: t.reason };
|
|
36900
|
+
}
|
|
36901
|
+
function validateCatalogListReply(frame, serverId, vendor) {
|
|
36902
|
+
if (!isBag2(frame) || frame.ok !== true || frame.server_id !== serverId || frame.vendor !== vendor) return null;
|
|
36903
|
+
if (typeof frame.listed_at !== "string" || !ISO_TIME.test(frame.listed_at) || typeof frame.truncated !== "boolean") return null;
|
|
36904
|
+
if (!Array.isArray(frame.tools) || frame.tools.length > MCP_CATALOG_MAX_TOOLS) return null;
|
|
36905
|
+
const tools = [];
|
|
36906
|
+
const names = /* @__PURE__ */ new Set();
|
|
36907
|
+
for (const t of frame.tools) {
|
|
36908
|
+
const v = validTool(t);
|
|
36909
|
+
if (v === null) return null;
|
|
36910
|
+
if (names.has(v.name)) return null;
|
|
36911
|
+
names.add(v.name);
|
|
36912
|
+
tools.push(v);
|
|
36913
|
+
}
|
|
36914
|
+
return { listedAt: frame.listed_at, truncated: frame.truncated, tools };
|
|
36915
|
+
}
|
|
36916
|
+
var utf8 = (v) => Buffer.byteLength(JSON.stringify(v), "utf8");
|
|
36917
|
+
var mcpCatalogBodyBytes = (b) => utf8(b);
|
|
36918
|
+
function buildMcpCatalogBody(i) {
|
|
36919
|
+
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 };
|
|
36920
|
+
const full = { ...head, tools: [...i.reply.tools], truncated: i.reply.truncated };
|
|
36921
|
+
if (mcpCatalogBodyBytes(full) <= MCP_CATALOG_MAX_BYTES) return full;
|
|
36922
|
+
const stub = (t) => ({ name: t.name, grantable: false, reason: "too_large" });
|
|
36923
|
+
const tools = i.reply.tools.map(stub);
|
|
36924
|
+
const body = { ...head, tools, truncated: true };
|
|
36925
|
+
let size = mcpCatalogBodyBytes(body);
|
|
36926
|
+
for (let k = 0; k < tools.length; k++) {
|
|
36927
|
+
const want = i.reply.tools[k];
|
|
36928
|
+
const delta = utf8(want) - utf8(tools[k]);
|
|
36929
|
+
if (size + delta <= MCP_CATALOG_MAX_BYTES) {
|
|
36930
|
+
tools[k] = want;
|
|
36931
|
+
size += delta;
|
|
36932
|
+
}
|
|
36933
|
+
}
|
|
36934
|
+
return body;
|
|
36935
|
+
}
|
|
36936
|
+
|
|
36937
|
+
// src/mcp-catalog-rate.ts
|
|
36938
|
+
var MINUTE_MS = 6e4;
|
|
36939
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
36940
|
+
var MCP_CATALOG_PER_SERVER_MINUTE = 3;
|
|
36941
|
+
var MCP_CATALOG_PER_SERVER_HOUR = 20;
|
|
36942
|
+
var MCP_CATALOG_PER_AGENT_HOUR = 40;
|
|
36943
|
+
var McpCatalogRateLimiter = class {
|
|
36944
|
+
now;
|
|
36945
|
+
onRateLimited;
|
|
36946
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
36947
|
+
perServer = /* @__PURE__ */ new Map();
|
|
36948
|
+
agent = [];
|
|
36949
|
+
/** Per scope key: no further log until this time (the end of the window that was full when it last logged). */
|
|
36950
|
+
quietUntil = /* @__PURE__ */ new Map();
|
|
36951
|
+
constructor(deps = {}) {
|
|
36952
|
+
this.now = deps.now ?? Date.now;
|
|
36953
|
+
this.onRateLimited = deps.onRateLimited ?? (() => {
|
|
36954
|
+
});
|
|
36955
|
+
}
|
|
36956
|
+
/** "ok" admits (and counts) the request and marks the server in flight; the caller MUST call release(serverId). */
|
|
36957
|
+
admit(serverId) {
|
|
36958
|
+
if (this.inFlight.has(serverId)) return "in_flight";
|
|
36959
|
+
const t = this.now();
|
|
36960
|
+
const server = (this.perServer.get(serverId) ?? []).filter((x) => t - x < HOUR_MS);
|
|
36961
|
+
this.perServer.set(serverId, server);
|
|
36962
|
+
this.agent = this.agent.filter((x) => t - x < HOUR_MS);
|
|
36963
|
+
const lastMinute = server.filter((x) => t - x < MINUTE_MS);
|
|
36964
|
+
if (lastMinute.length >= MCP_CATALOG_PER_SERVER_MINUTE) return this.limited(`m:${serverId}`, "server_minute", lastMinute[0] + MINUTE_MS, t);
|
|
36965
|
+
if (server.length >= MCP_CATALOG_PER_SERVER_HOUR) return this.limited(`h:${serverId}`, "server_hour", server[0] + HOUR_MS, t);
|
|
36966
|
+
if (this.agent.length >= MCP_CATALOG_PER_AGENT_HOUR) return this.limited("a", "agent_hour", this.agent[0] + HOUR_MS, t);
|
|
36967
|
+
server.push(t);
|
|
36968
|
+
this.agent.push(t);
|
|
36969
|
+
this.inFlight.add(serverId);
|
|
36970
|
+
return "ok";
|
|
36971
|
+
}
|
|
36972
|
+
release(serverId) {
|
|
36973
|
+
this.inFlight.delete(serverId);
|
|
36974
|
+
}
|
|
36975
|
+
limited(key, scope, windowEnd, t) {
|
|
36976
|
+
if (t >= (this.quietUntil.get(key) ?? -Infinity)) {
|
|
36977
|
+
this.quietUntil.set(key, windowEnd);
|
|
36978
|
+
this.onRateLimited(scope);
|
|
36979
|
+
}
|
|
36980
|
+
return "rate_limited";
|
|
36981
|
+
}
|
|
36982
|
+
};
|
|
36168
36983
|
|
|
36169
36984
|
// src/bridge-agent.ts
|
|
36170
36985
|
function logSendSupersededAdoption(kind2) {
|
|
@@ -36304,6 +37119,7 @@ function selectVoiceGuidance(isDm, voiceDefaultResponder) {
|
|
|
36304
37119
|
}
|
|
36305
37120
|
return VOICE_MODE_GUIDANCE;
|
|
36306
37121
|
}
|
|
37122
|
+
var MCP_INERT_STATES = /* @__PURE__ */ new Set(["suspended", "no_config", "switch_off", "no_manifest", "section_absent", "malformed", "no_capability"]);
|
|
36307
37123
|
var VOICE_COALESCE_WINDOW_MS = 0;
|
|
36308
37124
|
var VOICE_COALESCE_WINDOW_MAX_MS = 5e3;
|
|
36309
37125
|
function voiceCoalesceWindowFromEnv(env = process.env) {
|
|
@@ -36354,10 +37170,10 @@ var ChannelPendingError = class extends Error {
|
|
|
36354
37170
|
}
|
|
36355
37171
|
};
|
|
36356
37172
|
var RateLimitedError = class extends Error {
|
|
36357
|
-
constructor(channelId,
|
|
36358
|
-
super(`channel ${channelId} send rate limit exceeded (retry in ${
|
|
37173
|
+
constructor(channelId, retryAfterMs2) {
|
|
37174
|
+
super(`channel ${channelId} send rate limit exceeded (retry in ${retryAfterMs2}ms)`);
|
|
36359
37175
|
this.channelId = channelId;
|
|
36360
|
-
this.retryAfterMs =
|
|
37176
|
+
this.retryAfterMs = retryAfterMs2;
|
|
36361
37177
|
this.name = "RateLimitedError";
|
|
36362
37178
|
}
|
|
36363
37179
|
};
|
|
@@ -36485,6 +37301,31 @@ var SendForbiddenError = class extends Error {
|
|
|
36485
37301
|
this.name = "SendForbiddenError";
|
|
36486
37302
|
}
|
|
36487
37303
|
};
|
|
37304
|
+
var MCP_POLICY_RETRY_DELAYS_MS = Object.freeze([1e3, 5e3, 3e4, 12e4, 3e5]);
|
|
37305
|
+
function logMcpCatalogRequest(state) {
|
|
37306
|
+
writeServiceLog2(state === "sent" ? "info" : "warn", "mcp.catalog_request", { state: ServiceLogEnum.of(state, [
|
|
37307
|
+
"malformed",
|
|
37308
|
+
"not_owner_dm",
|
|
37309
|
+
"not_verified",
|
|
37310
|
+
"replay",
|
|
37311
|
+
"relay_leaf",
|
|
37312
|
+
"not_ready",
|
|
37313
|
+
"in_flight",
|
|
37314
|
+
"runtime_timeout",
|
|
37315
|
+
"runtime_refused",
|
|
37316
|
+
"withheld",
|
|
37317
|
+
"send_failed",
|
|
37318
|
+
"failed",
|
|
37319
|
+
"sent"
|
|
37320
|
+
]) });
|
|
37321
|
+
}
|
|
37322
|
+
var McpReplyWithheldError = class extends SendForbiddenError {
|
|
37323
|
+
constructor(channelId) {
|
|
37324
|
+
super(channelId);
|
|
37325
|
+
this.name = "McpReplyWithheldError";
|
|
37326
|
+
this.message = "reply withheld: the chat is no longer private to the owner and the agent; no retry";
|
|
37327
|
+
}
|
|
37328
|
+
};
|
|
36488
37329
|
var RoomClassChangedSendError = class extends SendForbiddenError {
|
|
36489
37330
|
constructor(channelId) {
|
|
36490
37331
|
super(channelId);
|
|
@@ -36504,6 +37345,7 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
|
|
|
36504
37345
|
var RoomGrantEpochMovedError = class extends Error {
|
|
36505
37346
|
};
|
|
36506
37347
|
var ROOM_GRANT_TRACK_MAX = 4096;
|
|
37348
|
+
var MCP_DELIVERED_TRACK_MAX = 4096;
|
|
36507
37349
|
var VOICE_FRAME_PARENT_MAX = 4096;
|
|
36508
37350
|
var SessionSupersededError = class extends Error {
|
|
36509
37351
|
constructor() {
|
|
@@ -36655,8 +37497,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36655
37497
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
|
|
36656
37498
|
});
|
|
36657
37499
|
this.home = deps.home ?? homedir4();
|
|
37500
|
+
this.mcpCatalog = deps.mcpCatalog;
|
|
37501
|
+
this.mcpDeliveredTrackMax = deps.mcpDeliveredTrackMax ?? MCP_DELIVERED_TRACK_MAX;
|
|
37502
|
+
this.mcpPolicyRetryDelaysMs = deps.mcpPolicyRetryDelaysMs?.length ? deps.mcpPolicyRetryDelaysMs : MCP_POLICY_RETRY_DELAYS_MS;
|
|
36658
37503
|
this.grantWriters = {
|
|
36659
37504
|
writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
|
|
37505
|
+
writeMcpPolicy: deps.grantWriters?.writeMcpPolicy ?? writeMcpPolicy,
|
|
36660
37506
|
writeManagedFiles: deps.grantWriters?.writeManagedFiles ?? writeManagedFiles,
|
|
36661
37507
|
bindRuntimeConfig: deps.grantWriters?.bindRuntimeConfig ?? bindRuntimeConfig,
|
|
36662
37508
|
removeManagedLlmSource: deps.grantWriters?.removeManagedLlmSource ?? removeManagedLlmSource
|
|
@@ -37142,6 +37988,62 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37142
37988
|
roomDefaultGrants = roomDefaultGrantsEnabled();
|
|
37143
37989
|
/** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
|
|
37144
37990
|
ownerDmManifestGrants = ownerDmManifestGrantsEnabled();
|
|
37991
|
+
/**
|
|
37992
|
+
* MCP client Task 16 (OQ-16, coordinator ruling D-7): SECURECOMS_MCP_GRANTS, read ONCE at construction. "off" writes
|
|
37993
|
+
* kill_switch (deleting); any value other than unset/"on"/"off" is suspended (non-deleting, one content-free warn).
|
|
37994
|
+
*/
|
|
37995
|
+
mcpHostSwitch = mcpGrantsHostSwitch(process.env);
|
|
37996
|
+
/** MCP Task 16 test seam (BridgeAgentDeps.mcpCatalog); undefined in production, so MCP_CATALOG decides. */
|
|
37997
|
+
mcpCatalog;
|
|
37998
|
+
/**
|
|
37999
|
+
* MCP Task 16 (OQ-1; PR-B1 audit M-2): the two raw /files fields from the LAST fetch. Any failed or rejected fetch
|
|
38000
|
+
* sets `fetched: false` (no_config, non-deleting): the last values are never kept to keep an `ok` document alive.
|
|
38001
|
+
*/
|
|
38002
|
+
mcpConfig = Object.freeze({ fetched: false });
|
|
38003
|
+
/**
|
|
38004
|
+
* MCP Task 16: the mcp-policy.json document this process last WROTE (the file the runtime reads), or null before the
|
|
38005
|
+
* first write and after a failed one (fail closed: issuance reads only a document known to be on disk).
|
|
38006
|
+
*/
|
|
38007
|
+
mcpPolicyWritten = null;
|
|
38008
|
+
mcpPolicyWrittenJson = null;
|
|
38009
|
+
/** Deletion-ack ruling §3: the manifest counter in force when `mcpPolicyWrittenJson` was written (never read live). */
|
|
38010
|
+
mcpPolicyWrittenCounter = null;
|
|
38011
|
+
/** Ruling §3: newest counter whose written policy the runtime fully acted on. Memory only, monotonic, starts at 0. */
|
|
38012
|
+
mcpDeletionsThrough = 0;
|
|
38013
|
+
/** Ruling §3: the last `credentials_present` an accepted frame carried; null until one arrives. */
|
|
38014
|
+
mcpCredentialsPresentValue = null;
|
|
38015
|
+
/** Ruling §3: per attached socket, whether its latest hello declared mcp_deletion_ack_v1 (harness noted false). */
|
|
38016
|
+
mcpDeletionAckSockets = /* @__PURE__ */ new Map();
|
|
38017
|
+
/** Ruling §3 rate: the ack-derived report values in the last report posted (null until one is). */
|
|
38018
|
+
lastReportedMcpAck = null;
|
|
38019
|
+
/** #1351 round 2 M-1: bumped on every byte-changing policy write (and on the inert document being put in force). */
|
|
38020
|
+
policyWriteSeq = 0;
|
|
38021
|
+
/** M-1: per ack-capable socket, the policyWriteSeq at the dispatch of each owner-DM turn, by message id (bounded). */
|
|
38022
|
+
mcpTurnSeq = /* @__PURE__ */ new Map();
|
|
38023
|
+
/**
|
|
38024
|
+
* #1351 round 2 H-1: the ack values the REPORT may carry. An ack lands seconds after an owner-DM turn and a report
|
|
38025
|
+
* posted then would time the turn (record_report sets applied_at). So, as the tool-status sender: `open` holds a
|
|
38026
|
+
* change while its 15-minute UTC bucket is open, `closed` what the report carries, and the only post is from one
|
|
38027
|
+
* timer at the next bucket boundary plus jitter.
|
|
38028
|
+
*/
|
|
38029
|
+
mcpAckOpen = null;
|
|
38030
|
+
mcpAckClosed = { through: 0, credentials: null };
|
|
38031
|
+
mcpAckTimer = null;
|
|
38032
|
+
/** PR-B2 follow-up: a failed write is retried on this backoff (the last value repeats) until one lands or close(). */
|
|
38033
|
+
mcpPolicyRetryDelaysMs;
|
|
38034
|
+
mcpPolicyRetryTimer = null;
|
|
38035
|
+
mcpPolicyRetryAttempt = 0;
|
|
38036
|
+
/** MCP Task 18: the loopback catalog listers of the attached runtime sockets that declared mcp_client_v1 (ws-server.ts). */
|
|
38037
|
+
mcpCatalogListers = /* @__PURE__ */ new Map();
|
|
38038
|
+
/** MCP Task 18 (OQ-15): per server 1 in flight, 3 / 60 s, 20 / hour; per agent 40 / hour. Memory only. */
|
|
38039
|
+
mcpCatalogRate = new McpCatalogRateLimiter({
|
|
38040
|
+
onRateLimited: (scope) => writeServiceLog2("warn", "mcp.catalog_request", {
|
|
38041
|
+
state: ServiceLogEnum.of("catalog_rate_limited", ["catalog_rate_limited"]),
|
|
38042
|
+
scope: ServiceLogEnum.of(scope, ["server_minute", "server_hour", "agent_hour"])
|
|
38043
|
+
})
|
|
38044
|
+
});
|
|
38045
|
+
/** MCP Task 18: answers in progress (outside the ingest cycle). Observable by tests; close() does not wait on them. */
|
|
38046
|
+
mcpCatalogWork = /* @__PURE__ */ new Set();
|
|
37145
38047
|
roomManifestGrants = roomManifestGrantsEnabled();
|
|
37146
38048
|
/**
|
|
37147
38049
|
* Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
|
|
@@ -37152,6 +38054,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37152
38054
|
// RECORDED here and in the sealed store; no grant code reads it in step 1.
|
|
37153
38055
|
toolGrantManifest = null;
|
|
37154
38056
|
toolGrantCounter = null;
|
|
38057
|
+
/**
|
|
38058
|
+
* Tool status Phase 0 (spec §3.1, §3.2; plan Task 8): the latest owner-DM last-turn record, in memory only (no store
|
|
38059
|
+
* table: after a restart there is none until the next typed owner-DM turn). ADVISORY: nothing reads it to decide a
|
|
38060
|
+
* grant. Content-free: enums, booleans, one 15-minute bucket `at` (ruling M-2), one counter, built-in names from the
|
|
38061
|
+
* allowlist. This is the latest record, open bucket included; what leaves the host is the sender's closed record.
|
|
38062
|
+
*/
|
|
38063
|
+
ownerDmLastTurn = null;
|
|
38064
|
+
/**
|
|
38065
|
+
* Test seams (Task 8; ruling M-2): the cause function, the bridge clock the record and the sender read, and the
|
|
38066
|
+
* sender's timer and jitter source.
|
|
38067
|
+
*/
|
|
38068
|
+
toolStatusCause = ownerDmCause;
|
|
38069
|
+
toolStatusNow = () => Date.now();
|
|
38070
|
+
toolStatusSetTimer = defaultToolStatusSetTimer;
|
|
38071
|
+
toolStatusClearTimer = defaultToolStatusClearTimer;
|
|
38072
|
+
toolStatusRandom = Math.random;
|
|
38073
|
+
/**
|
|
38074
|
+
* 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
|
|
38075
|
+
* UTC bucket boundary, and only a record whose bucket has closed.
|
|
38076
|
+
*/
|
|
38077
|
+
toolStatusSender = new ToolStatusSender({
|
|
38078
|
+
post: (rec) => this.postToolStatus(rec),
|
|
38079
|
+
now: () => this.toolStatusNow(),
|
|
38080
|
+
setTimer: (fn, ms) => this.toolStatusSetTimer(fn, ms),
|
|
38081
|
+
clearTimer: (h) => this.toolStatusClearTimer(h),
|
|
38082
|
+
random: () => this.toolStatusRandom(),
|
|
38083
|
+
onFailure: (kind2) => writeServiceLog2("warn", "tool_status.post_failed", { status: ServiceLogEnum.of(kind2, ["http_error", "timeout", "network"]) })
|
|
38084
|
+
});
|
|
38085
|
+
/**
|
|
38086
|
+
* MCP Phase 1 Task 14 (OQ-3 ruling item 3): the sticky R-3 flag, mirrored from the sealed store. Starts false and is
|
|
38087
|
+
* set true at start() by the store (or by a failed read, fail closed). Only applyToolGrantManifest changes it after.
|
|
38088
|
+
*/
|
|
38089
|
+
mcpSeen = false;
|
|
37155
38090
|
manifestState = "no_manifest";
|
|
37156
38091
|
/** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
|
|
37157
38092
|
ownerVerifyFailures = 0;
|
|
@@ -37617,10 +38552,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37617
38552
|
const payload = await res.json();
|
|
37618
38553
|
if (!payload) {
|
|
37619
38554
|
writeServiceLog2("warn", "runtime_config.empty_response");
|
|
38555
|
+
this.noteMcpConfigUnfetched();
|
|
37620
38556
|
return;
|
|
37621
38557
|
}
|
|
37622
38558
|
if (typeof payload.version !== "number") {
|
|
37623
38559
|
writeServiceLog2("warn", "runtime_config.invalid_version");
|
|
38560
|
+
this.noteMcpConfigUnfetched();
|
|
37624
38561
|
return;
|
|
37625
38562
|
}
|
|
37626
38563
|
const version = payload.version;
|
|
@@ -37634,6 +38571,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37634
38571
|
manifestStepThrew = true;
|
|
37635
38572
|
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
|
|
37636
38573
|
}
|
|
38574
|
+
const raw = payload;
|
|
38575
|
+
this.mcpConfig = Object.freeze({ fetched: true, mcp_entitled: raw.mcp_entitled, mcp_cause: raw.mcp_cause });
|
|
38576
|
+
this.recomputeMcpPolicy();
|
|
37637
38577
|
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
37638
38578
|
const inForce = pin !== null ? this.toolGrantManifest : null;
|
|
37639
38579
|
if (inForce !== null && pin !== null) {
|
|
@@ -37700,8 +38640,89 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37700
38640
|
await this.postRuntimeConfigReport(base, version, skillsJson);
|
|
37701
38641
|
} catch (e) {
|
|
37702
38642
|
writeServiceLog2("warn", "runtime_config.refetch_failed", {}, e);
|
|
38643
|
+
this.noteMcpConfigUnfetched();
|
|
37703
38644
|
}
|
|
37704
38645
|
}
|
|
38646
|
+
/** MCP Task 16 (OQ-1): the bridge's in-force entitlement reading; not fetched (or failed) is suspended. */
|
|
38647
|
+
mcpEntitlementInForce() {
|
|
38648
|
+
return this.mcpConfig.fetched ? mcpEntitlementReading(this.mcpConfig.mcp_entitled, this.mcpConfig.mcp_cause) : "suspended";
|
|
38649
|
+
}
|
|
38650
|
+
/** PR-B1 audit M-2: forget the last /files fields and rewrite mcp-policy.json (no_config: no grants, no deletion). */
|
|
38651
|
+
noteMcpConfigUnfetched() {
|
|
38652
|
+
this.mcpConfig = Object.freeze({ fetched: false });
|
|
38653
|
+
this.recomputeMcpPolicy();
|
|
38654
|
+
}
|
|
38655
|
+
/** MCP Task 16: the live inputs of mcp-policy.json (the pinned manifest in force only). */
|
|
38656
|
+
mcpPolicyInput() {
|
|
38657
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
38658
|
+
return {
|
|
38659
|
+
hostSwitch: this.mcpHostSwitch,
|
|
38660
|
+
config: this.mcpConfig,
|
|
38661
|
+
ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
|
|
38662
|
+
manifest: pin !== null ? this.toolGrantManifest : null,
|
|
38663
|
+
capability: this.mcpClientCapable(),
|
|
38664
|
+
mcpSeen: this.mcpSeen,
|
|
38665
|
+
...this.mcpCatalog ? { catalog: this.mcpCatalog } : {}
|
|
38666
|
+
};
|
|
38667
|
+
}
|
|
38668
|
+
/**
|
|
38669
|
+
* MCP Task 16 (spec §12.1): recompute mcp-policy.json and write it when it changed. The written document is what MCP
|
|
38670
|
+
* issuance and release-time re-narrowing read, so it is cleared BEFORE the write and set only after it lands: a failed
|
|
38671
|
+
* write leaves no document in force here (no MCP grant), and the next trigger tries again. Never throws.
|
|
38672
|
+
*/
|
|
38673
|
+
recomputeMcpPolicy() {
|
|
38674
|
+
const doc = mcpPolicyFor(this.mcpPolicyInput());
|
|
38675
|
+
const json2 = JSON.stringify(doc);
|
|
38676
|
+
if (json2 === this.mcpPolicyWrittenJson) {
|
|
38677
|
+
if (this.mcpPolicyWritten !== null && this.mcpPolicyWrittenCounter !== null) this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38678
|
+
return;
|
|
38679
|
+
}
|
|
38680
|
+
if (MCP_INERT_STATES.has(doc.state) && !doc.r3_hold && Object.keys(doc.servers).length === 0 && !existsSync5(join18(runtimeConfigDir(this.home, this.cfg.identityId), MCP_POLICY_FILE))) {
|
|
38681
|
+
this.mcpPolicyWritten = doc;
|
|
38682
|
+
this.mcpPolicyWrittenJson = json2;
|
|
38683
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38684
|
+
this.policyWriteSeq++;
|
|
38685
|
+
this.clearMcpPolicyRetry();
|
|
38686
|
+
return;
|
|
38687
|
+
}
|
|
38688
|
+
this.mcpPolicyWritten = null;
|
|
38689
|
+
this.mcpPolicyWrittenJson = null;
|
|
38690
|
+
this.mcpPolicyWrittenCounter = null;
|
|
38691
|
+
this.policyWriteSeq++;
|
|
38692
|
+
try {
|
|
38693
|
+
this.grantWriters.writeMcpPolicy(this.home, this.cfg.identityId, doc);
|
|
38694
|
+
} catch (e) {
|
|
38695
|
+
writeServiceLog2("warn", "authz.mcp_policy", { state: ServiceLogEnum.of("write_failed", ["write_failed"]) }, e);
|
|
38696
|
+
this.scheduleMcpPolicyRetry();
|
|
38697
|
+
return;
|
|
38698
|
+
}
|
|
38699
|
+
this.mcpPolicyWritten = doc;
|
|
38700
|
+
this.mcpPolicyWrittenJson = json2;
|
|
38701
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38702
|
+
this.clearMcpPolicyRetry();
|
|
38703
|
+
}
|
|
38704
|
+
/** A document is in force: no pending retry, and the next failure starts the backoff from the first delay. */
|
|
38705
|
+
clearMcpPolicyRetry() {
|
|
38706
|
+
this.mcpPolicyRetryAttempt = 0;
|
|
38707
|
+
if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
|
|
38708
|
+
this.mcpPolicyRetryTimer = null;
|
|
38709
|
+
}
|
|
38710
|
+
/**
|
|
38711
|
+
* PR-B2 follow-up: one pending retry at a time. The retry recomputes from the state THEN (never re-writes a stale
|
|
38712
|
+
* document); any other trigger that writes successfully meanwhile cancels it. Nothing is in force on the bridge side
|
|
38713
|
+
* until a write lands (mcpPolicyWritten stays null).
|
|
38714
|
+
*/
|
|
38715
|
+
scheduleMcpPolicyRetry() {
|
|
38716
|
+
if (this.closingDown || this.mcpPolicyRetryTimer !== null) return;
|
|
38717
|
+
const d = this.mcpPolicyRetryDelaysMs;
|
|
38718
|
+
const delay = d[Math.min(this.mcpPolicyRetryAttempt, d.length - 1)];
|
|
38719
|
+
this.mcpPolicyRetryAttempt++;
|
|
38720
|
+
this.mcpPolicyRetryTimer = setTimeout(() => {
|
|
38721
|
+
this.mcpPolicyRetryTimer = null;
|
|
38722
|
+
if (!this.closingDown) this.recomputeMcpPolicy();
|
|
38723
|
+
}, delay);
|
|
38724
|
+
this.mcpPolicyRetryTimer.unref?.();
|
|
38725
|
+
}
|
|
37705
38726
|
/**
|
|
37706
38727
|
* Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
|
|
37707
38728
|
* after the memory was applied; the caller then writes nothing else. Throws only from a file write, with
|
|
@@ -37793,6 +38814,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37793
38814
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
37794
38815
|
const reportedSearchProviders = this.searchProvidersCapable();
|
|
37795
38816
|
const reportedUtilityTools = this.utilityToolsCapable();
|
|
38817
|
+
const reportedMcpAck = this.mcpAckSignature();
|
|
38818
|
+
const carriedOwnerDmTurn = this.toolStatusSender.carry();
|
|
37796
38819
|
const report = {
|
|
37797
38820
|
applied_version: version,
|
|
37798
38821
|
paths: checkConfiguredPaths(skillsJson),
|
|
@@ -37862,8 +38885,20 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37862
38885
|
// D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
|
|
37863
38886
|
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders),
|
|
37864
38887
|
// Weather/currency §8 item 5: a separate key, false with no runtime attached.
|
|
37865
|
-
utility_tools_v1: reportedUtilityTools
|
|
37866
|
-
|
|
38888
|
+
utility_tools_v1: reportedUtilityTools,
|
|
38889
|
+
// MCP Task 19 (spec §12.3): separate from `capability`; content-free enums and pattern tokens only.
|
|
38890
|
+
mcp_capability: this.reportedMcpCapability(),
|
|
38891
|
+
mcp_servers: this.reportedMcpServers(),
|
|
38892
|
+
// Deletion-ack ruling §5.3: content-free scalars. `mcp_deletions_through` only while EVERY attached runtime socket
|
|
38893
|
+
// declared mcp_deletion_ack_v1 (absent = cannot acknowledge); `mcp_credentials_present` only once one reported.
|
|
38894
|
+
...this.reportedMcpAck(),
|
|
38895
|
+
// Tool status Phase 0 (spec §3.5, round 2 H-5): an updated bridge, with or without a record yet.
|
|
38896
|
+
tool_status_v1: true
|
|
38897
|
+
},
|
|
38898
|
+
// Tool status Phase 0 (spec §3.5): the record rides every apply-time report, so record_report's wholesale replace
|
|
38899
|
+
// never erases it. Ruling M-2: the sender's CLOSED record, never the latest one, so a report sent inside a bucket
|
|
38900
|
+
// never reveals that bucket's change early. Advisory and content-free.
|
|
38901
|
+
...carriedOwnerDmTurn ? { owner_dm_last_turn: carriedOwnerDmTurn } : {}
|
|
37867
38902
|
};
|
|
37868
38903
|
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
37869
38904
|
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
@@ -37885,6 +38920,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37885
38920
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
37886
38921
|
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
37887
38922
|
this.lastReportedUtilityTools = reportedUtilityTools;
|
|
38923
|
+
this.lastReportedMcpAck = reportedMcpAck;
|
|
37888
38924
|
if (version) writeServiceLog2("info", "runtime_config.applied", { version });
|
|
37889
38925
|
else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
|
|
37890
38926
|
}
|
|
@@ -37961,6 +38997,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37961
38997
|
}
|
|
37962
38998
|
await this.restoreRuntimeInbox();
|
|
37963
38999
|
await this.loadToolGrantState();
|
|
39000
|
+
this.recomputeMcpPolicy();
|
|
37964
39001
|
this.recomputeManifestPolicy(NO_FEATURES);
|
|
37965
39002
|
try {
|
|
37966
39003
|
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
@@ -38978,7 +40015,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38978
40015
|
* for every envelope built, BEFORE it is published (SEC-06 #18 persists it
|
|
38979
40016
|
* there so a retry can resend the exact bytes).
|
|
38980
40017
|
*/
|
|
38981
|
-
async publishBody(channelId, body, id, onEnvelope, sealCheck) {
|
|
40018
|
+
async publishBody(channelId, body, id, onEnvelope, sealCheck, inReplyTo, mcpTurn) {
|
|
40019
|
+
const mcpGuard = mcpTurn === true || this.mcpReplyGuarded(channelId, inReplyTo ?? void 0);
|
|
40020
|
+
let attempt = 0;
|
|
38982
40021
|
this.enforceBodyRateFloor(channelId, body);
|
|
38983
40022
|
this.statusGate.replyStarted(channelId);
|
|
38984
40023
|
const joined = await this.reconcileJoin(channelId);
|
|
@@ -39018,6 +40057,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39018
40057
|
);
|
|
39019
40058
|
},
|
|
39020
40059
|
async (envelope) => {
|
|
40060
|
+
attempt++;
|
|
40061
|
+
if (mcpGuard) this.assertMcpReplyRelayFree(channelId, envelope, attempt);
|
|
39021
40062
|
if (onEnvelope) await onEnvelope(envelope);
|
|
39022
40063
|
return this.publishEnvelope(
|
|
39023
40064
|
channelId,
|
|
@@ -39027,6 +40068,77 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39027
40068
|
);
|
|
39028
40069
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
39029
40070
|
}
|
|
40071
|
+
/**
|
|
40072
|
+
* MCP Task 16 (round 2 H-D): message ids whose DELIVERED authorization (after release-time re-narrowing) carried an
|
|
40073
|
+
* `mcp:` grant, with their channel and whether the runtime acked them. Recorded at the buffer exit, the one place
|
|
40074
|
+
* every consumer's copy leaves from (over-recording a turn a non-capable consumer then received without the grant only
|
|
40075
|
+
* adds a check). Kept after the ack, so a late frame citing the turn is still checked. Bounded: the oldest ACKED entry
|
|
40076
|
+
* goes first.
|
|
40077
|
+
*/
|
|
40078
|
+
mcpDeliveredTurns = /* @__PURE__ */ new Map();
|
|
40079
|
+
mcpDeliveredTrackMax;
|
|
40080
|
+
/**
|
|
40081
|
+
* Record `m` when its authorization carries an `mcp:` grant. Returns false ONLY when `m` is a new turn, the record is
|
|
40082
|
+
* at its bound and no entry is acked: the caller (the buffer exit) must then issue no mcp: grant (audit M-2, fail
|
|
40083
|
+
* closed). `rebuild` (PR-B2 audit C-1 (b): a restored runtime-inbox row, or a replayed turn) records unconditionally:
|
|
40084
|
+
* those grants were issued before, so the record may exceed its bound by at most the runtime inbox's size.
|
|
40085
|
+
*/
|
|
40086
|
+
noteMcpDelivered(m, rebuild = false) {
|
|
40087
|
+
if (!m.authorization?.tool_grants.some((g) => g.tool.startsWith(MCP_GRANT_PREFIX))) return true;
|
|
40088
|
+
if (!this.mcpDeliveredTurns.has(m.message_id) && this.mcpDeliveredTurns.size >= this.mcpDeliveredTrackMax) {
|
|
40089
|
+
let victim;
|
|
40090
|
+
for (const [id, r] of this.mcpDeliveredTurns) if (r.acked) {
|
|
40091
|
+
victim = id;
|
|
40092
|
+
break;
|
|
40093
|
+
}
|
|
40094
|
+
if (victim !== void 0) this.mcpDeliveredTurns.delete(victim);
|
|
40095
|
+
else if (!rebuild) return false;
|
|
40096
|
+
}
|
|
40097
|
+
this.mcpDeliveredTurns.set(m.message_id, { channelId: m.channel_id, acked: false });
|
|
40098
|
+
return true;
|
|
40099
|
+
}
|
|
40100
|
+
/**
|
|
40101
|
+
* H-D: whether a frame to `channelId` must pass the relay-free check: it answers a recorded MCP turn (any channel:
|
|
40102
|
+
* a frame of that turn sent elsewhere is checked against ITS channel), or it cites no turn while an MCP turn is
|
|
40103
|
+
* unacked in the same channel.
|
|
40104
|
+
*/
|
|
40105
|
+
mcpReplyGuarded(channelId, inReplyTo) {
|
|
40106
|
+
if (inReplyTo !== void 0 && this.mcpDeliveredTurns.has(inReplyTo)) return true;
|
|
40107
|
+
if (inReplyTo !== void 0) return false;
|
|
40108
|
+
for (const r of this.mcpDeliveredTurns.values()) if (!r.acked && r.channelId === channelId) return true;
|
|
40109
|
+
return false;
|
|
40110
|
+
}
|
|
40111
|
+
/**
|
|
40112
|
+
* H-D, synchronous: read the channel's local leaves from the warm manager in one step and tie the read to the
|
|
40113
|
+
* envelope's SEALED group and epoch, so the leaf set is exactly the membership that can open these bytes. Owner and
|
|
40114
|
+
* agent only: pass. Anyone else: withhold. The tree moved past the seal on the first attempt: StaleEpochError, which
|
|
40115
|
+
* sendWithSelfHeal answers with its one converge-and-re-seal (the existing semantics); still unprovable on the retry,
|
|
40116
|
+
* or no warm manager, no pin, or any throw: withhold (fail closed). Read-only; never touches the group.
|
|
40117
|
+
*
|
|
40118
|
+
* Scope (PR-B2 audit item 5): this guards the `send` path (publishBody) only. `status` detail, the `action_*` run
|
|
40119
|
+
* frames, `/v1/send-attachment` and summary frames are NOT checked; they are safe for MCP turns only because the
|
|
40120
|
+
* runtime emits none of them (pinned by connect-agent-runtime/src/outbound-frames.arch.test.ts). A runtime that starts
|
|
40121
|
+
* emitting any of them needs this guard extended to that frame first.
|
|
40122
|
+
*/
|
|
40123
|
+
assertMcpReplyRelayFree(channelId, envelope, attempt) {
|
|
40124
|
+
let verdict = "unknown";
|
|
40125
|
+
try {
|
|
40126
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
40127
|
+
const snap = this.provider.leafSnapshotCached(channelId);
|
|
40128
|
+
const sealedGroup = sealedGroupIdOf(envelope);
|
|
40129
|
+
if (pin !== null && snap !== null && sealedGroup !== void 0 && snap.groupId === sealedGroup) {
|
|
40130
|
+
if (snap.epoch === envelope.mls_group_epoch) {
|
|
40131
|
+
verdict = ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId }) ? "free" : "relay";
|
|
40132
|
+
} else if (snap.epoch > envelope.mls_group_epoch) verdict = "moved";
|
|
40133
|
+
}
|
|
40134
|
+
} catch {
|
|
40135
|
+
verdict = "unknown";
|
|
40136
|
+
}
|
|
40137
|
+
if (verdict === "free") return;
|
|
40138
|
+
if (verdict === "moved" && attempt === 1) throw new StaleEpochError(envelope.mls_group_epoch, envelope.mls_group_epoch + 1);
|
|
40139
|
+
writeServiceLog2("warn", "mcp.reply_withheld", { state: ServiceLogEnum.of(verdict, ["relay", "moved", "unknown"]) });
|
|
40140
|
+
throw new McpReplyWithheldError(channelId);
|
|
40141
|
+
}
|
|
39030
40142
|
/**
|
|
39031
40143
|
* Audit M-B1 (controller ruling): a reply to a room-granted turn is sealed
|
|
39032
40144
|
* only at the epoch the room was classified at. On a move, the room is
|
|
@@ -39038,9 +40150,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39038
40150
|
* the redelivery replay (replayReplyIntents, which has no in_reply_to in its
|
|
39039
40151
|
* options) is checked the same as a live send.
|
|
39040
40152
|
*/
|
|
39041
|
-
async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope) {
|
|
40153
|
+
async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope, mcpTurn) {
|
|
39042
40154
|
const grant = inReplyTo !== null ? this.roomGrantEpochs.get(inReplyTo) : void 0;
|
|
39043
|
-
if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope);
|
|
40155
|
+
if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope, void 0, inReplyTo, mcpTurn);
|
|
39044
40156
|
if (channelId !== grant.channelId) {
|
|
39045
40157
|
writeServiceLog2("warn", "reply.room_class_changed");
|
|
39046
40158
|
throw new RoomClassChangedSendError(channelId);
|
|
@@ -39062,7 +40174,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39062
40174
|
},
|
|
39063
40175
|
(epoch) => {
|
|
39064
40176
|
if (epoch !== grant.epoch) throw new RoomGrantEpochMovedError();
|
|
39065
|
-
}
|
|
40177
|
+
},
|
|
40178
|
+
inReplyTo,
|
|
40179
|
+
mcpTurn
|
|
39066
40180
|
);
|
|
39067
40181
|
} catch (e) {
|
|
39068
40182
|
if (!(e instanceof RoomGrantEpochMovedError)) throw e;
|
|
@@ -39197,14 +40311,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39197
40311
|
const k = replyIntentKey({ inReplyTo: opts.inReplyTo, idempotencyKey: opts.idempotencyKey, ordinal: () => 0 });
|
|
39198
40312
|
if (!k) throw new Error("idempotent send without a key");
|
|
39199
40313
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39200
|
-
const
|
|
39201
|
-
const
|
|
39202
|
-
|
|
40314
|
+
const mcpTurn = this.mcpReplyGuarded(channelId, opts.inReplyTo);
|
|
40315
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, mcpTurn);
|
|
40316
|
+
const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null, mcpTurn });
|
|
40317
|
+
if (intent.bodyMac !== replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, intent.mcpTurn) || intent.channelId !== channelId || intent.state === "skipped") {
|
|
39203
40318
|
throw new IdempotencyConflictError();
|
|
39204
40319
|
}
|
|
39205
40320
|
return k;
|
|
39206
40321
|
}
|
|
39207
|
-
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null);
|
|
40322
|
+
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null, false);
|
|
39208
40323
|
});
|
|
39209
40324
|
return this.publishIntent(key, opts);
|
|
39210
40325
|
}
|
|
@@ -39213,25 +40328,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39213
40328
|
* the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
|
|
39214
40329
|
* after a slot collision: the body must go to a NEW part.
|
|
39215
40330
|
*/
|
|
39216
|
-
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit) {
|
|
40331
|
+
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit, inheritedMcpTurn) {
|
|
39217
40332
|
const credit = replyCredit ?? void 0;
|
|
40333
|
+
const mcpTurn = inheritedMcpTurn || this.mcpReplyGuarded(channelId, inReplyTo);
|
|
39218
40334
|
const scope = `reply:${inReplyTo}`;
|
|
39219
40335
|
let latest = null;
|
|
39220
40336
|
for (const intent of await this.store.listReplyIntents(inReplyTo)) {
|
|
39221
40337
|
const n = intent.scope === scope ? partOrdinal(intent.part) : null;
|
|
39222
40338
|
if (n !== null && (latest === null || n > latest.ordinal)) latest = { ordinal: n, intent };
|
|
39223
40339
|
}
|
|
39224
|
-
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit)) {
|
|
40340
|
+
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)) {
|
|
39225
40341
|
return { scope, part: latest.intent.part, inReplyTo };
|
|
39226
40342
|
}
|
|
39227
40343
|
let ordinal = latest ? latest.ordinal + 1 : 0;
|
|
39228
40344
|
for (let i = 0; i < REPLY_MAX_PART_ADVANCES; i++, ordinal++) {
|
|
39229
40345
|
const k = { scope, part: `p:${ordinal}`, inReplyTo };
|
|
39230
40346
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39231
|
-
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit);
|
|
39232
|
-
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit });
|
|
40347
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, mcpTurn);
|
|
40348
|
+
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit, mcpTurn });
|
|
39233
40349
|
if (created) return k;
|
|
39234
|
-
if (matchRetry && intent.state !== "skipped" && intent.
|
|
40350
|
+
if (matchRetry && intent.state !== "skipped" && intent.channelId === channelId && intent.bodyMac === replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, intent.mcpTurn)) return k;
|
|
39235
40351
|
writeServiceLog2("info", "reply.part_slot_taken", { attempt: i + 1 });
|
|
39236
40352
|
}
|
|
39237
40353
|
throw new Error("could not claim a reply part");
|
|
@@ -39283,7 +40399,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39283
40399
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
|
|
39284
40400
|
const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
|
|
39285
40401
|
try {
|
|
39286
|
-
await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo);
|
|
40402
|
+
await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo, intent.mcpTurn);
|
|
39287
40403
|
} catch (e) {
|
|
39288
40404
|
if (e instanceof AgentPausedSendError || e instanceof SendForbiddenError) {
|
|
39289
40405
|
if (intent.inReplyTo !== null) this.store.deletePendingReplyIntents(intent.inReplyTo);
|
|
@@ -39310,7 +40426,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39310
40426
|
JSON.stringify(["scope", key.scope]),
|
|
39311
40427
|
() => (
|
|
39312
40428
|
// r2 BLOCKER 1: the colliding row's OWN credit (verified above), never `opts`.
|
|
39313
|
-
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null)
|
|
40429
|
+
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null, intent.mcpTurn)
|
|
39314
40430
|
)
|
|
39315
40431
|
);
|
|
39316
40432
|
return this.publishIntent(next, opts, advances + 1);
|
|
@@ -39332,7 +40448,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39332
40448
|
if (credit !== null && !isReplyCredit(credit)) return false;
|
|
39333
40449
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, intent.channelId, intent);
|
|
39334
40450
|
if (id !== intent.replyMessageId) return false;
|
|
39335
|
-
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0) === intent.bodyMac;
|
|
40451
|
+
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0, intent.mcpTurn === true) === intent.bodyMac;
|
|
39336
40452
|
}
|
|
39337
40453
|
/**
|
|
39338
40454
|
* Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
|
|
@@ -39353,12 +40469,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39353
40469
|
* failure. Either way the intent stays pending, so the redelivery
|
|
39354
40470
|
* republishes the same reply.
|
|
39355
40471
|
*/
|
|
39356
|
-
async publishIntentWithRetry(channelId, body, id, key, inReplyTo) {
|
|
40472
|
+
async publishIntentWithRetry(channelId, body, id, key, inReplyTo, mcpTurn) {
|
|
39357
40473
|
let rateLimited = 0;
|
|
39358
40474
|
let inFlight = 0;
|
|
39359
40475
|
for (; ; ) {
|
|
39360
40476
|
try {
|
|
39361
|
-
await this.publishIntentOnce(channelId, body, id, key, inReplyTo);
|
|
40477
|
+
await this.publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn);
|
|
39362
40478
|
return;
|
|
39363
40479
|
} catch (e) {
|
|
39364
40480
|
if (e instanceof ReplyInFlightError) {
|
|
@@ -39429,8 +40545,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39429
40545
|
* original ack; a conflict can only mean the slot belongs to another
|
|
39430
40546
|
* message (for example an old reply whose intent was pruned).
|
|
39431
40547
|
*/
|
|
39432
|
-
async publishIntentOnce(channelId, body, id, key, inReplyTo) {
|
|
40548
|
+
async publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn) {
|
|
39433
40549
|
const current = await this.store.getReplyIntent(key.scope, key.part);
|
|
40550
|
+
const guardMcp = mcpTurn || current?.mcpTurn === true;
|
|
39434
40551
|
try {
|
|
39435
40552
|
if (current?.envelope) {
|
|
39436
40553
|
const cached = JSON.parse(new TextDecoder().decode(current.envelope));
|
|
@@ -39480,7 +40597,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39480
40597
|
key.part,
|
|
39481
40598
|
new TextEncoder().encode(JSON.stringify(envelope)),
|
|
39482
40599
|
sealedGroupIdOf(envelope)
|
|
39483
|
-
)
|
|
40600
|
+
),
|
|
40601
|
+
guardMcp
|
|
39484
40602
|
);
|
|
39485
40603
|
} catch (e) {
|
|
39486
40604
|
if (publishConflictCode(e) === "message_id_conflict") throw new ReplyIdCollisionError();
|
|
@@ -41991,16 +43109,20 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41991
43109
|
}
|
|
41992
43110
|
if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
|
|
41993
43111
|
let leafClass = "ok";
|
|
43112
|
+
let relay = void 0;
|
|
41994
43113
|
if (this.freshMeta(keyChannel)?.is_dm === true) {
|
|
41995
43114
|
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
41996
43115
|
if (signedDm !== void 0 && signedDm !== keyChannel) return this.ownerTurnFailed(m, "wrong_dm_channel");
|
|
41997
|
-
|
|
43116
|
+
const leaves = (await this.provider.leafTreeSummary(m.channelId)).leaves;
|
|
43117
|
+
const relayIdentityId = this.toolGrantManifest?.owner_dm_relay_identity_id;
|
|
43118
|
+
leafClass = classifyOwnerDmLeaves(leaves, {
|
|
41998
43119
|
ownerIdentityId: pin.ownerIdentityId,
|
|
41999
43120
|
agentIdentityId: this.cfg.identityId,
|
|
42000
|
-
relayIdentityId
|
|
43121
|
+
relayIdentityId,
|
|
42001
43122
|
isServerRelay: (id) => this.freshRoster(m.channelId)?.get(id)?.is_relay === true
|
|
42002
43123
|
});
|
|
42003
43124
|
if (leafClass === "foreign") return this.ownerTurnFailed(m, "not_owner_dm_leaves");
|
|
43125
|
+
relay = ownerDmRelayOf(leafClass, leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId, relayIdentityId });
|
|
42004
43126
|
}
|
|
42005
43127
|
const held = await this.heldPosition(m.channelId);
|
|
42006
43128
|
if (held.groupId === null) return this.ownerTurnFailed(m, "verify_error");
|
|
@@ -42009,7 +43131,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42009
43131
|
}
|
|
42010
43132
|
const replay = Object.freeze({ channelId: m.channelId, groupId: held.groupId, epoch: v.mls_group_epoch });
|
|
42011
43133
|
this.ownerReplayPending.set(m.messageId, replay);
|
|
42012
|
-
|
|
43134
|
+
const relayField = relay !== void 0 ? { relay } : {};
|
|
43135
|
+
if (leafClass === "unsigned_relay") return Object.freeze({ kind: "constants_only", replay, ...relayField });
|
|
43136
|
+
return Object.freeze({ kind: "verified", replay, ...relayField, relayLeaf: relayLeafFromPhase0Relay(relay) });
|
|
42013
43137
|
} catch {
|
|
42014
43138
|
return this.ownerTurnFailed(m, "verify_error");
|
|
42015
43139
|
}
|
|
@@ -42085,6 +43209,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42085
43209
|
}
|
|
42086
43210
|
/** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
|
|
42087
43211
|
async loadToolGrantState() {
|
|
43212
|
+
try {
|
|
43213
|
+
this.mcpSeen = await this.store.loadMcpSeen();
|
|
43214
|
+
} catch (e) {
|
|
43215
|
+
this.mcpSeen = true;
|
|
43216
|
+
writeServiceLog2("warn", "authz.mcp_seen_load_failed", {}, e);
|
|
43217
|
+
}
|
|
42088
43218
|
const pin = this.cfg.ownerGrantPin?.pin;
|
|
42089
43219
|
if (!pin) return;
|
|
42090
43220
|
try {
|
|
@@ -42092,9 +43222,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42092
43222
|
this.store.dropToolGrantAppliedExcept(pin.publicKeyB64);
|
|
42093
43223
|
const saved = await this.store.loadToolGrantManifest(pin.publicKeyB64);
|
|
42094
43224
|
if (saved) {
|
|
42095
|
-
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null });
|
|
43225
|
+
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
|
|
42096
43226
|
if (v.state === "ok") {
|
|
42097
43227
|
this.toolGrantManifest = v.manifest;
|
|
43228
|
+
if (v.manifest.signedMcpPresent && !this.mcpSeen) await this.setMcpSeen(true);
|
|
42098
43229
|
this.toolGrantCounter = v.manifest.counter;
|
|
42099
43230
|
} else {
|
|
42100
43231
|
this.logManifest("malformed");
|
|
@@ -42119,8 +43250,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42119
43250
|
this.logManifest("no_manifest");
|
|
42120
43251
|
return;
|
|
42121
43252
|
}
|
|
42122
|
-
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter });
|
|
43253
|
+
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
|
|
42123
43254
|
if (v.state === "ok") {
|
|
43255
|
+
if (v.manifest.signedMcpPresent) await this.setMcpSeen(true);
|
|
42124
43256
|
try {
|
|
42125
43257
|
await this.store.saveToolGrantManifest(pin.publicKeyB64, v.manifest.counter, v.wrapper);
|
|
42126
43258
|
} catch {
|
|
@@ -42129,13 +43261,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42129
43261
|
}
|
|
42130
43262
|
this.toolGrantManifest = v.manifest;
|
|
42131
43263
|
this.toolGrantCounter = v.manifest.counter;
|
|
43264
|
+
this.recomputeMcpPolicy();
|
|
43265
|
+
if (v.manifest.mcpKey === "present" && !v.manifest.signedMcpPresent && this.mcpSeen) await this.setMcpSeen(false);
|
|
42132
43266
|
this.logManifest("ok");
|
|
42133
43267
|
if (v.droppedUnknown) this.logManifest("unknown_entry");
|
|
42134
43268
|
return;
|
|
42135
43269
|
}
|
|
42136
43270
|
this.logManifest(v.state);
|
|
43271
|
+
if (v.state === "wrong_owner") {
|
|
43272
|
+
this.dropOwnerDmLastTurn();
|
|
43273
|
+
this.mcpDeletionsThrough = 0;
|
|
43274
|
+
this.mcpPolicyWrittenCounter = null;
|
|
43275
|
+
this.noteMcpAckChanged();
|
|
43276
|
+
}
|
|
42137
43277
|
if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
|
|
42138
43278
|
}
|
|
43279
|
+
/**
|
|
43280
|
+
* 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
|
|
43281
|
+
* an owner_dm `mcp:` string, or while the sticky flag is set (an old client's re-sign cannot lift it, audit C-1).
|
|
43282
|
+
*/
|
|
43283
|
+
r3Hold() {
|
|
43284
|
+
return this.toolGrantManifest?.signedMcpPresent === true || this.mcpSeen;
|
|
43285
|
+
}
|
|
43286
|
+
/** Persist then mirror. Setting mirrors even on a failed write; clearing mirrors only after the write (fail closed). */
|
|
43287
|
+
async setMcpSeen(seen) {
|
|
43288
|
+
if (seen) this.mcpSeen = true;
|
|
43289
|
+
try {
|
|
43290
|
+
await this.store.saveMcpSeen(seen);
|
|
43291
|
+
} catch (e) {
|
|
43292
|
+
writeServiceLog2("warn", "authz.mcp_seen_save_failed", {}, e);
|
|
43293
|
+
return;
|
|
43294
|
+
}
|
|
43295
|
+
this.mcpSeen = seen;
|
|
43296
|
+
}
|
|
42139
43297
|
logManifest(state) {
|
|
42140
43298
|
this.manifestState = state;
|
|
42141
43299
|
const quiet = state === "ok" || state === "unchanged" || state === "no_pin" || state === "no_manifest";
|
|
@@ -42232,6 +43390,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42232
43390
|
let tool_grants = NO_TOOL_GRANTS;
|
|
42233
43391
|
let relaySpeaker;
|
|
42234
43392
|
let memoryWrite = false;
|
|
43393
|
+
let relayFree = false;
|
|
42235
43394
|
const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId) && (opts.voice === void 0 || rosterFreshAt(vEntry, opts.voice.heldEpoch, opts.voice.heldGroupId));
|
|
42236
43395
|
const ownerDmFresh = epochFresh || opts.voice === void 0 && this.ownerDmCycleFresh.has(channelId);
|
|
42237
43396
|
if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
|
|
@@ -42258,6 +43417,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42258
43417
|
tool_grants = ROOM_DEFAULT_READ_GRANTS;
|
|
42259
43418
|
}
|
|
42260
43419
|
let grantSource = tool_grants.length ? "constant" : "none";
|
|
43420
|
+
let ownerDmEffective = null;
|
|
42261
43421
|
const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
|
|
42262
43422
|
const binding = this.manifestBinding;
|
|
42263
43423
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
@@ -42276,6 +43436,21 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42276
43436
|
});
|
|
42277
43437
|
tool_grants = r.grants;
|
|
42278
43438
|
grantSource = r.source;
|
|
43439
|
+
ownerDmEffective = r;
|
|
43440
|
+
const mcp = mcpIssuance({
|
|
43441
|
+
source: r.source,
|
|
43442
|
+
verdict: opts.ownerVerdict,
|
|
43443
|
+
entitlement: this.mcpEntitlementInForce(),
|
|
43444
|
+
hostSwitch: this.mcpHostSwitch,
|
|
43445
|
+
ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
|
|
43446
|
+
disabled: this.payloadToolDeny,
|
|
43447
|
+
manifest,
|
|
43448
|
+
policy: this.mcpPolicyWritten
|
|
43449
|
+
});
|
|
43450
|
+
if (mcp.length > 0) {
|
|
43451
|
+
tool_grants = Object.freeze([...tool_grants, ...mcp]);
|
|
43452
|
+
relayFree = true;
|
|
43453
|
+
}
|
|
42279
43454
|
} else if (tool_grants === ROOM_DEFAULT_READ_GRANTS) {
|
|
42280
43455
|
const r = effectiveToolGrants({
|
|
42281
43456
|
audience: "team_room",
|
|
@@ -42293,12 +43468,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42293
43468
|
if (opts.ownerVerdict?.kind === "failed") {
|
|
42294
43469
|
tool_grants = NO_TOOL_GRANTS;
|
|
42295
43470
|
memoryWrite = false;
|
|
43471
|
+
relayFree = false;
|
|
42296
43472
|
relaySpeaker = void 0;
|
|
42297
43473
|
grantSource = "none";
|
|
42298
43474
|
}
|
|
42299
43475
|
const roomClass = meta?.is_dm !== false ? "na" : entry?.crossOrg === false ? "internal" : entry?.crossOrg === true ? "cross_org" : "unknown";
|
|
42300
43476
|
if (opts.log !== false) {
|
|
42301
43477
|
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite, grantSource);
|
|
43478
|
+
const pinOwner = this.cfg.ownerGrantPin?.pin?.ownerIdentityId;
|
|
43479
|
+
const verdict = opts.ownerVerdict;
|
|
43480
|
+
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")) {
|
|
43481
|
+
const pinned = this.channelOwner.get(channelId);
|
|
43482
|
+
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
43483
|
+
const ownerDirect = ownerId !== void 0 && audience === "owner_dm" && senderId === ownerId;
|
|
43484
|
+
this.noteOwnerDmTurn({
|
|
43485
|
+
cause: {
|
|
43486
|
+
killSwitchOff: !this.ownerDmDefaultGrants,
|
|
43487
|
+
manifestSwitchOff: !this.ownerDmManifestGrants,
|
|
43488
|
+
pinMissingOrAnomalous: ownerId === void 0,
|
|
43489
|
+
// OQ-5: only from a PRESENT roster entry; a missing (TTL-expired) entry is `stale` below.
|
|
43490
|
+
senderNotOwner: entry !== void 0 && (sender === void 0 || !ownerDirect),
|
|
43491
|
+
stale: !ownerDmFresh || entry === void 0,
|
|
43492
|
+
verdict,
|
|
43493
|
+
manifestPresent: manifest !== null,
|
|
43494
|
+
signedDmChannel: manifest?.owner_dm_channel_id,
|
|
43495
|
+
channelId,
|
|
43496
|
+
boundNow,
|
|
43497
|
+
consumerAttached: this.manifestConsumer !== null
|
|
43498
|
+
},
|
|
43499
|
+
grantSource,
|
|
43500
|
+
toolGrants: tool_grants,
|
|
43501
|
+
withheld: grantSource === "manifest" && ownerDmEffective !== null ? ownerDmEffective.withheld : []
|
|
43502
|
+
});
|
|
43503
|
+
}
|
|
42302
43504
|
}
|
|
42303
43505
|
return Object.freeze({
|
|
42304
43506
|
version: 1,
|
|
@@ -42306,9 +43508,78 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42306
43508
|
sender_membership: sender ? "active" : "unknown",
|
|
42307
43509
|
tool_grants,
|
|
42308
43510
|
...relaySpeaker ? { relay_speaker: relaySpeaker } : {},
|
|
42309
|
-
...memoryWrite ? { memory_write: true } : {}
|
|
43511
|
+
...memoryWrite ? { memory_write: true } : {},
|
|
43512
|
+
...relayFree ? { relay_free: true } : {}
|
|
42310
43513
|
});
|
|
42311
43514
|
}
|
|
43515
|
+
/**
|
|
43516
|
+
* 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
|
|
43517
|
+
* keep the previous one (and its `at`) when the change tuple is the same, so `at` means "last changed". Never throws
|
|
43518
|
+
* into the authorization path. Never logs a channel id, sender id or tool name.
|
|
43519
|
+
*/
|
|
43520
|
+
noteOwnerDmTurn(t) {
|
|
43521
|
+
try {
|
|
43522
|
+
const v = t.cause.verdict;
|
|
43523
|
+
const relay = (v.kind === "verified" || v.kind === "constants_only") && v.relay !== void 0 ? v.relay : "unknown";
|
|
43524
|
+
let reason = {};
|
|
43525
|
+
if (t.grantSource !== "manifest") {
|
|
43526
|
+
const c = this.toolStatusCause(t.cause);
|
|
43527
|
+
if (c !== null && c.source === t.grantSource && c.cause !== void 0) {
|
|
43528
|
+
reason = c.sub !== void 0 ? { cause: c.cause, sub: c.sub } : { cause: c.cause };
|
|
43529
|
+
} else {
|
|
43530
|
+
writeServiceLog2("warn", "tool_status.source_mismatch");
|
|
43531
|
+
}
|
|
43532
|
+
} else {
|
|
43533
|
+
const c = this.toolStatusCause(t.cause);
|
|
43534
|
+
if (c === null || c.source !== "manifest") writeServiceLog2("warn", "tool_status.source_mismatch");
|
|
43535
|
+
}
|
|
43536
|
+
const names = t.toolGrants.map((g) => g.tool);
|
|
43537
|
+
const candidate = Object.freeze({
|
|
43538
|
+
at: bucketStartAt(this.toolStatusNow()),
|
|
43539
|
+
// ruling M-2: the 15-minute UTC bucket, never the minute
|
|
43540
|
+
counter: this.toolGrantCounter,
|
|
43541
|
+
source: t.grantSource,
|
|
43542
|
+
...reason,
|
|
43543
|
+
relay,
|
|
43544
|
+
issued: Object.freeze(sanitizeIssued(names)),
|
|
43545
|
+
withheld: Object.freeze(t.withheld.map((w) => Object.freeze({ tool: w.tool, why: w.why }))),
|
|
43546
|
+
// A count only, never a name (content-free). 0 unless the MCP issuance above added mcp: grants.
|
|
43547
|
+
mcp_issued: Math.min(160, names.filter((n) => n.startsWith("mcp:")).length)
|
|
43548
|
+
});
|
|
43549
|
+
if (sameTuple(this.ownerDmLastTurn, candidate)) return;
|
|
43550
|
+
this.ownerDmLastTurn = candidate;
|
|
43551
|
+
this.toolStatusSender.note(candidate);
|
|
43552
|
+
} catch (e) {
|
|
43553
|
+
writeServiceLog2("warn", "tool_status.record_failed", {}, e);
|
|
43554
|
+
}
|
|
43555
|
+
}
|
|
43556
|
+
/** R2-M1: the served manifest names another owner than the pin; the held record belongs to the old owner. */
|
|
43557
|
+
dropOwnerDmLastTurn() {
|
|
43558
|
+
this.ownerDmLastTurn = null;
|
|
43559
|
+
this.toolStatusSender.drop();
|
|
43560
|
+
}
|
|
43561
|
+
/**
|
|
43562
|
+
* Phase 0 Task 9 (spec §3.5): POST the record, body = the record only, to the narrow agent-key route. The backend
|
|
43563
|
+
* resolves the identity from the device credential; no identity, channel or sender id is in the URL or the body.
|
|
43564
|
+
* Returns the status class only; never throws.
|
|
43565
|
+
*/
|
|
43566
|
+
async postToolStatus(rec) {
|
|
43567
|
+
try {
|
|
43568
|
+
const res = await this.fetchBounded("runtime_config_tool_status", "control", `${this.cfg.apiBase}/api/v1/agents/me/runtime-config/tool-status`, {
|
|
43569
|
+
method: "POST",
|
|
43570
|
+
headers: { authorization: `Bearer ${this.cfg.apiKey}`, "content-type": "application/json" },
|
|
43571
|
+
body: JSON.stringify(rec)
|
|
43572
|
+
});
|
|
43573
|
+
const retryAfter = res.headers.get("retry-after");
|
|
43574
|
+
try {
|
|
43575
|
+
await res.body?.cancel();
|
|
43576
|
+
} catch {
|
|
43577
|
+
}
|
|
43578
|
+
return { status: res.status, retryAfter };
|
|
43579
|
+
} catch (e) {
|
|
43580
|
+
return e?.name === "FetchDeadlineError" ? "timeout" : "network";
|
|
43581
|
+
}
|
|
43582
|
+
}
|
|
42312
43583
|
/**
|
|
42313
43584
|
* One content-free INFO line per authorization decision (every call site of
|
|
42314
43585
|
* authorizationFor stages exactly one runtime turn). It says WHY a turn got
|
|
@@ -42532,6 +43803,111 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42532
43803
|
* client's own wait already renders "Summary unavailable" — so there is
|
|
42533
43804
|
* nothing to catch here besides the send itself.
|
|
42534
43805
|
*/
|
|
43806
|
+
/**
|
|
43807
|
+
* 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
|
|
43808
|
+
* 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
|
|
43809
|
+
* that IS the signed owner DM; verifyOwnerTurn says `verified` (the pinned owner's signature, the signed DM, the leaf
|
|
43810
|
+
* check, not a replay) with `relayLeaf === false`; mcp-policy.json as written is `ok` with this server `ok`; a runtime
|
|
43811
|
+
* socket that declared mcp_client_v1 is attached; and the rate limits admit it. The replay row is written HERE
|
|
43812
|
+
* (M-3: the request never reaches stageRuntimeInbox, where it is otherwise written), for every verified request, before
|
|
43813
|
+
* anything is answered; the ownerReplayPending entry is removed in `finally`. A failure to record answers nothing.
|
|
43814
|
+
* Never throws. The answer runs outside the ingest cycle, so the row is acked now.
|
|
43815
|
+
*/
|
|
43816
|
+
async acceptMcpCatalogRequest(m, body) {
|
|
43817
|
+
try {
|
|
43818
|
+
const req = parseMcpCatalogRequest(body);
|
|
43819
|
+
if (req === null) return logMcpCatalogRequest("malformed");
|
|
43820
|
+
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
43821
|
+
if (this.keyChannelForFrame(m) !== m.channelId || this.freshMeta(m.channelId)?.is_dm !== true || signedDm === void 0 || signedDm !== m.channelId) {
|
|
43822
|
+
return logMcpCatalogRequest("not_owner_dm");
|
|
43823
|
+
}
|
|
43824
|
+
const verdict = await this.verifyOwnerTurn(m);
|
|
43825
|
+
if (verdict.kind !== "verified") return logMcpCatalogRequest(verdict.kind === "failed" && verdict.reason === "replay" ? "replay" : "not_verified");
|
|
43826
|
+
this.store.recordOwnerMessage(verdict.replay.channelId, m.messageId, verdict.replay.groupId, verdict.replay.epoch, Date.now());
|
|
43827
|
+
if (verdict.relayLeaf !== false) return logMcpCatalogRequest("relay_leaf");
|
|
43828
|
+
const target = this.mcpCatalogTarget(req.serverId);
|
|
43829
|
+
const lister = this.mcpClientCapable() ? this.mcpCatalogListers.values().next().value : void 0;
|
|
43830
|
+
if (target === null || lister === void 0) return logMcpCatalogRequest("not_ready");
|
|
43831
|
+
const admitted = this.mcpCatalogRate.admit(req.serverId);
|
|
43832
|
+
if (admitted !== "ok") return admitted === "in_flight" ? logMcpCatalogRequest("in_flight") : void 0;
|
|
43833
|
+
const work = this.outsideIngestCycle(() => this.answerMcpCatalogRequest(m.channelId, req, lister)).finally(() => {
|
|
43834
|
+
this.mcpCatalogRate.release(req.serverId);
|
|
43835
|
+
this.mcpCatalogWork.delete(work);
|
|
43836
|
+
});
|
|
43837
|
+
this.mcpCatalogWork.add(work);
|
|
43838
|
+
} catch {
|
|
43839
|
+
logMcpCatalogRequest("failed");
|
|
43840
|
+
} finally {
|
|
43841
|
+
this.ownerReplayPending.delete(m.messageId);
|
|
43842
|
+
}
|
|
43843
|
+
}
|
|
43844
|
+
/** The signed server's vendor and the bridge catalog row's issuer, when mcp-policy.json as WRITTEN is `ok` with it `ok`. */
|
|
43845
|
+
mcpCatalogTarget(serverId) {
|
|
43846
|
+
const doc = this.mcpPolicyWritten;
|
|
43847
|
+
if (doc === null || doc.state !== "ok" || !Object.prototype.hasOwnProperty.call(doc.servers, serverId)) return null;
|
|
43848
|
+
const s = doc.servers[serverId];
|
|
43849
|
+
if (s.state !== "ok" || s.vendor === void 0) return null;
|
|
43850
|
+
const catalog = this.mcpCatalog ?? MCP_CATALOG;
|
|
43851
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
|
|
43852
|
+
return { vendor: s.vendor, issuer: catalog[s.vendor].issuer };
|
|
43853
|
+
}
|
|
43854
|
+
/** Ask the runtime, re-check, build (384 KB cap) and send. Never throws. */
|
|
43855
|
+
async answerMcpCatalogRequest(channelId, req, lister) {
|
|
43856
|
+
try {
|
|
43857
|
+
const target = this.mcpCatalogTarget(req.serverId);
|
|
43858
|
+
if (target === null) return logMcpCatalogRequest("not_ready");
|
|
43859
|
+
const frame = await lister(req.serverId, MCP_CATALOG_LIST_TIMEOUT_MS);
|
|
43860
|
+
if (frame === null || frame === void 0) return logMcpCatalogRequest("runtime_timeout");
|
|
43861
|
+
const listed = validateCatalogListReply(frame, req.serverId, target.vendor);
|
|
43862
|
+
if (listed === null) return logMcpCatalogRequest("runtime_refused");
|
|
43863
|
+
const still = this.mcpCatalogTarget(req.serverId);
|
|
43864
|
+
if (still === null || still.vendor !== target.vendor) return logMcpCatalogRequest("not_ready");
|
|
43865
|
+
await this.sendMcpCatalog(channelId, buildMcpCatalogBody({ serverId: req.serverId, requestId: req.requestId, vendor: still.vendor, issuer: still.issuer, reply: listed }));
|
|
43866
|
+
logMcpCatalogRequest("sent");
|
|
43867
|
+
} catch (e) {
|
|
43868
|
+
logMcpCatalogRequest(e instanceof McpReplyWithheldError ? "withheld" : "send_failed");
|
|
43869
|
+
}
|
|
43870
|
+
}
|
|
43871
|
+
/**
|
|
43872
|
+
* The `mcp_catalog` reply: a system_event envelope with a body-level type (the sendSummary shape), on the shared
|
|
43873
|
+
* self-heal seam and the one publish chokepoint. H-D applies to EVERY attempt (it carries vendor-supplied tool
|
|
43874
|
+
* descriptions): after the seal, before the POST, the leaves at the SEALED group and epoch must be exactly the owner
|
|
43875
|
+
* and the agent, or the frame is withheld. One message id across the retry.
|
|
43876
|
+
*/
|
|
43877
|
+
async sendMcpCatalog(channelId, body) {
|
|
43878
|
+
const joined = await this.reconcileJoin(channelId);
|
|
43879
|
+
if (!joined) throw new ChannelPendingError(channelId);
|
|
43880
|
+
const messageId = crypto.randomUUID();
|
|
43881
|
+
let attempt = 0;
|
|
43882
|
+
await this.sendWithSelfHealTerminal(
|
|
43883
|
+
this.provider,
|
|
43884
|
+
channelId,
|
|
43885
|
+
(epoch) => buildOutboundEnvelope({
|
|
43886
|
+
custody: this.custody,
|
|
43887
|
+
groupKey: this.provider,
|
|
43888
|
+
messageId,
|
|
43889
|
+
channelId,
|
|
43890
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
43891
|
+
senderId: this.cfg.identityId,
|
|
43892
|
+
senderType: "agent",
|
|
43893
|
+
type: "system_event",
|
|
43894
|
+
bodyPlaintext: JSON.stringify(body),
|
|
43895
|
+
bindingId: "",
|
|
43896
|
+
bindingVersion: "0.0.0",
|
|
43897
|
+
agentSkillId: null,
|
|
43898
|
+
epoch,
|
|
43899
|
+
traceparent: ZERO_TRACEPARENT,
|
|
43900
|
+
references: [],
|
|
43901
|
+
stage5Outcome: "allow",
|
|
43902
|
+
stage5Evaluators: []
|
|
43903
|
+
}),
|
|
43904
|
+
async (envelope) => {
|
|
43905
|
+
attempt++;
|
|
43906
|
+
this.assertMcpReplyRelayFree(channelId, envelope, attempt);
|
|
43907
|
+
return this.publishEnvelope(channelId, () => this.transport.publish(envelope));
|
|
43908
|
+
}
|
|
43909
|
+
);
|
|
43910
|
+
}
|
|
42535
43911
|
async handleSummarize(channelId, request) {
|
|
42536
43912
|
const body = await runSummarize({
|
|
42537
43913
|
request,
|
|
@@ -43042,6 +44418,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43042
44418
|
new TextDecoder().decode(m.plaintext)
|
|
43043
44419
|
);
|
|
43044
44420
|
if (parsed.type && parsed.type !== "text") {
|
|
44421
|
+
if (parsed.type === "mcp_catalog_request" || parsed.type === "mcp_catalog") {
|
|
44422
|
+
if (parsed.type === "mcp_catalog_request") await this.acceptMcpCatalogRequest(m, parsed);
|
|
44423
|
+
acked.push(m.queueId);
|
|
44424
|
+
continue;
|
|
44425
|
+
}
|
|
43045
44426
|
if (parsed.type === "summarize") {
|
|
43046
44427
|
const request = parseSummarizeBody(parsed, this.cfg.identityId, writeServiceLog2);
|
|
43047
44428
|
if (request !== null) {
|
|
@@ -43550,7 +44931,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43550
44931
|
}
|
|
43551
44932
|
/** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
|
|
43552
44933
|
delivering(m) {
|
|
43553
|
-
|
|
44934
|
+
let out = this.renarrowManifestGrants(m);
|
|
44935
|
+
if (!this.noteMcpDelivered(out)) {
|
|
44936
|
+
out = withMcpCapability(out, false);
|
|
44937
|
+
writeServiceLog2("warn", "mcp.delivered_record_full");
|
|
44938
|
+
}
|
|
43554
44939
|
this.noteDeliveredFrame(out);
|
|
43555
44940
|
return out;
|
|
43556
44941
|
}
|
|
@@ -43561,6 +44946,50 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43561
44946
|
* audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
|
|
43562
44947
|
*/
|
|
43563
44948
|
renarrowManifestGrants(m) {
|
|
44949
|
+
const narrowed = this.renarrowManifestGrantsBase(m);
|
|
44950
|
+
return this.renarrowMcpGrants(narrowed);
|
|
44951
|
+
}
|
|
44952
|
+
/**
|
|
44953
|
+
* MCP Task 16 (audit H-2; round 2 LOW), after the base re-narrowing: a buffered `mcp:` grant survives release only
|
|
44954
|
+
* when, NOW, the turn is still a plain owner-DM turn in the signed DM channel, the host switch reads "on", the
|
|
44955
|
+
* entitlement reading is in force, the name is not in skills.disabled, the mcp-policy.json document last written
|
|
44956
|
+
* lists the server ok with the tool, and a FRESH synchronous read of the DM's local leaves holds exactly the pinned
|
|
44957
|
+
* owner and the agent (MlsGroupKeyProvider.leafSnapshotCached: one `state` object, no await; null fails closed). Any
|
|
44958
|
+
* other mcp: grant is dropped, never kept as a fallback. relay_free is dropped whenever no mcp: grant survives.
|
|
44959
|
+
* Read-only: never touches the MLS group. Only narrows; returns `m` by reference when nothing changes.
|
|
44960
|
+
*/
|
|
44961
|
+
renarrowMcpGrants(m) {
|
|
44962
|
+
const a = m.authorization;
|
|
44963
|
+
if (!a) return m;
|
|
44964
|
+
const isMcp = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
|
|
44965
|
+
if (!a.tool_grants.some(isMcp) && a.relay_free === void 0) return m;
|
|
44966
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
44967
|
+
const manifest = pin !== null ? this.toolGrantManifest : null;
|
|
44968
|
+
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);
|
|
44969
|
+
const kept = a.tool_grants.filter((g) => {
|
|
44970
|
+
if (!isMcp(g)) return true;
|
|
44971
|
+
const parsed = parseMcpGrant(g.tool);
|
|
44972
|
+
return turnOk && g.mode === "read" && g.source === "manifest" && parsed !== null && !this.payloadToolDeny.has(g.tool) && mcpPolicyAllows(this.mcpPolicyWritten, parsed.serverId, parsed.tool);
|
|
44973
|
+
});
|
|
44974
|
+
const keepRelayFree = a.relay_free === true && kept.some(isMcp);
|
|
44975
|
+
if (kept.length === a.tool_grants.length && keepRelayFree === (a.relay_free === true)) return m;
|
|
44976
|
+
const { relay_free: _relayFree, ...rest } = a;
|
|
44977
|
+
return { ...m, authorization: Object.freeze({ ...rest, tool_grants: Object.freeze(kept), ...keepRelayFree ? { relay_free: true } : {} }) };
|
|
44978
|
+
}
|
|
44979
|
+
/**
|
|
44980
|
+
* 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
|
|
44981
|
+
* owner and this agent. Synchronous (one manager `state`, so the leaves belong to one epoch); a cold manager or any
|
|
44982
|
+
* throw reads as NOT relay-free.
|
|
44983
|
+
*/
|
|
44984
|
+
ownerDmRelayFreeNow(channelId, ownerIdentityId) {
|
|
44985
|
+
try {
|
|
44986
|
+
const snap = this.provider.leafSnapshotCached(channelId);
|
|
44987
|
+
return snap !== null && ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId, agentIdentityId: this.cfg.identityId });
|
|
44988
|
+
} catch {
|
|
44989
|
+
return false;
|
|
44990
|
+
}
|
|
44991
|
+
}
|
|
44992
|
+
renarrowManifestGrantsBase(m) {
|
|
43564
44993
|
const a = m.authorization;
|
|
43565
44994
|
if (!a || !a.tool_grants.some((g) => g.mode === "read" && g.source === "manifest")) return m;
|
|
43566
44995
|
const audience = a.audience === "owner_dm" || a.audience === "team_room" ? a.audience : null;
|
|
@@ -43656,6 +45085,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43656
45085
|
return;
|
|
43657
45086
|
}
|
|
43658
45087
|
if (!this.admitRuntimeDelivery(m)) return;
|
|
45088
|
+
this.noteMcpDelivered(m, true);
|
|
43659
45089
|
const ch = m.channel_id;
|
|
43660
45090
|
this.replayingChannels.add(ch);
|
|
43661
45091
|
this.replyIntentReplays++;
|
|
@@ -44006,11 +45436,11 @@ ${channelId}`;
|
|
|
44006
45436
|
if (!res.ok) return void 0;
|
|
44007
45437
|
const rows = await res.json();
|
|
44008
45438
|
if (!Array.isArray(rows)) return void 0;
|
|
44009
|
-
const
|
|
45439
|
+
const own2 = rows.find(
|
|
44010
45440
|
(r) => r?.identity_id === this.cfg.identityId
|
|
44011
45441
|
);
|
|
44012
|
-
if (!
|
|
44013
|
-
return typeof
|
|
45442
|
+
if (!own2) return void 0;
|
|
45443
|
+
return typeof own2.paused_at === "string" && own2.paused_at.length > 0;
|
|
44014
45444
|
} catch (e) {
|
|
44015
45445
|
writeServiceLog2("warn", "pause.probe_failed", {}, e);
|
|
44016
45446
|
return void 0;
|
|
@@ -44043,6 +45473,10 @@ ${channelId}`;
|
|
|
44043
45473
|
/** Complete the durable handoff only after the runtime explicitly acks. */
|
|
44044
45474
|
ackRuntimeMessages(messageIds) {
|
|
44045
45475
|
for (const id of messageIds) this.roomGrantEpochs.delete(id);
|
|
45476
|
+
for (const id of messageIds) {
|
|
45477
|
+
const r = this.mcpDeliveredTurns.get(id);
|
|
45478
|
+
if (r) r.acked = true;
|
|
45479
|
+
}
|
|
44046
45480
|
for (const messageId of messageIds) {
|
|
44047
45481
|
this.store.ackRuntimeInbox(messageId);
|
|
44048
45482
|
this.runtimePendingIds.delete(messageId);
|
|
@@ -44124,6 +45558,7 @@ ${channelId}`;
|
|
|
44124
45558
|
const message = rest;
|
|
44125
45559
|
this.applyStagedMarks(message.message_id, marks);
|
|
44126
45560
|
this.runtimePendingIds.add(message.message_id);
|
|
45561
|
+
this.noteMcpDelivered(message, true);
|
|
44127
45562
|
if (this.cfg.ownerGrantPin?.pin && this.isOwnerVerifyFailed(message.message_id)) {
|
|
44128
45563
|
this.ownerVerifyFailedIds.set(message.message_id, message.channel_id);
|
|
44129
45564
|
}
|
|
@@ -44304,6 +45739,179 @@ ${channelId}`;
|
|
|
44304
45739
|
clearSearchProvidersSocket(token) {
|
|
44305
45740
|
if (this.searchProvidersSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
44306
45741
|
}
|
|
45742
|
+
/** MCP Task 16 (spec §15): the same AND-over-sockets state for mcp_client_v1, kept per socket. */
|
|
45743
|
+
mcpClientSockets = /* @__PURE__ */ new Map();
|
|
45744
|
+
noteMcpClientSocket(token, declared) {
|
|
45745
|
+
this.mcpClientSockets.set(token, declared);
|
|
45746
|
+
this.onCapabilityMaybeChanged();
|
|
45747
|
+
}
|
|
45748
|
+
clearMcpClientSocket(token) {
|
|
45749
|
+
if (this.mcpClientSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
45750
|
+
}
|
|
45751
|
+
/** Deletion-ack ruling §3: ws-server notes every runtime hello's mcp_deletion_ack_v1 declaration (harness: false). */
|
|
45752
|
+
noteMcpDeletionAckSocket(token, declared) {
|
|
45753
|
+
this.mcpDeletionAckSockets.set(token, declared);
|
|
45754
|
+
if (!declared) this.mcpTurnSeq.delete(token);
|
|
45755
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
45756
|
+
this.maybeReportMcpAck();
|
|
45757
|
+
}
|
|
45758
|
+
/** #1351 round 2 observation: with no ack-capable socket left (detached, or re-hello'd without the feature), the last
|
|
45759
|
+
* credentials value is no longer known: forget it, so a later socket does not inherit a stale value. */
|
|
45760
|
+
forgetCredentialsIfNoAckSocket() {
|
|
45761
|
+
if ([...this.mcpDeletionAckSockets.values()].some(Boolean)) return;
|
|
45762
|
+
this.mcpCredentialsPresentValue = null;
|
|
45763
|
+
this.mcpAckClosed = { ...this.mcpAckClosed, credentials: null };
|
|
45764
|
+
if (this.mcpAckOpen !== null) this.mcpAckOpen = { ...this.mcpAckOpen, credentials: null };
|
|
45765
|
+
}
|
|
45766
|
+
clearMcpDeletionAckSocket(token) {
|
|
45767
|
+
this.mcpTurnSeq.delete(token);
|
|
45768
|
+
if (!this.mcpDeletionAckSockets.delete(token)) return;
|
|
45769
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
45770
|
+
this.maybeReportMcpAck();
|
|
45771
|
+
}
|
|
45772
|
+
/** #1351 round 2 M-1: ws-server calls this when it sends an owner-DM turn to an ack-capable socket. Bounded. */
|
|
45773
|
+
noteMcpTurnDispatch(token, messageId) {
|
|
45774
|
+
let m = this.mcpTurnSeq.get(token);
|
|
45775
|
+
if (m === void 0) this.mcpTurnSeq.set(token, m = /* @__PURE__ */ new Map());
|
|
45776
|
+
m.set(messageId, this.policyWriteSeq);
|
|
45777
|
+
while (m.size > 64) m.delete(m.keys().next().value);
|
|
45778
|
+
}
|
|
45779
|
+
/** Ruling §3 / §5.3: the AND over attached sockets, false with none attached (as mcpClientCapable). */
|
|
45780
|
+
mcpDeletionAckCapable() {
|
|
45781
|
+
if (this.mcpDeletionAckSockets.size === 0) return false;
|
|
45782
|
+
for (const v of this.mcpDeletionAckSockets.values()) if (!v) return false;
|
|
45783
|
+
return true;
|
|
45784
|
+
}
|
|
45785
|
+
/**
|
|
45786
|
+
* Deletion-ack ruling (2026-10-31) §3 acceptance. ws-server has validated the frame shape and the socket (declared
|
|
45787
|
+
* mcp_deletion_ack_v1 and mcp_client_v1, no harness). `credentials_present` is taken as is. `applied_sha256` advances
|
|
45788
|
+
* `mcpDeletionsThrough` only when it is the SHA-256 of the bytes THIS bridge last wrote (JSON + newline) and still
|
|
45789
|
+
* holds in force, and that document's top state is one whose deletion set covers the signed removal; then to the
|
|
45790
|
+
* counter recorded at that write, never the live one. Anything else is ignored silently. Never throws.
|
|
45791
|
+
*/
|
|
45792
|
+
noteMcpDeletionAck(token, frame) {
|
|
45793
|
+
this.mcpCredentialsPresentValue = frame.credentials_present === true;
|
|
45794
|
+
const json2 = this.mcpPolicyWrittenJson;
|
|
45795
|
+
const doc = this.mcpPolicyWritten;
|
|
45796
|
+
const counter = this.mcpPolicyWrittenCounter;
|
|
45797
|
+
const turns = frame.message_id !== void 0 ? this.mcpTurnSeq.get(token) : void 0;
|
|
45798
|
+
const seqAtDispatch = frame.message_id !== void 0 ? turns?.get(frame.message_id) : void 0;
|
|
45799
|
+
if (frame.message_id !== void 0) turns?.delete(frame.message_id);
|
|
45800
|
+
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")) {
|
|
45801
|
+
this.mcpDeletionsThrough = Math.max(this.mcpDeletionsThrough, counter);
|
|
45802
|
+
}
|
|
45803
|
+
this.noteMcpAckChanged();
|
|
45804
|
+
}
|
|
45805
|
+
/** H-1: a change of the ack values goes into the open bucket; the report sees it only once the bucket has closed. */
|
|
45806
|
+
noteMcpAckChanged() {
|
|
45807
|
+
const now = this.toolStatusNow();
|
|
45808
|
+
this.promoteMcpAck(now);
|
|
45809
|
+
const live = { through: this.mcpDeletionsThrough, credentials: this.mcpCredentialsPresentValue };
|
|
45810
|
+
const base = this.mcpAckOpen ?? this.mcpAckClosed;
|
|
45811
|
+
if (base.through === live.through && base.credentials === live.credentials) return;
|
|
45812
|
+
this.mcpAckOpen = { ...live, end: bucketEndMs(bucketStartAt(now)) };
|
|
45813
|
+
this.scheduleMcpAckReport();
|
|
45814
|
+
}
|
|
45815
|
+
promoteMcpAck(now) {
|
|
45816
|
+
if (this.mcpAckOpen !== null && this.mcpAckOpen.end <= now) {
|
|
45817
|
+
this.mcpAckClosed = { through: this.mcpAckOpen.through, credentials: this.mcpAckOpen.credentials };
|
|
45818
|
+
this.mcpAckOpen = null;
|
|
45819
|
+
}
|
|
45820
|
+
}
|
|
45821
|
+
/** H-1: one timer, at the first bucket boundary strictly after now plus a jitter in [0, 60 s), as the tool-status sender. */
|
|
45822
|
+
scheduleMcpAckReport() {
|
|
45823
|
+
if (this.mcpAckTimer !== null || this.closingDown) return;
|
|
45824
|
+
const now = this.toolStatusNow();
|
|
45825
|
+
const boundary = (Math.floor(now / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
|
|
45826
|
+
const at = boundary + Math.floor(this.toolStatusRandom() * TOOL_STATUS_JITTER_MS);
|
|
45827
|
+
this.mcpAckTimer = this.toolStatusSetTimer(() => {
|
|
45828
|
+
this.mcpAckTimer = null;
|
|
45829
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
45830
|
+
if (this.lastReportedMcpAck !== this.mcpAckSignature() && this.bootConfigSettled && !this.closingDown) void this.refetchRuntimeConfig();
|
|
45831
|
+
if (this.mcpAckOpen !== null) this.scheduleMcpAckReport();
|
|
45832
|
+
}, Math.max(0, at - now));
|
|
45833
|
+
}
|
|
45834
|
+
/**
|
|
45835
|
+
* Ruling §5.3: the three keys. The two ack values come from the CLOSED bucket only (H-1), so no report, whatever
|
|
45836
|
+
* posts it, reveals an owner-DM turn before its 15-minute bucket has closed. `mcp_r3_hold` is not DM timing: live.
|
|
45837
|
+
*/
|
|
45838
|
+
reportedMcpAck() {
|
|
45839
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
45840
|
+
const ackCapable = this.mcpDeletionAckCapable();
|
|
45841
|
+
const anyAck = [...this.mcpDeletionAckSockets.values()].some(Boolean);
|
|
45842
|
+
const closed = this.mcpAckClosed;
|
|
45843
|
+
return {
|
|
45844
|
+
mcp_r3_hold: this.r3Hold(),
|
|
45845
|
+
...ackCapable ? { mcp_deletions_through: closed.through } : {},
|
|
45846
|
+
// `anyAck` is an equivalent mutant today (forgetCredentialsIfNoAckSocket nulls the value whenever no ack socket is
|
|
45847
|
+
// left), kept as defense in depth so the report never depends on that reset alone.
|
|
45848
|
+
...anyAck && closed.credentials !== null ? { mcp_credentials_present: closed.credentials } : {}
|
|
45849
|
+
};
|
|
45850
|
+
}
|
|
45851
|
+
mcpAckSignature() {
|
|
45852
|
+
return JSON.stringify(this.reportedMcpAck());
|
|
45853
|
+
}
|
|
45854
|
+
/**
|
|
45855
|
+
* A SOCKET change (attach, detach, a hello) can change which keys the report carries; their values are the closed
|
|
45856
|
+
* ones, so this is not owner-DM timing and may use the capability refetch. An ACK never comes here (H-1): it goes
|
|
45857
|
+
* through noteMcpAckChanged and the bucket timer.
|
|
45858
|
+
*/
|
|
45859
|
+
maybeReportMcpAck() {
|
|
45860
|
+
if (this.lastReportedMcpAck === this.mcpAckSignature()) return;
|
|
45861
|
+
this.scheduleCapabilityRefetch();
|
|
45862
|
+
}
|
|
45863
|
+
/** MCP Task 18: ws-server registers (or, with null, removes) a socket's loopback catalog lister. */
|
|
45864
|
+
noteMcpCatalogLister(token, lister) {
|
|
45865
|
+
if (lister === null) this.mcpCatalogListers.delete(token);
|
|
45866
|
+
else this.mcpCatalogListers.set(token, lister);
|
|
45867
|
+
}
|
|
45868
|
+
/**
|
|
45869
|
+
* MCP Task 19 (spec §12.3): "ok" when every attached runtime socket declared mcp_client_v1 (the AND, which harness
|
|
45870
|
+
* sockets hold false: a harness socket counts in the AND and never declares it); else "harness_consumer" when the
|
|
45871
|
+
* latest hello was a harness; else "missing".
|
|
45872
|
+
*/
|
|
45873
|
+
reportedMcpCapability() {
|
|
45874
|
+
if (this.mcpClientCapable()) return "ok";
|
|
45875
|
+
return this.lastConsumerCapability === "harness_consumer" ? "harness_consumer" : "missing";
|
|
45876
|
+
}
|
|
45877
|
+
/**
|
|
45878
|
+
* MCP Task 19 (spec §12.3): at most 8 `{server_id, vendor, state}` for the signed servers of the manifest in force,
|
|
45879
|
+
* from mcp-policy.json as WRITTEN. Only the states the bridge itself knows: top `kill_switch` and `not_entitled` give
|
|
45880
|
+
* that state per server; top `ok` gives `ok` for a server the bridge lists `ok` and `malformed` for one it refused.
|
|
45881
|
+
* Every other top state (no capability, suspended, no config, switch off, section absent, malformed section) gives no
|
|
45882
|
+
* row. The runtime-only states (needs_login, login_expired, definition_changed, server_version_unsupported,
|
|
45883
|
+
* server_unreachable) have no runtime-to-bridge report path in Phase 1. `vendor` is reported only when it is a key of
|
|
45884
|
+
* the bridge's compiled catalog; a signed vendor outside it gives no row. Never a tool name, URL or issuer.
|
|
45885
|
+
*/
|
|
45886
|
+
reportedMcpServers() {
|
|
45887
|
+
const doc = this.mcpPolicyWritten;
|
|
45888
|
+
const signed = (this.cfg.ownerGrantPin?.pin ?? null) !== null ? this.toolGrantManifest?.mcp?.servers : void 0;
|
|
45889
|
+
if (doc === null || signed === void 0) return [];
|
|
45890
|
+
const catalog = this.mcpCatalog ?? MCP_CATALOG;
|
|
45891
|
+
const out = [];
|
|
45892
|
+
for (const id of Object.keys(signed).sort()) {
|
|
45893
|
+
if (out.length >= 8) break;
|
|
45894
|
+
const vendor = signed[id].vendor;
|
|
45895
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, vendor)) continue;
|
|
45896
|
+
let state;
|
|
45897
|
+
if (doc.state === "kill_switch" || doc.state === "not_entitled") state = doc.state;
|
|
45898
|
+
else if (doc.state === "ok") state = Object.prototype.hasOwnProperty.call(doc.servers, id) && doc.servers[id].state === "ok" ? "ok" : "malformed";
|
|
45899
|
+
else continue;
|
|
45900
|
+
out.push({ server_id: id, vendor, state });
|
|
45901
|
+
}
|
|
45902
|
+
return out;
|
|
45903
|
+
}
|
|
45904
|
+
/**
|
|
45905
|
+
* MCP Task 16: every attached runtime socket declared mcp_client_v1; no socket attached means absent (false).
|
|
45906
|
+
* PR-B2 follow-up (documented): a HARNESS socket counts in this AND. ws-server notes every socket that said hello,
|
|
45907
|
+
* and a harness hello is always noted `false` (harnessClaimed never resets), so one attached harness connector holds
|
|
45908
|
+
* the capability false for files, the report and mcp-policy.json (top `no_capability`) while it is attached.
|
|
45909
|
+
*/
|
|
45910
|
+
mcpClientCapable() {
|
|
45911
|
+
if (this.mcpClientSockets.size === 0) return false;
|
|
45912
|
+
for (const v of this.mcpClientSockets.values()) if (!v) return false;
|
|
45913
|
+
return true;
|
|
45914
|
+
}
|
|
44307
45915
|
noteUtilityToolsSocket(token, declared) {
|
|
44308
45916
|
this.utilityToolsSockets.set(token, declared);
|
|
44309
45917
|
this.onCapabilityMaybeChanged();
|
|
@@ -44322,6 +45930,7 @@ ${channelId}`;
|
|
|
44322
45930
|
const out = /* @__PURE__ */ new Set();
|
|
44323
45931
|
if (this.searchProvidersCapable()) out.add("search_providers_v2");
|
|
44324
45932
|
if (this.utilityToolsCapable()) out.add("utility_tools_v1");
|
|
45933
|
+
if (this.mcpClientCapable()) out.add("mcp_client_v1");
|
|
44325
45934
|
return out;
|
|
44326
45935
|
}
|
|
44327
45936
|
/** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
|
|
@@ -44350,13 +45959,19 @@ ${channelId}`;
|
|
|
44350
45959
|
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
44351
45960
|
*/
|
|
44352
45961
|
onCapabilityMaybeChanged() {
|
|
44353
|
-
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable() };
|
|
45962
|
+
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable(), mcp: this.mcpClientCapable() };
|
|
44354
45963
|
const prev = this.capabilityState;
|
|
44355
|
-
if (prev !== null && prev.search === next.search && prev.utility === next.utility) return;
|
|
45964
|
+
if (prev !== null && prev.search === next.search && prev.utility === next.utility && prev.mcp === next.mcp) return;
|
|
44356
45965
|
this.capabilityState = next;
|
|
44357
45966
|
this.recomputeManifestPolicy(this.capabilityFeatures());
|
|
45967
|
+
this.recomputeMcpPolicy();
|
|
44358
45968
|
if (this.closingDown) return;
|
|
44359
45969
|
if (prev === null && !next.search && !next.utility) return;
|
|
45970
|
+
this.scheduleCapabilityRefetch();
|
|
45971
|
+
}
|
|
45972
|
+
/** Debounced, with a max wait: a burst that never pauses still refetches at most maxWait after its first change. */
|
|
45973
|
+
scheduleCapabilityRefetch() {
|
|
45974
|
+
if (this.closingDown) return;
|
|
44360
45975
|
const now = Date.now();
|
|
44361
45976
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
44362
45977
|
else this.searchCapabilityBurstStart = now;
|
|
@@ -44374,7 +45989,7 @@ ${channelId}`;
|
|
|
44374
45989
|
searchCapabilityRefetchNeeded() {
|
|
44375
45990
|
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
44376
45991
|
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
44377
|
-
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable();
|
|
45992
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable() || this.lastReportedMcpAck !== this.mcpAckSignature();
|
|
44378
45993
|
}
|
|
44379
45994
|
/**
|
|
44380
45995
|
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
@@ -44600,6 +46215,11 @@ ${channelId}`;
|
|
|
44600
46215
|
async closeOnce() {
|
|
44601
46216
|
this.closingDown = true;
|
|
44602
46217
|
this.statusGate.close();
|
|
46218
|
+
this.toolStatusSender.close();
|
|
46219
|
+
if (this.mcpAckTimer !== null) {
|
|
46220
|
+
this.toolStatusClearTimer(this.mcpAckTimer);
|
|
46221
|
+
this.mcpAckTimer = null;
|
|
46222
|
+
}
|
|
44603
46223
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
44604
46224
|
this.pauseRecheckTimers.clear();
|
|
44605
46225
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -44608,6 +46228,8 @@ ${channelId}`;
|
|
|
44608
46228
|
this.redeliveryTimer = null;
|
|
44609
46229
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
44610
46230
|
this.searchCapabilityRefetchTimer = null;
|
|
46231
|
+
if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
|
|
46232
|
+
this.mcpPolicyRetryTimer = null;
|
|
44611
46233
|
this.pingHandle?.close();
|
|
44612
46234
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
44613
46235
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
@@ -44962,63 +46584,6 @@ function wsErrorFrame(code, detail) {
|
|
|
44962
46584
|
return { type: "error", ...errorBody(code, detail) };
|
|
44963
46585
|
}
|
|
44964
46586
|
|
|
44965
|
-
// src/consumer-grants.ts
|
|
44966
|
-
var NO_GRANTS = Object.freeze([]);
|
|
44967
|
-
var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
|
|
44968
|
-
function withConsumerGrants(m, perToolConsumer) {
|
|
44969
|
-
if (perToolConsumer) return m;
|
|
44970
|
-
const a = m.authorization;
|
|
44971
|
-
if (!a) return m;
|
|
44972
|
-
const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
|
|
44973
|
-
const stripMemory = "memory_write" in a;
|
|
44974
|
-
if (!stripGrants && !stripMemory) return m;
|
|
44975
|
-
const { memory_write: _dropped, ...rest } = a;
|
|
44976
|
-
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
44977
|
-
}
|
|
44978
|
-
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
44979
|
-
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
44980
|
-
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
44981
|
-
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
44982
|
-
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
44983
|
-
team_room: Object.freeze(["current_time"])
|
|
44984
|
-
});
|
|
44985
|
-
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
44986
|
-
function withManifestCapability(m, manifestConsumer) {
|
|
44987
|
-
if (manifestConsumer) return m;
|
|
44988
|
-
const a = m.authorization;
|
|
44989
|
-
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
44990
|
-
const constants5 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
44991
|
-
const narrowed = a.tool_grants.flatMap((g) => {
|
|
44992
|
-
if (!isManifestGrant(g)) return [g];
|
|
44993
|
-
return constants5.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
44994
|
-
});
|
|
44995
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
44996
|
-
}
|
|
44997
|
-
function withHarnessGrants(m, withheld) {
|
|
44998
|
-
if (!withheld) return m;
|
|
44999
|
-
const a = m.authorization;
|
|
45000
|
-
if (!a || a.tool_grants.length === 0) return m;
|
|
45001
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
45002
|
-
}
|
|
45003
|
-
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
45004
|
-
if (consumerHasFeature) return m;
|
|
45005
|
-
const a = m.authorization;
|
|
45006
|
-
if (!a) return m;
|
|
45007
|
-
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
45008
|
-
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
45009
|
-
if (kept.length === a.tool_grants.length) return m;
|
|
45010
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
45011
|
-
}
|
|
45012
|
-
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
45013
|
-
function withUtilityCapability(m, consumerHasFeature) {
|
|
45014
|
-
if (consumerHasFeature) return m;
|
|
45015
|
-
const a = m.authorization;
|
|
45016
|
-
if (!a) return m;
|
|
45017
|
-
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
45018
|
-
if (kept.length === a.tool_grants.length) return m;
|
|
45019
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
45020
|
-
}
|
|
45021
|
-
|
|
45022
46587
|
// src/http-server.ts
|
|
45023
46588
|
function writeSse(res, msgs, onWriteError) {
|
|
45024
46589
|
for (let i = 0; i < msgs.length; i++) {
|
|
@@ -45041,7 +46606,8 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
45041
46606
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
45042
46607
|
const withheld = agent.harnessGrantsWithheld();
|
|
45043
46608
|
const provider = agent.signedSearchProvider();
|
|
45044
|
-
const
|
|
46609
|
+
const hold = agent.r3Hold();
|
|
46610
|
+
const left3 = writeSse(res, batch.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)), onWriteError);
|
|
45045
46611
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
45046
46612
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
45047
46613
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -45318,7 +46884,8 @@ function createHttpServer(agent, opts) {
|
|
|
45318
46884
|
const drained = await agent.drain(channel, wait);
|
|
45319
46885
|
const withheld = agent.harnessGrantsWithheld();
|
|
45320
46886
|
const provider = agent.signedSearchProvider();
|
|
45321
|
-
|
|
46887
|
+
const hold = agent.r3Hold();
|
|
46888
|
+
return json(res, 200, drained.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)));
|
|
45322
46889
|
}
|
|
45323
46890
|
if (method === "GET" && path === "/v1/events") {
|
|
45324
46891
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -45388,6 +46955,7 @@ function parseHarnessReport(raw) {
|
|
|
45388
46955
|
}
|
|
45389
46956
|
|
|
45390
46957
|
// src/ws-server.ts
|
|
46958
|
+
import { randomBytes as randomBytes12 } from "node:crypto";
|
|
45391
46959
|
import { WebSocketServer, WebSocket } from "ws";
|
|
45392
46960
|
|
|
45393
46961
|
// src/memory/consumer-notes.ts
|
|
@@ -45498,6 +47066,8 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45498
47066
|
manifestGrants: false,
|
|
45499
47067
|
searchProviders: false,
|
|
45500
47068
|
utilityTools: false,
|
|
47069
|
+
mcpClient: false,
|
|
47070
|
+
pendingCatalog: /* @__PURE__ */ new Map(),
|
|
45501
47071
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
45502
47072
|
deliveryOpen: helloWaitMs <= 0,
|
|
45503
47073
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -45551,14 +47121,14 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45551
47121
|
continue;
|
|
45552
47122
|
}
|
|
45553
47123
|
const out = withConsumerNotes(
|
|
45554
|
-
withUtilityCapability(withSearchCapability(
|
|
47124
|
+
withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(
|
|
45555
47125
|
withManifestCapability(
|
|
45556
47126
|
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
45557
47127
|
session.manifestGrants
|
|
45558
47128
|
),
|
|
45559
47129
|
session.searchProviders,
|
|
45560
47130
|
agent.signedSearchProvider()
|
|
45561
|
-
), session.utilityTools),
|
|
47131
|
+
), session.utilityTools), session.mcpClient), agent.r3Hold()),
|
|
45562
47132
|
batch[i],
|
|
45563
47133
|
session,
|
|
45564
47134
|
agent
|
|
@@ -45569,6 +47139,10 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45569
47139
|
try {
|
|
45570
47140
|
sock.send(JSON.stringify(frame));
|
|
45571
47141
|
inflight.set(batch[i].message_id, batch[i]);
|
|
47142
|
+
const a = frame.authorization;
|
|
47143
|
+
if (session.mcpDeletionAck === true && (a?.audience === "owner_dm" || a?.relay_speaker === "owner")) {
|
|
47144
|
+
agent.noteMcpTurnDispatch(session, batch[i].message_id);
|
|
47145
|
+
}
|
|
45572
47146
|
session.deliveryToken.set(batch[i].message_id, ++session.deliverySeq);
|
|
45573
47147
|
session.shownRecall.set(batch[i].message_id, { token: session.deliverySeq, ids: recallOut.shown });
|
|
45574
47148
|
session.shownUnified.set(batch[i].message_id, { token: session.deliverySeq, ids: unifiedOut.shown });
|
|
@@ -45616,6 +47190,11 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45616
47190
|
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
45617
47191
|
agent.clearSearchProvidersSocket(session);
|
|
45618
47192
|
agent.clearUtilityToolsSocket(session);
|
|
47193
|
+
agent.clearMcpClientSocket(session);
|
|
47194
|
+
agent.clearMcpDeletionAckSocket(session);
|
|
47195
|
+
agent.noteMcpCatalogLister(session, null);
|
|
47196
|
+
for (const settle of session.pendingCatalog.values()) settle(null);
|
|
47197
|
+
session.pendingCatalog.clear();
|
|
45619
47198
|
};
|
|
45620
47199
|
current = { sock, cleanup, sweepStale };
|
|
45621
47200
|
sock.on("close", cleanup);
|
|
@@ -45649,6 +47228,32 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45649
47228
|
}
|
|
45650
47229
|
};
|
|
45651
47230
|
}
|
|
47231
|
+
var MCP_CATALOG_PENDING_MAX = 8;
|
|
47232
|
+
function mcpCatalogLister(sock, session) {
|
|
47233
|
+
return (serverId, timeoutMs) => new Promise((resolve2) => {
|
|
47234
|
+
if (session.closed || sock.readyState !== WebSocket.OPEN || session.pendingCatalog.size >= MCP_CATALOG_PENDING_MAX) {
|
|
47235
|
+
resolve2(null);
|
|
47236
|
+
return;
|
|
47237
|
+
}
|
|
47238
|
+
const requestId = randomBytes12(16).toString("hex");
|
|
47239
|
+
const timer = setTimeout(() => {
|
|
47240
|
+
session.pendingCatalog.delete(requestId);
|
|
47241
|
+
resolve2(null);
|
|
47242
|
+
}, timeoutMs);
|
|
47243
|
+
timer.unref?.();
|
|
47244
|
+
session.pendingCatalog.set(requestId, (frame) => {
|
|
47245
|
+
clearTimeout(timer);
|
|
47246
|
+
resolve2(frame);
|
|
47247
|
+
});
|
|
47248
|
+
try {
|
|
47249
|
+
sock.send(JSON.stringify({ type: "mcp_catalog_list", request_id: requestId, server_id: serverId }));
|
|
47250
|
+
} catch {
|
|
47251
|
+
session.pendingCatalog.delete(requestId);
|
|
47252
|
+
clearTimeout(timer);
|
|
47253
|
+
resolve2(null);
|
|
47254
|
+
}
|
|
47255
|
+
});
|
|
47256
|
+
}
|
|
45652
47257
|
function sendJson(sock, body) {
|
|
45653
47258
|
if (sock.readyState !== WebSocket.OPEN) return;
|
|
45654
47259
|
try {
|
|
@@ -45722,6 +47327,11 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45722
47327
|
agent.noteSearchProvidersSocket(session, session.searchProviders);
|
|
45723
47328
|
session.utilityTools = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(UTILITY_TOOLS_FEATURE);
|
|
45724
47329
|
agent.noteUtilityToolsSocket(session, session.utilityTools);
|
|
47330
|
+
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);
|
|
47331
|
+
agent.noteMcpClientSocket(session, session.mcpClient);
|
|
47332
|
+
agent.noteMcpCatalogLister(session, session.mcpClient ? mcpCatalogLister(sock, session) : null);
|
|
47333
|
+
session.mcpDeletionAck = session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MCP_DELETION_ACK_FEATURE);
|
|
47334
|
+
agent.noteMcpDeletionAckSocket(session, session.mcpDeletionAck);
|
|
45725
47335
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
45726
47336
|
agent.noteNotesConsumer();
|
|
45727
47337
|
}
|
|
@@ -45729,13 +47339,21 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45729
47339
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
45730
47340
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
45731
47341
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
45732
|
-
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] });
|
|
47342
|
+
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] });
|
|
45733
47343
|
if (!session.deliveryOpen) {
|
|
45734
47344
|
session.deliveryOpen = true;
|
|
45735
47345
|
flush();
|
|
45736
47346
|
}
|
|
45737
47347
|
return;
|
|
45738
47348
|
}
|
|
47349
|
+
if (m.type === "mcp_catalog_list_result") {
|
|
47350
|
+
const id = m.request_id;
|
|
47351
|
+
const settle = typeof id === "string" ? session.pendingCatalog.get(id) : void 0;
|
|
47352
|
+
if (settle === void 0) return;
|
|
47353
|
+
session.pendingCatalog.delete(id);
|
|
47354
|
+
settle(m);
|
|
47355
|
+
return;
|
|
47356
|
+
}
|
|
45739
47357
|
if (m.type === "nack") {
|
|
45740
47358
|
if (typeof m.message_id !== "string" || !m.message_id) {
|
|
45741
47359
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
@@ -45952,6 +47570,20 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45952
47570
|
}
|
|
45953
47571
|
return;
|
|
45954
47572
|
}
|
|
47573
|
+
if (m.type === "mcp_deletion_ack") {
|
|
47574
|
+
const keys = Object.keys(m).sort().join(",");
|
|
47575
|
+
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));
|
|
47576
|
+
if (!valid) {
|
|
47577
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
47578
|
+
return;
|
|
47579
|
+
}
|
|
47580
|
+
agent.noteMcpDeletionAck(session, {
|
|
47581
|
+
credentials_present: m.credentials_present,
|
|
47582
|
+
...typeof m.message_id === "string" ? { message_id: m.message_id } : {},
|
|
47583
|
+
...typeof m.applied_sha256 === "string" ? { applied_sha256: m.applied_sha256 } : {}
|
|
47584
|
+
});
|
|
47585
|
+
return;
|
|
47586
|
+
}
|
|
45955
47587
|
if (m.type === "room_cap_reached") {
|
|
45956
47588
|
if (!session.manifestGrants || typeof m.channel_id !== "string" || !LOWER_UUID.test(m.channel_id)) {
|
|
45957
47589
|
sendJson(sock, wsErrorFrame("invalid_request"));
|