@securecoms/agent-bridge 0.34.0 → 0.36.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 +48 -3
- package/dist/attachments.d.ts +13 -0
- package/dist/bin.js +1974 -389
- package/dist/bridge-agent.d.ts +238 -9
- package/dist/consumer-grants.d.ts +28 -0
- package/dist/errors.d.ts +1 -1
- 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 +75 -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 +1910 -327
- package/dist/mcp-catalog-message.d.ts +79 -0
- package/dist/mcp-catalog-rate.d.ts +35 -0
- package/dist/outbox.d.ts +42 -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];
|
|
@@ -24728,8 +24755,8 @@ function openDeliveryPings(o) {
|
|
|
24728
24755
|
return Promise.resolve({ value: void 0, done: true });
|
|
24729
24756
|
if (queue.length)
|
|
24730
24757
|
return Promise.resolve({ value: queue.shift(), done: false });
|
|
24731
|
-
return new Promise((
|
|
24732
|
-
waiter =
|
|
24758
|
+
return new Promise((resolve2) => {
|
|
24759
|
+
waiter = resolve2;
|
|
24733
24760
|
});
|
|
24734
24761
|
}
|
|
24735
24762
|
};
|
|
@@ -25049,8 +25076,8 @@ var PingSubscriber = class {
|
|
|
25049
25076
|
return Promise.resolve({ value: void 0, done: true });
|
|
25050
25077
|
if (this.queue.length)
|
|
25051
25078
|
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
25052
|
-
return new Promise((
|
|
25053
|
-
this.waiter =
|
|
25079
|
+
return new Promise((resolve2) => {
|
|
25080
|
+
this.waiter = resolve2;
|
|
25054
25081
|
});
|
|
25055
25082
|
}
|
|
25056
25083
|
};
|
|
@@ -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
|
}
|
|
@@ -29664,8 +29748,9 @@ function decodeReplyBody(bytes) {
|
|
|
29664
29748
|
if (b?.kind === "a2a" && b.a2a && typeof b.a2a === "object") return b;
|
|
29665
29749
|
throw new Error("reply intent body is malformed");
|
|
29666
29750
|
}
|
|
29667
|
-
function replyBodyMac(key, replyId, channelId, body, credit) {
|
|
29668
|
-
|
|
29751
|
+
function replyBodyMac(key, replyId, channelId, body, credit, mcpTurn) {
|
|
29752
|
+
const label = mcpTurn === true ? ["reply-body", replyId, channelId, credit ?? null, "mcp_turn"] : credit === void 0 ? ["reply-body", replyId, channelId] : ["reply-body", replyId, channelId, credit];
|
|
29753
|
+
return createHmac("sha256", key).update(JSON.stringify(label)).update("\0").update(body).digest("hex");
|
|
29669
29754
|
}
|
|
29670
29755
|
function partOrdinal(part) {
|
|
29671
29756
|
const m = /^p:(\d+)$/.exec(part);
|
|
@@ -29706,8 +29791,96 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
29706
29791
|
}
|
|
29707
29792
|
|
|
29708
29793
|
// src/bridge-agent.ts
|
|
29709
|
-
import { existsSync as existsSync5, promises as
|
|
29710
|
-
|
|
29794
|
+
import { existsSync as existsSync5, promises as fsp2, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
|
|
29795
|
+
|
|
29796
|
+
// src/outbox.ts
|
|
29797
|
+
import { constants, mkdirSync, lstatSync, chmodSync, realpathSync, promises as fsp } from "node:fs";
|
|
29798
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
29799
|
+
var VAULT_OUTBOX_DIRNAME = "vault-outbox";
|
|
29800
|
+
var PathOutsideOutboxError = class extends Error {
|
|
29801
|
+
constructor() {
|
|
29802
|
+
super("path_outside_outbox");
|
|
29803
|
+
this.name = "PathOutsideOutboxError";
|
|
29804
|
+
}
|
|
29805
|
+
};
|
|
29806
|
+
var POSIX = process.platform !== "win32";
|
|
29807
|
+
function ownedPrivateDir(st) {
|
|
29808
|
+
if (st.isSymbolicLink() || !st.isDirectory()) return false;
|
|
29809
|
+
if (!POSIX) return true;
|
|
29810
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
29811
|
+
if (uid !== void 0 && st.uid !== uid) return false;
|
|
29812
|
+
return (st.mode & 511) === 448;
|
|
29813
|
+
}
|
|
29814
|
+
function ensureOutbox(dir) {
|
|
29815
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
29816
|
+
let st = lstatSync(dir);
|
|
29817
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new PathOutsideOutboxError();
|
|
29818
|
+
const uid = POSIX && typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
29819
|
+
if (uid !== void 0 && st.uid !== uid) throw new PathOutsideOutboxError();
|
|
29820
|
+
if (POSIX && (st.mode & 511) !== 448) {
|
|
29821
|
+
chmodSync(dir, 448);
|
|
29822
|
+
st = lstatSync(dir);
|
|
29823
|
+
}
|
|
29824
|
+
if (!ownedPrivateDir(st)) throw new PathOutsideOutboxError();
|
|
29825
|
+
return { dir, real: realpathSync(dir), dev: st.dev, ino: st.ino };
|
|
29826
|
+
}
|
|
29827
|
+
var NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
29828
|
+
var NONBLOCK = constants.O_NONBLOCK ?? 0;
|
|
29829
|
+
async function readOutboxFile(pin, callerPath) {
|
|
29830
|
+
if (!pin) throw new PathOutsideOutboxError();
|
|
29831
|
+
try {
|
|
29832
|
+
const st = await fsp.lstat(pin.dir);
|
|
29833
|
+
if (!ownedPrivateDir(st) || st.dev !== pin.dev || st.ino !== pin.ino) throw new PathOutsideOutboxError();
|
|
29834
|
+
} catch {
|
|
29835
|
+
throw new PathOutsideOutboxError();
|
|
29836
|
+
}
|
|
29837
|
+
const requested = resolve(pin.dir, callerPath);
|
|
29838
|
+
const name = basename(requested);
|
|
29839
|
+
let realParent;
|
|
29840
|
+
try {
|
|
29841
|
+
realParent = await fsp.realpath(dirname(requested));
|
|
29842
|
+
} catch {
|
|
29843
|
+
throw new PathOutsideOutboxError();
|
|
29844
|
+
}
|
|
29845
|
+
if (realParent !== pin.real) throw new PathOutsideOutboxError();
|
|
29846
|
+
const target = join(pin.real, name);
|
|
29847
|
+
const pre = await fsp.lstat(target);
|
|
29848
|
+
if (pre.isSymbolicLink() || !pre.isFile()) throw new PathOutsideOutboxError();
|
|
29849
|
+
let handle;
|
|
29850
|
+
try {
|
|
29851
|
+
handle = await fsp.open(target, constants.O_RDONLY | NOFOLLOW | NONBLOCK);
|
|
29852
|
+
} catch (e) {
|
|
29853
|
+
if (e.code === "ENOENT") throw e;
|
|
29854
|
+
throw new PathOutsideOutboxError();
|
|
29855
|
+
}
|
|
29856
|
+
try {
|
|
29857
|
+
const st = await handle.stat();
|
|
29858
|
+
if (!st.isFile() || st.dev !== pre.dev || st.ino !== pre.ino || st.nlink !== 1) {
|
|
29859
|
+
throw new PathOutsideOutboxError();
|
|
29860
|
+
}
|
|
29861
|
+
return { bytes: new Uint8Array(await handle.readFile()), path: target, dev: st.dev, ino: st.ino };
|
|
29862
|
+
} finally {
|
|
29863
|
+
await handle.close().catch(() => {
|
|
29864
|
+
});
|
|
29865
|
+
}
|
|
29866
|
+
}
|
|
29867
|
+
async function consumeOutboxFile(pin, read) {
|
|
29868
|
+
try {
|
|
29869
|
+
const dir = await fsp.lstat(pin.dir);
|
|
29870
|
+
if (!ownedPrivateDir(dir) || dir.dev !== pin.dev || dir.ino !== pin.ino) return false;
|
|
29871
|
+
if (await fsp.realpath(pin.dir) !== pin.real) return false;
|
|
29872
|
+
const target = join(pin.real, basename(read.path));
|
|
29873
|
+
const st = await fsp.lstat(target);
|
|
29874
|
+
if (st.isSymbolicLink() || st.dev !== read.dev || st.ino !== read.ino) return false;
|
|
29875
|
+
await fsp.unlink(target);
|
|
29876
|
+
return true;
|
|
29877
|
+
} catch {
|
|
29878
|
+
return false;
|
|
29879
|
+
}
|
|
29880
|
+
}
|
|
29881
|
+
|
|
29882
|
+
// src/bridge-agent.ts
|
|
29883
|
+
import { join as join18, dirname as dirname6 } from "node:path";
|
|
29711
29884
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29712
29885
|
|
|
29713
29886
|
// src/latency.ts
|
|
@@ -30205,11 +30378,21 @@ function pinCoverage(pinStripped, ownerMessage, coverTexts) {
|
|
|
30205
30378
|
}
|
|
30206
30379
|
|
|
30207
30380
|
// src/attachments.ts
|
|
30208
|
-
import { basename } from "node:path";
|
|
30381
|
+
import { basename as basename2 } from "node:path";
|
|
30209
30382
|
var VAULT_REF_MAX_SIZE_HINT = 100 * 1024 * 1024 - 16;
|
|
30210
30383
|
var VAULT_REF_MAX_NAME = 255;
|
|
30211
30384
|
var VAULT_REF_MAX_MIME = 127;
|
|
30212
30385
|
var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
30386
|
+
var InvalidBlobIdError = class extends Error {
|
|
30387
|
+
constructor() {
|
|
30388
|
+
super("invalid_blob_id");
|
|
30389
|
+
this.name = "InvalidBlobIdError";
|
|
30390
|
+
}
|
|
30391
|
+
};
|
|
30392
|
+
function assertBlobId(blobId) {
|
|
30393
|
+
if (typeof blobId !== "string" || !CANONICAL_UUID.test(blobId)) throw new InvalidBlobIdError();
|
|
30394
|
+
return blobId;
|
|
30395
|
+
}
|
|
30213
30396
|
var VAULT_PIN = /^blake2b:[0-9a-f]{64}$/;
|
|
30214
30397
|
var HINT_FORBIDDEN = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
|
|
30215
30398
|
var MIME_TYPE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
|
|
@@ -30248,7 +30431,7 @@ function toVaultRefEntry(body) {
|
|
|
30248
30431
|
return entry;
|
|
30249
30432
|
}
|
|
30250
30433
|
function sanitizeFilename(name, fallback) {
|
|
30251
|
-
const base =
|
|
30434
|
+
const base = basename2(name ?? "");
|
|
30252
30435
|
const cleaned = base.replace(/[/\\]/g, "").replace(/^\.+$/, "");
|
|
30253
30436
|
return cleaned.length > 0 ? cleaned : fallback;
|
|
30254
30437
|
}
|
|
@@ -30278,7 +30461,7 @@ function mimeForName(name) {
|
|
|
30278
30461
|
async function prepareAttachment(filePath2, caption, channelId, deps) {
|
|
30279
30462
|
await deps.ready();
|
|
30280
30463
|
const bytes = await deps.readFile(filePath2);
|
|
30281
|
-
const name =
|
|
30464
|
+
const name = basename2(filePath2);
|
|
30282
30465
|
const { encryptedData, fileKey, fileNonce, digest } = await deps.encryptFile(bytes);
|
|
30283
30466
|
const blobId = await deps.uploadBlob(encryptedData, channelId);
|
|
30284
30467
|
const body = {
|
|
@@ -30296,8 +30479,9 @@ async function prepareAttachment(filePath2, caption, channelId, deps) {
|
|
|
30296
30479
|
return body;
|
|
30297
30480
|
}
|
|
30298
30481
|
async function materializeAttachment(body, ctx, deps) {
|
|
30482
|
+
const blobId = assertBlobId(body.blobId);
|
|
30299
30483
|
await deps.ready();
|
|
30300
|
-
const ciphertext = await deps.downloadBlob(
|
|
30484
|
+
const ciphertext = await deps.downloadBlob(blobId);
|
|
30301
30485
|
const sha25612 = await deps.computeFileDigest(ciphertext);
|
|
30302
30486
|
if (sha25612 !== body.digest) {
|
|
30303
30487
|
throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
|
|
@@ -30323,13 +30507,13 @@ function ensureSodiumReady2() {
|
|
|
30323
30507
|
// src/memory/hermes-session-map.ts
|
|
30324
30508
|
import { readFileSync } from "node:fs";
|
|
30325
30509
|
import { homedir } from "node:os";
|
|
30326
|
-
import { join } from "node:path";
|
|
30510
|
+
import { join as join2 } from "node:path";
|
|
30327
30511
|
function expandHome(p) {
|
|
30328
|
-
return p === "~" || p.startsWith("~/") ?
|
|
30512
|
+
return p === "~" || p.startsWith("~/") ? join2(homedir(), p.slice(1)) : p;
|
|
30329
30513
|
}
|
|
30330
30514
|
function resolveHermesSession(baseDir, conversationId) {
|
|
30331
30515
|
try {
|
|
30332
|
-
const path =
|
|
30516
|
+
const path = join2(expandHome(baseDir), "sessions", "sessions.json");
|
|
30333
30517
|
const map = JSON.parse(readFileSync(path, "utf8"));
|
|
30334
30518
|
const entry = map[`agent:main:securecoms:dm:${conversationId}`];
|
|
30335
30519
|
const sid = entry?.session_id;
|
|
@@ -30341,11 +30525,11 @@ function resolveHermesSession(baseDir, conversationId) {
|
|
|
30341
30525
|
|
|
30342
30526
|
// src/memory/hermes-store.ts
|
|
30343
30527
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
30344
|
-
import { join as
|
|
30528
|
+
import { join as join3 } from "node:path";
|
|
30345
30529
|
var HermesStore = class {
|
|
30346
30530
|
dbPath;
|
|
30347
30531
|
constructor(baseDir) {
|
|
30348
|
-
this.dbPath =
|
|
30532
|
+
this.dbPath = join3(expandHome(baseDir), "state.db");
|
|
30349
30533
|
}
|
|
30350
30534
|
/** Recent-K conversational turns for a session, chronological. Fail-safe → []. */
|
|
30351
30535
|
readRecent(sessionId, limit) {
|
|
@@ -30487,10 +30671,10 @@ var HermesMemoryProvider = class {
|
|
|
30487
30671
|
|
|
30488
30672
|
// src/memory/openclaw-session-map.ts
|
|
30489
30673
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
30490
|
-
import { join as
|
|
30674
|
+
import { join as join4 } from "node:path";
|
|
30491
30675
|
function resolveOwnerDmSession(baseDir, agent) {
|
|
30492
30676
|
try {
|
|
30493
|
-
const path =
|
|
30677
|
+
const path = join4(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
|
|
30494
30678
|
const map = JSON.parse(readFileSync2(path, "utf8"));
|
|
30495
30679
|
let best = null;
|
|
30496
30680
|
for (const [key, entry] of Object.entries(map)) {
|
|
@@ -30508,11 +30692,11 @@ function resolveOwnerDmSession(baseDir, agent) {
|
|
|
30508
30692
|
|
|
30509
30693
|
// src/memory/openclaw-session-log.ts
|
|
30510
30694
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
30511
|
-
import { join as
|
|
30695
|
+
import { join as join5 } from "node:path";
|
|
30512
30696
|
function readRecentTurns(baseDir, agent, sessionId, k) {
|
|
30513
30697
|
let raw;
|
|
30514
30698
|
try {
|
|
30515
|
-
raw = readFileSync3(
|
|
30699
|
+
raw = readFileSync3(join5(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
|
|
30516
30700
|
} catch {
|
|
30517
30701
|
return [];
|
|
30518
30702
|
}
|
|
@@ -30543,7 +30727,7 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
|
|
|
30543
30727
|
|
|
30544
30728
|
// src/memory/openclaw-workspace-facts.ts
|
|
30545
30729
|
import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
30546
|
-
import { join as
|
|
30730
|
+
import { join as join6 } from "node:path";
|
|
30547
30731
|
var DEFAULT_FACTS_BUDGET = 8e3;
|
|
30548
30732
|
var MEMORY_MD_CAP = 6e3;
|
|
30549
30733
|
var MIN_FRAGMENT = 200;
|
|
@@ -30554,7 +30738,7 @@ function resolveWorkspaceDir(baseDir, agent, override) {
|
|
|
30554
30738
|
const base = expandHome(baseDir);
|
|
30555
30739
|
const isMain = agent === "" || agent === "main";
|
|
30556
30740
|
try {
|
|
30557
|
-
const cfg = JSON.parse(readFileSync4(
|
|
30741
|
+
const cfg = JSON.parse(readFileSync4(join6(base, "openclaw.json"), "utf8"));
|
|
30558
30742
|
const agents = cfg?.agents;
|
|
30559
30743
|
if (agents && typeof agents === "object") {
|
|
30560
30744
|
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
@@ -30567,7 +30751,7 @@ function resolveWorkspaceDir(baseDir, agent, override) {
|
|
|
30567
30751
|
}
|
|
30568
30752
|
} catch {
|
|
30569
30753
|
}
|
|
30570
|
-
return isMain ?
|
|
30754
|
+
return isMain ? join6(base, "workspace") : join6(base, `workspace-${agent}`);
|
|
30571
30755
|
}
|
|
30572
30756
|
var TRUNC = "\u2026[truncated]";
|
|
30573
30757
|
function headSlice(s, n) {
|
|
@@ -30597,7 +30781,7 @@ function readWorkspaceFacts(baseDir, agent, opts) {
|
|
|
30597
30781
|
let truncated = false;
|
|
30598
30782
|
let remaining = budget;
|
|
30599
30783
|
try {
|
|
30600
|
-
const r = readBounded(
|
|
30784
|
+
const r = readBounded(join6(dir, "MEMORY.md"));
|
|
30601
30785
|
if (r.oversize) truncated = true;
|
|
30602
30786
|
else if (r.text && r.text.trim()) {
|
|
30603
30787
|
const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
|
|
@@ -30610,7 +30794,7 @@ ${TRUNC}` : ""}`);
|
|
|
30610
30794
|
} catch {
|
|
30611
30795
|
}
|
|
30612
30796
|
try {
|
|
30613
|
-
const dailies = readdirSync(
|
|
30797
|
+
const dailies = readdirSync(join6(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
|
|
30614
30798
|
for (const f of dailies) {
|
|
30615
30799
|
if (remaining < MIN_FRAGMENT) {
|
|
30616
30800
|
truncated = true;
|
|
@@ -30618,7 +30802,7 @@ ${TRUNC}` : ""}`);
|
|
|
30618
30802
|
}
|
|
30619
30803
|
let r;
|
|
30620
30804
|
try {
|
|
30621
|
-
r = readBounded(
|
|
30805
|
+
r = readBounded(join6(dir, "memory", f));
|
|
30622
30806
|
} catch {
|
|
30623
30807
|
continue;
|
|
30624
30808
|
}
|
|
@@ -31281,8 +31465,8 @@ var MembershipPurgeSweeper = class {
|
|
|
31281
31465
|
const dms = /* @__PURE__ */ new Set();
|
|
31282
31466
|
for (const c of this.d.conversations()) {
|
|
31283
31467
|
if (stopping()) return { purged: [], held: 0, absent: 0 };
|
|
31284
|
-
const
|
|
31285
|
-
const channels =
|
|
31468
|
+
const own2 = this.d.enclaveOf(c);
|
|
31469
|
+
const channels = own2 === void 0 ? void 0 : listed.get(own2);
|
|
31286
31470
|
if (channels === void 0) continue;
|
|
31287
31471
|
if (channels.has(c)) {
|
|
31288
31472
|
this.d.clearAbsence(c);
|
|
@@ -31357,21 +31541,21 @@ function parsePayloadMemory(raw) {
|
|
|
31357
31541
|
|
|
31358
31542
|
// src/memory/embedder/client.ts
|
|
31359
31543
|
import { connect } from "node:net";
|
|
31360
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
31544
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync6, statSync as statSync2 } from "node:fs";
|
|
31361
31545
|
import { fileURLToPath } from "node:url";
|
|
31362
31546
|
|
|
31363
31547
|
// src/memory/embedder/download.ts
|
|
31364
31548
|
import { createHash as createHash2, randomBytes as randomBytes6 } from "node:crypto";
|
|
31365
|
-
import { chmodSync as
|
|
31549
|
+
import { chmodSync as chmodSync3, rmSync as rmSync2 } from "node:fs";
|
|
31366
31550
|
import { open } from "node:fs/promises";
|
|
31367
|
-
import { join as
|
|
31551
|
+
import { join as join8 } from "node:path";
|
|
31368
31552
|
|
|
31369
31553
|
// src/memory/embedder/layout.ts
|
|
31370
|
-
import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
31554
|
+
import { chmodSync as chmodSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, renameSync, rmSync } from "node:fs";
|
|
31371
31555
|
import { homedir as homedir2 } from "node:os";
|
|
31372
|
-
import { basename as
|
|
31373
|
-
var defaultModelsRoot = (home = homedir2()) =>
|
|
31374
|
-
var filePath = (root3, modelId, f) =>
|
|
31556
|
+
import { basename as basename3, dirname as dirname2, join as join7 } from "node:path";
|
|
31557
|
+
var defaultModelsRoot = (home = homedir2()) => join7(home, ".securecoms", "models", "embeddings");
|
|
31558
|
+
var filePath = (root3, modelId, f) => join7(root3, modelId, f.sha256, f.name);
|
|
31375
31559
|
var LayoutRefused = class extends Error {
|
|
31376
31560
|
constructor() {
|
|
31377
31561
|
super("the model directory is not a plain directory");
|
|
@@ -31379,32 +31563,32 @@ var LayoutRefused = class extends Error {
|
|
|
31379
31563
|
};
|
|
31380
31564
|
function kind(p) {
|
|
31381
31565
|
try {
|
|
31382
|
-
return
|
|
31566
|
+
return lstatSync2(p).isDirectory() ? "dir" : "other";
|
|
31383
31567
|
} catch (e) {
|
|
31384
31568
|
if (e.code === "ENOENT") return "absent";
|
|
31385
31569
|
throw e;
|
|
31386
31570
|
}
|
|
31387
31571
|
}
|
|
31388
31572
|
function plainDirChain(root3, parts, opts) {
|
|
31389
|
-
const modelsDir =
|
|
31573
|
+
const modelsDir = dirname2(root3);
|
|
31390
31574
|
for (const part of parts) if (!part || part === "." || part === ".." || part.includes("/") || part.includes("\\")) throw new LayoutRefused();
|
|
31391
31575
|
const k = kind(modelsDir);
|
|
31392
31576
|
if (k === "other") throw new LayoutRefused();
|
|
31393
31577
|
if (k === "absent") {
|
|
31394
31578
|
if (!opts.create) return null;
|
|
31395
|
-
|
|
31579
|
+
mkdirSync2(modelsDir, { recursive: true, mode: 448 });
|
|
31396
31580
|
if (kind(modelsDir) !== "dir") throw new LayoutRefused();
|
|
31397
31581
|
}
|
|
31398
|
-
const chain = opts.fromModelsDir ? parts : [
|
|
31582
|
+
const chain = opts.fromModelsDir ? parts : [basename3(root3), ...parts];
|
|
31399
31583
|
let p = modelsDir;
|
|
31400
31584
|
for (let i = 0; i < chain.length; i++) {
|
|
31401
|
-
p =
|
|
31585
|
+
p = join7(p, chain[i]);
|
|
31402
31586
|
const c = kind(p);
|
|
31403
31587
|
if (c === "other") throw new LayoutRefused();
|
|
31404
31588
|
if (c === "absent") {
|
|
31405
31589
|
if (!opts.create) return null;
|
|
31406
31590
|
try {
|
|
31407
|
-
|
|
31591
|
+
mkdirSync2(p, { mode: i === chain.length - 1 && opts.mode !== void 0 ? opts.mode : 493 });
|
|
31408
31592
|
} catch (e) {
|
|
31409
31593
|
if (e.code !== "EEXIST") throw e;
|
|
31410
31594
|
}
|
|
@@ -31415,7 +31599,7 @@ function plainDirChain(root3, parts, opts) {
|
|
|
31415
31599
|
}
|
|
31416
31600
|
function presentWithSize(p, size) {
|
|
31417
31601
|
try {
|
|
31418
|
-
const s =
|
|
31602
|
+
const s = lstatSync2(p);
|
|
31419
31603
|
return s.isFile() && s.size === size;
|
|
31420
31604
|
} catch {
|
|
31421
31605
|
return false;
|
|
@@ -31423,7 +31607,7 @@ function presentWithSize(p, size) {
|
|
|
31423
31607
|
}
|
|
31424
31608
|
function wrongTypeAt(p) {
|
|
31425
31609
|
try {
|
|
31426
|
-
const s =
|
|
31610
|
+
const s = lstatSync2(p);
|
|
31427
31611
|
return !s.isFile() && !s.isSymbolicLink();
|
|
31428
31612
|
} catch {
|
|
31429
31613
|
return false;
|
|
@@ -31436,16 +31620,16 @@ function installVerified(root3, modelId, f, partial, opts) {
|
|
|
31436
31620
|
dir = plainDirChain(root3, [modelId, f.sha256], { create: true });
|
|
31437
31621
|
if (!dir) throw new LayoutRefused();
|
|
31438
31622
|
if (!opts.replace && presentWithSize(dest, f.size)) return;
|
|
31439
|
-
|
|
31623
|
+
chmodSync2(dir, 493);
|
|
31440
31624
|
try {
|
|
31441
31625
|
renameSync(partial, dest);
|
|
31442
31626
|
} catch (e) {
|
|
31443
31627
|
if (!presentWithSize(dest, f.size)) throw e;
|
|
31444
31628
|
return;
|
|
31445
31629
|
}
|
|
31446
|
-
|
|
31630
|
+
chmodSync2(dest, 292);
|
|
31447
31631
|
} finally {
|
|
31448
|
-
if (dir)
|
|
31632
|
+
if (dir) chmodSync2(dir, 365);
|
|
31449
31633
|
rmSync(partial, { force: true });
|
|
31450
31634
|
}
|
|
31451
31635
|
}
|
|
@@ -31465,8 +31649,8 @@ function removeModelFiles(root3, m) {
|
|
|
31465
31649
|
for (const f of m.files) {
|
|
31466
31650
|
const dir = plainDirChain(root3, [m.modelId, f.sha256], { create: false });
|
|
31467
31651
|
if (!dir) continue;
|
|
31468
|
-
|
|
31469
|
-
rmSync2(
|
|
31652
|
+
chmodSync3(dir, 493);
|
|
31653
|
+
rmSync2(join8(dir, f.name), { force: true });
|
|
31470
31654
|
}
|
|
31471
31655
|
}
|
|
31472
31656
|
var VerifyFailed = class extends Error {
|
|
@@ -31513,7 +31697,7 @@ async function fetchSameOrigin(url, origin, fetchImpl, signal, stallMs, abort) {
|
|
|
31513
31697
|
async function downloadOne(f, o) {
|
|
31514
31698
|
if (wrongTypeAt(filePath(o.root, o.modelId, f))) throw new WrongType("wrong type at model path");
|
|
31515
31699
|
const pdir = plainDirChain(o.root, [".partial"], { create: true, fromModelsDir: true, mode: 448 });
|
|
31516
|
-
const partial =
|
|
31700
|
+
const partial = join8(pdir, randomBytes6(12).toString("hex"));
|
|
31517
31701
|
const fh = await open(partial, "wx", 384);
|
|
31518
31702
|
const ac = new AbortController();
|
|
31519
31703
|
const abort = () => ac.abort();
|
|
@@ -31589,17 +31773,17 @@ async function ensureModelFiles(o) {
|
|
|
31589
31773
|
}
|
|
31590
31774
|
|
|
31591
31775
|
// src/memory/embedder/gc.ts
|
|
31592
|
-
import { chmodSync as
|
|
31593
|
-
import { join as
|
|
31776
|
+
import { chmodSync as chmodSync4, closeSync, constants as constants2, fstatSync, futimesSync, lstatSync as lstatSync3, openSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
|
|
31777
|
+
import { join as join9 } from "node:path";
|
|
31594
31778
|
var THIRTY_DAYS = 30 * 864e5;
|
|
31595
31779
|
var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
31596
31780
|
function touchUserMarker(root3, modelId, identityId) {
|
|
31597
31781
|
if (!SAFE_ID.test(identityId)) throw new Error("invalid identity id for the model marker");
|
|
31598
31782
|
const dir = plainDirChain(root3, [modelId, ".users"], { create: true });
|
|
31599
|
-
const p =
|
|
31783
|
+
const p = join9(dir, identityId);
|
|
31600
31784
|
let fd;
|
|
31601
31785
|
try {
|
|
31602
|
-
fd = openSync(p,
|
|
31786
|
+
fd = openSync(p, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_NOFOLLOW | constants2.O_NONBLOCK, 420);
|
|
31603
31787
|
} catch {
|
|
31604
31788
|
throw new Error("the model marker is not a regular file");
|
|
31605
31789
|
}
|
|
@@ -31613,22 +31797,22 @@ function touchUserMarker(root3, modelId, identityId) {
|
|
|
31613
31797
|
}
|
|
31614
31798
|
function isRealDir(p) {
|
|
31615
31799
|
try {
|
|
31616
|
-
return
|
|
31800
|
+
return lstatSync3(p).isDirectory();
|
|
31617
31801
|
} catch {
|
|
31618
31802
|
return false;
|
|
31619
31803
|
}
|
|
31620
31804
|
}
|
|
31621
31805
|
function makeWritable(p) {
|
|
31622
31806
|
if (!isRealDir(p)) return;
|
|
31623
|
-
|
|
31624
|
-
for (const e of readdirSync2(p)) makeWritable(
|
|
31807
|
+
chmodSync4(p, 493);
|
|
31808
|
+
for (const e of readdirSync2(p)) makeWritable(join9(p, e));
|
|
31625
31809
|
}
|
|
31626
31810
|
function newestMarkerMs(users) {
|
|
31627
31811
|
if (!isRealDir(users)) return void 0;
|
|
31628
31812
|
let newest;
|
|
31629
31813
|
for (const u of readdirSync2(users)) {
|
|
31630
31814
|
try {
|
|
31631
|
-
const s =
|
|
31815
|
+
const s = lstatSync3(join9(users, u));
|
|
31632
31816
|
if (!s.isFile()) continue;
|
|
31633
31817
|
newest = newest === void 0 ? s.mtimeMs : Math.max(newest, s.mtimeMs);
|
|
31634
31818
|
} catch {
|
|
@@ -31641,9 +31825,9 @@ function gcModels(root3, keepModelIds, now, maxAgeMs = THIRTY_DAYS) {
|
|
|
31641
31825
|
const removed = [];
|
|
31642
31826
|
for (const modelId of readdirSync2(root3).sort()) {
|
|
31643
31827
|
if (modelId.startsWith(".") || keepModelIds.includes(modelId)) continue;
|
|
31644
|
-
const tree =
|
|
31828
|
+
const tree = join9(root3, modelId);
|
|
31645
31829
|
if (!isRealDir(tree)) continue;
|
|
31646
|
-
const lastUse = newestMarkerMs(
|
|
31830
|
+
const lastUse = newestMarkerMs(join9(tree, ".users")) ?? lstatSync3(tree).mtimeMs;
|
|
31647
31831
|
if (now - lastUse <= maxAgeMs) continue;
|
|
31648
31832
|
makeWritable(tree);
|
|
31649
31833
|
rmSync3(tree, { recursive: true, force: true });
|
|
@@ -31776,44 +31960,44 @@ var SealedChannel = class {
|
|
|
31776
31960
|
// src/memory/embedder/embedder-paths.ts
|
|
31777
31961
|
import { createHash as createHash3 } from "node:crypto";
|
|
31778
31962
|
import { homedir as homedir3 } from "node:os";
|
|
31779
|
-
import { join as
|
|
31963
|
+
import { join as join10 } from "node:path";
|
|
31780
31964
|
function embedderKey(m) {
|
|
31781
31965
|
const model = m.files.find((f) => f.name === "model.onnx");
|
|
31782
31966
|
return createHash3("sha256").update(`${EMBEDDER_PROTOCOL}\0${m.modelId}\0${model.sha256}\0${m.wasmSha256}`).digest("hex").slice(0, 16);
|
|
31783
31967
|
}
|
|
31784
|
-
var embedderRunDir = (home = homedir3()) =>
|
|
31968
|
+
var embedderRunDir = (home = homedir3()) => join10(home, ".securecoms", "run", "embedder");
|
|
31785
31969
|
var embedderPaths = (dir, key) => ({
|
|
31786
|
-
sock:
|
|
31787
|
-
token:
|
|
31788
|
-
lock:
|
|
31789
|
-
status:
|
|
31790
|
-
pid:
|
|
31970
|
+
sock: join10(dir, `${key}.sock`),
|
|
31971
|
+
token: join10(dir, `${key}.token`),
|
|
31972
|
+
lock: join10(dir, `${key}.spawn.lock`),
|
|
31973
|
+
status: join10(dir, `${key}.status`),
|
|
31974
|
+
pid: join10(dir, `${key}.pid`)
|
|
31791
31975
|
// E1 delta H1: the instance's lifetime lock
|
|
31792
31976
|
});
|
|
31793
31977
|
var SOCKET_PATH_MAX = 103;
|
|
31794
31978
|
var windowsRunDir = (env = process.env) => {
|
|
31795
31979
|
if (!env.LOCALAPPDATA) throw new Error("LOCALAPPDATA unset");
|
|
31796
|
-
return
|
|
31980
|
+
return join10(env.LOCALAPPDATA, "SecureComs", "run", "embedder");
|
|
31797
31981
|
};
|
|
31798
31982
|
|
|
31799
31983
|
// src/memory/embedder/spawn.ts
|
|
31800
31984
|
import { spawn } from "node:child_process";
|
|
31801
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
31985
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync4, realpathSync as realpathSync2 } from "node:fs";
|
|
31802
31986
|
|
|
31803
31987
|
// src/memory/embedder/run-dir.ts
|
|
31804
|
-
import { existsSync, lstatSync as
|
|
31805
|
-
import { dirname as
|
|
31988
|
+
import { existsSync, lstatSync as lstatSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
31989
|
+
import { dirname as dirname3 } from "node:path";
|
|
31806
31990
|
var RunDirRefused = class extends Error {
|
|
31807
31991
|
};
|
|
31808
|
-
function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat =
|
|
31809
|
-
const parent3 =
|
|
31810
|
-
if (!existsSync(parent3))
|
|
31992
|
+
function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync4) {
|
|
31993
|
+
const parent3 = dirname3(dir);
|
|
31994
|
+
if (!existsSync(parent3)) mkdirSync3(parent3, { recursive: true, mode: 448 });
|
|
31811
31995
|
const ps = lstat(parent3);
|
|
31812
31996
|
if (ps.isSymbolicLink() || !ps.isDirectory()) throw new RunDirRefused("run directory parent is not a real directory");
|
|
31813
31997
|
if (ps.uid !== uid) throw new RunDirRefused("run directory parent is not owned by this user");
|
|
31814
31998
|
if ((ps.mode & 18) !== 0) throw new RunDirRefused("run directory parent is group or world writable");
|
|
31815
31999
|
try {
|
|
31816
|
-
|
|
32000
|
+
mkdirSync3(dir, { mode: 448 });
|
|
31817
32001
|
} catch (e) {
|
|
31818
32002
|
if (e.code !== "EEXIST") throw e;
|
|
31819
32003
|
}
|
|
@@ -31824,10 +32008,10 @@ function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync3) {
|
|
|
31824
32008
|
}
|
|
31825
32009
|
|
|
31826
32010
|
// src/memory/embedder/spawn.ts
|
|
31827
|
-
import { basename as
|
|
32011
|
+
import { basename as basename4, dirname as dirname4, join as join11 } from "node:path";
|
|
31828
32012
|
|
|
31829
32013
|
// src/memory/embedder/locks.ts
|
|
31830
|
-
import { closeSync as closeSync2, constants as
|
|
32014
|
+
import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, linkSync, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync4, writeSync } from "node:fs";
|
|
31831
32015
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
31832
32016
|
import { uptime } from "node:os";
|
|
31833
32017
|
var LOCK_STALE_MS = 1e4;
|
|
@@ -31849,7 +32033,7 @@ var PROCESS_LOCK_ENV = {
|
|
|
31849
32033
|
function inspect(path) {
|
|
31850
32034
|
let fd;
|
|
31851
32035
|
try {
|
|
31852
|
-
fd = openSync2(path,
|
|
32036
|
+
fd = openSync2(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
|
|
31853
32037
|
} catch {
|
|
31854
32038
|
return null;
|
|
31855
32039
|
}
|
|
@@ -31873,7 +32057,7 @@ function takeLock(path, now = Date.now, env = PROCESS_LOCK_ENV) {
|
|
|
31873
32057
|
const id = `${env.pid} ${env.bootTime()} ${env.startTime()} ${randomBytes9(8).toString("hex")}`;
|
|
31874
32058
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
31875
32059
|
try {
|
|
31876
|
-
const fd = openSync2(path,
|
|
32060
|
+
const fd = openSync2(path, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY, 384);
|
|
31877
32061
|
try {
|
|
31878
32062
|
writeSync(fd, id);
|
|
31879
32063
|
} finally {
|
|
@@ -31934,7 +32118,7 @@ function embedderEnv(env = process.env, platform = process.platform) {
|
|
|
31934
32118
|
}
|
|
31935
32119
|
var withReal = (p) => {
|
|
31936
32120
|
try {
|
|
31937
|
-
const r =
|
|
32121
|
+
const r = realpathSync2(p);
|
|
31938
32122
|
return r === p ? [p] : [p, r];
|
|
31939
32123
|
} catch {
|
|
31940
32124
|
return [p];
|
|
@@ -31942,10 +32126,10 @@ var withReal = (p) => {
|
|
|
31942
32126
|
};
|
|
31943
32127
|
var realish = (p) => {
|
|
31944
32128
|
try {
|
|
31945
|
-
return
|
|
32129
|
+
return realpathSync2(p);
|
|
31946
32130
|
} catch {
|
|
31947
|
-
const up =
|
|
31948
|
-
return up === p ? p :
|
|
32131
|
+
const up = dirname4(p);
|
|
32132
|
+
return up === p ? p : join11(realish(up), basename4(p));
|
|
31949
32133
|
}
|
|
31950
32134
|
};
|
|
31951
32135
|
var both = (p) => {
|
|
@@ -31957,7 +32141,7 @@ function spawnEmbedder(o) {
|
|
|
31957
32141
|
const run = o.runDir ?? embedderRunDir(o.home);
|
|
31958
32142
|
const win = (o.platform ?? process.platform) === "win32";
|
|
31959
32143
|
if (o.createRunDir !== false) {
|
|
31960
|
-
if (win)
|
|
32144
|
+
if (win) mkdirSync4(run, { recursive: true, mode: 448 });
|
|
31961
32145
|
else ensureRunDir(run);
|
|
31962
32146
|
}
|
|
31963
32147
|
const perm = permissionFlags();
|
|
@@ -31966,13 +32150,13 @@ function spawnEmbedder(o) {
|
|
|
31966
32150
|
o.onNoPermissionModel?.();
|
|
31967
32151
|
}
|
|
31968
32152
|
const reads = perm.length === 0 ? [] : [
|
|
31969
|
-
...withReal(
|
|
32153
|
+
...withReal(dirname4(o.distDir)),
|
|
31970
32154
|
...existsSync2(defaultModelsRoot(o.home)) ? withReal(defaultModelsRoot(o.home)) : [defaultModelsRoot(o.home)],
|
|
31971
|
-
...both(
|
|
31972
|
-
...win ? both(
|
|
32155
|
+
...both(dirname4(run)),
|
|
32156
|
+
...win ? both(join11(o.home, ".securecoms", "run")) : []
|
|
31973
32157
|
// the voice-call heartbeats (the run dir lives elsewhere)
|
|
31974
32158
|
];
|
|
31975
|
-
const script = withReal(
|
|
32159
|
+
const script = withReal(join11(o.distDir, "embedder.js")).at(-1);
|
|
31976
32160
|
const net = (o.nodeFlags ?? process.allowedNodeEnvironmentFlags).has("--allow-net") ? ["--allow-net"] : [];
|
|
31977
32161
|
const flags = perm.length === 0 ? [] : [...perm, ...reads.map((r) => `--allow-fs-read=${r}`), ...both(run).map((w) => `--allow-fs-write=${w}`), "--allow-worker", ...net];
|
|
31978
32162
|
const child = (o.spawnFn ?? spawn)(
|
|
@@ -31981,7 +32165,7 @@ function spawnEmbedder(o) {
|
|
|
31981
32165
|
// 11c review LOW 3, fixed by the Task 12 Mini gate: cwd is the run directory's PARENT, which the bridge created (0700)
|
|
31982
32166
|
// before spawning; the run directory itself is created by the embedder, so on a first start it does not exist yet and
|
|
31983
32167
|
// a cwd there fails the spawn with ENOENT. Never the bridge's working directory.
|
|
31984
|
-
{ detached: true, stdio: "ignore", windowsHide: true, cwd:
|
|
32168
|
+
{ detached: true, stdio: "ignore", windowsHide: true, cwd: dirname4(run), env: win ? { ...embedderEnv(process.env, "win32"), USERPROFILE: o.home, LOCALAPPDATA: dirname4(dirname4(dirname4(run))) } : { ...embedderEnv(), HOME: o.home } }
|
|
31985
32169
|
);
|
|
31986
32170
|
child.unref();
|
|
31987
32171
|
child.on("error", () => {
|
|
@@ -31993,8 +32177,8 @@ function spawnEmbedder(o) {
|
|
|
31993
32177
|
import { execFile } from "node:child_process";
|
|
31994
32178
|
import { promisify } from "node:util";
|
|
31995
32179
|
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
31996
|
-
import { lstatSync as
|
|
31997
|
-
import { join as
|
|
32180
|
+
import { lstatSync as lstatSync5, readFileSync as readFileSync6, rmSync as rmSync5 } from "node:fs";
|
|
32181
|
+
import { join as join12, win32 } from "node:path";
|
|
31998
32182
|
var WINDOWS_SEMANTIC_ENABLED = false;
|
|
31999
32183
|
var windowsSemanticOn = (platform, enabled = WINDOWS_SEMANTIC_ENABLED) => platform !== "win32" || enabled;
|
|
32000
32184
|
function parseSaveSddl(saveFile) {
|
|
@@ -32022,7 +32206,7 @@ function aclAllows(sddl, ownerSid) {
|
|
|
32022
32206
|
if (aces.length === 0 || rest !== "" && !rest.startsWith("S:")) return false;
|
|
32023
32207
|
return aces.every((a) => a[0] === "A" && allowed.has(a[5] ?? "") || a[0] === "D");
|
|
32024
32208
|
}
|
|
32025
|
-
function hasReparsePoint(path, lstat =
|
|
32209
|
+
function hasReparsePoint(path, lstat = lstatSync5) {
|
|
32026
32210
|
const root3 = win32.parse(path).root;
|
|
32027
32211
|
let cur = root3;
|
|
32028
32212
|
for (const part of path.slice(root3.length).split(/[\\/]/).filter(Boolean)) {
|
|
@@ -32041,7 +32225,7 @@ async function currentUserSid(run = defaultRunner) {
|
|
|
32041
32225
|
}
|
|
32042
32226
|
async function verifyRunDirAcl(path, deps = {}, sid) {
|
|
32043
32227
|
const run = deps.run ?? defaultRunner;
|
|
32044
|
-
const out =
|
|
32228
|
+
const out = join12(deps.tmp ?? process.env.TEMP ?? process.env.TMP ?? ".", `sc-acl-${randomBytes10(8).toString("hex")}.txt`);
|
|
32045
32229
|
try {
|
|
32046
32230
|
if (hasReparsePoint(path, deps.lstat)) return false;
|
|
32047
32231
|
const user = await (sid ? sid() : currentUserSid(run));
|
|
@@ -32122,12 +32306,12 @@ var EmbedderClient = class {
|
|
|
32122
32306
|
ready(ms) {
|
|
32123
32307
|
if (this.st.state === "ready") return Promise.resolve(true);
|
|
32124
32308
|
if (this.st.state !== "preparing" || !(ms > 0)) return Promise.resolve(false);
|
|
32125
|
-
return new Promise((
|
|
32309
|
+
return new Promise((resolve2) => {
|
|
32126
32310
|
let t;
|
|
32127
32311
|
const w = (ok) => {
|
|
32128
32312
|
clearTimeout(t);
|
|
32129
32313
|
this.readyWaiters.delete(w);
|
|
32130
|
-
|
|
32314
|
+
resolve2(ok);
|
|
32131
32315
|
};
|
|
32132
32316
|
this.readyWaiters.add(w);
|
|
32133
32317
|
t = setTimeout(() => w(false), ms);
|
|
@@ -32236,7 +32420,7 @@ var EmbedderClient = class {
|
|
|
32236
32420
|
const p = this.paths();
|
|
32237
32421
|
if (!this.tcp() && Buffer.byteLength(`${p.sock}.00000000`) > SOCKET_PATH_MAX) return this.fail("run_dir_refused");
|
|
32238
32422
|
if (this.tcp()) {
|
|
32239
|
-
|
|
32423
|
+
mkdirSync5(this.runDir(), { recursive: true, mode: 448 });
|
|
32240
32424
|
if (!await this.aclOk(true)) return this.fail("acl_unverified");
|
|
32241
32425
|
}
|
|
32242
32426
|
if (!this.tcp()) {
|
|
@@ -32372,7 +32556,7 @@ var EmbedderClient = class {
|
|
|
32372
32556
|
}
|
|
32373
32557
|
if (this.closed) return false;
|
|
32374
32558
|
}
|
|
32375
|
-
return new Promise((
|
|
32559
|
+
return new Promise((resolve2) => {
|
|
32376
32560
|
const s = this.tcp() ? connect({ host: "127.0.0.1", port: pub.port }) : connect(p.sock);
|
|
32377
32561
|
const nc = nonce();
|
|
32378
32562
|
let ns = "";
|
|
@@ -32385,7 +32569,7 @@ var EmbedderClient = class {
|
|
|
32385
32569
|
if (!settled) {
|
|
32386
32570
|
settled = true;
|
|
32387
32571
|
clearTimeout(t);
|
|
32388
|
-
|
|
32572
|
+
resolve2(ok);
|
|
32389
32573
|
}
|
|
32390
32574
|
}
|
|
32391
32575
|
const refuse2 = (reason) => {
|
|
@@ -32950,12 +33134,12 @@ var ReembedQueue = class {
|
|
|
32950
33134
|
};
|
|
32951
33135
|
|
|
32952
33136
|
// src/memory/recall/call-heartbeat.ts
|
|
32953
|
-
import { closeSync as closeSync3, constants as
|
|
32954
|
-
import { join as
|
|
33137
|
+
import { closeSync as closeSync3, constants as constants4, fstatSync as fstatSync3, futimesSync as futimesSync2, openSync as openSync3, rmSync as rmSync7 } from "node:fs";
|
|
33138
|
+
import { join as join14 } from "node:path";
|
|
32955
33139
|
|
|
32956
33140
|
// src/memory/embedder/heartbeats.ts
|
|
32957
|
-
import { lstatSync as
|
|
32958
|
-
import { join as
|
|
33141
|
+
import { lstatSync as lstatSync6, readdirSync as readdirSync3 } from "node:fs";
|
|
33142
|
+
import { join as join13 } from "node:path";
|
|
32959
33143
|
var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
|
|
32960
33144
|
var HEARTBEAT_STALE_MS = 3e4;
|
|
32961
33145
|
function heartbeatAgeFresh(ageMs, staleMs = HEARTBEAT_STALE_MS) {
|
|
@@ -32970,7 +33154,7 @@ function anyHeartbeatFresh(dir, now, staleMs = HEARTBEAT_STALE_MS) {
|
|
|
32970
33154
|
}
|
|
32971
33155
|
return names.some((n) => {
|
|
32972
33156
|
try {
|
|
32973
|
-
const st =
|
|
33157
|
+
const st = lstatSync6(join13(dir, n));
|
|
32974
33158
|
return st.isFile() && heartbeatAgeFresh(now - st.mtimeMs, staleMs);
|
|
32975
33159
|
} catch {
|
|
32976
33160
|
return false;
|
|
@@ -33036,7 +33220,7 @@ var CallHeartbeat = class {
|
|
|
33036
33220
|
if (dir === null) return;
|
|
33037
33221
|
let fd;
|
|
33038
33222
|
try {
|
|
33039
|
-
fd = openSync3(
|
|
33223
|
+
fd = openSync3(join14(dir, this.identityId), constants4.O_WRONLY | constants4.O_CREAT | constants4.O_NOFOLLOW | constants4.O_NONBLOCK, 384);
|
|
33040
33224
|
if (!fstatSync3(fd).isFile()) return;
|
|
33041
33225
|
const t = new Date(this.now());
|
|
33042
33226
|
futimesSync2(fd, t, t);
|
|
@@ -33051,7 +33235,7 @@ var CallHeartbeat = class {
|
|
|
33051
33235
|
const dir = SAFE_ID2.test(this.identityId) ? this.dirOk() : null;
|
|
33052
33236
|
if (dir === null) return;
|
|
33053
33237
|
try {
|
|
33054
|
-
rmSync7(
|
|
33238
|
+
rmSync7(join14(dir, this.identityId), { force: true });
|
|
33055
33239
|
this.wrote = false;
|
|
33056
33240
|
} catch (e) {
|
|
33057
33241
|
this.log("warn", "memory.call_heartbeat_failed", {}, e);
|
|
@@ -33114,9 +33298,9 @@ function queryTokenMacs(key, text, max = QUERY_MACS_MAX) {
|
|
|
33114
33298
|
|
|
33115
33299
|
// src/memory/unified/ranking-client.ts
|
|
33116
33300
|
import { Worker } from "node:worker_threads";
|
|
33117
|
-
import { join as
|
|
33301
|
+
import { join as join15 } from "node:path";
|
|
33118
33302
|
function workerTransport(init, distDir) {
|
|
33119
|
-
const w = new Worker(
|
|
33303
|
+
const w = new Worker(join15(distDir, "memory-worker.js"), { workerData: init });
|
|
33120
33304
|
return {
|
|
33121
33305
|
post: (m) => w.postMessage(m),
|
|
33122
33306
|
onMessage: (cb) => {
|
|
@@ -33176,15 +33360,15 @@ var RankingClient = class {
|
|
|
33176
33360
|
ask(build, deadlineAt) {
|
|
33177
33361
|
const id = ++this.seq;
|
|
33178
33362
|
if (this.closed) return Promise.resolve("failed");
|
|
33179
|
-
return new Promise((
|
|
33363
|
+
return new Promise((resolve2) => {
|
|
33180
33364
|
const timer = setTimeout(() => {
|
|
33181
33365
|
this.waiting.delete(id);
|
|
33182
33366
|
this.misses++;
|
|
33183
|
-
|
|
33367
|
+
resolve2("late");
|
|
33184
33368
|
}, Math.max(0, deadlineAt - Date.now()));
|
|
33185
33369
|
this.waiting.set(id, { resolve: (m) => {
|
|
33186
33370
|
clearTimeout(timer);
|
|
33187
|
-
|
|
33371
|
+
resolve2(m);
|
|
33188
33372
|
} });
|
|
33189
33373
|
try {
|
|
33190
33374
|
this.transport().post(build(id));
|
|
@@ -33193,7 +33377,7 @@ var RankingClient = class {
|
|
|
33193
33377
|
this.waiting.delete(id);
|
|
33194
33378
|
this.misses++;
|
|
33195
33379
|
(this.d.log ?? writeServiceLog2)("warn", "memory.ranking_worker_spawn_failed", void 0, e);
|
|
33196
|
-
|
|
33380
|
+
resolve2("failed");
|
|
33197
33381
|
}
|
|
33198
33382
|
});
|
|
33199
33383
|
}
|
|
@@ -34787,17 +34971,17 @@ async function runSummarize(opts) {
|
|
|
34787
34971
|
// src/service/runtime-config-dir.ts
|
|
34788
34972
|
import { createHash as createHash5, randomUUID } from "node:crypto";
|
|
34789
34973
|
import {
|
|
34790
|
-
chmodSync as
|
|
34974
|
+
chmodSync as chmodSync5,
|
|
34791
34975
|
existsSync as existsSync4,
|
|
34792
|
-
lstatSync as
|
|
34793
|
-
mkdirSync as
|
|
34976
|
+
lstatSync as lstatSync7,
|
|
34977
|
+
mkdirSync as mkdirSync6,
|
|
34794
34978
|
readFileSync as readFileSync8,
|
|
34795
34979
|
renameSync as renameSync3,
|
|
34796
34980
|
statSync as statSync3,
|
|
34797
34981
|
unlinkSync,
|
|
34798
34982
|
writeFileSync
|
|
34799
34983
|
} from "node:fs";
|
|
34800
|
-
import { basename as
|
|
34984
|
+
import { basename as basename5, isAbsolute, join as join16 } from "node:path";
|
|
34801
34985
|
|
|
34802
34986
|
// src/grants/search-providers.ts
|
|
34803
34987
|
var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
|
|
@@ -34809,18 +34993,18 @@ function isSearchProvider(value) {
|
|
|
34809
34993
|
// src/service/runtime-config-dir.ts
|
|
34810
34994
|
var MEMORY_FILE = "MEMORY.md";
|
|
34811
34995
|
var REAL_FS = {
|
|
34812
|
-
mkdir: (p, mode) =>
|
|
34813
|
-
chmod: (p, mode) =>
|
|
34996
|
+
mkdir: (p, mode) => mkdirSync6(p, { recursive: true, mode }),
|
|
34997
|
+
chmod: (p, mode) => chmodSync5(p, mode),
|
|
34814
34998
|
writeFile: (p, data, mode) => writeFileSync(p, data, { mode }),
|
|
34815
34999
|
rename: (a, b) => renameSync3(a, b),
|
|
34816
35000
|
readFile: (p) => readFileSync8(p, "utf8"),
|
|
34817
35001
|
unlink: (p) => unlinkSync(p)
|
|
34818
35002
|
};
|
|
34819
35003
|
function runtimeConfigDir(home, identityId) {
|
|
34820
|
-
return
|
|
35004
|
+
return join16(home, ".securecoms", "runtime-config", identityId);
|
|
34821
35005
|
}
|
|
34822
35006
|
function assertManagedName(name) {
|
|
34823
|
-
if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !==
|
|
35007
|
+
if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename5(name)) {
|
|
34824
35008
|
throw new Error(`refusing managed config file name ${JSON.stringify(name)}: not a plain filename`);
|
|
34825
35009
|
}
|
|
34826
35010
|
if (isLocalOnlyName(name)) {
|
|
@@ -34829,24 +35013,46 @@ function assertManagedName(name) {
|
|
|
34829
35013
|
}
|
|
34830
35014
|
function isLocalOnlyName(name) {
|
|
34831
35015
|
const n = name.toLowerCase().replace(/[. ]+$/, "");
|
|
34832
|
-
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE;
|
|
35016
|
+
return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE || n === MCP_POLICY_FILE;
|
|
34833
35017
|
}
|
|
34834
35018
|
var MANIFEST_POLICY_FILE = "manifest-policy.json";
|
|
34835
35019
|
function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
|
|
34836
35020
|
try {
|
|
34837
|
-
deps.unlink(
|
|
35021
|
+
deps.unlink(join16(runtimeConfigDir(home, identityId), "llm-source.json"));
|
|
34838
35022
|
} catch (e) {
|
|
34839
35023
|
if (e?.code !== "ENOENT") throw e;
|
|
34840
35024
|
}
|
|
34841
35025
|
}
|
|
34842
35026
|
function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
|
|
34843
|
-
const parent3 =
|
|
35027
|
+
const parent3 = join16(home, ".securecoms", "runtime-config");
|
|
35028
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
35029
|
+
for (const d of [join16(home, ".securecoms"), parent3, dir]) {
|
|
35030
|
+
deps.mkdir(d, 448);
|
|
35031
|
+
deps.chmod(d, 448);
|
|
35032
|
+
}
|
|
35033
|
+
atomicWrite(join16(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
|
|
35034
|
+
}
|
|
35035
|
+
var MCP_POLICY_FILE = "mcp-policy.json";
|
|
35036
|
+
var MCP_POLICY_TOP_STATES = Object.freeze([
|
|
35037
|
+
"ok",
|
|
35038
|
+
"not_entitled",
|
|
35039
|
+
"kill_switch",
|
|
35040
|
+
"suspended",
|
|
35041
|
+
"no_config",
|
|
35042
|
+
"switch_off",
|
|
35043
|
+
"no_manifest",
|
|
35044
|
+
"malformed",
|
|
35045
|
+
"no_capability",
|
|
35046
|
+
"section_absent"
|
|
35047
|
+
]);
|
|
35048
|
+
function writeMcpPolicy(home, identityId, doc, deps = REAL_FS) {
|
|
35049
|
+
const parent3 = join16(home, ".securecoms", "runtime-config");
|
|
34844
35050
|
const dir = runtimeConfigDir(home, identityId);
|
|
34845
|
-
for (const d of [
|
|
35051
|
+
for (const d of [join16(home, ".securecoms"), parent3, dir]) {
|
|
34846
35052
|
deps.mkdir(d, 448);
|
|
34847
35053
|
deps.chmod(d, 448);
|
|
34848
35054
|
}
|
|
34849
|
-
atomicWrite(
|
|
35055
|
+
atomicWrite(join16(dir, MCP_POLICY_FILE), JSON.stringify(doc) + "\n", deps);
|
|
34850
35056
|
}
|
|
34851
35057
|
function atomicWrite(path, body, deps) {
|
|
34852
35058
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -34865,13 +35071,13 @@ function atomicWrite(path, body, deps) {
|
|
|
34865
35071
|
function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
|
|
34866
35072
|
const entries = Object.entries(files);
|
|
34867
35073
|
for (const [name] of entries) assertManagedName(name);
|
|
34868
|
-
const parent3 =
|
|
35074
|
+
const parent3 = join16(home, ".securecoms", "runtime-config");
|
|
34869
35075
|
const dir = runtimeConfigDir(home, identityId);
|
|
34870
|
-
for (const d of [
|
|
35076
|
+
for (const d of [join16(home, ".securecoms"), parent3, dir]) {
|
|
34871
35077
|
deps.mkdir(d, 448);
|
|
34872
35078
|
deps.chmod(d, 448);
|
|
34873
35079
|
}
|
|
34874
|
-
for (const [name, body] of entries) atomicWrite(
|
|
35080
|
+
for (const [name, body] of entries) atomicWrite(join16(dir, name), body, deps);
|
|
34875
35081
|
}
|
|
34876
35082
|
function checkConfiguredPaths(skillsJson, statFn = (p) => {
|
|
34877
35083
|
const s = statSync3(p);
|
|
@@ -34950,7 +35156,7 @@ function localLegacySources(doc) {
|
|
|
34950
35156
|
const channelId = source.channelId;
|
|
34951
35157
|
if (kind2 !== "global-memory" && kind2 !== "conversation-history" && kind2 !== "captured-memory" || typeof path !== "string" || !isAbsolute(path) || channelId !== void 0 && typeof channelId !== "string") continue;
|
|
34952
35158
|
try {
|
|
34953
|
-
const st =
|
|
35159
|
+
const st = lstatSync7(path);
|
|
34954
35160
|
if (!st.isFile() || st.isSymbolicLink()) continue;
|
|
34955
35161
|
if (st.size <= LEGACY_MEMORY_SOURCE_MAX_BYTES && st.size > contentBudget) continue;
|
|
34956
35162
|
const oversize = st.size > LEGACY_MEMORY_SOURCE_MAX_BYTES;
|
|
@@ -34978,7 +35184,7 @@ function localLegacySources(doc) {
|
|
|
34978
35184
|
return out;
|
|
34979
35185
|
}
|
|
34980
35186
|
function readLocalReport(home, identityId) {
|
|
34981
|
-
const p =
|
|
35187
|
+
const p = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
34982
35188
|
if (!existsSync4(p)) return null;
|
|
34983
35189
|
let doc;
|
|
34984
35190
|
try {
|
|
@@ -35013,7 +35219,7 @@ function readLocalReport(home, identityId) {
|
|
|
35013
35219
|
}
|
|
35014
35220
|
function deleteLegacyMemorySources(home, identityId, ids) {
|
|
35015
35221
|
if (ids.length === 0) return 0;
|
|
35016
|
-
const p =
|
|
35222
|
+
const p = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
35017
35223
|
if (!existsSync4(p)) return 0;
|
|
35018
35224
|
let doc;
|
|
35019
35225
|
try {
|
|
@@ -35056,7 +35262,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
|
|
|
35056
35262
|
group.set(decision.fact_hash, decision);
|
|
35057
35263
|
bySource.set(decision.source_id, group);
|
|
35058
35264
|
}
|
|
35059
|
-
const reportPath =
|
|
35265
|
+
const reportPath = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
35060
35266
|
if (!existsSync4(reportPath)) return { sourcesDeleted: 0, factsAssigned: 0 };
|
|
35061
35267
|
let doc;
|
|
35062
35268
|
try {
|
|
@@ -35094,7 +35300,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
|
|
|
35094
35300
|
}
|
|
35095
35301
|
function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
35096
35302
|
const dir = runtimeConfigDir(home, identityId);
|
|
35097
|
-
const path =
|
|
35303
|
+
const path = join16(dir, MEMORY_FILE);
|
|
35098
35304
|
if (!existsSync4(path)) return 0;
|
|
35099
35305
|
const drop = new Set(hashes);
|
|
35100
35306
|
const before = deps.readFile(path).split("\n");
|
|
@@ -35127,9 +35333,9 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
|
|
|
35127
35333
|
import { ed25519 as ed255195 } from "@noble/curves/ed25519";
|
|
35128
35334
|
|
|
35129
35335
|
// src/grants/pin.ts
|
|
35130
|
-
import { lstatSync as
|
|
35336
|
+
import { lstatSync as lstatSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
35131
35337
|
import { ed25519 as ed255194 } from "@noble/curves/ed25519";
|
|
35132
|
-
import { dirname as
|
|
35338
|
+
import { dirname as dirname5, join as join17 } from "node:path";
|
|
35133
35339
|
var PIN_FILE_NAME = "owner-signing-key.json";
|
|
35134
35340
|
var UUID_LOWER = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
35135
35341
|
var PIN_KEYS = ["key_algo", "owner_identity_id", "public_key"];
|
|
@@ -35159,7 +35365,7 @@ function parsePinObject(v) {
|
|
|
35159
35365
|
pin: Object.freeze({ ownerIdentityId: o.owner_identity_id, keyAlgo: "ed25519", publicKey, publicKeyB64: o.public_key })
|
|
35160
35366
|
};
|
|
35161
35367
|
}
|
|
35162
|
-
var NODE_FS = { lstat: (p) =>
|
|
35368
|
+
var NODE_FS = { lstat: (p) => lstatSync8(p), readFile: (p) => readFileSync9(p, "utf8") };
|
|
35163
35369
|
function fromValue(v) {
|
|
35164
35370
|
const r = parsePinObject(v);
|
|
35165
35371
|
return r.ok ? { kind: "ok", pin: r.pin } : { kind: "bad", state: r.state };
|
|
@@ -35193,7 +35399,7 @@ function samePin(a, b) {
|
|
|
35193
35399
|
}
|
|
35194
35400
|
function loadOwnerGrantPin(opts) {
|
|
35195
35401
|
const fs = opts.fs ?? NODE_FS;
|
|
35196
|
-
const file2 = readPinFile(
|
|
35402
|
+
const file2 = readPinFile(join17(dirname5(opts.credentialPath), PIN_FILE_NAME), fs);
|
|
35197
35403
|
const field = "owner_signing_key" in opts.credentialJson && opts.credentialJson.owner_signing_key !== void 0 ? fromValue(opts.credentialJson.owner_signing_key) : { kind: "absent" };
|
|
35198
35404
|
if (file2.kind === "bad") return { state: file2.state, pin: null };
|
|
35199
35405
|
if (field.kind === "bad") return { state: field.state, pin: null };
|
|
@@ -35205,6 +35411,193 @@ function loadOwnerGrantPin(opts) {
|
|
|
35205
35411
|
return { state: "ok", pin: one };
|
|
35206
35412
|
}
|
|
35207
35413
|
|
|
35414
|
+
// src/grants/mcp-section.ts
|
|
35415
|
+
var MCP_MAX_SERVERS = 8;
|
|
35416
|
+
var MCP_MAX_TOOLS_PER_SERVER = 64;
|
|
35417
|
+
var MCP_MAX_SIGNED_TOOLS = 160;
|
|
35418
|
+
var MCP_MAX_REMOVED = 8;
|
|
35419
|
+
var SECURECOMS_CIMD_CLIENT_ID = "https://app.securecoms.ai/oauth/securecoms-agent-client.json";
|
|
35420
|
+
var MCP_SERVER_ID = /^[a-z0-9-]{1,24}$/;
|
|
35421
|
+
var MCP_TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
35422
|
+
var VENDOR_KEY = /^[a-z0-9_]{1,32}$/;
|
|
35423
|
+
var MCP_RESERVED_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
35424
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
35425
|
+
var PRINTABLE_ASCII = /^[\x21-\x7e]{1,256}$/;
|
|
35426
|
+
var MCP_GRANT = /^mcp:([a-z0-9-]{1,24})\/([A-Za-z0-9_.-]{1,64})$/;
|
|
35427
|
+
var isBag = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35428
|
+
var keysExactly = (o, keys) => {
|
|
35429
|
+
const have = Object.keys(o);
|
|
35430
|
+
return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(o, k));
|
|
35431
|
+
};
|
|
35432
|
+
function canonicalHttpsUrl(v) {
|
|
35433
|
+
if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
|
|
35434
|
+
const u = new URL(v);
|
|
35435
|
+
return u.protocol === "https:" && u.username === "" && u.password === "" && u.href === v;
|
|
35436
|
+
}
|
|
35437
|
+
function httpsIssuer(v) {
|
|
35438
|
+
if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
|
|
35439
|
+
const u = new URL(v);
|
|
35440
|
+
return u.protocol === "https:" && u.username === "" && u.password === "" && u.search === "" && u.hash === "" && (u.href === v || u.href === `${v}/`);
|
|
35441
|
+
}
|
|
35442
|
+
var MCP_AUTH_KEYS = Object.freeze({
|
|
35443
|
+
oauth_securecoms_cimd: Object.freeze(["mode", "issuer", "client"]),
|
|
35444
|
+
oauth_customer: Object.freeze(["mode", "issuer", "client", "client_id"]),
|
|
35445
|
+
key: Object.freeze(["mode"])
|
|
35446
|
+
});
|
|
35447
|
+
function parseAuth(v) {
|
|
35448
|
+
if (!isBag(v)) return null;
|
|
35449
|
+
if (v.mode === "key") return keysExactly(v, MCP_AUTH_KEYS.key) ? Object.freeze({ mode: "key" }) : null;
|
|
35450
|
+
if (v.mode !== "oauth" || !httpsIssuer(v.issuer)) return null;
|
|
35451
|
+
if (v.client === "securecoms_cimd") {
|
|
35452
|
+
if (!keysExactly(v, MCP_AUTH_KEYS.oauth_securecoms_cimd)) return null;
|
|
35453
|
+
return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "securecoms_cimd" });
|
|
35454
|
+
}
|
|
35455
|
+
if (v.client === "customer") {
|
|
35456
|
+
if (!keysExactly(v, MCP_AUTH_KEYS.oauth_customer)) return null;
|
|
35457
|
+
if (typeof v.client_id !== "string" || !PRINTABLE_ASCII.test(v.client_id)) return null;
|
|
35458
|
+
return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "customer", client_id: v.client_id });
|
|
35459
|
+
}
|
|
35460
|
+
return null;
|
|
35461
|
+
}
|
|
35462
|
+
function parseTools(v) {
|
|
35463
|
+
if (!isBag(v)) return null;
|
|
35464
|
+
const entries = Object.entries(v);
|
|
35465
|
+
if (entries.length > MCP_MAX_TOOLS_PER_SERVER) return null;
|
|
35466
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
35467
|
+
for (const [name, t] of entries) {
|
|
35468
|
+
if (!MCP_TOOL_NAME.test(name) || MCP_RESERVED_NAMES.has(name) || !isBag(t) || !keysExactly(t, ["sha256", "class"])) return null;
|
|
35469
|
+
if (typeof t.sha256 !== "string" || !HEX64.test(t.sha256) || t.class !== "read") return null;
|
|
35470
|
+
out[name] = Object.freeze({ sha256: t.sha256, class: "read" });
|
|
35471
|
+
}
|
|
35472
|
+
return out;
|
|
35473
|
+
}
|
|
35474
|
+
function parseServer(v) {
|
|
35475
|
+
if (!isBag(v) || !keysExactly(v, ["vendor", "url", "auth", "tools"])) return null;
|
|
35476
|
+
if (typeof v.vendor !== "string" || !VENDOR_KEY.test(v.vendor) || !canonicalHttpsUrl(v.url)) return null;
|
|
35477
|
+
const auth = parseAuth(v.auth);
|
|
35478
|
+
const tools = parseTools(v.tools);
|
|
35479
|
+
if (auth === null || tools === null) return null;
|
|
35480
|
+
return Object.freeze({ vendor: v.vendor, url: v.url, auth, tools: Object.freeze(tools) });
|
|
35481
|
+
}
|
|
35482
|
+
function parseRemoved(v, servers) {
|
|
35483
|
+
if (v === "*") return Object.keys(servers).length === 0 ? "*" : null;
|
|
35484
|
+
if (!Array.isArray(v) || v.length < 1 || v.length > MCP_MAX_REMOVED) return null;
|
|
35485
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35486
|
+
for (const id of v) {
|
|
35487
|
+
if (typeof id !== "string" || !MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
|
|
35488
|
+
if (seen.has(id) || Object.prototype.hasOwnProperty.call(servers, id)) return null;
|
|
35489
|
+
seen.add(id);
|
|
35490
|
+
}
|
|
35491
|
+
return Object.freeze([...seen]);
|
|
35492
|
+
}
|
|
35493
|
+
var NOT_REMOVED = Object.freeze([]);
|
|
35494
|
+
function parseMcp(v) {
|
|
35495
|
+
if (!isBag(v) || !(keysExactly(v, ["servers"]) || keysExactly(v, ["servers", "removed"])) || !isBag(v.servers)) return null;
|
|
35496
|
+
const entries = Object.entries(v.servers);
|
|
35497
|
+
if (entries.length > MCP_MAX_SERVERS) return null;
|
|
35498
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
35499
|
+
let signedTools = 0;
|
|
35500
|
+
for (const [id, raw] of entries) {
|
|
35501
|
+
if (!MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
|
|
35502
|
+
const s = parseServer(raw);
|
|
35503
|
+
if (s === null) return null;
|
|
35504
|
+
signedTools += Object.keys(s.tools).length;
|
|
35505
|
+
if (signedTools > MCP_MAX_SIGNED_TOOLS) return null;
|
|
35506
|
+
servers[id] = s;
|
|
35507
|
+
}
|
|
35508
|
+
let removed = NOT_REMOVED;
|
|
35509
|
+
if (Object.prototype.hasOwnProperty.call(v, "removed")) {
|
|
35510
|
+
const r = parseRemoved(v.removed, servers);
|
|
35511
|
+
if (r === null) return null;
|
|
35512
|
+
removed = r;
|
|
35513
|
+
}
|
|
35514
|
+
return Object.freeze({ servers: Object.freeze(servers), removed });
|
|
35515
|
+
}
|
|
35516
|
+
function mcpServerCatalogRow(s, catalog) {
|
|
35517
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
|
|
35518
|
+
const row = catalog[s.vendor];
|
|
35519
|
+
if (s.url !== row.readOnlyUrl) return null;
|
|
35520
|
+
if (s.auth.mode === "oauth" && s.auth.issuer !== row.issuer) return null;
|
|
35521
|
+
return row;
|
|
35522
|
+
}
|
|
35523
|
+
function parseMcpGrant(name) {
|
|
35524
|
+
const g = MCP_GRANT.exec(name);
|
|
35525
|
+
return g === null ? null : { serverId: g[1], tool: g[2] };
|
|
35526
|
+
}
|
|
35527
|
+
function mcpGrantWithinCeiling(name, mcp, catalog) {
|
|
35528
|
+
const g = parseMcpGrant(name);
|
|
35529
|
+
if (g === null || mcp === null || !Object.prototype.hasOwnProperty.call(mcp.servers, g.serverId)) return false;
|
|
35530
|
+
const s = mcp.servers[g.serverId];
|
|
35531
|
+
if (!Object.prototype.hasOwnProperty.call(s.tools, g.tool)) return false;
|
|
35532
|
+
const row = mcpServerCatalogRow(s, catalog);
|
|
35533
|
+
return row !== null && row.readTools.includes(g.tool);
|
|
35534
|
+
}
|
|
35535
|
+
|
|
35536
|
+
// src/grants/mcp-catalog.ts
|
|
35537
|
+
var LINEAR = Object.freeze({
|
|
35538
|
+
displayName: "Linear",
|
|
35539
|
+
host: "mcp.linear.app",
|
|
35540
|
+
readOnlyUrl: "https://mcp.linear.app/mcp/readonly",
|
|
35541
|
+
readTools: Object.freeze([
|
|
35542
|
+
"get_attachment",
|
|
35543
|
+
"list_agent_skills",
|
|
35544
|
+
"get_agent_skill",
|
|
35545
|
+
"list_comments",
|
|
35546
|
+
"list_cycles",
|
|
35547
|
+
"list_custom_views",
|
|
35548
|
+
"get_document",
|
|
35549
|
+
"list_documents",
|
|
35550
|
+
"extract_images",
|
|
35551
|
+
"get_issue",
|
|
35552
|
+
"list_issue_statuses",
|
|
35553
|
+
"get_issue_status",
|
|
35554
|
+
"list_issue_labels",
|
|
35555
|
+
"list_projects",
|
|
35556
|
+
"get_project",
|
|
35557
|
+
"list_project_labels",
|
|
35558
|
+
"list_release_pipelines",
|
|
35559
|
+
"list_releases",
|
|
35560
|
+
"get_release",
|
|
35561
|
+
"list_release_notes",
|
|
35562
|
+
"get_release_note",
|
|
35563
|
+
"list_milestones",
|
|
35564
|
+
"get_milestone",
|
|
35565
|
+
"get_notifications",
|
|
35566
|
+
"list_teams",
|
|
35567
|
+
"get_team",
|
|
35568
|
+
"get_triage_responsibility",
|
|
35569
|
+
"list_templates",
|
|
35570
|
+
"get_template",
|
|
35571
|
+
"list_users",
|
|
35572
|
+
"get_user",
|
|
35573
|
+
"get_workspace",
|
|
35574
|
+
"search_documentation",
|
|
35575
|
+
"get_diff",
|
|
35576
|
+
"list_diffs",
|
|
35577
|
+
"get_diff_threads",
|
|
35578
|
+
"list_initiatives",
|
|
35579
|
+
"get_initiative",
|
|
35580
|
+
"list_initiative_labels",
|
|
35581
|
+
"get_status_updates"
|
|
35582
|
+
]),
|
|
35583
|
+
scopes: Object.freeze(["read"]),
|
|
35584
|
+
issuer: "https://mcp.linear.app",
|
|
35585
|
+
authorizationEndpoint: "https://mcp.linear.app/authorize",
|
|
35586
|
+
tokenEndpoint: "https://mcp.linear.app/token",
|
|
35587
|
+
revocationEndpoint: "https://mcp.linear.app/token",
|
|
35588
|
+
docs: Object.freeze({
|
|
35589
|
+
host: "https://linear.app/docs/mcp",
|
|
35590
|
+
readOnlyUrl: "https://linear.app/docs/mcp",
|
|
35591
|
+
readTools: "https://linear.app/docs/mcp",
|
|
35592
|
+
scopes: "https://linear.app/docs/mcp",
|
|
35593
|
+
issuer: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35594
|
+
authorizationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35595
|
+
tokenEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
|
|
35596
|
+
revocationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server"
|
|
35597
|
+
})
|
|
35598
|
+
});
|
|
35599
|
+
var MCP_CATALOG = Object.freeze({ linear: LINEAR });
|
|
35600
|
+
|
|
35208
35601
|
// src/grants/manifest.ts
|
|
35209
35602
|
var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
|
|
35210
35603
|
var MANIFEST_KNOWN_TOOLS = Object.freeze({
|
|
@@ -35230,9 +35623,9 @@ function wellTyped(v) {
|
|
|
35230
35623
|
return false;
|
|
35231
35624
|
}
|
|
35232
35625
|
var WRAPPER_KEYS = ["algo", "manifest", "signature"];
|
|
35233
|
-
var
|
|
35234
|
-
var
|
|
35235
|
-
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") &&
|
|
35626
|
+
var HEX642 = /^[0-9a-f]{64}$/;
|
|
35627
|
+
var PRINTABLE_ASCII2 = /^[\x21-\x7e]+$/;
|
|
35628
|
+
var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII2.test(v);
|
|
35236
35629
|
var exactKeys = (o, required, optional = []) => required.every((k) => k in o) && Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
|
|
35237
35630
|
var isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
35238
35631
|
function parseEndpoints(v) {
|
|
@@ -35244,7 +35637,7 @@ function parseEndpoints(v) {
|
|
|
35244
35637
|
const e = { ...raw };
|
|
35245
35638
|
if (e.headersFile === "") delete e.headersFile;
|
|
35246
35639
|
if (!exactKeys(e, ["baseUrl"], ["headersFile"])) return null;
|
|
35247
|
-
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !
|
|
35640
|
+
if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII2.test(e.baseUrl)) return null;
|
|
35248
35641
|
let url;
|
|
35249
35642
|
try {
|
|
35250
35643
|
url = new URL(e.baseUrl);
|
|
@@ -35275,8 +35668,8 @@ function parseDatabases(v) {
|
|
|
35275
35668
|
function parseConfig(v) {
|
|
35276
35669
|
if (!isPlainObject(v)) return null;
|
|
35277
35670
|
if (!exactKeys(v, ["persona_sha256", "flags", "memory", "legacy_assign_sha256"], ["llm", "search", "tools_allow"])) return null;
|
|
35278
|
-
if (typeof v.persona_sha256 !== "string" || !
|
|
35279
|
-
if (typeof v.legacy_assign_sha256 !== "string" || !
|
|
35671
|
+
if (typeof v.persona_sha256 !== "string" || !HEX642.test(v.persona_sha256)) return null;
|
|
35672
|
+
if (typeof v.legacy_assign_sha256 !== "string" || !HEX642.test(v.legacy_assign_sha256)) return null;
|
|
35280
35673
|
if (!isStringArray(v.flags)) return null;
|
|
35281
35674
|
if ("tools_allow" in v && !isStringArray(v.tools_allow)) return null;
|
|
35282
35675
|
if ("llm" in v) {
|
|
@@ -35320,7 +35713,7 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35320
35713
|
const m = raw.manifest;
|
|
35321
35714
|
if (!isPlainObject(m)) return { state: "malformed" };
|
|
35322
35715
|
if (m.type !== "securecoms.tool-grants" || m.v !== 1) return { state: "malformed" };
|
|
35323
|
-
if (!wellTyped(m)) return { state: "malformed" };
|
|
35716
|
+
if (!wellTyped(Object.fromEntries(Object.entries(m).filter(([k]) => k !== "mcp")))) return { state: "malformed" };
|
|
35324
35717
|
if (typeof m.agent_identity_id !== "string" || typeof m.owner_identity_id !== "string") return { state: "malformed" };
|
|
35325
35718
|
for (const k of ["owner_dm_channel_id", "owner_dm_relay_identity_id"]) {
|
|
35326
35719
|
if (k in m && typeof m[k] !== "string") return { state: "malformed" };
|
|
@@ -35350,6 +35743,11 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35350
35743
|
if (counter === ctx.storedCounter) return { state: "unchanged" };
|
|
35351
35744
|
if (counter < ctx.storedCounter) return { state: "rollback" };
|
|
35352
35745
|
}
|
|
35746
|
+
const rawOwnerDm = m.grants.owner_dm;
|
|
35747
|
+
const signedMcpPresent = Array.isArray(rawOwnerDm) && rawOwnerDm.some((t) => t.startsWith("mcp:"));
|
|
35748
|
+
const mcpKey = "mcp" in m ? "present" : "absent";
|
|
35749
|
+
const mcp = mcpKey === "present" ? parseMcp(m.mcp) : null;
|
|
35750
|
+
const catalog = ctx.mcpCatalog ?? MCP_CATALOG;
|
|
35353
35751
|
let droppedUnknown = false;
|
|
35354
35752
|
const grants = {};
|
|
35355
35753
|
for (const [audience, list] of Object.entries(m.grants)) {
|
|
@@ -35358,7 +35756,7 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35358
35756
|
continue;
|
|
35359
35757
|
}
|
|
35360
35758
|
const known = MANIFEST_KNOWN_TOOLS[audience];
|
|
35361
|
-
const kept = list.filter((t) => known.includes(t));
|
|
35759
|
+
const kept = list.filter((t) => known.includes(t) || audience === "owner_dm" && mcpGrantWithinCeiling(t, mcp, catalog));
|
|
35362
35760
|
if (kept.length !== list.length) droppedUnknown = true;
|
|
35363
35761
|
grants[audience] = Object.freeze(kept);
|
|
35364
35762
|
}
|
|
@@ -35373,7 +35771,10 @@ function verifyToolGrantManifest(raw, ctx) {
|
|
|
35373
35771
|
grants: Object.freeze(grants),
|
|
35374
35772
|
endpoints: Object.freeze(endpoints),
|
|
35375
35773
|
databases: Object.freeze(databases),
|
|
35376
|
-
config
|
|
35774
|
+
config,
|
|
35775
|
+
mcp,
|
|
35776
|
+
mcpKey,
|
|
35777
|
+
signedMcpPresent
|
|
35377
35778
|
});
|
|
35378
35779
|
return { state: "ok", manifest, droppedUnknown, wrapper: raw };
|
|
35379
35780
|
}
|
|
@@ -35694,21 +36095,32 @@ var AGENT_TOTAL_KEY = "agent-total";
|
|
|
35694
36095
|
// src/grants/effective.ts
|
|
35695
36096
|
var TOOL_GRANT_CODE_CEILING = MANIFEST_KNOWN_TOOLS;
|
|
35696
36097
|
var CURRENT_TIME = Object.freeze({ tool: "current_time", mode: "read" });
|
|
36098
|
+
var NO_WITHHELD = Object.freeze([]);
|
|
35697
36099
|
function effectiveToolGrants(i) {
|
|
35698
|
-
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false };
|
|
35699
|
-
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false };
|
|
36100
|
+
if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
36101
|
+
if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
35700
36102
|
const ceiling = TOOL_GRANT_CODE_CEILING[i.audience];
|
|
35701
36103
|
const listed = new Set(i.manifest.grants[i.audience] ?? []);
|
|
35702
36104
|
const signed = ceiling.filter((t) => listed.has(t));
|
|
35703
36105
|
if (!i.eligible || !i.manifestSwitchOn) {
|
|
35704
36106
|
const fallback = i.base.filter((g) => g.tool !== "current_time" && signed.includes(g.tool));
|
|
35705
|
-
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false };
|
|
36107
|
+
return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false, withheld: NO_WITHHELD };
|
|
35706
36108
|
}
|
|
35707
36109
|
let withheld = false;
|
|
36110
|
+
const dropped = [];
|
|
35708
36111
|
const tools = signed.filter((t) => {
|
|
35709
|
-
if (i.disabled.has(t))
|
|
35710
|
-
|
|
35711
|
-
|
|
36112
|
+
if (i.disabled.has(t)) {
|
|
36113
|
+
dropped.push(Object.freeze({ tool: t, why: "disabled_by_owner" }));
|
|
36114
|
+
return false;
|
|
36115
|
+
}
|
|
36116
|
+
if (t === "http_request" && i.signedTargets.endpoints === 0) {
|
|
36117
|
+
dropped.push(Object.freeze({ tool: t, why: "no_endpoint" }));
|
|
36118
|
+
return false;
|
|
36119
|
+
}
|
|
36120
|
+
if (t === "sql_query" && i.signedTargets.databases === 0) {
|
|
36121
|
+
dropped.push(Object.freeze({ tool: t, why: "no_database" }));
|
|
36122
|
+
return false;
|
|
36123
|
+
}
|
|
35712
36124
|
if (i.audience === "team_room" && t === "web_search" && i.roomSearchCapped) {
|
|
35713
36125
|
withheld = true;
|
|
35714
36126
|
return false;
|
|
@@ -35718,7 +36130,8 @@ function effectiveToolGrants(i) {
|
|
|
35718
36130
|
return {
|
|
35719
36131
|
grants: Object.freeze([CURRENT_TIME, ...tools.map((tool) => Object.freeze({ tool, mode: "read", source: "manifest" }))]),
|
|
35720
36132
|
source: "manifest",
|
|
35721
|
-
withheldBySearchCap: withheld
|
|
36133
|
+
withheldBySearchCap: withheld,
|
|
36134
|
+
withheld: Object.freeze(dropped)
|
|
35722
36135
|
};
|
|
35723
36136
|
}
|
|
35724
36137
|
|
|
@@ -36023,6 +36436,200 @@ function classifyVoiceFrame(f) {
|
|
|
36023
36436
|
return b.text.trim() === VOICE_CALL_START_MARKER ? "greeting" : "drop";
|
|
36024
36437
|
}
|
|
36025
36438
|
|
|
36439
|
+
// src/grants/tool-status.ts
|
|
36440
|
+
var TOOL_STATUS_ALLOWLIST = [
|
|
36441
|
+
"current_time",
|
|
36442
|
+
"web_search",
|
|
36443
|
+
"web_extract",
|
|
36444
|
+
"fetch_url",
|
|
36445
|
+
"http_request",
|
|
36446
|
+
"sql_query",
|
|
36447
|
+
"weather_forecast",
|
|
36448
|
+
"currency_convert"
|
|
36449
|
+
];
|
|
36450
|
+
var ALLOW2 = new Set(TOOL_STATUS_ALLOWLIST);
|
|
36451
|
+
var TOOL_STATUS_BUCKET_MS = 9e5;
|
|
36452
|
+
function bucketStartAt(ms) {
|
|
36453
|
+
return new Date(Math.floor(ms / TOOL_STATUS_BUCKET_MS) * TOOL_STATUS_BUCKET_MS).toISOString().slice(0, 16) + "Z";
|
|
36454
|
+
}
|
|
36455
|
+
function bucketEndMs(at) {
|
|
36456
|
+
return Date.parse(at) + TOOL_STATUS_BUCKET_MS;
|
|
36457
|
+
}
|
|
36458
|
+
var SIGNATURE_FAILURES = /* @__PURE__ */ new Set(["bad_signature", "malformed", "unknown_algo", "no_signature"]);
|
|
36459
|
+
function ownerDmCause(i) {
|
|
36460
|
+
try {
|
|
36461
|
+
const v = i.verdict;
|
|
36462
|
+
if (v.kind === "failed" && v.reason === "replay") return null;
|
|
36463
|
+
if (v.kind !== "verified" && v.kind !== "constants_only" && v.kind !== "failed") return null;
|
|
36464
|
+
const failedReason = v.kind === "failed" ? v.reason : void 0;
|
|
36465
|
+
const blocked = i.killSwitchOff || i.pinMissingOrAnomalous || i.senderNotOwner || i.stale || v.kind === "failed";
|
|
36466
|
+
const eligible = v.kind === "verified" && i.signedDmChannel === i.channelId && i.boundNow && i.consumerAttached;
|
|
36467
|
+
const source = blocked ? "none" : !i.manifestPresent ? "constant" : !eligible || i.manifestSwitchOff ? "fallback" : "manifest";
|
|
36468
|
+
const at = (cause, sub) => sub === void 0 ? { source, cause } : { source, cause, sub };
|
|
36469
|
+
if (i.killSwitchOff) return at("owner_dm_unavailable", "kill_switch");
|
|
36470
|
+
if (i.manifestSwitchOff) return at("manifest_switch_off");
|
|
36471
|
+
if (i.pinMissingOrAnomalous) return at("owner_dm_unavailable", "pin");
|
|
36472
|
+
if (i.senderNotOwner) return at("owner_dm_unavailable", "sender_not_owner");
|
|
36473
|
+
if (i.stale) return at("owner_dm_unavailable", "stale");
|
|
36474
|
+
if (failedReason === "not_owner_dm_leaves") return at("unexpected_member_in_dm");
|
|
36475
|
+
if (failedReason !== void 0 && SIGNATURE_FAILURES.has(failedReason)) return at("owner_turn_failed");
|
|
36476
|
+
if (failedReason === "verify_error") return at("agent_state_error");
|
|
36477
|
+
if (failedReason === "wrong_dm_channel" || v.kind === "verified" && i.manifestPresent && i.signedDmChannel !== void 0 && i.signedDmChannel !== i.channelId) {
|
|
36478
|
+
return at("dm_channel_mismatch");
|
|
36479
|
+
}
|
|
36480
|
+
if (failedReason !== void 0) return null;
|
|
36481
|
+
if (i.manifestPresent && i.signedDmChannel === void 0) return at("dm_not_linked");
|
|
36482
|
+
if (v.kind === "constants_only") return at("unsigned_relay_in_dm");
|
|
36483
|
+
if (!i.manifestPresent || !i.boundNow) return at("manifest_not_bound");
|
|
36484
|
+
if (!i.consumerAttached) return at("no_runtime_consumer");
|
|
36485
|
+
return source === "manifest" ? { source } : null;
|
|
36486
|
+
} catch {
|
|
36487
|
+
return null;
|
|
36488
|
+
}
|
|
36489
|
+
}
|
|
36490
|
+
function sameTuple(a, b) {
|
|
36491
|
+
if (!a || !b) return false;
|
|
36492
|
+
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);
|
|
36493
|
+
}
|
|
36494
|
+
function sanitizeIssued(names) {
|
|
36495
|
+
return [...new Set(names.filter((n) => ALLOW2.has(n)))].sort();
|
|
36496
|
+
}
|
|
36497
|
+
|
|
36498
|
+
// src/tool-status-sender.ts
|
|
36499
|
+
var TOOL_STATUS_JITTER_MS = 6e4;
|
|
36500
|
+
var RETRY_AFTER_FLOOR_MS = 6e4;
|
|
36501
|
+
function defaultToolStatusSetTimer(fn, ms) {
|
|
36502
|
+
const t = setTimeout(fn, ms);
|
|
36503
|
+
t.unref?.();
|
|
36504
|
+
return t;
|
|
36505
|
+
}
|
|
36506
|
+
function defaultToolStatusClearTimer(h) {
|
|
36507
|
+
clearTimeout(h);
|
|
36508
|
+
}
|
|
36509
|
+
function retryAfterMs(raw) {
|
|
36510
|
+
if (typeof raw !== "string" || !/^\d{1,9}$/.test(raw.trim())) return RETRY_AFTER_FLOOR_MS;
|
|
36511
|
+
return Math.max(RETRY_AFTER_FLOOR_MS, Number(raw.trim()) * 1e3);
|
|
36512
|
+
}
|
|
36513
|
+
var ToolStatusSender = class {
|
|
36514
|
+
postFn;
|
|
36515
|
+
now;
|
|
36516
|
+
setTimer;
|
|
36517
|
+
clearTimer;
|
|
36518
|
+
random;
|
|
36519
|
+
onFailure;
|
|
36520
|
+
/** The latest noted record while its bucket is still open. */
|
|
36521
|
+
open = null;
|
|
36522
|
+
/** The latest noted record whose bucket has closed: the only record an attempt or carry() may send. */
|
|
36523
|
+
closed = null;
|
|
36524
|
+
lastPosted = null;
|
|
36525
|
+
terminalFor = null;
|
|
36526
|
+
notBefore = Number.NEGATIVE_INFINITY;
|
|
36527
|
+
timer = null;
|
|
36528
|
+
inFlight = false;
|
|
36529
|
+
shut = false;
|
|
36530
|
+
constructor(o) {
|
|
36531
|
+
this.postFn = o.post;
|
|
36532
|
+
this.now = o.now ?? (() => Date.now());
|
|
36533
|
+
this.setTimer = o.setTimer ?? defaultToolStatusSetTimer;
|
|
36534
|
+
this.clearTimer = o.clearTimer ?? defaultToolStatusClearTimer;
|
|
36535
|
+
this.random = o.random ?? Math.random;
|
|
36536
|
+
this.onFailure = o.onFailure ?? (() => {
|
|
36537
|
+
});
|
|
36538
|
+
}
|
|
36539
|
+
note(rec) {
|
|
36540
|
+
if (this.shut) return;
|
|
36541
|
+
try {
|
|
36542
|
+
this.promote(this.now());
|
|
36543
|
+
this.open = rec;
|
|
36544
|
+
this.schedule();
|
|
36545
|
+
} catch {
|
|
36546
|
+
}
|
|
36547
|
+
}
|
|
36548
|
+
/** Ruling M-2: what the apply-time report carries. The closed record only; an open bucket is never revealed early. */
|
|
36549
|
+
carry() {
|
|
36550
|
+
try {
|
|
36551
|
+
this.promote(this.now());
|
|
36552
|
+
} catch {
|
|
36553
|
+
}
|
|
36554
|
+
return this.closed;
|
|
36555
|
+
}
|
|
36556
|
+
/** R2-M1: the record belongs to an owner who is no longer the manifest's; forget it and what was posted. */
|
|
36557
|
+
drop() {
|
|
36558
|
+
this.open = null;
|
|
36559
|
+
this.closed = null;
|
|
36560
|
+
this.lastPosted = null;
|
|
36561
|
+
this.terminalFor = null;
|
|
36562
|
+
this.cancelTimer();
|
|
36563
|
+
}
|
|
36564
|
+
close() {
|
|
36565
|
+
this.shut = true;
|
|
36566
|
+
this.cancelTimer();
|
|
36567
|
+
}
|
|
36568
|
+
cancelTimer() {
|
|
36569
|
+
if (this.timer !== null) this.clearTimer(this.timer);
|
|
36570
|
+
this.timer = null;
|
|
36571
|
+
}
|
|
36572
|
+
promote(t) {
|
|
36573
|
+
if (this.open !== null && bucketEndMs(this.open.at) <= t) {
|
|
36574
|
+
this.closed = this.open;
|
|
36575
|
+
this.open = null;
|
|
36576
|
+
}
|
|
36577
|
+
}
|
|
36578
|
+
pending() {
|
|
36579
|
+
return this.closed !== null && !sameTuple(this.closed, this.lastPosted) && !sameTuple(this.closed, this.terminalFor);
|
|
36580
|
+
}
|
|
36581
|
+
schedule() {
|
|
36582
|
+
if (this.shut || this.inFlight || this.timer !== null) return;
|
|
36583
|
+
if (this.open === null && !this.pending()) return;
|
|
36584
|
+
const t = this.now();
|
|
36585
|
+
let b = (Math.floor(t / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
|
|
36586
|
+
while (b < this.notBefore) b += TOOL_STATUS_BUCKET_MS;
|
|
36587
|
+
const jitter = Math.floor(this.random() * TOOL_STATUS_JITTER_MS);
|
|
36588
|
+
this.timer = this.setTimer(() => {
|
|
36589
|
+
this.timer = null;
|
|
36590
|
+
this.fire();
|
|
36591
|
+
}, b - t + jitter);
|
|
36592
|
+
}
|
|
36593
|
+
fire() {
|
|
36594
|
+
try {
|
|
36595
|
+
if (this.shut) return;
|
|
36596
|
+
this.promote(this.now());
|
|
36597
|
+
if (this.pending()) {
|
|
36598
|
+
void this.attempt(this.closed);
|
|
36599
|
+
return;
|
|
36600
|
+
}
|
|
36601
|
+
this.schedule();
|
|
36602
|
+
} catch {
|
|
36603
|
+
}
|
|
36604
|
+
}
|
|
36605
|
+
async attempt(rec) {
|
|
36606
|
+
this.inFlight = true;
|
|
36607
|
+
const attemptAt = this.now();
|
|
36608
|
+
let result;
|
|
36609
|
+
try {
|
|
36610
|
+
result = await this.postFn(rec);
|
|
36611
|
+
} catch {
|
|
36612
|
+
result = "network";
|
|
36613
|
+
}
|
|
36614
|
+
this.inFlight = false;
|
|
36615
|
+
try {
|
|
36616
|
+
if (typeof result === "object" && result.status >= 200 && result.status < 300) {
|
|
36617
|
+
this.lastPosted = rec;
|
|
36618
|
+
} else if (typeof result === "object" && result.status === 429) {
|
|
36619
|
+
this.notBefore = attemptAt + retryAfterMs(result.retryAfter);
|
|
36620
|
+
this.onFailure("http_error");
|
|
36621
|
+
} else if (typeof result === "object" && result.status >= 400 && result.status < 500) {
|
|
36622
|
+
this.terminalFor = rec;
|
|
36623
|
+
this.onFailure("http_error");
|
|
36624
|
+
} else {
|
|
36625
|
+
this.onFailure(typeof result === "object" ? "http_error" : result);
|
|
36626
|
+
}
|
|
36627
|
+
this.schedule();
|
|
36628
|
+
} catch {
|
|
36629
|
+
}
|
|
36630
|
+
}
|
|
36631
|
+
};
|
|
36632
|
+
|
|
36026
36633
|
// src/grants/owner-turn.ts
|
|
36027
36634
|
import { ed25519 as ed255196 } from "@noble/curves/ed25519";
|
|
36028
36635
|
var HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
@@ -36066,6 +36673,310 @@ function classifyOwnerDmLeaves(leaves, allowed) {
|
|
|
36066
36673
|
}
|
|
36067
36674
|
return "foreign";
|
|
36068
36675
|
}
|
|
36676
|
+
function ownerDmLeavesRelayFree(leaves, ids) {
|
|
36677
|
+
let owner = false;
|
|
36678
|
+
let agent = false;
|
|
36679
|
+
for (const l of leaves) {
|
|
36680
|
+
if (!l.basic || l.identity === null) return false;
|
|
36681
|
+
if (l.identity === ids.ownerIdentityId) owner = true;
|
|
36682
|
+
else if (l.identity === ids.agentIdentityId) agent = true;
|
|
36683
|
+
else return false;
|
|
36684
|
+
}
|
|
36685
|
+
return owner && agent;
|
|
36686
|
+
}
|
|
36687
|
+
function relayLeafFromPhase0Relay(relay) {
|
|
36688
|
+
return relay !== "none";
|
|
36689
|
+
}
|
|
36690
|
+
function ownerDmRelayOf(leafClass, leaves, ids) {
|
|
36691
|
+
if (leafClass === "unsigned_relay") return "unsigned";
|
|
36692
|
+
if (ownerDmLeavesRelayFree(leaves, ids)) return "none";
|
|
36693
|
+
if (ids.relayIdentityId !== void 0 && leaves.some((l) => l.identity === ids.relayIdentityId)) return "signed";
|
|
36694
|
+
return void 0;
|
|
36695
|
+
}
|
|
36696
|
+
|
|
36697
|
+
// src/grants/mcp-policy.ts
|
|
36698
|
+
var MCP_POLICY_WRITER_FEATURE = "mcp_policy_writer_v1";
|
|
36699
|
+
function mcpEntitlementReading(mcpEntitled, mcpCause) {
|
|
36700
|
+
if (mcpEntitled === true && mcpCause === "entitled") return "entitled";
|
|
36701
|
+
if (mcpEntitled === false && mcpCause === "not_entitled") return "not_entitled";
|
|
36702
|
+
return "suspended";
|
|
36703
|
+
}
|
|
36704
|
+
function mcpGrantsHostSwitch(env, log = writeServiceLog2) {
|
|
36705
|
+
const v = env.SECURECOMS_MCP_GRANTS;
|
|
36706
|
+
if (v === void 0 || v === "on") return "on";
|
|
36707
|
+
if (v === "off") return "off";
|
|
36708
|
+
log("warn", "config.mcp_grants_switch_suspended", { state: ServiceLogEnum.of("suspended", ["suspended"]) });
|
|
36709
|
+
return "suspended";
|
|
36710
|
+
}
|
|
36711
|
+
var empty = (state, r3Hold) => Object.freeze({ v: 1, state, r3_hold: r3Hold, servers: Object.freeze({}), remove: NO_REMOVE });
|
|
36712
|
+
var NO_REMOVE = Object.freeze([]);
|
|
36713
|
+
function policyAuth(s) {
|
|
36714
|
+
if (s.auth.mode === "key") return Object.freeze({ mode: "key" });
|
|
36715
|
+
const clientId = s.auth.client === "customer" ? s.auth.client_id : SECURECOMS_CIMD_CLIENT_ID;
|
|
36716
|
+
return Object.freeze({ mode: "oauth", issuer: s.auth.issuer, client: s.auth.client, client_id: clientId });
|
|
36717
|
+
}
|
|
36718
|
+
function mcpPolicyFor(i) {
|
|
36719
|
+
const m = i.manifest;
|
|
36720
|
+
const r3Hold = m?.signedMcpPresent === true || i.mcpSeen;
|
|
36721
|
+
if (i.hostSwitch === "off") return empty("kill_switch", r3Hold);
|
|
36722
|
+
if (i.hostSwitch !== "on") return empty("suspended", r3Hold);
|
|
36723
|
+
if (!i.config.fetched) return empty("no_config", r3Hold);
|
|
36724
|
+
const ent = mcpEntitlementReading(i.config.mcp_entitled, i.config.mcp_cause);
|
|
36725
|
+
if (ent !== "entitled") return empty(ent, r3Hold);
|
|
36726
|
+
if (!i.ownerDmManifestSwitchOn) return empty("switch_off", r3Hold);
|
|
36727
|
+
if (m === null) return empty("no_manifest", r3Hold);
|
|
36728
|
+
if (m.mcpKey === "absent") return empty("section_absent", r3Hold);
|
|
36729
|
+
if (m.mcp === null) return empty("malformed", r3Hold);
|
|
36730
|
+
if (!i.capability) return empty("no_capability", r3Hold);
|
|
36731
|
+
const catalog = i.catalog ?? MCP_CATALOG;
|
|
36732
|
+
const granted = /* @__PURE__ */ new Map();
|
|
36733
|
+
for (const name of m.grants.owner_dm ?? []) {
|
|
36734
|
+
const g = parseMcpGrant(name);
|
|
36735
|
+
if (g === null) continue;
|
|
36736
|
+
if (!granted.has(g.serverId)) granted.set(g.serverId, /* @__PURE__ */ new Set());
|
|
36737
|
+
granted.get(g.serverId).add(g.tool);
|
|
36738
|
+
}
|
|
36739
|
+
const servers = {};
|
|
36740
|
+
for (const id of Object.keys(m.mcp.servers).sort()) {
|
|
36741
|
+
const s = m.mcp.servers[id];
|
|
36742
|
+
const row = mcpServerCatalogRow(s, catalog);
|
|
36743
|
+
if (row === null) {
|
|
36744
|
+
servers[id] = Object.freeze({ state: "refused" });
|
|
36745
|
+
continue;
|
|
36746
|
+
}
|
|
36747
|
+
const tools = {};
|
|
36748
|
+
for (const t of Object.keys(s.tools).sort()) {
|
|
36749
|
+
if (granted.get(id)?.has(t) === true && row.readTools.includes(t)) tools[t] = Object.freeze({ sha256: s.tools[t].sha256, class: "read" });
|
|
36750
|
+
}
|
|
36751
|
+
servers[id] = Object.freeze({ state: "ok", vendor: s.vendor, url: row.readOnlyUrl, auth: policyAuth(s), tools: Object.freeze(tools) });
|
|
36752
|
+
}
|
|
36753
|
+
const remove = m.mcp.removed === "*" ? "*" : Object.freeze([...m.mcp.removed]);
|
|
36754
|
+
return Object.freeze({ v: 1, state: "ok", r3_hold: r3Hold, servers: Object.freeze(servers), remove });
|
|
36755
|
+
}
|
|
36756
|
+
|
|
36757
|
+
// src/grants/mcp-issuance.ts
|
|
36758
|
+
var NONE = Object.freeze([]);
|
|
36759
|
+
var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
36760
|
+
function mcpPolicyAllows(policy, serverId, tool) {
|
|
36761
|
+
if (policy === null || policy.state !== "ok" || !own(policy.servers, serverId)) return false;
|
|
36762
|
+
const s = policy.servers[serverId];
|
|
36763
|
+
return s.state === "ok" && own(s.tools, tool);
|
|
36764
|
+
}
|
|
36765
|
+
function mcpIssuance(i) {
|
|
36766
|
+
if (i.source !== "manifest") return NONE;
|
|
36767
|
+
if (i.verdict?.kind !== "verified" || i.verdict.relayLeaf !== false) return NONE;
|
|
36768
|
+
if (i.entitlement !== "entitled" || i.hostSwitch !== "on" || !i.ownerDmManifestSwitchOn) return NONE;
|
|
36769
|
+
if (i.manifest === null || i.policy === null || i.policy.state !== "ok") return NONE;
|
|
36770
|
+
const out = [];
|
|
36771
|
+
for (const name of i.manifest.grants.owner_dm ?? []) {
|
|
36772
|
+
const g = parseMcpGrant(name);
|
|
36773
|
+
if (g === null || i.disabled.has(name) || !mcpPolicyAllows(i.policy, g.serverId, g.tool)) continue;
|
|
36774
|
+
out.push(Object.freeze({ tool: name, mode: "read", source: "manifest" }));
|
|
36775
|
+
}
|
|
36776
|
+
return out.length === 0 ? NONE : Object.freeze(out);
|
|
36777
|
+
}
|
|
36778
|
+
|
|
36779
|
+
// src/consumer-grants.ts
|
|
36780
|
+
var NO_GRANTS = Object.freeze([]);
|
|
36781
|
+
var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
|
|
36782
|
+
function withConsumerGrants(m, perToolConsumer) {
|
|
36783
|
+
if (perToolConsumer) return m;
|
|
36784
|
+
const a = m.authorization;
|
|
36785
|
+
if (!a) return m;
|
|
36786
|
+
const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
|
|
36787
|
+
const stripMemory = "memory_write" in a;
|
|
36788
|
+
if (!stripGrants && !stripMemory) return m;
|
|
36789
|
+
const { memory_write: _dropped, ...rest } = a;
|
|
36790
|
+
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
36791
|
+
}
|
|
36792
|
+
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
36793
|
+
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
36794
|
+
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
36795
|
+
var MCP_CLIENT_FEATURE = "mcp_client_v1";
|
|
36796
|
+
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
36797
|
+
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
36798
|
+
team_room: Object.freeze(["current_time"])
|
|
36799
|
+
});
|
|
36800
|
+
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
36801
|
+
function withManifestCapability(m, manifestConsumer) {
|
|
36802
|
+
if (manifestConsumer) return m;
|
|
36803
|
+
const a = m.authorization;
|
|
36804
|
+
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
36805
|
+
const constants5 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
36806
|
+
const narrowed = a.tool_grants.flatMap((g) => {
|
|
36807
|
+
if (!isManifestGrant(g)) return [g];
|
|
36808
|
+
return constants5.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
36809
|
+
});
|
|
36810
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
36811
|
+
}
|
|
36812
|
+
function withHarnessGrants(m, withheld) {
|
|
36813
|
+
if (!withheld) return m;
|
|
36814
|
+
const a = m.authorization;
|
|
36815
|
+
if (!a || a.tool_grants.length === 0) return m;
|
|
36816
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
36817
|
+
}
|
|
36818
|
+
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
36819
|
+
if (consumerHasFeature) return m;
|
|
36820
|
+
const a = m.authorization;
|
|
36821
|
+
if (!a) return m;
|
|
36822
|
+
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
36823
|
+
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
36824
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
36825
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36826
|
+
}
|
|
36827
|
+
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
36828
|
+
function withUtilityCapability(m, consumerHasFeature) {
|
|
36829
|
+
if (consumerHasFeature) return m;
|
|
36830
|
+
const a = m.authorization;
|
|
36831
|
+
if (!a) return m;
|
|
36832
|
+
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
36833
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
36834
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36835
|
+
}
|
|
36836
|
+
var MCP_GRANT_PREFIX = "mcp:";
|
|
36837
|
+
var isMcpGrant = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
|
|
36838
|
+
function withMcpCapability(m, consumerHasFeature) {
|
|
36839
|
+
const a = m.authorization;
|
|
36840
|
+
if (!a) return m;
|
|
36841
|
+
const { relay_free: _relayFree, ...rest } = a;
|
|
36842
|
+
if (!a.tool_grants.some(isMcpGrant)) return "relay_free" in a ? { ...m, authorization: Object.freeze(rest) } : m;
|
|
36843
|
+
if (consumerHasFeature && a.audience === "owner_dm" && a.relay_speaker === void 0) return m;
|
|
36844
|
+
const kept = a.tool_grants.filter((g) => !isMcpGrant(g));
|
|
36845
|
+
return { ...m, authorization: Object.freeze({ ...rest, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36846
|
+
}
|
|
36847
|
+
var R3_KEEP_TOOLS = /* @__PURE__ */ new Set(["current_time", "web_search", "weather_forecast", "currency_convert"]);
|
|
36848
|
+
function withR3Holds(m, hold) {
|
|
36849
|
+
if (!hold) return m;
|
|
36850
|
+
const a = m.authorization;
|
|
36851
|
+
if (!a || !(a.audience === "owner_dm" || a.relay_speaker === "owner")) return m;
|
|
36852
|
+
const kept = a.tool_grants.filter((g) => R3_KEEP_TOOLS.has(g.tool) || isMcpGrant(g));
|
|
36853
|
+
if (kept.length === a.tool_grants.length) return m;
|
|
36854
|
+
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
36855
|
+
}
|
|
36856
|
+
|
|
36857
|
+
// src/mcp-catalog-message.ts
|
|
36858
|
+
var MCP_CATALOG_MAX_BYTES = 384 * 1024;
|
|
36859
|
+
var MCP_CATALOG_MAX_TOOLS = 64;
|
|
36860
|
+
var MCP_CATALOG_LIST_TIMEOUT_MS = 5e4;
|
|
36861
|
+
var REQUEST_ID = /^[0-9a-f]{32}$/;
|
|
36862
|
+
var SERVER_ID = /^[a-z0-9-]{1,24}$/;
|
|
36863
|
+
var TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
36864
|
+
var RESERVED = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
36865
|
+
var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
36866
|
+
var MCP_CATALOG_REASONS = /* @__PURE__ */ new Set(["catalog", "write_name", "hint", "schema", "header_param", "too_large"]);
|
|
36867
|
+
var isBag2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
36868
|
+
var keysExactly2 = (o, keys) => {
|
|
36869
|
+
const k = Object.keys(o);
|
|
36870
|
+
return k.length === keys.length && keys.every((x) => Object.prototype.hasOwnProperty.call(o, x));
|
|
36871
|
+
};
|
|
36872
|
+
function parseMcpCatalogRequest(body) {
|
|
36873
|
+
if (!isBag2(body) || !keysExactly2(body, ["v", "type", "server_id", "request_id"])) return null;
|
|
36874
|
+
if (body.v !== 1 || body.type !== "mcp_catalog_request") return null;
|
|
36875
|
+
const { server_id: serverId, request_id: requestId } = body;
|
|
36876
|
+
if (typeof serverId !== "string" || !SERVER_ID.test(serverId) || RESERVED.has(serverId)) return null;
|
|
36877
|
+
if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) return null;
|
|
36878
|
+
return Object.freeze({ serverId, requestId });
|
|
36879
|
+
}
|
|
36880
|
+
var PROJECTION_KEYS = /* @__PURE__ */ new Set(["name", "title", "description", "inputSchema", "annotations"]);
|
|
36881
|
+
function validProjection(p, toolName) {
|
|
36882
|
+
if (!Object.keys(p).every((k) => PROJECTION_KEYS.has(k))) return false;
|
|
36883
|
+
if (p.name !== toolName || typeof p.description !== "string" || !isBag2(p.inputSchema)) return false;
|
|
36884
|
+
if (Object.prototype.hasOwnProperty.call(p, "title") && typeof p.title !== "string") return false;
|
|
36885
|
+
if (Object.prototype.hasOwnProperty.call(p, "annotations") && !isBag2(p.annotations)) return false;
|
|
36886
|
+
return true;
|
|
36887
|
+
}
|
|
36888
|
+
function validTool(t) {
|
|
36889
|
+
if (!isBag2(t) || typeof t.name !== "string" || typeof t.grantable !== "boolean") return null;
|
|
36890
|
+
if (t.grantable) {
|
|
36891
|
+
if (!TOOL_NAME.test(t.name) || t.reason !== null || !isBag2(t.projection) || !validProjection(t.projection, t.name)) return null;
|
|
36892
|
+
return { name: t.name, projection: t.projection, grantable: true, reason: null };
|
|
36893
|
+
}
|
|
36894
|
+
if (t.projection !== void 0 || typeof t.reason !== "string" || !MCP_CATALOG_REASONS.has(t.reason)) return null;
|
|
36895
|
+
if (t.name.length < 1 || t.name.length > 64 || !/^[\x20-\x7e]+$/.test(t.name)) return null;
|
|
36896
|
+
return { name: t.name, grantable: false, reason: t.reason };
|
|
36897
|
+
}
|
|
36898
|
+
function validateCatalogListReply(frame, serverId, vendor) {
|
|
36899
|
+
if (!isBag2(frame) || frame.ok !== true || frame.server_id !== serverId || frame.vendor !== vendor) return null;
|
|
36900
|
+
if (typeof frame.listed_at !== "string" || !ISO_TIME.test(frame.listed_at) || typeof frame.truncated !== "boolean") return null;
|
|
36901
|
+
if (!Array.isArray(frame.tools) || frame.tools.length > MCP_CATALOG_MAX_TOOLS) return null;
|
|
36902
|
+
const tools = [];
|
|
36903
|
+
const names = /* @__PURE__ */ new Set();
|
|
36904
|
+
for (const t of frame.tools) {
|
|
36905
|
+
const v = validTool(t);
|
|
36906
|
+
if (v === null) return null;
|
|
36907
|
+
if (names.has(v.name)) return null;
|
|
36908
|
+
names.add(v.name);
|
|
36909
|
+
tools.push(v);
|
|
36910
|
+
}
|
|
36911
|
+
return { listedAt: frame.listed_at, truncated: frame.truncated, tools };
|
|
36912
|
+
}
|
|
36913
|
+
var utf8 = (v) => Buffer.byteLength(JSON.stringify(v), "utf8");
|
|
36914
|
+
var mcpCatalogBodyBytes = (b) => utf8(b);
|
|
36915
|
+
function buildMcpCatalogBody(i) {
|
|
36916
|
+
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 };
|
|
36917
|
+
const full = { ...head, tools: [...i.reply.tools], truncated: i.reply.truncated };
|
|
36918
|
+
if (mcpCatalogBodyBytes(full) <= MCP_CATALOG_MAX_BYTES) return full;
|
|
36919
|
+
const stub = (t) => ({ name: t.name, grantable: false, reason: "too_large" });
|
|
36920
|
+
const tools = i.reply.tools.map(stub);
|
|
36921
|
+
const body = { ...head, tools, truncated: true };
|
|
36922
|
+
let size = mcpCatalogBodyBytes(body);
|
|
36923
|
+
for (let k = 0; k < tools.length; k++) {
|
|
36924
|
+
const want = i.reply.tools[k];
|
|
36925
|
+
const delta = utf8(want) - utf8(tools[k]);
|
|
36926
|
+
if (size + delta <= MCP_CATALOG_MAX_BYTES) {
|
|
36927
|
+
tools[k] = want;
|
|
36928
|
+
size += delta;
|
|
36929
|
+
}
|
|
36930
|
+
}
|
|
36931
|
+
return body;
|
|
36932
|
+
}
|
|
36933
|
+
|
|
36934
|
+
// src/mcp-catalog-rate.ts
|
|
36935
|
+
var MINUTE_MS = 6e4;
|
|
36936
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
36937
|
+
var MCP_CATALOG_PER_SERVER_MINUTE = 3;
|
|
36938
|
+
var MCP_CATALOG_PER_SERVER_HOUR = 20;
|
|
36939
|
+
var MCP_CATALOG_PER_AGENT_HOUR = 40;
|
|
36940
|
+
var McpCatalogRateLimiter = class {
|
|
36941
|
+
now;
|
|
36942
|
+
onRateLimited;
|
|
36943
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
36944
|
+
perServer = /* @__PURE__ */ new Map();
|
|
36945
|
+
agent = [];
|
|
36946
|
+
/** Per scope key: no further log until this time (the end of the window that was full when it last logged). */
|
|
36947
|
+
quietUntil = /* @__PURE__ */ new Map();
|
|
36948
|
+
constructor(deps = {}) {
|
|
36949
|
+
this.now = deps.now ?? Date.now;
|
|
36950
|
+
this.onRateLimited = deps.onRateLimited ?? (() => {
|
|
36951
|
+
});
|
|
36952
|
+
}
|
|
36953
|
+
/** "ok" admits (and counts) the request and marks the server in flight; the caller MUST call release(serverId). */
|
|
36954
|
+
admit(serverId) {
|
|
36955
|
+
if (this.inFlight.has(serverId)) return "in_flight";
|
|
36956
|
+
const t = this.now();
|
|
36957
|
+
const server = (this.perServer.get(serverId) ?? []).filter((x) => t - x < HOUR_MS);
|
|
36958
|
+
this.perServer.set(serverId, server);
|
|
36959
|
+
this.agent = this.agent.filter((x) => t - x < HOUR_MS);
|
|
36960
|
+
const lastMinute = server.filter((x) => t - x < MINUTE_MS);
|
|
36961
|
+
if (lastMinute.length >= MCP_CATALOG_PER_SERVER_MINUTE) return this.limited(`m:${serverId}`, "server_minute", lastMinute[0] + MINUTE_MS, t);
|
|
36962
|
+
if (server.length >= MCP_CATALOG_PER_SERVER_HOUR) return this.limited(`h:${serverId}`, "server_hour", server[0] + HOUR_MS, t);
|
|
36963
|
+
if (this.agent.length >= MCP_CATALOG_PER_AGENT_HOUR) return this.limited("a", "agent_hour", this.agent[0] + HOUR_MS, t);
|
|
36964
|
+
server.push(t);
|
|
36965
|
+
this.agent.push(t);
|
|
36966
|
+
this.inFlight.add(serverId);
|
|
36967
|
+
return "ok";
|
|
36968
|
+
}
|
|
36969
|
+
release(serverId) {
|
|
36970
|
+
this.inFlight.delete(serverId);
|
|
36971
|
+
}
|
|
36972
|
+
limited(key, scope, windowEnd, t) {
|
|
36973
|
+
if (t >= (this.quietUntil.get(key) ?? -Infinity)) {
|
|
36974
|
+
this.quietUntil.set(key, windowEnd);
|
|
36975
|
+
this.onRateLimited(scope);
|
|
36976
|
+
}
|
|
36977
|
+
return "rate_limited";
|
|
36978
|
+
}
|
|
36979
|
+
};
|
|
36069
36980
|
|
|
36070
36981
|
// src/bridge-agent.ts
|
|
36071
36982
|
function logSendSupersededAdoption(kind2) {
|
|
@@ -36205,6 +37116,7 @@ function selectVoiceGuidance(isDm, voiceDefaultResponder) {
|
|
|
36205
37116
|
}
|
|
36206
37117
|
return VOICE_MODE_GUIDANCE;
|
|
36207
37118
|
}
|
|
37119
|
+
var MCP_INERT_STATES = /* @__PURE__ */ new Set(["suspended", "no_config", "switch_off", "no_manifest", "section_absent", "malformed", "no_capability"]);
|
|
36208
37120
|
var VOICE_COALESCE_WINDOW_MS = 0;
|
|
36209
37121
|
var VOICE_COALESCE_WINDOW_MAX_MS = 5e3;
|
|
36210
37122
|
function voiceCoalesceWindowFromEnv(env = process.env) {
|
|
@@ -36217,8 +37129,8 @@ function voiceWindowEligible(t) {
|
|
|
36217
37129
|
}
|
|
36218
37130
|
function waitMs(ms) {
|
|
36219
37131
|
if (ms <= 0) return Promise.resolve();
|
|
36220
|
-
return new Promise((
|
|
36221
|
-
const t = setTimeout(
|
|
37132
|
+
return new Promise((resolve2) => {
|
|
37133
|
+
const t = setTimeout(resolve2, ms);
|
|
36222
37134
|
t.unref?.();
|
|
36223
37135
|
});
|
|
36224
37136
|
}
|
|
@@ -36255,10 +37167,10 @@ var ChannelPendingError = class extends Error {
|
|
|
36255
37167
|
}
|
|
36256
37168
|
};
|
|
36257
37169
|
var RateLimitedError = class extends Error {
|
|
36258
|
-
constructor(channelId,
|
|
36259
|
-
super(`channel ${channelId} send rate limit exceeded (retry in ${
|
|
37170
|
+
constructor(channelId, retryAfterMs2) {
|
|
37171
|
+
super(`channel ${channelId} send rate limit exceeded (retry in ${retryAfterMs2}ms)`);
|
|
36260
37172
|
this.channelId = channelId;
|
|
36261
|
-
this.retryAfterMs =
|
|
37173
|
+
this.retryAfterMs = retryAfterMs2;
|
|
36262
37174
|
this.name = "RateLimitedError";
|
|
36263
37175
|
}
|
|
36264
37176
|
};
|
|
@@ -36355,7 +37267,7 @@ var FileOwnerAuthorityStore = class {
|
|
|
36355
37267
|
}
|
|
36356
37268
|
async readAll() {
|
|
36357
37269
|
try {
|
|
36358
|
-
return JSON.parse(await
|
|
37270
|
+
return JSON.parse(await fsp2.readFile(this.path, "utf8"));
|
|
36359
37271
|
} catch (e) {
|
|
36360
37272
|
if (e.code === "ENOENT") return {};
|
|
36361
37273
|
throw e;
|
|
@@ -36368,8 +37280,8 @@ var FileOwnerAuthorityStore = class {
|
|
|
36368
37280
|
async save(enclaveId, pin) {
|
|
36369
37281
|
const all = await this.readAll();
|
|
36370
37282
|
all[enclaveId] = pin;
|
|
36371
|
-
await
|
|
36372
|
-
await
|
|
37283
|
+
await fsp2.mkdir(dirname6(this.path), { recursive: true });
|
|
37284
|
+
await fsp2.writeFile(this.path, JSON.stringify(all), "utf8");
|
|
36373
37285
|
}
|
|
36374
37286
|
};
|
|
36375
37287
|
var AgentPausedSendError = class extends Error {
|
|
@@ -36386,6 +37298,31 @@ var SendForbiddenError = class extends Error {
|
|
|
36386
37298
|
this.name = "SendForbiddenError";
|
|
36387
37299
|
}
|
|
36388
37300
|
};
|
|
37301
|
+
var MCP_POLICY_RETRY_DELAYS_MS = Object.freeze([1e3, 5e3, 3e4, 12e4, 3e5]);
|
|
37302
|
+
function logMcpCatalogRequest(state) {
|
|
37303
|
+
writeServiceLog2(state === "sent" ? "info" : "warn", "mcp.catalog_request", { state: ServiceLogEnum.of(state, [
|
|
37304
|
+
"malformed",
|
|
37305
|
+
"not_owner_dm",
|
|
37306
|
+
"not_verified",
|
|
37307
|
+
"replay",
|
|
37308
|
+
"relay_leaf",
|
|
37309
|
+
"not_ready",
|
|
37310
|
+
"in_flight",
|
|
37311
|
+
"runtime_timeout",
|
|
37312
|
+
"runtime_refused",
|
|
37313
|
+
"withheld",
|
|
37314
|
+
"send_failed",
|
|
37315
|
+
"failed",
|
|
37316
|
+
"sent"
|
|
37317
|
+
]) });
|
|
37318
|
+
}
|
|
37319
|
+
var McpReplyWithheldError = class extends SendForbiddenError {
|
|
37320
|
+
constructor(channelId) {
|
|
37321
|
+
super(channelId);
|
|
37322
|
+
this.name = "McpReplyWithheldError";
|
|
37323
|
+
this.message = "reply withheld: the chat is no longer private to the owner and the agent; no retry";
|
|
37324
|
+
}
|
|
37325
|
+
};
|
|
36389
37326
|
var RoomClassChangedSendError = class extends SendForbiddenError {
|
|
36390
37327
|
constructor(channelId) {
|
|
36391
37328
|
super(channelId);
|
|
@@ -36405,6 +37342,7 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
|
|
|
36405
37342
|
var RoomGrantEpochMovedError = class extends Error {
|
|
36406
37343
|
};
|
|
36407
37344
|
var ROOM_GRANT_TRACK_MAX = 4096;
|
|
37345
|
+
var MCP_DELIVERED_TRACK_MAX = 4096;
|
|
36408
37346
|
var VOICE_FRAME_PARENT_MAX = 4096;
|
|
36409
37347
|
var SessionSupersededError = class extends Error {
|
|
36410
37348
|
constructor() {
|
|
@@ -36556,8 +37494,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36556
37494
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
|
|
36557
37495
|
});
|
|
36558
37496
|
this.home = deps.home ?? homedir4();
|
|
37497
|
+
this.mcpCatalog = deps.mcpCatalog;
|
|
37498
|
+
this.mcpDeliveredTrackMax = deps.mcpDeliveredTrackMax ?? MCP_DELIVERED_TRACK_MAX;
|
|
37499
|
+
this.mcpPolicyRetryDelaysMs = deps.mcpPolicyRetryDelaysMs?.length ? deps.mcpPolicyRetryDelaysMs : MCP_POLICY_RETRY_DELAYS_MS;
|
|
36559
37500
|
this.grantWriters = {
|
|
36560
37501
|
writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
|
|
37502
|
+
writeMcpPolicy: deps.grantWriters?.writeMcpPolicy ?? writeMcpPolicy,
|
|
36561
37503
|
writeManagedFiles: deps.grantWriters?.writeManagedFiles ?? writeManagedFiles,
|
|
36562
37504
|
bindRuntimeConfig: deps.grantWriters?.bindRuntimeConfig ?? bindRuntimeConfig,
|
|
36563
37505
|
removeManagedLlmSource: deps.grantWriters?.removeManagedLlmSource ?? removeManagedLlmSource
|
|
@@ -36576,7 +37518,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36576
37518
|
threshold: EMBEDDING_MANIFEST.similarityThreshold,
|
|
36577
37519
|
warm: windowsSemanticOn(this.platform)
|
|
36578
37520
|
};
|
|
36579
|
-
return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init,
|
|
37521
|
+
return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname6(fileURLToPath2(import.meta.url)));
|
|
36580
37522
|
}
|
|
36581
37523
|
});
|
|
36582
37524
|
this.embedder = deps.embedder ?? (deps.hostMachine === true && deps.home === void 0 ? new EmbedderClient({ identityId: cfg.identityId, home: homedir4(), source: cfg.modelSource, fetchImpl: this.fetchImpl, platform: this.platform }) : INERT_EMBEDDER);
|
|
@@ -36587,7 +37529,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36587
37529
|
modelId: EMBEDDING_MANIFEST.modelId,
|
|
36588
37530
|
threshold: EMBEDDING_MANIFEST.similarityThreshold
|
|
36589
37531
|
});
|
|
36590
|
-
this.heartbeat = new CallHeartbeat(this.machinePaths ?
|
|
37532
|
+
this.heartbeat = new CallHeartbeat(this.machinePaths ? join18(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
|
|
36591
37533
|
this.reembed = new ReembedQueue({
|
|
36592
37534
|
store: this.store,
|
|
36593
37535
|
embedder: this.embedder,
|
|
@@ -36646,15 +37588,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36646
37588
|
this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
|
|
36647
37589
|
this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
|
|
36648
37590
|
this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
|
|
36649
|
-
this.replySleep = deps.replySleep ?? ((ms) => new Promise((
|
|
36650
|
-
const t = setTimeout(
|
|
37591
|
+
this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve2) => {
|
|
37592
|
+
const t = setTimeout(resolve2, ms);
|
|
36651
37593
|
t.unref?.();
|
|
36652
37594
|
}));
|
|
36653
37595
|
this.actionRateFloor = new TokenBucketFloor({ capacity: 1, refillPerMinute: 30, now: this.actionClock });
|
|
36654
37596
|
this.globalActionRateFloor = new TokenBucketFloor({ capacity: 1, refillPerMinute: 30, now: this.actionClock });
|
|
36655
37597
|
const self = this;
|
|
36656
37598
|
this.downloadBlobImpl = deps.downloadBlob ?? async function(blobId) {
|
|
36657
|
-
const url = `${cfg.apiBase}/api/v1/attachments/${encodeURIComponent(blobId)}?caller_identity_id=${encodeURIComponent(cfg.identityId)}`;
|
|
37599
|
+
const url = `${cfg.apiBase}/api/v1/attachments/${encodeURIComponent(assertBlobId(blobId))}?caller_identity_id=${encodeURIComponent(cfg.identityId)}`;
|
|
36658
37600
|
const res = await fetchWithDeadline(self.fetchImpl, url, { headers: { authorization: `Bearer ${cfg.apiKey}` } }, {
|
|
36659
37601
|
op: "attachment_download",
|
|
36660
37602
|
timeoutMs: FETCH_DEADLINE_MS.blobBase,
|
|
@@ -36674,8 +37616,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
36674
37616
|
const j = await res.json();
|
|
36675
37617
|
return String(j.attachment_id);
|
|
36676
37618
|
};
|
|
36677
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
36678
|
-
this.
|
|
37619
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join18(dirname6(cfg.storePath), "attachments");
|
|
37620
|
+
this.vaultOutboxDirImpl = deps.vaultOutboxDir !== void 0 ? deps.vaultOutboxDir : cfg.storePath === ":memory:" ? null : join18(dirname6(cfg.storePath), VAULT_OUTBOX_DIRNAME);
|
|
37621
|
+
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join18(dirname6(cfg.storePath), "vault-owner-pins.json"));
|
|
36679
37622
|
this.action = {
|
|
36680
37623
|
begin: (channelId) => this.actionBegin(channelId),
|
|
36681
37624
|
start: (opts) => this.actionStart(opts),
|
|
@@ -37042,6 +37985,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37042
37985
|
roomDefaultGrants = roomDefaultGrantsEnabled();
|
|
37043
37986
|
/** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
|
|
37044
37987
|
ownerDmManifestGrants = ownerDmManifestGrantsEnabled();
|
|
37988
|
+
/**
|
|
37989
|
+
* MCP client Task 16 (OQ-16, coordinator ruling D-7): SECURECOMS_MCP_GRANTS, read ONCE at construction. "off" writes
|
|
37990
|
+
* kill_switch (deleting); any value other than unset/"on"/"off" is suspended (non-deleting, one content-free warn).
|
|
37991
|
+
*/
|
|
37992
|
+
mcpHostSwitch = mcpGrantsHostSwitch(process.env);
|
|
37993
|
+
/** MCP Task 16 test seam (BridgeAgentDeps.mcpCatalog); undefined in production, so MCP_CATALOG decides. */
|
|
37994
|
+
mcpCatalog;
|
|
37995
|
+
/**
|
|
37996
|
+
* MCP Task 16 (OQ-1; PR-B1 audit M-2): the two raw /files fields from the LAST fetch. Any failed or rejected fetch
|
|
37997
|
+
* sets `fetched: false` (no_config, non-deleting): the last values are never kept to keep an `ok` document alive.
|
|
37998
|
+
*/
|
|
37999
|
+
mcpConfig = Object.freeze({ fetched: false });
|
|
38000
|
+
/**
|
|
38001
|
+
* MCP Task 16: the mcp-policy.json document this process last WROTE (the file the runtime reads), or null before the
|
|
38002
|
+
* first write and after a failed one (fail closed: issuance reads only a document known to be on disk).
|
|
38003
|
+
*/
|
|
38004
|
+
mcpPolicyWritten = null;
|
|
38005
|
+
mcpPolicyWrittenJson = null;
|
|
38006
|
+
/** PR-B2 follow-up: a failed write is retried on this backoff (the last value repeats) until one lands or close(). */
|
|
38007
|
+
mcpPolicyRetryDelaysMs;
|
|
38008
|
+
mcpPolicyRetryTimer = null;
|
|
38009
|
+
mcpPolicyRetryAttempt = 0;
|
|
38010
|
+
/** MCP Task 18: the loopback catalog listers of the attached runtime sockets that declared mcp_client_v1 (ws-server.ts). */
|
|
38011
|
+
mcpCatalogListers = /* @__PURE__ */ new Map();
|
|
38012
|
+
/** MCP Task 18 (OQ-15): per server 1 in flight, 3 / 60 s, 20 / hour; per agent 40 / hour. Memory only. */
|
|
38013
|
+
mcpCatalogRate = new McpCatalogRateLimiter({
|
|
38014
|
+
onRateLimited: (scope) => writeServiceLog2("warn", "mcp.catalog_request", {
|
|
38015
|
+
state: ServiceLogEnum.of("catalog_rate_limited", ["catalog_rate_limited"]),
|
|
38016
|
+
scope: ServiceLogEnum.of(scope, ["server_minute", "server_hour", "agent_hour"])
|
|
38017
|
+
})
|
|
38018
|
+
});
|
|
38019
|
+
/** MCP Task 18: answers in progress (outside the ingest cycle). Observable by tests; close() does not wait on them. */
|
|
38020
|
+
mcpCatalogWork = /* @__PURE__ */ new Set();
|
|
37045
38021
|
roomManifestGrants = roomManifestGrantsEnabled();
|
|
37046
38022
|
/**
|
|
37047
38023
|
* Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
|
|
@@ -37052,6 +38028,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37052
38028
|
// RECORDED here and in the sealed store; no grant code reads it in step 1.
|
|
37053
38029
|
toolGrantManifest = null;
|
|
37054
38030
|
toolGrantCounter = null;
|
|
38031
|
+
/**
|
|
38032
|
+
* Tool status Phase 0 (spec §3.1, §3.2; plan Task 8): the latest owner-DM last-turn record, in memory only (no store
|
|
38033
|
+
* table: after a restart there is none until the next typed owner-DM turn). ADVISORY: nothing reads it to decide a
|
|
38034
|
+
* grant. Content-free: enums, booleans, one 15-minute bucket `at` (ruling M-2), one counter, built-in names from the
|
|
38035
|
+
* allowlist. This is the latest record, open bucket included; what leaves the host is the sender's closed record.
|
|
38036
|
+
*/
|
|
38037
|
+
ownerDmLastTurn = null;
|
|
38038
|
+
/**
|
|
38039
|
+
* Test seams (Task 8; ruling M-2): the cause function, the bridge clock the record and the sender read, and the
|
|
38040
|
+
* sender's timer and jitter source.
|
|
38041
|
+
*/
|
|
38042
|
+
toolStatusCause = ownerDmCause;
|
|
38043
|
+
toolStatusNow = () => Date.now();
|
|
38044
|
+
toolStatusSetTimer = defaultToolStatusSetTimer;
|
|
38045
|
+
toolStatusClearTimer = defaultToolStatusClearTimer;
|
|
38046
|
+
toolStatusRandom = Math.random;
|
|
38047
|
+
/**
|
|
38048
|
+
* 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
|
|
38049
|
+
* UTC bucket boundary, and only a record whose bucket has closed.
|
|
38050
|
+
*/
|
|
38051
|
+
toolStatusSender = new ToolStatusSender({
|
|
38052
|
+
post: (rec) => this.postToolStatus(rec),
|
|
38053
|
+
now: () => this.toolStatusNow(),
|
|
38054
|
+
setTimer: (fn, ms) => this.toolStatusSetTimer(fn, ms),
|
|
38055
|
+
clearTimer: (h) => this.toolStatusClearTimer(h),
|
|
38056
|
+
random: () => this.toolStatusRandom(),
|
|
38057
|
+
onFailure: (kind2) => writeServiceLog2("warn", "tool_status.post_failed", { status: ServiceLogEnum.of(kind2, ["http_error", "timeout", "network"]) })
|
|
38058
|
+
});
|
|
38059
|
+
/**
|
|
38060
|
+
* MCP Phase 1 Task 14 (OQ-3 ruling item 3): the sticky R-3 flag, mirrored from the sealed store. Starts false and is
|
|
38061
|
+
* set true at start() by the store (or by a failed read, fail closed). Only applyToolGrantManifest changes it after.
|
|
38062
|
+
*/
|
|
38063
|
+
mcpSeen = false;
|
|
37055
38064
|
manifestState = "no_manifest";
|
|
37056
38065
|
/** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
|
|
37057
38066
|
ownerVerifyFailures = 0;
|
|
@@ -37406,6 +38415,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37406
38415
|
uploadBlobImpl;
|
|
37407
38416
|
/** Track 5: directory where received attachments are saved. */
|
|
37408
38417
|
attachmentsDirImpl;
|
|
38418
|
+
vaultOutboxDirImpl;
|
|
38419
|
+
/** Audit 2026-10-30 HIGH 2: the outbox pinned at start(); null = every upload refused. */
|
|
38420
|
+
outboxPin = null;
|
|
37409
38421
|
/** B7: persistence for the vault kekset's owner-pin TOFU state. */
|
|
37410
38422
|
vaultOwnerStoreImpl;
|
|
37411
38423
|
/** B7: last-seen kekset generation per enclave, enforced monotonic across
|
|
@@ -37514,10 +38526,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37514
38526
|
const payload = await res.json();
|
|
37515
38527
|
if (!payload) {
|
|
37516
38528
|
writeServiceLog2("warn", "runtime_config.empty_response");
|
|
38529
|
+
this.noteMcpConfigUnfetched();
|
|
37517
38530
|
return;
|
|
37518
38531
|
}
|
|
37519
38532
|
if (typeof payload.version !== "number") {
|
|
37520
38533
|
writeServiceLog2("warn", "runtime_config.invalid_version");
|
|
38534
|
+
this.noteMcpConfigUnfetched();
|
|
37521
38535
|
return;
|
|
37522
38536
|
}
|
|
37523
38537
|
const version = payload.version;
|
|
@@ -37531,6 +38545,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37531
38545
|
manifestStepThrew = true;
|
|
37532
38546
|
writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
|
|
37533
38547
|
}
|
|
38548
|
+
const raw = payload;
|
|
38549
|
+
this.mcpConfig = Object.freeze({ fetched: true, mcp_entitled: raw.mcp_entitled, mcp_cause: raw.mcp_cause });
|
|
38550
|
+
this.recomputeMcpPolicy();
|
|
37534
38551
|
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
37535
38552
|
const inForce = pin !== null ? this.toolGrantManifest : null;
|
|
37536
38553
|
if (inForce !== null && pin !== null) {
|
|
@@ -37597,8 +38614,81 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37597
38614
|
await this.postRuntimeConfigReport(base, version, skillsJson);
|
|
37598
38615
|
} catch (e) {
|
|
37599
38616
|
writeServiceLog2("warn", "runtime_config.refetch_failed", {}, e);
|
|
38617
|
+
this.noteMcpConfigUnfetched();
|
|
37600
38618
|
}
|
|
37601
38619
|
}
|
|
38620
|
+
/** MCP Task 16 (OQ-1): the bridge's in-force entitlement reading; not fetched (or failed) is suspended. */
|
|
38621
|
+
mcpEntitlementInForce() {
|
|
38622
|
+
return this.mcpConfig.fetched ? mcpEntitlementReading(this.mcpConfig.mcp_entitled, this.mcpConfig.mcp_cause) : "suspended";
|
|
38623
|
+
}
|
|
38624
|
+
/** PR-B1 audit M-2: forget the last /files fields and rewrite mcp-policy.json (no_config: no grants, no deletion). */
|
|
38625
|
+
noteMcpConfigUnfetched() {
|
|
38626
|
+
this.mcpConfig = Object.freeze({ fetched: false });
|
|
38627
|
+
this.recomputeMcpPolicy();
|
|
38628
|
+
}
|
|
38629
|
+
/** MCP Task 16: the live inputs of mcp-policy.json (the pinned manifest in force only). */
|
|
38630
|
+
mcpPolicyInput() {
|
|
38631
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
38632
|
+
return {
|
|
38633
|
+
hostSwitch: this.mcpHostSwitch,
|
|
38634
|
+
config: this.mcpConfig,
|
|
38635
|
+
ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
|
|
38636
|
+
manifest: pin !== null ? this.toolGrantManifest : null,
|
|
38637
|
+
capability: this.mcpClientCapable(),
|
|
38638
|
+
mcpSeen: this.mcpSeen,
|
|
38639
|
+
...this.mcpCatalog ? { catalog: this.mcpCatalog } : {}
|
|
38640
|
+
};
|
|
38641
|
+
}
|
|
38642
|
+
/**
|
|
38643
|
+
* MCP Task 16 (spec §12.1): recompute mcp-policy.json and write it when it changed. The written document is what MCP
|
|
38644
|
+
* issuance and release-time re-narrowing read, so it is cleared BEFORE the write and set only after it lands: a failed
|
|
38645
|
+
* write leaves no document in force here (no MCP grant), and the next trigger tries again. Never throws.
|
|
38646
|
+
*/
|
|
38647
|
+
recomputeMcpPolicy() {
|
|
38648
|
+
const doc = mcpPolicyFor(this.mcpPolicyInput());
|
|
38649
|
+
const json2 = JSON.stringify(doc);
|
|
38650
|
+
if (json2 === this.mcpPolicyWrittenJson) return;
|
|
38651
|
+
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))) {
|
|
38652
|
+
this.mcpPolicyWritten = doc;
|
|
38653
|
+
this.mcpPolicyWrittenJson = json2;
|
|
38654
|
+
this.clearMcpPolicyRetry();
|
|
38655
|
+
return;
|
|
38656
|
+
}
|
|
38657
|
+
this.mcpPolicyWritten = null;
|
|
38658
|
+
this.mcpPolicyWrittenJson = null;
|
|
38659
|
+
try {
|
|
38660
|
+
this.grantWriters.writeMcpPolicy(this.home, this.cfg.identityId, doc);
|
|
38661
|
+
} catch (e) {
|
|
38662
|
+
writeServiceLog2("warn", "authz.mcp_policy", { state: ServiceLogEnum.of("write_failed", ["write_failed"]) }, e);
|
|
38663
|
+
this.scheduleMcpPolicyRetry();
|
|
38664
|
+
return;
|
|
38665
|
+
}
|
|
38666
|
+
this.mcpPolicyWritten = doc;
|
|
38667
|
+
this.mcpPolicyWrittenJson = json2;
|
|
38668
|
+
this.clearMcpPolicyRetry();
|
|
38669
|
+
}
|
|
38670
|
+
/** A document is in force: no pending retry, and the next failure starts the backoff from the first delay. */
|
|
38671
|
+
clearMcpPolicyRetry() {
|
|
38672
|
+
this.mcpPolicyRetryAttempt = 0;
|
|
38673
|
+
if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
|
|
38674
|
+
this.mcpPolicyRetryTimer = null;
|
|
38675
|
+
}
|
|
38676
|
+
/**
|
|
38677
|
+
* PR-B2 follow-up: one pending retry at a time. The retry recomputes from the state THEN (never re-writes a stale
|
|
38678
|
+
* document); any other trigger that writes successfully meanwhile cancels it. Nothing is in force on the bridge side
|
|
38679
|
+
* until a write lands (mcpPolicyWritten stays null).
|
|
38680
|
+
*/
|
|
38681
|
+
scheduleMcpPolicyRetry() {
|
|
38682
|
+
if (this.closingDown || this.mcpPolicyRetryTimer !== null) return;
|
|
38683
|
+
const d = this.mcpPolicyRetryDelaysMs;
|
|
38684
|
+
const delay = d[Math.min(this.mcpPolicyRetryAttempt, d.length - 1)];
|
|
38685
|
+
this.mcpPolicyRetryAttempt++;
|
|
38686
|
+
this.mcpPolicyRetryTimer = setTimeout(() => {
|
|
38687
|
+
this.mcpPolicyRetryTimer = null;
|
|
38688
|
+
if (!this.closingDown) this.recomputeMcpPolicy();
|
|
38689
|
+
}, delay);
|
|
38690
|
+
this.mcpPolicyRetryTimer.unref?.();
|
|
38691
|
+
}
|
|
37602
38692
|
/**
|
|
37603
38693
|
* Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
|
|
37604
38694
|
* after the memory was applied; the caller then writes nothing else. Throws only from a file write, with
|
|
@@ -37682,7 +38772,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37682
38772
|
}
|
|
37683
38773
|
/** No manifest in force: a manifest-policy.json left by an earlier one is replaced by an empty tool list. */
|
|
37684
38774
|
overwriteStalePolicy() {
|
|
37685
|
-
if (!existsSync5(
|
|
38775
|
+
if (!existsSync5(join18(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
|
|
37686
38776
|
this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, { v: 1, manifest_tools: [], secret_path_mode: "enforce" });
|
|
37687
38777
|
}
|
|
37688
38778
|
/** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
|
|
@@ -37690,6 +38780,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37690
38780
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
37691
38781
|
const reportedSearchProviders = this.searchProvidersCapable();
|
|
37692
38782
|
const reportedUtilityTools = this.utilityToolsCapable();
|
|
38783
|
+
const carriedOwnerDmTurn = this.toolStatusSender.carry();
|
|
37693
38784
|
const report = {
|
|
37694
38785
|
applied_version: version,
|
|
37695
38786
|
paths: checkConfiguredPaths(skillsJson),
|
|
@@ -37759,8 +38850,17 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37759
38850
|
// D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
|
|
37760
38851
|
search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders),
|
|
37761
38852
|
// Weather/currency §8 item 5: a separate key, false with no runtime attached.
|
|
37762
|
-
utility_tools_v1: reportedUtilityTools
|
|
37763
|
-
|
|
38853
|
+
utility_tools_v1: reportedUtilityTools,
|
|
38854
|
+
// MCP Task 19 (spec §12.3): separate from `capability`; content-free enums and pattern tokens only.
|
|
38855
|
+
mcp_capability: this.reportedMcpCapability(),
|
|
38856
|
+
mcp_servers: this.reportedMcpServers(),
|
|
38857
|
+
// Tool status Phase 0 (spec §3.5, round 2 H-5): an updated bridge, with or without a record yet.
|
|
38858
|
+
tool_status_v1: true
|
|
38859
|
+
},
|
|
38860
|
+
// Tool status Phase 0 (spec §3.5): the record rides every apply-time report, so record_report's wholesale replace
|
|
38861
|
+
// never erases it. Ruling M-2: the sender's CLOSED record, never the latest one, so a report sent inside a bucket
|
|
38862
|
+
// never reveals that bucket's change early. Advisory and content-free.
|
|
38863
|
+
...carriedOwnerDmTurn ? { owner_dm_last_turn: carriedOwnerDmTurn } : {}
|
|
37764
38864
|
};
|
|
37765
38865
|
const reportedVerifyFailures = this.ownerVerifyFailures;
|
|
37766
38866
|
const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
|
|
@@ -37849,8 +38949,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37849
38949
|
}
|
|
37850
38950
|
async startBody() {
|
|
37851
38951
|
this.startedAt = Date.now();
|
|
38952
|
+
if (this.vaultOutboxDirImpl) {
|
|
38953
|
+
try {
|
|
38954
|
+
this.outboxPin = ensureOutbox(this.vaultOutboxDirImpl);
|
|
38955
|
+
} catch (e) {
|
|
38956
|
+
writeServiceLog2("warn", "vault_outbox.unavailable", {}, e);
|
|
38957
|
+
}
|
|
38958
|
+
}
|
|
37852
38959
|
await this.restoreRuntimeInbox();
|
|
37853
38960
|
await this.loadToolGrantState();
|
|
38961
|
+
this.recomputeMcpPolicy();
|
|
37854
38962
|
this.recomputeManifestPolicy(NO_FEATURES);
|
|
37855
38963
|
try {
|
|
37856
38964
|
this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
|
|
@@ -37874,8 +38982,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37874
38982
|
const drain = this.ingestOnce().then(() => true, () => true);
|
|
37875
38983
|
const drainedInTime = await Promise.race([
|
|
37876
38984
|
drain,
|
|
37877
|
-
new Promise((
|
|
37878
|
-
const t = setTimeout(() =>
|
|
38985
|
+
new Promise((resolve2) => {
|
|
38986
|
+
const t = setTimeout(() => resolve2(false), this.bootDrainWaitMs);
|
|
37879
38987
|
t.unref?.();
|
|
37880
38988
|
void drain.then(() => clearTimeout(t));
|
|
37881
38989
|
})
|
|
@@ -37885,7 +38993,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
37885
38993
|
await this.bootReconcileKeyPackage();
|
|
37886
38994
|
} else {
|
|
37887
38995
|
writeServiceLog2("warn", "boot.drain_slow", { wait_ms: this.bootDrainWaitMs });
|
|
37888
|
-
const settled = new Promise((
|
|
38996
|
+
const settled = new Promise((resolve2) => this.ingestSettledWaiters.push(resolve2));
|
|
37889
38997
|
this.deferredBootPublish = settled.then(async () => {
|
|
37890
38998
|
this.bootDrainPending = false;
|
|
37891
38999
|
if (this.closingDown) return;
|
|
@@ -38868,7 +39976,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38868
39976
|
* for every envelope built, BEFORE it is published (SEC-06 #18 persists it
|
|
38869
39977
|
* there so a retry can resend the exact bytes).
|
|
38870
39978
|
*/
|
|
38871
|
-
async publishBody(channelId, body, id, onEnvelope, sealCheck) {
|
|
39979
|
+
async publishBody(channelId, body, id, onEnvelope, sealCheck, inReplyTo, mcpTurn) {
|
|
39980
|
+
const mcpGuard = mcpTurn === true || this.mcpReplyGuarded(channelId, inReplyTo ?? void 0);
|
|
39981
|
+
let attempt = 0;
|
|
38872
39982
|
this.enforceBodyRateFloor(channelId, body);
|
|
38873
39983
|
this.statusGate.replyStarted(channelId);
|
|
38874
39984
|
const joined = await this.reconcileJoin(channelId);
|
|
@@ -38908,6 +40018,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38908
40018
|
);
|
|
38909
40019
|
},
|
|
38910
40020
|
async (envelope) => {
|
|
40021
|
+
attempt++;
|
|
40022
|
+
if (mcpGuard) this.assertMcpReplyRelayFree(channelId, envelope, attempt);
|
|
38911
40023
|
if (onEnvelope) await onEnvelope(envelope);
|
|
38912
40024
|
return this.publishEnvelope(
|
|
38913
40025
|
channelId,
|
|
@@ -38917,6 +40029,77 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38917
40029
|
);
|
|
38918
40030
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
38919
40031
|
}
|
|
40032
|
+
/**
|
|
40033
|
+
* MCP Task 16 (round 2 H-D): message ids whose DELIVERED authorization (after release-time re-narrowing) carried an
|
|
40034
|
+
* `mcp:` grant, with their channel and whether the runtime acked them. Recorded at the buffer exit, the one place
|
|
40035
|
+
* every consumer's copy leaves from (over-recording a turn a non-capable consumer then received without the grant only
|
|
40036
|
+
* adds a check). Kept after the ack, so a late frame citing the turn is still checked. Bounded: the oldest ACKED entry
|
|
40037
|
+
* goes first.
|
|
40038
|
+
*/
|
|
40039
|
+
mcpDeliveredTurns = /* @__PURE__ */ new Map();
|
|
40040
|
+
mcpDeliveredTrackMax;
|
|
40041
|
+
/**
|
|
40042
|
+
* Record `m` when its authorization carries an `mcp:` grant. Returns false ONLY when `m` is a new turn, the record is
|
|
40043
|
+
* at its bound and no entry is acked: the caller (the buffer exit) must then issue no mcp: grant (audit M-2, fail
|
|
40044
|
+
* closed). `rebuild` (PR-B2 audit C-1 (b): a restored runtime-inbox row, or a replayed turn) records unconditionally:
|
|
40045
|
+
* those grants were issued before, so the record may exceed its bound by at most the runtime inbox's size.
|
|
40046
|
+
*/
|
|
40047
|
+
noteMcpDelivered(m, rebuild = false) {
|
|
40048
|
+
if (!m.authorization?.tool_grants.some((g) => g.tool.startsWith(MCP_GRANT_PREFIX))) return true;
|
|
40049
|
+
if (!this.mcpDeliveredTurns.has(m.message_id) && this.mcpDeliveredTurns.size >= this.mcpDeliveredTrackMax) {
|
|
40050
|
+
let victim;
|
|
40051
|
+
for (const [id, r] of this.mcpDeliveredTurns) if (r.acked) {
|
|
40052
|
+
victim = id;
|
|
40053
|
+
break;
|
|
40054
|
+
}
|
|
40055
|
+
if (victim !== void 0) this.mcpDeliveredTurns.delete(victim);
|
|
40056
|
+
else if (!rebuild) return false;
|
|
40057
|
+
}
|
|
40058
|
+
this.mcpDeliveredTurns.set(m.message_id, { channelId: m.channel_id, acked: false });
|
|
40059
|
+
return true;
|
|
40060
|
+
}
|
|
40061
|
+
/**
|
|
40062
|
+
* H-D: whether a frame to `channelId` must pass the relay-free check: it answers a recorded MCP turn (any channel:
|
|
40063
|
+
* a frame of that turn sent elsewhere is checked against ITS channel), or it cites no turn while an MCP turn is
|
|
40064
|
+
* unacked in the same channel.
|
|
40065
|
+
*/
|
|
40066
|
+
mcpReplyGuarded(channelId, inReplyTo) {
|
|
40067
|
+
if (inReplyTo !== void 0 && this.mcpDeliveredTurns.has(inReplyTo)) return true;
|
|
40068
|
+
if (inReplyTo !== void 0) return false;
|
|
40069
|
+
for (const r of this.mcpDeliveredTurns.values()) if (!r.acked && r.channelId === channelId) return true;
|
|
40070
|
+
return false;
|
|
40071
|
+
}
|
|
40072
|
+
/**
|
|
40073
|
+
* H-D, synchronous: read the channel's local leaves from the warm manager in one step and tie the read to the
|
|
40074
|
+
* envelope's SEALED group and epoch, so the leaf set is exactly the membership that can open these bytes. Owner and
|
|
40075
|
+
* agent only: pass. Anyone else: withhold. The tree moved past the seal on the first attempt: StaleEpochError, which
|
|
40076
|
+
* sendWithSelfHeal answers with its one converge-and-re-seal (the existing semantics); still unprovable on the retry,
|
|
40077
|
+
* or no warm manager, no pin, or any throw: withhold (fail closed). Read-only; never touches the group.
|
|
40078
|
+
*
|
|
40079
|
+
* Scope (PR-B2 audit item 5): this guards the `send` path (publishBody) only. `status` detail, the `action_*` run
|
|
40080
|
+
* frames, `/v1/send-attachment` and summary frames are NOT checked; they are safe for MCP turns only because the
|
|
40081
|
+
* runtime emits none of them (pinned by connect-agent-runtime/src/outbound-frames.arch.test.ts). A runtime that starts
|
|
40082
|
+
* emitting any of them needs this guard extended to that frame first.
|
|
40083
|
+
*/
|
|
40084
|
+
assertMcpReplyRelayFree(channelId, envelope, attempt) {
|
|
40085
|
+
let verdict = "unknown";
|
|
40086
|
+
try {
|
|
40087
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
40088
|
+
const snap = this.provider.leafSnapshotCached(channelId);
|
|
40089
|
+
const sealedGroup = sealedGroupIdOf(envelope);
|
|
40090
|
+
if (pin !== null && snap !== null && sealedGroup !== void 0 && snap.groupId === sealedGroup) {
|
|
40091
|
+
if (snap.epoch === envelope.mls_group_epoch) {
|
|
40092
|
+
verdict = ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId }) ? "free" : "relay";
|
|
40093
|
+
} else if (snap.epoch > envelope.mls_group_epoch) verdict = "moved";
|
|
40094
|
+
}
|
|
40095
|
+
} catch {
|
|
40096
|
+
verdict = "unknown";
|
|
40097
|
+
}
|
|
40098
|
+
if (verdict === "free") return;
|
|
40099
|
+
if (verdict === "moved" && attempt === 1) throw new StaleEpochError(envelope.mls_group_epoch, envelope.mls_group_epoch + 1);
|
|
40100
|
+
writeServiceLog2("warn", "mcp.reply_withheld", { state: ServiceLogEnum.of(verdict, ["relay", "moved", "unknown"]) });
|
|
40101
|
+
throw new McpReplyWithheldError(channelId);
|
|
40102
|
+
}
|
|
38920
40103
|
/**
|
|
38921
40104
|
* Audit M-B1 (controller ruling): a reply to a room-granted turn is sealed
|
|
38922
40105
|
* only at the epoch the room was classified at. On a move, the room is
|
|
@@ -38928,9 +40111,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38928
40111
|
* the redelivery replay (replayReplyIntents, which has no in_reply_to in its
|
|
38929
40112
|
* options) is checked the same as a live send.
|
|
38930
40113
|
*/
|
|
38931
|
-
async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope) {
|
|
40114
|
+
async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope, mcpTurn) {
|
|
38932
40115
|
const grant = inReplyTo !== null ? this.roomGrantEpochs.get(inReplyTo) : void 0;
|
|
38933
|
-
if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope);
|
|
40116
|
+
if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope, void 0, inReplyTo, mcpTurn);
|
|
38934
40117
|
if (channelId !== grant.channelId) {
|
|
38935
40118
|
writeServiceLog2("warn", "reply.room_class_changed");
|
|
38936
40119
|
throw new RoomClassChangedSendError(channelId);
|
|
@@ -38952,7 +40135,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38952
40135
|
},
|
|
38953
40136
|
(epoch) => {
|
|
38954
40137
|
if (epoch !== grant.epoch) throw new RoomGrantEpochMovedError();
|
|
38955
|
-
}
|
|
40138
|
+
},
|
|
40139
|
+
inReplyTo,
|
|
40140
|
+
mcpTurn
|
|
38956
40141
|
);
|
|
38957
40142
|
} catch (e) {
|
|
38958
40143
|
if (!(e instanceof RoomGrantEpochMovedError)) throw e;
|
|
@@ -39087,14 +40272,15 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39087
40272
|
const k = replyIntentKey({ inReplyTo: opts.inReplyTo, idempotencyKey: opts.idempotencyKey, ordinal: () => 0 });
|
|
39088
40273
|
if (!k) throw new Error("idempotent send without a key");
|
|
39089
40274
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39090
|
-
const
|
|
39091
|
-
const
|
|
39092
|
-
|
|
40275
|
+
const mcpTurn = this.mcpReplyGuarded(channelId, opts.inReplyTo);
|
|
40276
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, mcpTurn);
|
|
40277
|
+
const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null, mcpTurn });
|
|
40278
|
+
if (intent.bodyMac !== replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, intent.mcpTurn) || intent.channelId !== channelId || intent.state === "skipped") {
|
|
39093
40279
|
throw new IdempotencyConflictError();
|
|
39094
40280
|
}
|
|
39095
40281
|
return k;
|
|
39096
40282
|
}
|
|
39097
|
-
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null);
|
|
40283
|
+
return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null, false);
|
|
39098
40284
|
});
|
|
39099
40285
|
return this.publishIntent(key, opts);
|
|
39100
40286
|
}
|
|
@@ -39103,25 +40289,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39103
40289
|
* the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
|
|
39104
40290
|
* after a slot collision: the body must go to a NEW part.
|
|
39105
40291
|
*/
|
|
39106
|
-
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit) {
|
|
40292
|
+
async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit, inheritedMcpTurn) {
|
|
39107
40293
|
const credit = replyCredit ?? void 0;
|
|
40294
|
+
const mcpTurn = inheritedMcpTurn || this.mcpReplyGuarded(channelId, inReplyTo);
|
|
39108
40295
|
const scope = `reply:${inReplyTo}`;
|
|
39109
40296
|
let latest = null;
|
|
39110
40297
|
for (const intent of await this.store.listReplyIntents(inReplyTo)) {
|
|
39111
40298
|
const n = intent.scope === scope ? partOrdinal(intent.part) : null;
|
|
39112
40299
|
if (n !== null && (latest === null || n > latest.ordinal)) latest = { ordinal: n, intent };
|
|
39113
40300
|
}
|
|
39114
|
-
if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit)) {
|
|
40301
|
+
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)) {
|
|
39115
40302
|
return { scope, part: latest.intent.part, inReplyTo };
|
|
39116
40303
|
}
|
|
39117
40304
|
let ordinal = latest ? latest.ordinal + 1 : 0;
|
|
39118
40305
|
for (let i = 0; i < REPLY_MAX_PART_ADVANCES; i++, ordinal++) {
|
|
39119
40306
|
const k = { scope, part: `p:${ordinal}`, inReplyTo };
|
|
39120
40307
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
|
|
39121
|
-
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit);
|
|
39122
|
-
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit });
|
|
40308
|
+
const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, mcpTurn);
|
|
40309
|
+
const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit, mcpTurn });
|
|
39123
40310
|
if (created) return k;
|
|
39124
|
-
if (matchRetry && intent.state !== "skipped" && intent.
|
|
40311
|
+
if (matchRetry && intent.state !== "skipped" && intent.channelId === channelId && intent.bodyMac === replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, intent.mcpTurn)) return k;
|
|
39125
40312
|
writeServiceLog2("info", "reply.part_slot_taken", { attempt: i + 1 });
|
|
39126
40313
|
}
|
|
39127
40314
|
throw new Error("could not claim a reply part");
|
|
@@ -39173,7 +40360,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39173
40360
|
const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
|
|
39174
40361
|
const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
|
|
39175
40362
|
try {
|
|
39176
|
-
await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo);
|
|
40363
|
+
await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo, intent.mcpTurn);
|
|
39177
40364
|
} catch (e) {
|
|
39178
40365
|
if (e instanceof AgentPausedSendError || e instanceof SendForbiddenError) {
|
|
39179
40366
|
if (intent.inReplyTo !== null) this.store.deletePendingReplyIntents(intent.inReplyTo);
|
|
@@ -39200,7 +40387,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39200
40387
|
JSON.stringify(["scope", key.scope]),
|
|
39201
40388
|
() => (
|
|
39202
40389
|
// r2 BLOCKER 1: the colliding row's OWN credit (verified above), never `opts`.
|
|
39203
|
-
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null)
|
|
40390
|
+
this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null, intent.mcpTurn)
|
|
39204
40391
|
)
|
|
39205
40392
|
);
|
|
39206
40393
|
return this.publishIntent(next, opts, advances + 1);
|
|
@@ -39222,7 +40409,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39222
40409
|
if (credit !== null && !isReplyCredit(credit)) return false;
|
|
39223
40410
|
const id = replyMessageId(this.replyIdKey, this.cfg.identityId, intent.channelId, intent);
|
|
39224
40411
|
if (id !== intent.replyMessageId) return false;
|
|
39225
|
-
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0) === intent.bodyMac;
|
|
40412
|
+
return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0, intent.mcpTurn === true) === intent.bodyMac;
|
|
39226
40413
|
}
|
|
39227
40414
|
/**
|
|
39228
40415
|
* Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
|
|
@@ -39243,12 +40430,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39243
40430
|
* failure. Either way the intent stays pending, so the redelivery
|
|
39244
40431
|
* republishes the same reply.
|
|
39245
40432
|
*/
|
|
39246
|
-
async publishIntentWithRetry(channelId, body, id, key, inReplyTo) {
|
|
40433
|
+
async publishIntentWithRetry(channelId, body, id, key, inReplyTo, mcpTurn) {
|
|
39247
40434
|
let rateLimited = 0;
|
|
39248
40435
|
let inFlight = 0;
|
|
39249
40436
|
for (; ; ) {
|
|
39250
40437
|
try {
|
|
39251
|
-
await this.publishIntentOnce(channelId, body, id, key, inReplyTo);
|
|
40438
|
+
await this.publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn);
|
|
39252
40439
|
return;
|
|
39253
40440
|
} catch (e) {
|
|
39254
40441
|
if (e instanceof ReplyInFlightError) {
|
|
@@ -39319,8 +40506,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39319
40506
|
* original ack; a conflict can only mean the slot belongs to another
|
|
39320
40507
|
* message (for example an old reply whose intent was pruned).
|
|
39321
40508
|
*/
|
|
39322
|
-
async publishIntentOnce(channelId, body, id, key, inReplyTo) {
|
|
40509
|
+
async publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn) {
|
|
39323
40510
|
const current = await this.store.getReplyIntent(key.scope, key.part);
|
|
40511
|
+
const guardMcp = mcpTurn || current?.mcpTurn === true;
|
|
39324
40512
|
try {
|
|
39325
40513
|
if (current?.envelope) {
|
|
39326
40514
|
const cached = JSON.parse(new TextDecoder().decode(current.envelope));
|
|
@@ -39370,7 +40558,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39370
40558
|
key.part,
|
|
39371
40559
|
new TextEncoder().encode(JSON.stringify(envelope)),
|
|
39372
40560
|
sealedGroupIdOf(envelope)
|
|
39373
|
-
)
|
|
40561
|
+
),
|
|
40562
|
+
guardMcp
|
|
39374
40563
|
);
|
|
39375
40564
|
} catch (e) {
|
|
39376
40565
|
if (publishConflictCode(e) === "message_id_conflict") throw new ReplyIdCollisionError();
|
|
@@ -39392,7 +40581,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39392
40581
|
uploadBlob: (ciphertext, channelId) => this.uploadBlobImpl(ciphertext, channelId),
|
|
39393
40582
|
encryptFile,
|
|
39394
40583
|
bytesToBase64: bytesToBase642,
|
|
39395
|
-
|
|
40584
|
+
// Security fix 2026-10-30: attachments upload only from the outbox.
|
|
40585
|
+
readFile: (p) => readOutboxFile(this.outboxPin, p).then((r) => r.bytes)
|
|
39396
40586
|
};
|
|
39397
40587
|
}
|
|
39398
40588
|
receiveDeps() {
|
|
@@ -39402,8 +40592,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39402
40592
|
decryptFile,
|
|
39403
40593
|
computeFileDigest,
|
|
39404
40594
|
base64ToBytes: base64ToBytes2,
|
|
39405
|
-
mkdirp: (d) =>
|
|
39406
|
-
writeFile: (p, data) =>
|
|
40595
|
+
mkdirp: (d) => fsp2.mkdir(d, { recursive: true }).then(() => void 0),
|
|
40596
|
+
writeFile: (p, data) => fsp2.writeFile(p, data)
|
|
39407
40597
|
};
|
|
39408
40598
|
}
|
|
39409
40599
|
async sendAttachment(channelId, filePath2, caption) {
|
|
@@ -39411,8 +40601,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39411
40601
|
const joined = await this.reconcileJoin(channelId);
|
|
39412
40602
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
39413
40603
|
let body;
|
|
40604
|
+
let read;
|
|
39414
40605
|
try {
|
|
39415
|
-
|
|
40606
|
+
read = await readOutboxFile(this.outboxPin, filePath2);
|
|
40607
|
+
const bytes = read.bytes;
|
|
40608
|
+
body = await prepareAttachment(filePath2, caption, channelId, { ...this.sendDeps(), readFile: async () => bytes });
|
|
39416
40609
|
} catch (e) {
|
|
39417
40610
|
if (e.code === "ENOENT" || e.code === "EACCES" || e.code === "ENOTDIR" || e.code === "EISDIR") {
|
|
39418
40611
|
throw new InvalidPathError(filePath2, e);
|
|
@@ -39453,6 +40646,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39453
40646
|
);
|
|
39454
40647
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
39455
40648
|
this.agentHops.noteOwnSend(channelId);
|
|
40649
|
+
await this.consumeUploaded(read);
|
|
39456
40650
|
return { message_id: id, status: "sent" };
|
|
39457
40651
|
}
|
|
39458
40652
|
// ── B7: FileSafe vault commands (loopback boundary) ──────────────────────
|
|
@@ -39978,25 +41172,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39978
41172
|
}
|
|
39979
41173
|
};
|
|
39980
41174
|
}
|
|
39981
|
-
/** Read `path`
|
|
41175
|
+
/** Read `path` from the vault outbox (security fix 2026-10-30; see
|
|
41176
|
+
* ./outbox.ts), encrypt + wrap its DEK under the vault's
|
|
39982
41177
|
* current KEK generation, and create+upload+commit it as a new vault
|
|
39983
41178
|
* file node named `name` (which may differ from `path`'s basename). */
|
|
39984
41179
|
async vaultWrite(enclaveId, vaultChannelId, path, name) {
|
|
39985
|
-
|
|
39986
|
-
let bytes;
|
|
41180
|
+
let read;
|
|
39987
41181
|
try {
|
|
39988
|
-
|
|
41182
|
+
read = await readOutboxFile(this.outboxPin, path);
|
|
39989
41183
|
} catch (e) {
|
|
39990
41184
|
const code = e.code;
|
|
39991
41185
|
if (code === "ENOENT" || code === "EACCES" || code === "ENOTDIR" || code === "EISDIR") throw new InvalidPathError(path, e);
|
|
39992
41186
|
throw e;
|
|
39993
41187
|
}
|
|
41188
|
+
const session = await this.getVaultSession(enclaveId, await this.resolveVaultChannelId(enclaveId, vaultChannelId));
|
|
39994
41189
|
const nodeId = crypto.randomUUID();
|
|
39995
|
-
|
|
41190
|
+
const written = await vaultWriteImpl({
|
|
39996
41191
|
enclaveId,
|
|
39997
41192
|
nodeId,
|
|
39998
41193
|
name,
|
|
39999
|
-
bytes,
|
|
41194
|
+
bytes: read.bytes,
|
|
40000
41195
|
deps: {
|
|
40001
41196
|
exporterKekForGeneration: session.kekForGeneration,
|
|
40002
41197
|
currentGeneration: session.currentGeneration,
|
|
@@ -40005,6 +41200,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
40005
41200
|
commitNode: (committedNodeId) => this.vaultCommitNode(enclaveId, committedNodeId)
|
|
40006
41201
|
}
|
|
40007
41202
|
});
|
|
41203
|
+
await this.consumeUploaded(read);
|
|
41204
|
+
return written;
|
|
41205
|
+
}
|
|
41206
|
+
/** Audit 2026-10-30 MED 5: an uploaded file leaves the outbox (no plaintext lingers). Content-free log. */
|
|
41207
|
+
async consumeUploaded(read) {
|
|
41208
|
+
if (!this.outboxPin || !await consumeOutboxFile(this.outboxPin, read)) writeServiceLog2("warn", "vault_outbox.consume_failed");
|
|
41209
|
+
}
|
|
41210
|
+
/** Audit 2026-10-30 MED 3: the pinned outbox path, served only on the authenticated GET /v1/vault-outbox. */
|
|
41211
|
+
vaultOutboxPath() {
|
|
41212
|
+
return this.outboxPin?.dir ?? null;
|
|
40008
41213
|
}
|
|
40009
41214
|
/** Download + verify + decrypt a vault file node. Returns the plaintext
|
|
40010
41215
|
* base64-encoded (the loopback boundary is JSON-only, mirroring how
|
|
@@ -41865,16 +43070,20 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41865
43070
|
}
|
|
41866
43071
|
if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
|
|
41867
43072
|
let leafClass = "ok";
|
|
43073
|
+
let relay = void 0;
|
|
41868
43074
|
if (this.freshMeta(keyChannel)?.is_dm === true) {
|
|
41869
43075
|
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
41870
43076
|
if (signedDm !== void 0 && signedDm !== keyChannel) return this.ownerTurnFailed(m, "wrong_dm_channel");
|
|
41871
|
-
|
|
43077
|
+
const leaves = (await this.provider.leafTreeSummary(m.channelId)).leaves;
|
|
43078
|
+
const relayIdentityId = this.toolGrantManifest?.owner_dm_relay_identity_id;
|
|
43079
|
+
leafClass = classifyOwnerDmLeaves(leaves, {
|
|
41872
43080
|
ownerIdentityId: pin.ownerIdentityId,
|
|
41873
43081
|
agentIdentityId: this.cfg.identityId,
|
|
41874
|
-
relayIdentityId
|
|
43082
|
+
relayIdentityId,
|
|
41875
43083
|
isServerRelay: (id) => this.freshRoster(m.channelId)?.get(id)?.is_relay === true
|
|
41876
43084
|
});
|
|
41877
43085
|
if (leafClass === "foreign") return this.ownerTurnFailed(m, "not_owner_dm_leaves");
|
|
43086
|
+
relay = ownerDmRelayOf(leafClass, leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId, relayIdentityId });
|
|
41878
43087
|
}
|
|
41879
43088
|
const held = await this.heldPosition(m.channelId);
|
|
41880
43089
|
if (held.groupId === null) return this.ownerTurnFailed(m, "verify_error");
|
|
@@ -41883,7 +43092,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41883
43092
|
}
|
|
41884
43093
|
const replay = Object.freeze({ channelId: m.channelId, groupId: held.groupId, epoch: v.mls_group_epoch });
|
|
41885
43094
|
this.ownerReplayPending.set(m.messageId, replay);
|
|
41886
|
-
|
|
43095
|
+
const relayField = relay !== void 0 ? { relay } : {};
|
|
43096
|
+
if (leafClass === "unsigned_relay") return Object.freeze({ kind: "constants_only", replay, ...relayField });
|
|
43097
|
+
return Object.freeze({ kind: "verified", replay, ...relayField, relayLeaf: relayLeafFromPhase0Relay(relay) });
|
|
41887
43098
|
} catch {
|
|
41888
43099
|
return this.ownerTurnFailed(m, "verify_error");
|
|
41889
43100
|
}
|
|
@@ -41959,6 +43170,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41959
43170
|
}
|
|
41960
43171
|
/** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
|
|
41961
43172
|
async loadToolGrantState() {
|
|
43173
|
+
try {
|
|
43174
|
+
this.mcpSeen = await this.store.loadMcpSeen();
|
|
43175
|
+
} catch (e) {
|
|
43176
|
+
this.mcpSeen = true;
|
|
43177
|
+
writeServiceLog2("warn", "authz.mcp_seen_load_failed", {}, e);
|
|
43178
|
+
}
|
|
41962
43179
|
const pin = this.cfg.ownerGrantPin?.pin;
|
|
41963
43180
|
if (!pin) return;
|
|
41964
43181
|
try {
|
|
@@ -41966,9 +43183,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41966
43183
|
this.store.dropToolGrantAppliedExcept(pin.publicKeyB64);
|
|
41967
43184
|
const saved = await this.store.loadToolGrantManifest(pin.publicKeyB64);
|
|
41968
43185
|
if (saved) {
|
|
41969
|
-
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null });
|
|
43186
|
+
const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
|
|
41970
43187
|
if (v.state === "ok") {
|
|
41971
43188
|
this.toolGrantManifest = v.manifest;
|
|
43189
|
+
if (v.manifest.signedMcpPresent && !this.mcpSeen) await this.setMcpSeen(true);
|
|
41972
43190
|
this.toolGrantCounter = v.manifest.counter;
|
|
41973
43191
|
} else {
|
|
41974
43192
|
this.logManifest("malformed");
|
|
@@ -41993,8 +43211,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
41993
43211
|
this.logManifest("no_manifest");
|
|
41994
43212
|
return;
|
|
41995
43213
|
}
|
|
41996
|
-
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter });
|
|
43214
|
+
const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
|
|
41997
43215
|
if (v.state === "ok") {
|
|
43216
|
+
if (v.manifest.signedMcpPresent) await this.setMcpSeen(true);
|
|
41998
43217
|
try {
|
|
41999
43218
|
await this.store.saveToolGrantManifest(pin.publicKeyB64, v.manifest.counter, v.wrapper);
|
|
42000
43219
|
} catch {
|
|
@@ -42003,13 +43222,34 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42003
43222
|
}
|
|
42004
43223
|
this.toolGrantManifest = v.manifest;
|
|
42005
43224
|
this.toolGrantCounter = v.manifest.counter;
|
|
43225
|
+
this.recomputeMcpPolicy();
|
|
43226
|
+
if (v.manifest.mcpKey === "present" && !v.manifest.signedMcpPresent && this.mcpSeen) await this.setMcpSeen(false);
|
|
42006
43227
|
this.logManifest("ok");
|
|
42007
43228
|
if (v.droppedUnknown) this.logManifest("unknown_entry");
|
|
42008
43229
|
return;
|
|
42009
43230
|
}
|
|
42010
43231
|
this.logManifest(v.state);
|
|
43232
|
+
if (v.state === "wrong_owner") this.dropOwnerDmLastTurn();
|
|
42011
43233
|
if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
|
|
42012
43234
|
}
|
|
43235
|
+
/**
|
|
43236
|
+
* 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
|
|
43237
|
+
* an owner_dm `mcp:` string, or while the sticky flag is set (an old client's re-sign cannot lift it, audit C-1).
|
|
43238
|
+
*/
|
|
43239
|
+
r3Hold() {
|
|
43240
|
+
return this.toolGrantManifest?.signedMcpPresent === true || this.mcpSeen;
|
|
43241
|
+
}
|
|
43242
|
+
/** Persist then mirror. Setting mirrors even on a failed write; clearing mirrors only after the write (fail closed). */
|
|
43243
|
+
async setMcpSeen(seen) {
|
|
43244
|
+
if (seen) this.mcpSeen = true;
|
|
43245
|
+
try {
|
|
43246
|
+
await this.store.saveMcpSeen(seen);
|
|
43247
|
+
} catch (e) {
|
|
43248
|
+
writeServiceLog2("warn", "authz.mcp_seen_save_failed", {}, e);
|
|
43249
|
+
return;
|
|
43250
|
+
}
|
|
43251
|
+
this.mcpSeen = seen;
|
|
43252
|
+
}
|
|
42013
43253
|
logManifest(state) {
|
|
42014
43254
|
this.manifestState = state;
|
|
42015
43255
|
const quiet = state === "ok" || state === "unchanged" || state === "no_pin" || state === "no_manifest";
|
|
@@ -42106,6 +43346,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42106
43346
|
let tool_grants = NO_TOOL_GRANTS;
|
|
42107
43347
|
let relaySpeaker;
|
|
42108
43348
|
let memoryWrite = false;
|
|
43349
|
+
let relayFree = false;
|
|
42109
43350
|
const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId) && (opts.voice === void 0 || rosterFreshAt(vEntry, opts.voice.heldEpoch, opts.voice.heldGroupId));
|
|
42110
43351
|
const ownerDmFresh = epochFresh || opts.voice === void 0 && this.ownerDmCycleFresh.has(channelId);
|
|
42111
43352
|
if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
|
|
@@ -42132,6 +43373,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42132
43373
|
tool_grants = ROOM_DEFAULT_READ_GRANTS;
|
|
42133
43374
|
}
|
|
42134
43375
|
let grantSource = tool_grants.length ? "constant" : "none";
|
|
43376
|
+
let ownerDmEffective = null;
|
|
42135
43377
|
const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
|
|
42136
43378
|
const binding = this.manifestBinding;
|
|
42137
43379
|
const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
|
|
@@ -42150,6 +43392,21 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42150
43392
|
});
|
|
42151
43393
|
tool_grants = r.grants;
|
|
42152
43394
|
grantSource = r.source;
|
|
43395
|
+
ownerDmEffective = r;
|
|
43396
|
+
const mcp = mcpIssuance({
|
|
43397
|
+
source: r.source,
|
|
43398
|
+
verdict: opts.ownerVerdict,
|
|
43399
|
+
entitlement: this.mcpEntitlementInForce(),
|
|
43400
|
+
hostSwitch: this.mcpHostSwitch,
|
|
43401
|
+
ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
|
|
43402
|
+
disabled: this.payloadToolDeny,
|
|
43403
|
+
manifest,
|
|
43404
|
+
policy: this.mcpPolicyWritten
|
|
43405
|
+
});
|
|
43406
|
+
if (mcp.length > 0) {
|
|
43407
|
+
tool_grants = Object.freeze([...tool_grants, ...mcp]);
|
|
43408
|
+
relayFree = true;
|
|
43409
|
+
}
|
|
42153
43410
|
} else if (tool_grants === ROOM_DEFAULT_READ_GRANTS) {
|
|
42154
43411
|
const r = effectiveToolGrants({
|
|
42155
43412
|
audience: "team_room",
|
|
@@ -42167,12 +43424,39 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42167
43424
|
if (opts.ownerVerdict?.kind === "failed") {
|
|
42168
43425
|
tool_grants = NO_TOOL_GRANTS;
|
|
42169
43426
|
memoryWrite = false;
|
|
43427
|
+
relayFree = false;
|
|
42170
43428
|
relaySpeaker = void 0;
|
|
42171
43429
|
grantSource = "none";
|
|
42172
43430
|
}
|
|
42173
43431
|
const roomClass = meta?.is_dm !== false ? "na" : entry?.crossOrg === false ? "internal" : entry?.crossOrg === true ? "cross_org" : "unknown";
|
|
42174
43432
|
if (opts.log !== false) {
|
|
42175
43433
|
this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite, grantSource);
|
|
43434
|
+
const pinOwner = this.cfg.ownerGrantPin?.pin?.ownerIdentityId;
|
|
43435
|
+
const verdict = opts.ownerVerdict;
|
|
43436
|
+
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")) {
|
|
43437
|
+
const pinned = this.channelOwner.get(channelId);
|
|
43438
|
+
const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
|
|
43439
|
+
const ownerDirect = ownerId !== void 0 && audience === "owner_dm" && senderId === ownerId;
|
|
43440
|
+
this.noteOwnerDmTurn({
|
|
43441
|
+
cause: {
|
|
43442
|
+
killSwitchOff: !this.ownerDmDefaultGrants,
|
|
43443
|
+
manifestSwitchOff: !this.ownerDmManifestGrants,
|
|
43444
|
+
pinMissingOrAnomalous: ownerId === void 0,
|
|
43445
|
+
// OQ-5: only from a PRESENT roster entry; a missing (TTL-expired) entry is `stale` below.
|
|
43446
|
+
senderNotOwner: entry !== void 0 && (sender === void 0 || !ownerDirect),
|
|
43447
|
+
stale: !ownerDmFresh || entry === void 0,
|
|
43448
|
+
verdict,
|
|
43449
|
+
manifestPresent: manifest !== null,
|
|
43450
|
+
signedDmChannel: manifest?.owner_dm_channel_id,
|
|
43451
|
+
channelId,
|
|
43452
|
+
boundNow,
|
|
43453
|
+
consumerAttached: this.manifestConsumer !== null
|
|
43454
|
+
},
|
|
43455
|
+
grantSource,
|
|
43456
|
+
toolGrants: tool_grants,
|
|
43457
|
+
withheld: grantSource === "manifest" && ownerDmEffective !== null ? ownerDmEffective.withheld : []
|
|
43458
|
+
});
|
|
43459
|
+
}
|
|
42176
43460
|
}
|
|
42177
43461
|
return Object.freeze({
|
|
42178
43462
|
version: 1,
|
|
@@ -42180,9 +43464,78 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42180
43464
|
sender_membership: sender ? "active" : "unknown",
|
|
42181
43465
|
tool_grants,
|
|
42182
43466
|
...relaySpeaker ? { relay_speaker: relaySpeaker } : {},
|
|
42183
|
-
...memoryWrite ? { memory_write: true } : {}
|
|
43467
|
+
...memoryWrite ? { memory_write: true } : {},
|
|
43468
|
+
...relayFree ? { relay_free: true } : {}
|
|
42184
43469
|
});
|
|
42185
43470
|
}
|
|
43471
|
+
/**
|
|
43472
|
+
* 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
|
|
43473
|
+
* keep the previous one (and its `at`) when the change tuple is the same, so `at` means "last changed". Never throws
|
|
43474
|
+
* into the authorization path. Never logs a channel id, sender id or tool name.
|
|
43475
|
+
*/
|
|
43476
|
+
noteOwnerDmTurn(t) {
|
|
43477
|
+
try {
|
|
43478
|
+
const v = t.cause.verdict;
|
|
43479
|
+
const relay = (v.kind === "verified" || v.kind === "constants_only") && v.relay !== void 0 ? v.relay : "unknown";
|
|
43480
|
+
let reason = {};
|
|
43481
|
+
if (t.grantSource !== "manifest") {
|
|
43482
|
+
const c = this.toolStatusCause(t.cause);
|
|
43483
|
+
if (c !== null && c.source === t.grantSource && c.cause !== void 0) {
|
|
43484
|
+
reason = c.sub !== void 0 ? { cause: c.cause, sub: c.sub } : { cause: c.cause };
|
|
43485
|
+
} else {
|
|
43486
|
+
writeServiceLog2("warn", "tool_status.source_mismatch");
|
|
43487
|
+
}
|
|
43488
|
+
} else {
|
|
43489
|
+
const c = this.toolStatusCause(t.cause);
|
|
43490
|
+
if (c === null || c.source !== "manifest") writeServiceLog2("warn", "tool_status.source_mismatch");
|
|
43491
|
+
}
|
|
43492
|
+
const names = t.toolGrants.map((g) => g.tool);
|
|
43493
|
+
const candidate = Object.freeze({
|
|
43494
|
+
at: bucketStartAt(this.toolStatusNow()),
|
|
43495
|
+
// ruling M-2: the 15-minute UTC bucket, never the minute
|
|
43496
|
+
counter: this.toolGrantCounter,
|
|
43497
|
+
source: t.grantSource,
|
|
43498
|
+
...reason,
|
|
43499
|
+
relay,
|
|
43500
|
+
issued: Object.freeze(sanitizeIssued(names)),
|
|
43501
|
+
withheld: Object.freeze(t.withheld.map((w) => Object.freeze({ tool: w.tool, why: w.why }))),
|
|
43502
|
+
// A count only, never a name (content-free). 0 unless the MCP issuance above added mcp: grants.
|
|
43503
|
+
mcp_issued: Math.min(160, names.filter((n) => n.startsWith("mcp:")).length)
|
|
43504
|
+
});
|
|
43505
|
+
if (sameTuple(this.ownerDmLastTurn, candidate)) return;
|
|
43506
|
+
this.ownerDmLastTurn = candidate;
|
|
43507
|
+
this.toolStatusSender.note(candidate);
|
|
43508
|
+
} catch (e) {
|
|
43509
|
+
writeServiceLog2("warn", "tool_status.record_failed", {}, e);
|
|
43510
|
+
}
|
|
43511
|
+
}
|
|
43512
|
+
/** R2-M1: the served manifest names another owner than the pin; the held record belongs to the old owner. */
|
|
43513
|
+
dropOwnerDmLastTurn() {
|
|
43514
|
+
this.ownerDmLastTurn = null;
|
|
43515
|
+
this.toolStatusSender.drop();
|
|
43516
|
+
}
|
|
43517
|
+
/**
|
|
43518
|
+
* Phase 0 Task 9 (spec §3.5): POST the record, body = the record only, to the narrow agent-key route. The backend
|
|
43519
|
+
* resolves the identity from the device credential; no identity, channel or sender id is in the URL or the body.
|
|
43520
|
+
* Returns the status class only; never throws.
|
|
43521
|
+
*/
|
|
43522
|
+
async postToolStatus(rec) {
|
|
43523
|
+
try {
|
|
43524
|
+
const res = await this.fetchBounded("runtime_config_tool_status", "control", `${this.cfg.apiBase}/api/v1/agents/me/runtime-config/tool-status`, {
|
|
43525
|
+
method: "POST",
|
|
43526
|
+
headers: { authorization: `Bearer ${this.cfg.apiKey}`, "content-type": "application/json" },
|
|
43527
|
+
body: JSON.stringify(rec)
|
|
43528
|
+
});
|
|
43529
|
+
const retryAfter = res.headers.get("retry-after");
|
|
43530
|
+
try {
|
|
43531
|
+
await res.body?.cancel();
|
|
43532
|
+
} catch {
|
|
43533
|
+
}
|
|
43534
|
+
return { status: res.status, retryAfter };
|
|
43535
|
+
} catch (e) {
|
|
43536
|
+
return e?.name === "FetchDeadlineError" ? "timeout" : "network";
|
|
43537
|
+
}
|
|
43538
|
+
}
|
|
42186
43539
|
/**
|
|
42187
43540
|
* One content-free INFO line per authorization decision (every call site of
|
|
42188
43541
|
* authorizationFor stages exactly one runtime turn). It says WHY a turn got
|
|
@@ -42406,6 +43759,111 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42406
43759
|
* client's own wait already renders "Summary unavailable" — so there is
|
|
42407
43760
|
* nothing to catch here besides the send itself.
|
|
42408
43761
|
*/
|
|
43762
|
+
/**
|
|
43763
|
+
* 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
|
|
43764
|
+
* 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
|
|
43765
|
+
* that IS the signed owner DM; verifyOwnerTurn says `verified` (the pinned owner's signature, the signed DM, the leaf
|
|
43766
|
+
* check, not a replay) with `relayLeaf === false`; mcp-policy.json as written is `ok` with this server `ok`; a runtime
|
|
43767
|
+
* socket that declared mcp_client_v1 is attached; and the rate limits admit it. The replay row is written HERE
|
|
43768
|
+
* (M-3: the request never reaches stageRuntimeInbox, where it is otherwise written), for every verified request, before
|
|
43769
|
+
* anything is answered; the ownerReplayPending entry is removed in `finally`. A failure to record answers nothing.
|
|
43770
|
+
* Never throws. The answer runs outside the ingest cycle, so the row is acked now.
|
|
43771
|
+
*/
|
|
43772
|
+
async acceptMcpCatalogRequest(m, body) {
|
|
43773
|
+
try {
|
|
43774
|
+
const req = parseMcpCatalogRequest(body);
|
|
43775
|
+
if (req === null) return logMcpCatalogRequest("malformed");
|
|
43776
|
+
const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
|
|
43777
|
+
if (this.keyChannelForFrame(m) !== m.channelId || this.freshMeta(m.channelId)?.is_dm !== true || signedDm === void 0 || signedDm !== m.channelId) {
|
|
43778
|
+
return logMcpCatalogRequest("not_owner_dm");
|
|
43779
|
+
}
|
|
43780
|
+
const verdict = await this.verifyOwnerTurn(m);
|
|
43781
|
+
if (verdict.kind !== "verified") return logMcpCatalogRequest(verdict.kind === "failed" && verdict.reason === "replay" ? "replay" : "not_verified");
|
|
43782
|
+
this.store.recordOwnerMessage(verdict.replay.channelId, m.messageId, verdict.replay.groupId, verdict.replay.epoch, Date.now());
|
|
43783
|
+
if (verdict.relayLeaf !== false) return logMcpCatalogRequest("relay_leaf");
|
|
43784
|
+
const target = this.mcpCatalogTarget(req.serverId);
|
|
43785
|
+
const lister = this.mcpClientCapable() ? this.mcpCatalogListers.values().next().value : void 0;
|
|
43786
|
+
if (target === null || lister === void 0) return logMcpCatalogRequest("not_ready");
|
|
43787
|
+
const admitted = this.mcpCatalogRate.admit(req.serverId);
|
|
43788
|
+
if (admitted !== "ok") return admitted === "in_flight" ? logMcpCatalogRequest("in_flight") : void 0;
|
|
43789
|
+
const work = this.outsideIngestCycle(() => this.answerMcpCatalogRequest(m.channelId, req, lister)).finally(() => {
|
|
43790
|
+
this.mcpCatalogRate.release(req.serverId);
|
|
43791
|
+
this.mcpCatalogWork.delete(work);
|
|
43792
|
+
});
|
|
43793
|
+
this.mcpCatalogWork.add(work);
|
|
43794
|
+
} catch {
|
|
43795
|
+
logMcpCatalogRequest("failed");
|
|
43796
|
+
} finally {
|
|
43797
|
+
this.ownerReplayPending.delete(m.messageId);
|
|
43798
|
+
}
|
|
43799
|
+
}
|
|
43800
|
+
/** The signed server's vendor and the bridge catalog row's issuer, when mcp-policy.json as WRITTEN is `ok` with it `ok`. */
|
|
43801
|
+
mcpCatalogTarget(serverId) {
|
|
43802
|
+
const doc = this.mcpPolicyWritten;
|
|
43803
|
+
if (doc === null || doc.state !== "ok" || !Object.prototype.hasOwnProperty.call(doc.servers, serverId)) return null;
|
|
43804
|
+
const s = doc.servers[serverId];
|
|
43805
|
+
if (s.state !== "ok" || s.vendor === void 0) return null;
|
|
43806
|
+
const catalog = this.mcpCatalog ?? MCP_CATALOG;
|
|
43807
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
|
|
43808
|
+
return { vendor: s.vendor, issuer: catalog[s.vendor].issuer };
|
|
43809
|
+
}
|
|
43810
|
+
/** Ask the runtime, re-check, build (384 KB cap) and send. Never throws. */
|
|
43811
|
+
async answerMcpCatalogRequest(channelId, req, lister) {
|
|
43812
|
+
try {
|
|
43813
|
+
const target = this.mcpCatalogTarget(req.serverId);
|
|
43814
|
+
if (target === null) return logMcpCatalogRequest("not_ready");
|
|
43815
|
+
const frame = await lister(req.serverId, MCP_CATALOG_LIST_TIMEOUT_MS);
|
|
43816
|
+
if (frame === null || frame === void 0) return logMcpCatalogRequest("runtime_timeout");
|
|
43817
|
+
const listed = validateCatalogListReply(frame, req.serverId, target.vendor);
|
|
43818
|
+
if (listed === null) return logMcpCatalogRequest("runtime_refused");
|
|
43819
|
+
const still = this.mcpCatalogTarget(req.serverId);
|
|
43820
|
+
if (still === null || still.vendor !== target.vendor) return logMcpCatalogRequest("not_ready");
|
|
43821
|
+
await this.sendMcpCatalog(channelId, buildMcpCatalogBody({ serverId: req.serverId, requestId: req.requestId, vendor: still.vendor, issuer: still.issuer, reply: listed }));
|
|
43822
|
+
logMcpCatalogRequest("sent");
|
|
43823
|
+
} catch (e) {
|
|
43824
|
+
logMcpCatalogRequest(e instanceof McpReplyWithheldError ? "withheld" : "send_failed");
|
|
43825
|
+
}
|
|
43826
|
+
}
|
|
43827
|
+
/**
|
|
43828
|
+
* The `mcp_catalog` reply: a system_event envelope with a body-level type (the sendSummary shape), on the shared
|
|
43829
|
+
* self-heal seam and the one publish chokepoint. H-D applies to EVERY attempt (it carries vendor-supplied tool
|
|
43830
|
+
* descriptions): after the seal, before the POST, the leaves at the SEALED group and epoch must be exactly the owner
|
|
43831
|
+
* and the agent, or the frame is withheld. One message id across the retry.
|
|
43832
|
+
*/
|
|
43833
|
+
async sendMcpCatalog(channelId, body) {
|
|
43834
|
+
const joined = await this.reconcileJoin(channelId);
|
|
43835
|
+
if (!joined) throw new ChannelPendingError(channelId);
|
|
43836
|
+
const messageId = crypto.randomUUID();
|
|
43837
|
+
let attempt = 0;
|
|
43838
|
+
await this.sendWithSelfHealTerminal(
|
|
43839
|
+
this.provider,
|
|
43840
|
+
channelId,
|
|
43841
|
+
(epoch) => buildOutboundEnvelope({
|
|
43842
|
+
custody: this.custody,
|
|
43843
|
+
groupKey: this.provider,
|
|
43844
|
+
messageId,
|
|
43845
|
+
channelId,
|
|
43846
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
43847
|
+
senderId: this.cfg.identityId,
|
|
43848
|
+
senderType: "agent",
|
|
43849
|
+
type: "system_event",
|
|
43850
|
+
bodyPlaintext: JSON.stringify(body),
|
|
43851
|
+
bindingId: "",
|
|
43852
|
+
bindingVersion: "0.0.0",
|
|
43853
|
+
agentSkillId: null,
|
|
43854
|
+
epoch,
|
|
43855
|
+
traceparent: ZERO_TRACEPARENT,
|
|
43856
|
+
references: [],
|
|
43857
|
+
stage5Outcome: "allow",
|
|
43858
|
+
stage5Evaluators: []
|
|
43859
|
+
}),
|
|
43860
|
+
async (envelope) => {
|
|
43861
|
+
attempt++;
|
|
43862
|
+
this.assertMcpReplyRelayFree(channelId, envelope, attempt);
|
|
43863
|
+
return this.publishEnvelope(channelId, () => this.transport.publish(envelope));
|
|
43864
|
+
}
|
|
43865
|
+
);
|
|
43866
|
+
}
|
|
42409
43867
|
async handleSummarize(channelId, request) {
|
|
42410
43868
|
const body = await runSummarize({
|
|
42411
43869
|
request,
|
|
@@ -42437,13 +43895,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42437
43895
|
throw new Error("summarize_no_runtime");
|
|
42438
43896
|
}
|
|
42439
43897
|
const messages = [{ role: "user", content: prompt }];
|
|
42440
|
-
return new Promise((
|
|
43898
|
+
return new Promise((resolve2, reject) => {
|
|
42441
43899
|
const onAbort = () => reject(new Error("aborted"));
|
|
42442
43900
|
signal.addEventListener("abort", onAbort);
|
|
42443
43901
|
this.fastLlm.complete(messages).then(
|
|
42444
43902
|
(text) => {
|
|
42445
43903
|
signal.removeEventListener("abort", onAbort);
|
|
42446
|
-
|
|
43904
|
+
resolve2(text);
|
|
42447
43905
|
},
|
|
42448
43906
|
(err) => {
|
|
42449
43907
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -42916,6 +44374,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
42916
44374
|
new TextDecoder().decode(m.plaintext)
|
|
42917
44375
|
);
|
|
42918
44376
|
if (parsed.type && parsed.type !== "text") {
|
|
44377
|
+
if (parsed.type === "mcp_catalog_request" || parsed.type === "mcp_catalog") {
|
|
44378
|
+
if (parsed.type === "mcp_catalog_request") await this.acceptMcpCatalogRequest(m, parsed);
|
|
44379
|
+
acked.push(m.queueId);
|
|
44380
|
+
continue;
|
|
44381
|
+
}
|
|
42919
44382
|
if (parsed.type === "summarize") {
|
|
42920
44383
|
const request = parseSummarizeBody(parsed, this.cfg.identityId, writeServiceLog2);
|
|
42921
44384
|
if (request !== null) {
|
|
@@ -43391,7 +44854,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43391
44854
|
return got;
|
|
43392
44855
|
}
|
|
43393
44856
|
const cap = Math.min(waitSeconds, 50) * 1e3;
|
|
43394
|
-
await new Promise((
|
|
44857
|
+
await new Promise((resolve2) => {
|
|
43395
44858
|
let fired = false;
|
|
43396
44859
|
let timer;
|
|
43397
44860
|
const wake = () => {
|
|
@@ -43400,7 +44863,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43400
44863
|
clearTimeout(timer);
|
|
43401
44864
|
const i = this.waiters.indexOf(wake);
|
|
43402
44865
|
if (i >= 0) this.waiters.splice(i, 1);
|
|
43403
|
-
|
|
44866
|
+
resolve2();
|
|
43404
44867
|
};
|
|
43405
44868
|
timer = setTimeout(wake, cap);
|
|
43406
44869
|
this.waiters.push(wake);
|
|
@@ -43424,7 +44887,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43424
44887
|
}
|
|
43425
44888
|
/** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
|
|
43426
44889
|
delivering(m) {
|
|
43427
|
-
|
|
44890
|
+
let out = this.renarrowManifestGrants(m);
|
|
44891
|
+
if (!this.noteMcpDelivered(out)) {
|
|
44892
|
+
out = withMcpCapability(out, false);
|
|
44893
|
+
writeServiceLog2("warn", "mcp.delivered_record_full");
|
|
44894
|
+
}
|
|
43428
44895
|
this.noteDeliveredFrame(out);
|
|
43429
44896
|
return out;
|
|
43430
44897
|
}
|
|
@@ -43435,6 +44902,50 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43435
44902
|
* audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
|
|
43436
44903
|
*/
|
|
43437
44904
|
renarrowManifestGrants(m) {
|
|
44905
|
+
const narrowed = this.renarrowManifestGrantsBase(m);
|
|
44906
|
+
return this.renarrowMcpGrants(narrowed);
|
|
44907
|
+
}
|
|
44908
|
+
/**
|
|
44909
|
+
* MCP Task 16 (audit H-2; round 2 LOW), after the base re-narrowing: a buffered `mcp:` grant survives release only
|
|
44910
|
+
* when, NOW, the turn is still a plain owner-DM turn in the signed DM channel, the host switch reads "on", the
|
|
44911
|
+
* entitlement reading is in force, the name is not in skills.disabled, the mcp-policy.json document last written
|
|
44912
|
+
* lists the server ok with the tool, and a FRESH synchronous read of the DM's local leaves holds exactly the pinned
|
|
44913
|
+
* owner and the agent (MlsGroupKeyProvider.leafSnapshotCached: one `state` object, no await; null fails closed). Any
|
|
44914
|
+
* other mcp: grant is dropped, never kept as a fallback. relay_free is dropped whenever no mcp: grant survives.
|
|
44915
|
+
* Read-only: never touches the MLS group. Only narrows; returns `m` by reference when nothing changes.
|
|
44916
|
+
*/
|
|
44917
|
+
renarrowMcpGrants(m) {
|
|
44918
|
+
const a = m.authorization;
|
|
44919
|
+
if (!a) return m;
|
|
44920
|
+
const isMcp = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
|
|
44921
|
+
if (!a.tool_grants.some(isMcp) && a.relay_free === void 0) return m;
|
|
44922
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
44923
|
+
const manifest = pin !== null ? this.toolGrantManifest : null;
|
|
44924
|
+
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);
|
|
44925
|
+
const kept = a.tool_grants.filter((g) => {
|
|
44926
|
+
if (!isMcp(g)) return true;
|
|
44927
|
+
const parsed = parseMcpGrant(g.tool);
|
|
44928
|
+
return turnOk && g.mode === "read" && g.source === "manifest" && parsed !== null && !this.payloadToolDeny.has(g.tool) && mcpPolicyAllows(this.mcpPolicyWritten, parsed.serverId, parsed.tool);
|
|
44929
|
+
});
|
|
44930
|
+
const keepRelayFree = a.relay_free === true && kept.some(isMcp);
|
|
44931
|
+
if (kept.length === a.tool_grants.length && keepRelayFree === (a.relay_free === true)) return m;
|
|
44932
|
+
const { relay_free: _relayFree, ...rest } = a;
|
|
44933
|
+
return { ...m, authorization: Object.freeze({ ...rest, tool_grants: Object.freeze(kept), ...keepRelayFree ? { relay_free: true } : {} }) };
|
|
44934
|
+
}
|
|
44935
|
+
/**
|
|
44936
|
+
* 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
|
|
44937
|
+
* owner and this agent. Synchronous (one manager `state`, so the leaves belong to one epoch); a cold manager or any
|
|
44938
|
+
* throw reads as NOT relay-free.
|
|
44939
|
+
*/
|
|
44940
|
+
ownerDmRelayFreeNow(channelId, ownerIdentityId) {
|
|
44941
|
+
try {
|
|
44942
|
+
const snap = this.provider.leafSnapshotCached(channelId);
|
|
44943
|
+
return snap !== null && ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId, agentIdentityId: this.cfg.identityId });
|
|
44944
|
+
} catch {
|
|
44945
|
+
return false;
|
|
44946
|
+
}
|
|
44947
|
+
}
|
|
44948
|
+
renarrowManifestGrantsBase(m) {
|
|
43438
44949
|
const a = m.authorization;
|
|
43439
44950
|
if (!a || !a.tool_grants.some((g) => g.mode === "read" && g.source === "manifest")) return m;
|
|
43440
44951
|
const audience = a.audience === "owner_dm" || a.audience === "team_room" ? a.audience : null;
|
|
@@ -43443,13 +44954,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43443
44954
|
const bound = b !== null && b.state === "bound" && b.counter === this.toolGrantCounter;
|
|
43444
44955
|
const switchOn = audience === "owner_dm" ? this.ownerDmManifestGrants : audience === "team_room" ? this.roomManifestGrants : false;
|
|
43445
44956
|
const signedNow = new Set(audience && manifest ? manifest.grants[audience] ?? [] : []);
|
|
43446
|
-
const
|
|
44957
|
+
const constants5 = new Set(audience === "owner_dm" ? OWNER_DM_DEFAULT_READ_GRANTS.map((g) => g.tool) : audience === "team_room" ? ROOM_DEFAULT_READ_GRANTS.map((g) => g.tool) : []);
|
|
43447
44958
|
let changed = false;
|
|
43448
44959
|
const out = a.tool_grants.flatMap((g) => {
|
|
43449
44960
|
if (!(g.mode === "read" && g.source === "manifest")) return [g];
|
|
43450
44961
|
if (switchOn && bound && signedNow.has(g.tool)) return [g];
|
|
43451
44962
|
changed = true;
|
|
43452
|
-
return
|
|
44963
|
+
return constants5.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
43453
44964
|
});
|
|
43454
44965
|
if (!changed) return m;
|
|
43455
44966
|
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(out) }) };
|
|
@@ -43530,6 +45041,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43530
45041
|
return;
|
|
43531
45042
|
}
|
|
43532
45043
|
if (!this.admitRuntimeDelivery(m)) return;
|
|
45044
|
+
this.noteMcpDelivered(m, true);
|
|
43533
45045
|
const ch = m.channel_id;
|
|
43534
45046
|
this.replayingChannels.add(ch);
|
|
43535
45047
|
this.replyIntentReplays++;
|
|
@@ -43880,11 +45392,11 @@ ${channelId}`;
|
|
|
43880
45392
|
if (!res.ok) return void 0;
|
|
43881
45393
|
const rows = await res.json();
|
|
43882
45394
|
if (!Array.isArray(rows)) return void 0;
|
|
43883
|
-
const
|
|
45395
|
+
const own2 = rows.find(
|
|
43884
45396
|
(r) => r?.identity_id === this.cfg.identityId
|
|
43885
45397
|
);
|
|
43886
|
-
if (!
|
|
43887
|
-
return typeof
|
|
45398
|
+
if (!own2) return void 0;
|
|
45399
|
+
return typeof own2.paused_at === "string" && own2.paused_at.length > 0;
|
|
43888
45400
|
} catch (e) {
|
|
43889
45401
|
writeServiceLog2("warn", "pause.probe_failed", {}, e);
|
|
43890
45402
|
return void 0;
|
|
@@ -43917,6 +45429,10 @@ ${channelId}`;
|
|
|
43917
45429
|
/** Complete the durable handoff only after the runtime explicitly acks. */
|
|
43918
45430
|
ackRuntimeMessages(messageIds) {
|
|
43919
45431
|
for (const id of messageIds) this.roomGrantEpochs.delete(id);
|
|
45432
|
+
for (const id of messageIds) {
|
|
45433
|
+
const r = this.mcpDeliveredTurns.get(id);
|
|
45434
|
+
if (r) r.acked = true;
|
|
45435
|
+
}
|
|
43920
45436
|
for (const messageId of messageIds) {
|
|
43921
45437
|
this.store.ackRuntimeInbox(messageId);
|
|
43922
45438
|
this.runtimePendingIds.delete(messageId);
|
|
@@ -43998,6 +45514,7 @@ ${channelId}`;
|
|
|
43998
45514
|
const message = rest;
|
|
43999
45515
|
this.applyStagedMarks(message.message_id, marks);
|
|
44000
45516
|
this.runtimePendingIds.add(message.message_id);
|
|
45517
|
+
this.noteMcpDelivered(message, true);
|
|
44001
45518
|
if (this.cfg.ownerGrantPin?.pin && this.isOwnerVerifyFailed(message.message_id)) {
|
|
44002
45519
|
this.ownerVerifyFailedIds.set(message.message_id, message.channel_id);
|
|
44003
45520
|
}
|
|
@@ -44178,6 +45695,67 @@ ${channelId}`;
|
|
|
44178
45695
|
clearSearchProvidersSocket(token) {
|
|
44179
45696
|
if (this.searchProvidersSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
44180
45697
|
}
|
|
45698
|
+
/** MCP Task 16 (spec §15): the same AND-over-sockets state for mcp_client_v1, kept per socket. */
|
|
45699
|
+
mcpClientSockets = /* @__PURE__ */ new Map();
|
|
45700
|
+
noteMcpClientSocket(token, declared) {
|
|
45701
|
+
this.mcpClientSockets.set(token, declared);
|
|
45702
|
+
this.onCapabilityMaybeChanged();
|
|
45703
|
+
}
|
|
45704
|
+
clearMcpClientSocket(token) {
|
|
45705
|
+
if (this.mcpClientSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
45706
|
+
}
|
|
45707
|
+
/** MCP Task 18: ws-server registers (or, with null, removes) a socket's loopback catalog lister. */
|
|
45708
|
+
noteMcpCatalogLister(token, lister) {
|
|
45709
|
+
if (lister === null) this.mcpCatalogListers.delete(token);
|
|
45710
|
+
else this.mcpCatalogListers.set(token, lister);
|
|
45711
|
+
}
|
|
45712
|
+
/**
|
|
45713
|
+
* MCP Task 19 (spec §12.3): "ok" when every attached runtime socket declared mcp_client_v1 (the AND, which harness
|
|
45714
|
+
* sockets hold false: a harness socket counts in the AND and never declares it); else "harness_consumer" when the
|
|
45715
|
+
* latest hello was a harness; else "missing".
|
|
45716
|
+
*/
|
|
45717
|
+
reportedMcpCapability() {
|
|
45718
|
+
if (this.mcpClientCapable()) return "ok";
|
|
45719
|
+
return this.lastConsumerCapability === "harness_consumer" ? "harness_consumer" : "missing";
|
|
45720
|
+
}
|
|
45721
|
+
/**
|
|
45722
|
+
* MCP Task 19 (spec §12.3): at most 8 `{server_id, vendor, state}` for the signed servers of the manifest in force,
|
|
45723
|
+
* from mcp-policy.json as WRITTEN. Only the states the bridge itself knows: top `kill_switch` and `not_entitled` give
|
|
45724
|
+
* that state per server; top `ok` gives `ok` for a server the bridge lists `ok` and `malformed` for one it refused.
|
|
45725
|
+
* Every other top state (no capability, suspended, no config, switch off, section absent, malformed section) gives no
|
|
45726
|
+
* row. The runtime-only states (needs_login, login_expired, definition_changed, server_version_unsupported,
|
|
45727
|
+
* server_unreachable) have no runtime-to-bridge report path in Phase 1. `vendor` is reported only when it is a key of
|
|
45728
|
+
* the bridge's compiled catalog; a signed vendor outside it gives no row. Never a tool name, URL or issuer.
|
|
45729
|
+
*/
|
|
45730
|
+
reportedMcpServers() {
|
|
45731
|
+
const doc = this.mcpPolicyWritten;
|
|
45732
|
+
const signed = (this.cfg.ownerGrantPin?.pin ?? null) !== null ? this.toolGrantManifest?.mcp?.servers : void 0;
|
|
45733
|
+
if (doc === null || signed === void 0) return [];
|
|
45734
|
+
const catalog = this.mcpCatalog ?? MCP_CATALOG;
|
|
45735
|
+
const out = [];
|
|
45736
|
+
for (const id of Object.keys(signed).sort()) {
|
|
45737
|
+
if (out.length >= 8) break;
|
|
45738
|
+
const vendor = signed[id].vendor;
|
|
45739
|
+
if (!Object.prototype.hasOwnProperty.call(catalog, vendor)) continue;
|
|
45740
|
+
let state;
|
|
45741
|
+
if (doc.state === "kill_switch" || doc.state === "not_entitled") state = doc.state;
|
|
45742
|
+
else if (doc.state === "ok") state = Object.prototype.hasOwnProperty.call(doc.servers, id) && doc.servers[id].state === "ok" ? "ok" : "malformed";
|
|
45743
|
+
else continue;
|
|
45744
|
+
out.push({ server_id: id, vendor, state });
|
|
45745
|
+
}
|
|
45746
|
+
return out;
|
|
45747
|
+
}
|
|
45748
|
+
/**
|
|
45749
|
+
* MCP Task 16: every attached runtime socket declared mcp_client_v1; no socket attached means absent (false).
|
|
45750
|
+
* PR-B2 follow-up (documented): a HARNESS socket counts in this AND. ws-server notes every socket that said hello,
|
|
45751
|
+
* and a harness hello is always noted `false` (harnessClaimed never resets), so one attached harness connector holds
|
|
45752
|
+
* the capability false for files, the report and mcp-policy.json (top `no_capability`) while it is attached.
|
|
45753
|
+
*/
|
|
45754
|
+
mcpClientCapable() {
|
|
45755
|
+
if (this.mcpClientSockets.size === 0) return false;
|
|
45756
|
+
for (const v of this.mcpClientSockets.values()) if (!v) return false;
|
|
45757
|
+
return true;
|
|
45758
|
+
}
|
|
44181
45759
|
noteUtilityToolsSocket(token, declared) {
|
|
44182
45760
|
this.utilityToolsSockets.set(token, declared);
|
|
44183
45761
|
this.onCapabilityMaybeChanged();
|
|
@@ -44196,6 +45774,7 @@ ${channelId}`;
|
|
|
44196
45774
|
const out = /* @__PURE__ */ new Set();
|
|
44197
45775
|
if (this.searchProvidersCapable()) out.add("search_providers_v2");
|
|
44198
45776
|
if (this.utilityToolsCapable()) out.add("utility_tools_v1");
|
|
45777
|
+
if (this.mcpClientCapable()) out.add("mcp_client_v1");
|
|
44199
45778
|
return out;
|
|
44200
45779
|
}
|
|
44201
45780
|
/** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
|
|
@@ -44224,11 +45803,12 @@ ${channelId}`;
|
|
|
44224
45803
|
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
44225
45804
|
*/
|
|
44226
45805
|
onCapabilityMaybeChanged() {
|
|
44227
|
-
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable() };
|
|
45806
|
+
const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable(), mcp: this.mcpClientCapable() };
|
|
44228
45807
|
const prev = this.capabilityState;
|
|
44229
|
-
if (prev !== null && prev.search === next.search && prev.utility === next.utility) return;
|
|
45808
|
+
if (prev !== null && prev.search === next.search && prev.utility === next.utility && prev.mcp === next.mcp) return;
|
|
44230
45809
|
this.capabilityState = next;
|
|
44231
45810
|
this.recomputeManifestPolicy(this.capabilityFeatures());
|
|
45811
|
+
this.recomputeMcpPolicy();
|
|
44232
45812
|
if (this.closingDown) return;
|
|
44233
45813
|
if (prev === null && !next.search && !next.utility) return;
|
|
44234
45814
|
const now = Date.now();
|
|
@@ -44266,7 +45846,7 @@ ${channelId}`;
|
|
|
44266
45846
|
}
|
|
44267
45847
|
const withheld = new Set(Object.keys(TOOL_FEATURE).filter((t) => !toolAllowedByFeatures(t, features)));
|
|
44268
45848
|
if (withheld.size === 0) return;
|
|
44269
|
-
const path =
|
|
45849
|
+
const path = join18(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
|
|
44270
45850
|
if (!existsSync5(path)) return;
|
|
44271
45851
|
let doc;
|
|
44272
45852
|
try {
|
|
@@ -44474,6 +46054,7 @@ ${channelId}`;
|
|
|
44474
46054
|
async closeOnce() {
|
|
44475
46055
|
this.closingDown = true;
|
|
44476
46056
|
this.statusGate.close();
|
|
46057
|
+
this.toolStatusSender.close();
|
|
44477
46058
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
44478
46059
|
this.pauseRecheckTimers.clear();
|
|
44479
46060
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -44482,6 +46063,8 @@ ${channelId}`;
|
|
|
44482
46063
|
this.redeliveryTimer = null;
|
|
44483
46064
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
44484
46065
|
this.searchCapabilityRefetchTimer = null;
|
|
46066
|
+
if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
|
|
46067
|
+
this.mcpPolicyRetryTimer = null;
|
|
44485
46068
|
this.pingHandle?.close();
|
|
44486
46069
|
if (this.safetyTimer) clearInterval(this.safetyTimer);
|
|
44487
46070
|
if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
|
|
@@ -44491,8 +46074,8 @@ ${channelId}`;
|
|
|
44491
46074
|
let timer;
|
|
44492
46075
|
await Promise.race([
|
|
44493
46076
|
ingest.released,
|
|
44494
|
-
new Promise((
|
|
44495
|
-
timer = setTimeout(
|
|
46077
|
+
new Promise((resolve2) => {
|
|
46078
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44496
46079
|
})
|
|
44497
46080
|
]);
|
|
44498
46081
|
if (timer) clearTimeout(timer);
|
|
@@ -44502,8 +46085,8 @@ ${channelId}`;
|
|
|
44502
46085
|
let timer;
|
|
44503
46086
|
await Promise.race([
|
|
44504
46087
|
starting,
|
|
44505
|
-
new Promise((
|
|
44506
|
-
timer = setTimeout(
|
|
46088
|
+
new Promise((resolve2) => {
|
|
46089
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44507
46090
|
})
|
|
44508
46091
|
]);
|
|
44509
46092
|
if (timer) clearTimeout(timer);
|
|
@@ -44512,8 +46095,8 @@ ${channelId}`;
|
|
|
44512
46095
|
let timer;
|
|
44513
46096
|
await Promise.race([
|
|
44514
46097
|
this.membershipSweepWork,
|
|
44515
|
-
new Promise((
|
|
44516
|
-
timer = setTimeout(
|
|
46098
|
+
new Promise((resolve2) => {
|
|
46099
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44517
46100
|
})
|
|
44518
46101
|
]);
|
|
44519
46102
|
if (timer) clearTimeout(timer);
|
|
@@ -44523,8 +46106,8 @@ ${channelId}`;
|
|
|
44523
46106
|
let timer;
|
|
44524
46107
|
await Promise.race([
|
|
44525
46108
|
this.recallPruneWork,
|
|
44526
|
-
new Promise((
|
|
44527
|
-
timer = setTimeout(
|
|
46109
|
+
new Promise((resolve2) => {
|
|
46110
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44528
46111
|
})
|
|
44529
46112
|
]);
|
|
44530
46113
|
if (timer) clearTimeout(timer);
|
|
@@ -44534,8 +46117,8 @@ ${channelId}`;
|
|
|
44534
46117
|
let timer;
|
|
44535
46118
|
await Promise.race([
|
|
44536
46119
|
this.recallMaintenanceWork,
|
|
44537
|
-
new Promise((
|
|
44538
|
-
timer = setTimeout(
|
|
46120
|
+
new Promise((resolve2) => {
|
|
46121
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44539
46122
|
})
|
|
44540
46123
|
]);
|
|
44541
46124
|
if (timer) clearTimeout(timer);
|
|
@@ -44547,8 +46130,8 @@ ${channelId}`;
|
|
|
44547
46130
|
let timer;
|
|
44548
46131
|
await Promise.race([
|
|
44549
46132
|
Promise.allSettled([...this.recallTapWork]),
|
|
44550
|
-
new Promise((
|
|
44551
|
-
timer = setTimeout(
|
|
46133
|
+
new Promise((resolve2) => {
|
|
46134
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44552
46135
|
})
|
|
44553
46136
|
]);
|
|
44554
46137
|
if (timer) clearTimeout(timer);
|
|
@@ -44557,8 +46140,8 @@ ${channelId}`;
|
|
|
44557
46140
|
let timer;
|
|
44558
46141
|
await Promise.race([
|
|
44559
46142
|
Promise.allSettled([...this.unifiedWork]),
|
|
44560
|
-
new Promise((
|
|
44561
|
-
timer = setTimeout(
|
|
46143
|
+
new Promise((resolve2) => {
|
|
46144
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44562
46145
|
})
|
|
44563
46146
|
]);
|
|
44564
46147
|
if (timer) clearTimeout(timer);
|
|
@@ -44567,8 +46150,8 @@ ${channelId}`;
|
|
|
44567
46150
|
let timer;
|
|
44568
46151
|
await Promise.race([
|
|
44569
46152
|
this.recallPrewarmWork,
|
|
44570
|
-
new Promise((
|
|
44571
|
-
timer = setTimeout(
|
|
46153
|
+
new Promise((resolve2) => {
|
|
46154
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44572
46155
|
})
|
|
44573
46156
|
]);
|
|
44574
46157
|
if (timer) clearTimeout(timer);
|
|
@@ -44578,8 +46161,8 @@ ${channelId}`;
|
|
|
44578
46161
|
let timer;
|
|
44579
46162
|
await Promise.race([
|
|
44580
46163
|
Promise.allSettled([...this.replyWork]),
|
|
44581
|
-
new Promise((
|
|
44582
|
-
timer = setTimeout(
|
|
46164
|
+
new Promise((resolve2) => {
|
|
46165
|
+
timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
|
|
44583
46166
|
})
|
|
44584
46167
|
]);
|
|
44585
46168
|
if (timer) clearTimeout(timer);
|
|
@@ -44592,7 +46175,7 @@ ${channelId}`;
|
|
|
44592
46175
|
|
|
44593
46176
|
// src/config.ts
|
|
44594
46177
|
import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
|
|
44595
|
-
import { dirname as
|
|
46178
|
+
import { dirname as dirname7, join as join19 } from "node:path";
|
|
44596
46179
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
44597
46180
|
var CONFIG_ERROR_MESSAGE = {
|
|
44598
46181
|
credential_not_found: "credential file not found",
|
|
@@ -44630,7 +46213,7 @@ function loadConfig(opts) {
|
|
|
44630
46213
|
throw new ConfigError("credential_not_json");
|
|
44631
46214
|
}
|
|
44632
46215
|
for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new ConfigError("credential_missing_field", k);
|
|
44633
|
-
const storePath = opts.storePath ??
|
|
46216
|
+
const storePath = opts.storePath ?? join19(dirname7(opts.credentialPath), "bridge-state.db");
|
|
44634
46217
|
const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
|
|
44635
46218
|
writeServiceLog2(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
|
|
44636
46219
|
state: ServiceLogEnum.of(ownerGrantPin.state, ["ok", "no_pin", "pin_conflict", "malformed", "unsupported_algo", "symlink_refused", "unreadable"])
|
|
@@ -44818,6 +46401,10 @@ var BRIDGE_ERRORS = {
|
|
|
44818
46401
|
// Weather/currency Task 17 (OQ-P1): a send frame's credit is not the closed value, or rides an a2a frame. TERMINAL:
|
|
44819
46402
|
// the inbound is retired, so the model (and the weather call) never runs again for it.
|
|
44820
46403
|
reply_credit_unknown: { http: 400, message: "reply credit not recognised; not sent" },
|
|
46404
|
+
// Security fix 2026-10-30: an upload route was given a path that is not a
|
|
46405
|
+
// regular file directly inside the bridge's vault outbox (`vault_outbox` on
|
|
46406
|
+
// /healthz). Fixed and content-free: the path is never echoed or logged.
|
|
46407
|
+
path_outside_outbox: { http: 400, message: "uploads must come from the bridge vault outbox; copy the file there first" },
|
|
44821
46408
|
internal: { http: 500, message: "internal error" }
|
|
44822
46409
|
};
|
|
44823
46410
|
function errorBody(code, detail) {
|
|
@@ -44832,63 +46419,6 @@ function wsErrorFrame(code, detail) {
|
|
|
44832
46419
|
return { type: "error", ...errorBody(code, detail) };
|
|
44833
46420
|
}
|
|
44834
46421
|
|
|
44835
|
-
// src/consumer-grants.ts
|
|
44836
|
-
var NO_GRANTS = Object.freeze([]);
|
|
44837
|
-
var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
|
|
44838
|
-
function withConsumerGrants(m, perToolConsumer) {
|
|
44839
|
-
if (perToolConsumer) return m;
|
|
44840
|
-
const a = m.authorization;
|
|
44841
|
-
if (!a) return m;
|
|
44842
|
-
const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
|
|
44843
|
-
const stripMemory = "memory_write" in a;
|
|
44844
|
-
if (!stripGrants && !stripMemory) return m;
|
|
44845
|
-
const { memory_write: _dropped, ...rest } = a;
|
|
44846
|
-
return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
|
|
44847
|
-
}
|
|
44848
|
-
var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
|
|
44849
|
-
var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
|
|
44850
|
-
var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
|
|
44851
|
-
var CONSTANT_TOOL_NAMES = Object.freeze({
|
|
44852
|
-
owner_dm: Object.freeze(["current_time", "web_search"]),
|
|
44853
|
-
team_room: Object.freeze(["current_time"])
|
|
44854
|
-
});
|
|
44855
|
-
var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
|
|
44856
|
-
function withManifestCapability(m, manifestConsumer) {
|
|
44857
|
-
if (manifestConsumer) return m;
|
|
44858
|
-
const a = m.authorization;
|
|
44859
|
-
if (!a || !a.tool_grants.some(isManifestGrant)) return m;
|
|
44860
|
-
const constants4 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
|
|
44861
|
-
const narrowed = a.tool_grants.flatMap((g) => {
|
|
44862
|
-
if (!isManifestGrant(g)) return [g];
|
|
44863
|
-
return constants4.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
|
|
44864
|
-
});
|
|
44865
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
|
|
44866
|
-
}
|
|
44867
|
-
function withHarnessGrants(m, withheld) {
|
|
44868
|
-
if (!withheld) return m;
|
|
44869
|
-
const a = m.authorization;
|
|
44870
|
-
if (!a || a.tool_grants.length === 0) return m;
|
|
44871
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
|
|
44872
|
-
}
|
|
44873
|
-
function withSearchCapability(m, consumerHasFeature, signedProvider) {
|
|
44874
|
-
if (consumerHasFeature) return m;
|
|
44875
|
-
const a = m.authorization;
|
|
44876
|
-
if (!a) return m;
|
|
44877
|
-
const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
|
|
44878
|
-
const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
|
|
44879
|
-
if (kept.length === a.tool_grants.length) return m;
|
|
44880
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
44881
|
-
}
|
|
44882
|
-
var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
|
|
44883
|
-
function withUtilityCapability(m, consumerHasFeature) {
|
|
44884
|
-
if (consumerHasFeature) return m;
|
|
44885
|
-
const a = m.authorization;
|
|
44886
|
-
if (!a) return m;
|
|
44887
|
-
const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
|
|
44888
|
-
if (kept.length === a.tool_grants.length) return m;
|
|
44889
|
-
return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
|
|
44890
|
-
}
|
|
44891
|
-
|
|
44892
46422
|
// src/http-server.ts
|
|
44893
46423
|
function writeSse(res, msgs, onWriteError) {
|
|
44894
46424
|
for (let i = 0; i < msgs.length; i++) {
|
|
@@ -44911,7 +46441,8 @@ data: ${JSON.stringify(msgs[i])}
|
|
|
44911
46441
|
function deliverSseBatch(res, batch, agent, onWriteError) {
|
|
44912
46442
|
const withheld = agent.harnessGrantsWithheld();
|
|
44913
46443
|
const provider = agent.signedSearchProvider();
|
|
44914
|
-
const
|
|
46444
|
+
const hold = agent.r3Hold();
|
|
46445
|
+
const left3 = writeSse(res, batch.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)), onWriteError);
|
|
44915
46446
|
const leftIds = new Set(left3.map((message) => message.message_id));
|
|
44916
46447
|
agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
|
|
44917
46448
|
if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
|
|
@@ -45018,7 +46549,7 @@ function jsonError(res, code, detail) {
|
|
|
45018
46549
|
json(res, httpStatus(code), errorBody(code, detail));
|
|
45019
46550
|
}
|
|
45020
46551
|
function readBody(req, cap) {
|
|
45021
|
-
return new Promise((
|
|
46552
|
+
return new Promise((resolve2, reject) => {
|
|
45022
46553
|
let size = 0;
|
|
45023
46554
|
let done = false;
|
|
45024
46555
|
const chunks = [];
|
|
@@ -45038,7 +46569,7 @@ function readBody(req, cap) {
|
|
|
45038
46569
|
req.on("end", () => {
|
|
45039
46570
|
if (!done) {
|
|
45040
46571
|
done = true;
|
|
45041
|
-
|
|
46572
|
+
resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
45042
46573
|
}
|
|
45043
46574
|
});
|
|
45044
46575
|
req.on("error", (e) => {
|
|
@@ -45116,6 +46647,9 @@ function createHttpServer(agent, opts) {
|
|
|
45116
46647
|
}
|
|
45117
46648
|
return json(res, 200, r);
|
|
45118
46649
|
}
|
|
46650
|
+
if (method === "GET" && path === "/v1/vault-outbox") {
|
|
46651
|
+
return json(res, 200, { path: agent.vaultOutboxPath() });
|
|
46652
|
+
}
|
|
45119
46653
|
if (method === "POST" && path === "/v1/send-attachment") {
|
|
45120
46654
|
const parsed = sendAttachmentSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
45121
46655
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
@@ -45185,7 +46719,8 @@ function createHttpServer(agent, opts) {
|
|
|
45185
46719
|
const drained = await agent.drain(channel, wait);
|
|
45186
46720
|
const withheld = agent.harnessGrantsWithheld();
|
|
45187
46721
|
const provider = agent.signedSearchProvider();
|
|
45188
|
-
|
|
46722
|
+
const hold = agent.r3Hold();
|
|
46723
|
+
return json(res, 200, drained.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)));
|
|
45189
46724
|
}
|
|
45190
46725
|
if (method === "GET" && path === "/v1/events") {
|
|
45191
46726
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
@@ -45227,6 +46762,10 @@ function createHttpServer(agent, opts) {
|
|
|
45227
46762
|
if (e instanceof SendForbiddenError) return jsonError(res, "send_forbidden");
|
|
45228
46763
|
if (e instanceof GroupSupersededSendError) return jsonError(res, "group_superseded");
|
|
45229
46764
|
if (e instanceof IdempotencyConflictError) return jsonError(res, "invalid_request", e.message);
|
|
46765
|
+
if (e instanceof PathOutsideOutboxError) {
|
|
46766
|
+
writeServiceLog2("warn", "vault_outbox.refused");
|
|
46767
|
+
return jsonError(res, "path_outside_outbox");
|
|
46768
|
+
}
|
|
45230
46769
|
if (e instanceof InvalidPathError) return jsonError(res, "invalid_request", e.message);
|
|
45231
46770
|
if (e instanceof VaultPinMismatchError) return jsonError(res, "vault_ref_changed", { pin: e.pin });
|
|
45232
46771
|
if (e instanceof VaultIntegrityError) return jsonError(res, "vault_integrity_failed", e.message);
|
|
@@ -45251,6 +46790,7 @@ function parseHarnessReport(raw) {
|
|
|
45251
46790
|
}
|
|
45252
46791
|
|
|
45253
46792
|
// src/ws-server.ts
|
|
46793
|
+
import { randomBytes as randomBytes12 } from "node:crypto";
|
|
45254
46794
|
import { WebSocketServer, WebSocket } from "ws";
|
|
45255
46795
|
|
|
45256
46796
|
// src/memory/consumer-notes.ts
|
|
@@ -45361,6 +46901,8 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45361
46901
|
manifestGrants: false,
|
|
45362
46902
|
searchProviders: false,
|
|
45363
46903
|
utilityTools: false,
|
|
46904
|
+
mcpClient: false,
|
|
46905
|
+
pendingCatalog: /* @__PURE__ */ new Map(),
|
|
45364
46906
|
// helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
|
|
45365
46907
|
deliveryOpen: helloWaitMs <= 0,
|
|
45366
46908
|
deliveryToken: /* @__PURE__ */ new Map(),
|
|
@@ -45414,14 +46956,14 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45414
46956
|
continue;
|
|
45415
46957
|
}
|
|
45416
46958
|
const out = withConsumerNotes(
|
|
45417
|
-
withUtilityCapability(withSearchCapability(
|
|
46959
|
+
withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(
|
|
45418
46960
|
withManifestCapability(
|
|
45419
46961
|
withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
|
|
45420
46962
|
session.manifestGrants
|
|
45421
46963
|
),
|
|
45422
46964
|
session.searchProviders,
|
|
45423
46965
|
agent.signedSearchProvider()
|
|
45424
|
-
), session.utilityTools),
|
|
46966
|
+
), session.utilityTools), session.mcpClient), agent.r3Hold()),
|
|
45425
46967
|
batch[i],
|
|
45426
46968
|
session,
|
|
45427
46969
|
agent
|
|
@@ -45479,6 +47021,10 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45479
47021
|
if (session.manifestGrants) agent.clearManifestConsumer(session);
|
|
45480
47022
|
agent.clearSearchProvidersSocket(session);
|
|
45481
47023
|
agent.clearUtilityToolsSocket(session);
|
|
47024
|
+
agent.clearMcpClientSocket(session);
|
|
47025
|
+
agent.noteMcpCatalogLister(session, null);
|
|
47026
|
+
for (const settle of session.pendingCatalog.values()) settle(null);
|
|
47027
|
+
session.pendingCatalog.clear();
|
|
45482
47028
|
};
|
|
45483
47029
|
current = { sock, cleanup, sweepStale };
|
|
45484
47030
|
sock.on("close", cleanup);
|
|
@@ -45512,6 +47058,32 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
45512
47058
|
}
|
|
45513
47059
|
};
|
|
45514
47060
|
}
|
|
47061
|
+
var MCP_CATALOG_PENDING_MAX = 8;
|
|
47062
|
+
function mcpCatalogLister(sock, session) {
|
|
47063
|
+
return (serverId, timeoutMs) => new Promise((resolve2) => {
|
|
47064
|
+
if (session.closed || sock.readyState !== WebSocket.OPEN || session.pendingCatalog.size >= MCP_CATALOG_PENDING_MAX) {
|
|
47065
|
+
resolve2(null);
|
|
47066
|
+
return;
|
|
47067
|
+
}
|
|
47068
|
+
const requestId = randomBytes12(16).toString("hex");
|
|
47069
|
+
const timer = setTimeout(() => {
|
|
47070
|
+
session.pendingCatalog.delete(requestId);
|
|
47071
|
+
resolve2(null);
|
|
47072
|
+
}, timeoutMs);
|
|
47073
|
+
timer.unref?.();
|
|
47074
|
+
session.pendingCatalog.set(requestId, (frame) => {
|
|
47075
|
+
clearTimeout(timer);
|
|
47076
|
+
resolve2(frame);
|
|
47077
|
+
});
|
|
47078
|
+
try {
|
|
47079
|
+
sock.send(JSON.stringify({ type: "mcp_catalog_list", request_id: requestId, server_id: serverId }));
|
|
47080
|
+
} catch {
|
|
47081
|
+
session.pendingCatalog.delete(requestId);
|
|
47082
|
+
clearTimeout(timer);
|
|
47083
|
+
resolve2(null);
|
|
47084
|
+
}
|
|
47085
|
+
});
|
|
47086
|
+
}
|
|
45515
47087
|
function sendJson(sock, body) {
|
|
45516
47088
|
if (sock.readyState !== WebSocket.OPEN) return;
|
|
45517
47089
|
try {
|
|
@@ -45585,6 +47157,9 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45585
47157
|
agent.noteSearchProvidersSocket(session, session.searchProviders);
|
|
45586
47158
|
session.utilityTools = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(UTILITY_TOOLS_FEATURE);
|
|
45587
47159
|
agent.noteUtilityToolsSocket(session, session.utilityTools);
|
|
47160
|
+
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);
|
|
47161
|
+
agent.noteMcpClientSocket(session, session.mcpClient);
|
|
47162
|
+
agent.noteMcpCatalogLister(session, session.mcpClient ? mcpCatalogLister(sock, session) : null);
|
|
45588
47163
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
45589
47164
|
agent.noteNotesConsumer();
|
|
45590
47165
|
}
|
|
@@ -45592,13 +47167,21 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
45592
47167
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
45593
47168
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
45594
47169
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
45595
|
-
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE, REPLY_CREDIT_FEATURE] });
|
|
47170
|
+
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] });
|
|
45596
47171
|
if (!session.deliveryOpen) {
|
|
45597
47172
|
session.deliveryOpen = true;
|
|
45598
47173
|
flush();
|
|
45599
47174
|
}
|
|
45600
47175
|
return;
|
|
45601
47176
|
}
|
|
47177
|
+
if (m.type === "mcp_catalog_list_result") {
|
|
47178
|
+
const id = m.request_id;
|
|
47179
|
+
const settle = typeof id === "string" ? session.pendingCatalog.get(id) : void 0;
|
|
47180
|
+
if (settle === void 0) return;
|
|
47181
|
+
session.pendingCatalog.delete(id);
|
|
47182
|
+
settle(m);
|
|
47183
|
+
return;
|
|
47184
|
+
}
|
|
45602
47185
|
if (m.type === "nack") {
|
|
45603
47186
|
if (typeof m.message_id !== "string" || !m.message_id) {
|
|
45604
47187
|
sendJson(sock, wsErrorFrame("invalid_request"));
|