@securecoms/agent-bridge 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/bin.js +192 -6
- package/dist/bridge-agent.d.ts +64 -0
- package/dist/grants/mcp-policy.d.ts +7 -0
- package/dist/index.js +190 -4
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -973,6 +973,21 @@ copied after the owner's next typed message.
|
|
|
973
973
|
|
|
974
974
|
---
|
|
975
975
|
|
|
976
|
+
## Release notes: 0.37.0
|
|
977
|
+
|
|
978
|
+
<!-- [COPY, approved by Eric 2026-11-01 ("Notes ok"), connected apps removal confirmation] -->
|
|
979
|
+
|
|
980
|
+
#### What's new
|
|
981
|
+
- **Connected apps are ready on the agent side.** When you remove a connected app, your agent now confirms that it deleted the app's sign-in. SecureComs shows the removal as done only after your agent confirms it.
|
|
982
|
+
- The agent sends this confirmation only after a message in your private chat with it. Like the tool status summary, it gives the time only to the 15-minute period. Only you can see it.
|
|
983
|
+
- You still cannot turn connected apps on. That comes in a later SecureComs update.
|
|
984
|
+
|
|
985
|
+
#### Upgrade
|
|
986
|
+
|
|
987
|
+
Upgrade the bridge and the runtime together, then restart your agent. Connected apps need @securecoms/agent-runtime 0.17.0.
|
|
988
|
+
|
|
989
|
+
If you go back to 0.36.0, your agent keeps working.
|
|
990
|
+
|
|
976
991
|
## Release notes: 0.36.0
|
|
977
992
|
|
|
978
993
|
<!-- [COPY, approved by Eric 2026-10-31 ("notes OK, ship now"), tool status + connected apps groundwork] -->
|
package/dist/bin.js
CHANGED
|
@@ -7555,7 +7555,7 @@ __export(import_bundle_exports, {
|
|
|
7555
7555
|
ModelImportError: () => ModelImportError,
|
|
7556
7556
|
importModelBundle: () => importModelBundle
|
|
7557
7557
|
});
|
|
7558
|
-
import { createHash as
|
|
7558
|
+
import { createHash as createHash7, randomBytes as randomBytes13 } from "node:crypto";
|
|
7559
7559
|
import { readFileSync as readFileSync16, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7560
7560
|
import { join as join25 } from "node:path";
|
|
7561
7561
|
async function importModelBundle(srcDir, root3, m) {
|
|
@@ -7578,7 +7578,7 @@ async function importModelBundle(srcDir, root3, m) {
|
|
|
7578
7578
|
} catch {
|
|
7579
7579
|
throw new ModelImportError(`${f.name} is missing from the import directory`);
|
|
7580
7580
|
}
|
|
7581
|
-
if (bytes.length !== f.size ||
|
|
7581
|
+
if (bytes.length !== f.size || createHash7("sha256").update(bytes).digest("hex") !== f.sha256) throw mismatch;
|
|
7582
7582
|
verified.push({ f, bytes });
|
|
7583
7583
|
}
|
|
7584
7584
|
const imported = [];
|
|
@@ -30465,6 +30465,7 @@ function checkGroupLink(v) {
|
|
|
30465
30465
|
}
|
|
30466
30466
|
|
|
30467
30467
|
// src/bridge-agent.ts
|
|
30468
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
30468
30469
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
30469
30470
|
|
|
30470
30471
|
// src/reply-intent.ts
|
|
@@ -36982,6 +36983,8 @@ function ownerDmRelayOf(leafClass, leaves, ids) {
|
|
|
36982
36983
|
|
|
36983
36984
|
// src/grants/mcp-policy.ts
|
|
36984
36985
|
var MCP_POLICY_WRITER_FEATURE = "mcp_policy_writer_v1";
|
|
36986
|
+
var MCP_DELETION_ACK_FEATURE = "mcp_deletion_ack_v1";
|
|
36987
|
+
var MCP_ACK_STATES = /* @__PURE__ */ new Set(["ok", "not_entitled", "kill_switch"]);
|
|
36985
36988
|
function mcpEntitlementReading(mcpEntitled, mcpCause) {
|
|
36986
36989
|
if (mcpEntitled === true && mcpCause === "entitled") return "entitled";
|
|
36987
36990
|
if (mcpEntitled === false && mcpCause === "not_entitled") return "not_entitled";
|
|
@@ -38289,6 +38292,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38289
38292
|
*/
|
|
38290
38293
|
mcpPolicyWritten = null;
|
|
38291
38294
|
mcpPolicyWrittenJson = null;
|
|
38295
|
+
/** Deletion-ack ruling §3: the manifest counter in force when `mcpPolicyWrittenJson` was written (never read live). */
|
|
38296
|
+
mcpPolicyWrittenCounter = null;
|
|
38297
|
+
/** Ruling §3: newest counter whose written policy the runtime fully acted on. Memory only, monotonic, starts at 0. */
|
|
38298
|
+
mcpDeletionsThrough = 0;
|
|
38299
|
+
/** Ruling §3: the last `credentials_present` an accepted frame carried; null until one arrives. */
|
|
38300
|
+
mcpCredentialsPresentValue = null;
|
|
38301
|
+
/** Ruling §3: per attached socket, whether its latest hello declared mcp_deletion_ack_v1 (harness noted false). */
|
|
38302
|
+
mcpDeletionAckSockets = /* @__PURE__ */ new Map();
|
|
38303
|
+
/** Ruling §3 rate: the ack-derived report values in the last report posted (null until one is). */
|
|
38304
|
+
lastReportedMcpAck = null;
|
|
38305
|
+
/** #1351 round 2 M-1: bumped on every byte-changing policy write (and on the inert document being put in force). */
|
|
38306
|
+
policyWriteSeq = 0;
|
|
38307
|
+
/** M-1: per ack-capable socket, the policyWriteSeq at the dispatch of each owner-DM turn, by message id (bounded). */
|
|
38308
|
+
mcpTurnSeq = /* @__PURE__ */ new Map();
|
|
38309
|
+
/**
|
|
38310
|
+
* #1351 round 2 H-1: the ack values the REPORT may carry. An ack lands seconds after an owner-DM turn and a report
|
|
38311
|
+
* posted then would time the turn (record_report sets applied_at). So, as the tool-status sender: `open` holds a
|
|
38312
|
+
* change while its 15-minute UTC bucket is open, `closed` what the report carries, and the only post is from one
|
|
38313
|
+
* timer at the next bucket boundary plus jitter.
|
|
38314
|
+
*/
|
|
38315
|
+
mcpAckOpen = null;
|
|
38316
|
+
mcpAckClosed = { through: 0, credentials: null };
|
|
38317
|
+
mcpAckTimer = null;
|
|
38292
38318
|
/** PR-B2 follow-up: a failed write is retried on this backoff (the last value repeats) until one lands or close(). */
|
|
38293
38319
|
mcpPolicyRetryDelaysMs;
|
|
38294
38320
|
mcpPolicyRetryTimer = null;
|
|
@@ -38933,15 +38959,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38933
38959
|
recomputeMcpPolicy() {
|
|
38934
38960
|
const doc = mcpPolicyFor(this.mcpPolicyInput());
|
|
38935
38961
|
const json2 = JSON.stringify(doc);
|
|
38936
|
-
if (json2 === this.mcpPolicyWrittenJson)
|
|
38962
|
+
if (json2 === this.mcpPolicyWrittenJson) {
|
|
38963
|
+
if (this.mcpPolicyWritten !== null && this.mcpPolicyWrittenCounter !== null) this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38964
|
+
return;
|
|
38965
|
+
}
|
|
38937
38966
|
if (MCP_INERT_STATES.has(doc.state) && !doc.r3_hold && Object.keys(doc.servers).length === 0 && !existsSync5(join20(runtimeConfigDir(this.home, this.cfg.identityId), MCP_POLICY_FILE))) {
|
|
38938
38967
|
this.mcpPolicyWritten = doc;
|
|
38939
38968
|
this.mcpPolicyWrittenJson = json2;
|
|
38969
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38970
|
+
this.policyWriteSeq++;
|
|
38940
38971
|
this.clearMcpPolicyRetry();
|
|
38941
38972
|
return;
|
|
38942
38973
|
}
|
|
38943
38974
|
this.mcpPolicyWritten = null;
|
|
38944
38975
|
this.mcpPolicyWrittenJson = null;
|
|
38976
|
+
this.mcpPolicyWrittenCounter = null;
|
|
38977
|
+
this.policyWriteSeq++;
|
|
38945
38978
|
try {
|
|
38946
38979
|
this.grantWriters.writeMcpPolicy(this.home, this.cfg.identityId, doc);
|
|
38947
38980
|
} catch (e) {
|
|
@@ -38951,6 +38984,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38951
38984
|
}
|
|
38952
38985
|
this.mcpPolicyWritten = doc;
|
|
38953
38986
|
this.mcpPolicyWrittenJson = json2;
|
|
38987
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38954
38988
|
this.clearMcpPolicyRetry();
|
|
38955
38989
|
}
|
|
38956
38990
|
/** A document is in force: no pending retry, and the next failure starts the backoff from the first delay. */
|
|
@@ -39066,6 +39100,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39066
39100
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
39067
39101
|
const reportedSearchProviders = this.searchProvidersCapable();
|
|
39068
39102
|
const reportedUtilityTools = this.utilityToolsCapable();
|
|
39103
|
+
const reportedMcpAck = this.mcpAckSignature();
|
|
39069
39104
|
const carriedOwnerDmTurn = this.toolStatusSender.carry();
|
|
39070
39105
|
const report = {
|
|
39071
39106
|
applied_version: version,
|
|
@@ -39140,6 +39175,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39140
39175
|
// MCP Task 19 (spec §12.3): separate from `capability`; content-free enums and pattern tokens only.
|
|
39141
39176
|
mcp_capability: this.reportedMcpCapability(),
|
|
39142
39177
|
mcp_servers: this.reportedMcpServers(),
|
|
39178
|
+
// Deletion-ack ruling §5.3: content-free scalars. `mcp_deletions_through` only while EVERY attached runtime socket
|
|
39179
|
+
// declared mcp_deletion_ack_v1 (absent = cannot acknowledge); `mcp_credentials_present` only once one reported.
|
|
39180
|
+
...this.reportedMcpAck(),
|
|
39143
39181
|
// Tool status Phase 0 (spec §3.5, round 2 H-5): an updated bridge, with or without a record yet.
|
|
39144
39182
|
tool_status_v1: true
|
|
39145
39183
|
},
|
|
@@ -39168,6 +39206,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
39168
39206
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
39169
39207
|
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
39170
39208
|
this.lastReportedUtilityTools = reportedUtilityTools;
|
|
39209
|
+
this.lastReportedMcpAck = reportedMcpAck;
|
|
39171
39210
|
if (version) writeServiceLog("info", "runtime_config.applied", { version });
|
|
39172
39211
|
else writeServiceLog("info", "runtime_config.legacy_memory_reported");
|
|
39173
39212
|
}
|
|
@@ -43515,7 +43554,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43515
43554
|
return;
|
|
43516
43555
|
}
|
|
43517
43556
|
this.logManifest(v.state);
|
|
43518
|
-
if (v.state === "wrong_owner")
|
|
43557
|
+
if (v.state === "wrong_owner") {
|
|
43558
|
+
this.dropOwnerDmLastTurn();
|
|
43559
|
+
this.mcpDeletionsThrough = 0;
|
|
43560
|
+
this.mcpPolicyWrittenCounter = null;
|
|
43561
|
+
this.noteMcpAckChanged();
|
|
43562
|
+
}
|
|
43519
43563
|
if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
|
|
43520
43564
|
}
|
|
43521
43565
|
/**
|
|
@@ -45990,6 +46034,118 @@ ${channelId}`;
|
|
|
45990
46034
|
clearMcpClientSocket(token) {
|
|
45991
46035
|
if (this.mcpClientSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
45992
46036
|
}
|
|
46037
|
+
/** Deletion-ack ruling §3: ws-server notes every runtime hello's mcp_deletion_ack_v1 declaration (harness: false). */
|
|
46038
|
+
noteMcpDeletionAckSocket(token, declared) {
|
|
46039
|
+
this.mcpDeletionAckSockets.set(token, declared);
|
|
46040
|
+
if (!declared) this.mcpTurnSeq.delete(token);
|
|
46041
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
46042
|
+
this.maybeReportMcpAck();
|
|
46043
|
+
}
|
|
46044
|
+
/** #1351 round 2 observation: with no ack-capable socket left (detached, or re-hello'd without the feature), the last
|
|
46045
|
+
* credentials value is no longer known: forget it, so a later socket does not inherit a stale value. */
|
|
46046
|
+
forgetCredentialsIfNoAckSocket() {
|
|
46047
|
+
if ([...this.mcpDeletionAckSockets.values()].some(Boolean)) return;
|
|
46048
|
+
this.mcpCredentialsPresentValue = null;
|
|
46049
|
+
this.mcpAckClosed = { ...this.mcpAckClosed, credentials: null };
|
|
46050
|
+
if (this.mcpAckOpen !== null) this.mcpAckOpen = { ...this.mcpAckOpen, credentials: null };
|
|
46051
|
+
}
|
|
46052
|
+
clearMcpDeletionAckSocket(token) {
|
|
46053
|
+
this.mcpTurnSeq.delete(token);
|
|
46054
|
+
if (!this.mcpDeletionAckSockets.delete(token)) return;
|
|
46055
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
46056
|
+
this.maybeReportMcpAck();
|
|
46057
|
+
}
|
|
46058
|
+
/** #1351 round 2 M-1: ws-server calls this when it sends an owner-DM turn to an ack-capable socket. Bounded. */
|
|
46059
|
+
noteMcpTurnDispatch(token, messageId) {
|
|
46060
|
+
let m = this.mcpTurnSeq.get(token);
|
|
46061
|
+
if (m === void 0) this.mcpTurnSeq.set(token, m = /* @__PURE__ */ new Map());
|
|
46062
|
+
m.set(messageId, this.policyWriteSeq);
|
|
46063
|
+
while (m.size > 64) m.delete(m.keys().next().value);
|
|
46064
|
+
}
|
|
46065
|
+
/** Ruling §3 / §5.3: the AND over attached sockets, false with none attached (as mcpClientCapable). */
|
|
46066
|
+
mcpDeletionAckCapable() {
|
|
46067
|
+
if (this.mcpDeletionAckSockets.size === 0) return false;
|
|
46068
|
+
for (const v of this.mcpDeletionAckSockets.values()) if (!v) return false;
|
|
46069
|
+
return true;
|
|
46070
|
+
}
|
|
46071
|
+
/**
|
|
46072
|
+
* Deletion-ack ruling (2026-10-31) §3 acceptance. ws-server has validated the frame shape and the socket (declared
|
|
46073
|
+
* mcp_deletion_ack_v1 and mcp_client_v1, no harness). `credentials_present` is taken as is. `applied_sha256` advances
|
|
46074
|
+
* `mcpDeletionsThrough` only when it is the SHA-256 of the bytes THIS bridge last wrote (JSON + newline) and still
|
|
46075
|
+
* holds in force, and that document's top state is one whose deletion set covers the signed removal; then to the
|
|
46076
|
+
* counter recorded at that write, never the live one. Anything else is ignored silently. Never throws.
|
|
46077
|
+
*/
|
|
46078
|
+
noteMcpDeletionAck(token, frame) {
|
|
46079
|
+
this.mcpCredentialsPresentValue = frame.credentials_present === true;
|
|
46080
|
+
const json2 = this.mcpPolicyWrittenJson;
|
|
46081
|
+
const doc = this.mcpPolicyWritten;
|
|
46082
|
+
const counter = this.mcpPolicyWrittenCounter;
|
|
46083
|
+
const turns = frame.message_id !== void 0 ? this.mcpTurnSeq.get(token) : void 0;
|
|
46084
|
+
const seqAtDispatch = frame.message_id !== void 0 ? turns?.get(frame.message_id) : void 0;
|
|
46085
|
+
if (frame.message_id !== void 0) turns?.delete(frame.message_id);
|
|
46086
|
+
if (frame.applied_sha256 !== void 0 && seqAtDispatch !== void 0 && seqAtDispatch === this.policyWriteSeq && json2 !== null && doc !== null && counter !== null && MCP_ACK_STATES.has(doc.state) && frame.applied_sha256 === createHash6("sha256").update(json2 + "\n", "utf8").digest("hex")) {
|
|
46087
|
+
this.mcpDeletionsThrough = Math.max(this.mcpDeletionsThrough, counter);
|
|
46088
|
+
}
|
|
46089
|
+
this.noteMcpAckChanged();
|
|
46090
|
+
}
|
|
46091
|
+
/** H-1: a change of the ack values goes into the open bucket; the report sees it only once the bucket has closed. */
|
|
46092
|
+
noteMcpAckChanged() {
|
|
46093
|
+
const now = this.toolStatusNow();
|
|
46094
|
+
this.promoteMcpAck(now);
|
|
46095
|
+
const live = { through: this.mcpDeletionsThrough, credentials: this.mcpCredentialsPresentValue };
|
|
46096
|
+
const base = this.mcpAckOpen ?? this.mcpAckClosed;
|
|
46097
|
+
if (base.through === live.through && base.credentials === live.credentials) return;
|
|
46098
|
+
this.mcpAckOpen = { ...live, end: bucketEndMs(bucketStartAt(now)) };
|
|
46099
|
+
this.scheduleMcpAckReport();
|
|
46100
|
+
}
|
|
46101
|
+
promoteMcpAck(now) {
|
|
46102
|
+
if (this.mcpAckOpen !== null && this.mcpAckOpen.end <= now) {
|
|
46103
|
+
this.mcpAckClosed = { through: this.mcpAckOpen.through, credentials: this.mcpAckOpen.credentials };
|
|
46104
|
+
this.mcpAckOpen = null;
|
|
46105
|
+
}
|
|
46106
|
+
}
|
|
46107
|
+
/** H-1: one timer, at the first bucket boundary strictly after now plus a jitter in [0, 60 s), as the tool-status sender. */
|
|
46108
|
+
scheduleMcpAckReport() {
|
|
46109
|
+
if (this.mcpAckTimer !== null || this.closingDown) return;
|
|
46110
|
+
const now = this.toolStatusNow();
|
|
46111
|
+
const boundary = (Math.floor(now / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
|
|
46112
|
+
const at = boundary + Math.floor(this.toolStatusRandom() * TOOL_STATUS_JITTER_MS);
|
|
46113
|
+
this.mcpAckTimer = this.toolStatusSetTimer(() => {
|
|
46114
|
+
this.mcpAckTimer = null;
|
|
46115
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
46116
|
+
if (this.lastReportedMcpAck !== this.mcpAckSignature() && this.bootConfigSettled && !this.closingDown) void this.refetchRuntimeConfig();
|
|
46117
|
+
if (this.mcpAckOpen !== null) this.scheduleMcpAckReport();
|
|
46118
|
+
}, Math.max(0, at - now));
|
|
46119
|
+
}
|
|
46120
|
+
/**
|
|
46121
|
+
* Ruling §5.3: the three keys. The two ack values come from the CLOSED bucket only (H-1), so no report, whatever
|
|
46122
|
+
* posts it, reveals an owner-DM turn before its 15-minute bucket has closed. `mcp_r3_hold` is not DM timing: live.
|
|
46123
|
+
*/
|
|
46124
|
+
reportedMcpAck() {
|
|
46125
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
46126
|
+
const ackCapable = this.mcpDeletionAckCapable();
|
|
46127
|
+
const anyAck = [...this.mcpDeletionAckSockets.values()].some(Boolean);
|
|
46128
|
+
const closed = this.mcpAckClosed;
|
|
46129
|
+
return {
|
|
46130
|
+
mcp_r3_hold: this.r3Hold(),
|
|
46131
|
+
...ackCapable ? { mcp_deletions_through: closed.through } : {},
|
|
46132
|
+
// `anyAck` is an equivalent mutant today (forgetCredentialsIfNoAckSocket nulls the value whenever no ack socket is
|
|
46133
|
+
// left), kept as defense in depth so the report never depends on that reset alone.
|
|
46134
|
+
...anyAck && closed.credentials !== null ? { mcp_credentials_present: closed.credentials } : {}
|
|
46135
|
+
};
|
|
46136
|
+
}
|
|
46137
|
+
mcpAckSignature() {
|
|
46138
|
+
return JSON.stringify(this.reportedMcpAck());
|
|
46139
|
+
}
|
|
46140
|
+
/**
|
|
46141
|
+
* A SOCKET change (attach, detach, a hello) can change which keys the report carries; their values are the closed
|
|
46142
|
+
* ones, so this is not owner-DM timing and may use the capability refetch. An ACK never comes here (H-1): it goes
|
|
46143
|
+
* through noteMcpAckChanged and the bucket timer.
|
|
46144
|
+
*/
|
|
46145
|
+
maybeReportMcpAck() {
|
|
46146
|
+
if (this.lastReportedMcpAck === this.mcpAckSignature()) return;
|
|
46147
|
+
this.scheduleCapabilityRefetch();
|
|
46148
|
+
}
|
|
45993
46149
|
/** MCP Task 18: ws-server registers (or, with null, removes) a socket's loopback catalog lister. */
|
|
45994
46150
|
noteMcpCatalogLister(token, lister) {
|
|
45995
46151
|
if (lister === null) this.mcpCatalogListers.delete(token);
|
|
@@ -46097,6 +46253,11 @@ ${channelId}`;
|
|
|
46097
46253
|
this.recomputeMcpPolicy();
|
|
46098
46254
|
if (this.closingDown) return;
|
|
46099
46255
|
if (prev === null && !next.search && !next.utility) return;
|
|
46256
|
+
this.scheduleCapabilityRefetch();
|
|
46257
|
+
}
|
|
46258
|
+
/** Debounced, with a max wait: a burst that never pauses still refetches at most maxWait after its first change. */
|
|
46259
|
+
scheduleCapabilityRefetch() {
|
|
46260
|
+
if (this.closingDown) return;
|
|
46100
46261
|
const now = Date.now();
|
|
46101
46262
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
46102
46263
|
else this.searchCapabilityBurstStart = now;
|
|
@@ -46114,7 +46275,7 @@ ${channelId}`;
|
|
|
46114
46275
|
searchCapabilityRefetchNeeded() {
|
|
46115
46276
|
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
46116
46277
|
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
46117
|
-
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable();
|
|
46278
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable() || this.lastReportedMcpAck !== this.mcpAckSignature();
|
|
46118
46279
|
}
|
|
46119
46280
|
/**
|
|
46120
46281
|
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
@@ -46341,6 +46502,10 @@ ${channelId}`;
|
|
|
46341
46502
|
this.closingDown = true;
|
|
46342
46503
|
this.statusGate.close();
|
|
46343
46504
|
this.toolStatusSender.close();
|
|
46505
|
+
if (this.mcpAckTimer !== null) {
|
|
46506
|
+
this.toolStatusClearTimer(this.mcpAckTimer);
|
|
46507
|
+
this.mcpAckTimer = null;
|
|
46508
|
+
}
|
|
46344
46509
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
46345
46510
|
this.pauseRecheckTimers.clear();
|
|
46346
46511
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -47114,6 +47279,10 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
47114
47279
|
try {
|
|
47115
47280
|
sock.send(JSON.stringify(frame));
|
|
47116
47281
|
inflight.set(batch[i].message_id, batch[i]);
|
|
47282
|
+
const a = frame.authorization;
|
|
47283
|
+
if (session.mcpDeletionAck === true && (a?.audience === "owner_dm" || a?.relay_speaker === "owner")) {
|
|
47284
|
+
agent.noteMcpTurnDispatch(session, batch[i].message_id);
|
|
47285
|
+
}
|
|
47117
47286
|
session.deliveryToken.set(batch[i].message_id, ++session.deliverySeq);
|
|
47118
47287
|
session.shownRecall.set(batch[i].message_id, { token: session.deliverySeq, ids: recallOut.shown });
|
|
47119
47288
|
session.shownUnified.set(batch[i].message_id, { token: session.deliverySeq, ids: unifiedOut.shown });
|
|
@@ -47162,6 +47331,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
47162
47331
|
agent.clearSearchProvidersSocket(session);
|
|
47163
47332
|
agent.clearUtilityToolsSocket(session);
|
|
47164
47333
|
agent.clearMcpClientSocket(session);
|
|
47334
|
+
agent.clearMcpDeletionAckSocket(session);
|
|
47165
47335
|
agent.noteMcpCatalogLister(session, null);
|
|
47166
47336
|
for (const settle of session.pendingCatalog.values()) settle(null);
|
|
47167
47337
|
session.pendingCatalog.clear();
|
|
@@ -47300,6 +47470,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
47300
47470
|
session.mcpClient = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(MANIFEST_GRANTS_FEATURE) && m.features.includes(MCP_CLIENT_FEATURE);
|
|
47301
47471
|
agent.noteMcpClientSocket(session, session.mcpClient);
|
|
47302
47472
|
agent.noteMcpCatalogLister(session, session.mcpClient ? mcpCatalogLister(sock, session) : null);
|
|
47473
|
+
session.mcpDeletionAck = session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MCP_DELETION_ACK_FEATURE);
|
|
47474
|
+
agent.noteMcpDeletionAckSocket(session, session.mcpDeletionAck);
|
|
47303
47475
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
47304
47476
|
agent.noteNotesConsumer();
|
|
47305
47477
|
}
|
|
@@ -47307,7 +47479,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
47307
47479
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
47308
47480
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
47309
47481
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
47310
|
-
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] });
|
|
47482
|
+
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE, REPLY_CREDIT_FEATURE, MCP_POLICY_WRITER_FEATURE, MCP_DELETION_ACK_FEATURE] });
|
|
47311
47483
|
if (!session.deliveryOpen) {
|
|
47312
47484
|
session.deliveryOpen = true;
|
|
47313
47485
|
flush();
|
|
@@ -47538,6 +47710,20 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
47538
47710
|
}
|
|
47539
47711
|
return;
|
|
47540
47712
|
}
|
|
47713
|
+
if (m.type === "mcp_deletion_ack") {
|
|
47714
|
+
const keys = Object.keys(m).sort().join(",");
|
|
47715
|
+
const valid = session.mcpDeletionAck === true && session.mcpClient === true && session.harnessClaimed !== true && (keys === "credentials_present,type" || keys === "credentials_present,message_id,type" || keys === "applied_sha256,credentials_present,message_id,type") && typeof m.credentials_present === "boolean" && (m.message_id === void 0 || typeof m.message_id === "string" && m.message_id.length > 0 && m.message_id.length <= 256) && (m.applied_sha256 === void 0 || typeof m.applied_sha256 === "string" && /^[0-9a-f]{64}$/.test(m.applied_sha256));
|
|
47716
|
+
if (!valid) {
|
|
47717
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
47718
|
+
return;
|
|
47719
|
+
}
|
|
47720
|
+
agent.noteMcpDeletionAck(session, {
|
|
47721
|
+
credentials_present: m.credentials_present,
|
|
47722
|
+
...typeof m.message_id === "string" ? { message_id: m.message_id } : {},
|
|
47723
|
+
...typeof m.applied_sha256 === "string" ? { applied_sha256: m.applied_sha256 } : {}
|
|
47724
|
+
});
|
|
47725
|
+
return;
|
|
47726
|
+
}
|
|
47541
47727
|
if (m.type === "room_cap_reached") {
|
|
47542
47728
|
if (!session.manifestGrants || typeof m.channel_id !== "string" || !LOWER_UUID.test(m.channel_id)) {
|
|
47543
47729
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -1240,6 +1240,29 @@ export declare class BridgeAgent {
|
|
|
1240
1240
|
*/
|
|
1241
1241
|
private mcpPolicyWritten;
|
|
1242
1242
|
private mcpPolicyWrittenJson;
|
|
1243
|
+
/** Deletion-ack ruling §3: the manifest counter in force when `mcpPolicyWrittenJson` was written (never read live). */
|
|
1244
|
+
private mcpPolicyWrittenCounter;
|
|
1245
|
+
/** Ruling §3: newest counter whose written policy the runtime fully acted on. Memory only, monotonic, starts at 0. */
|
|
1246
|
+
private mcpDeletionsThrough;
|
|
1247
|
+
/** Ruling §3: the last `credentials_present` an accepted frame carried; null until one arrives. */
|
|
1248
|
+
private mcpCredentialsPresentValue;
|
|
1249
|
+
/** Ruling §3: per attached socket, whether its latest hello declared mcp_deletion_ack_v1 (harness noted false). */
|
|
1250
|
+
private readonly mcpDeletionAckSockets;
|
|
1251
|
+
/** Ruling §3 rate: the ack-derived report values in the last report posted (null until one is). */
|
|
1252
|
+
private lastReportedMcpAck;
|
|
1253
|
+
/** #1351 round 2 M-1: bumped on every byte-changing policy write (and on the inert document being put in force). */
|
|
1254
|
+
private policyWriteSeq;
|
|
1255
|
+
/** M-1: per ack-capable socket, the policyWriteSeq at the dispatch of each owner-DM turn, by message id (bounded). */
|
|
1256
|
+
private readonly mcpTurnSeq;
|
|
1257
|
+
/**
|
|
1258
|
+
* #1351 round 2 H-1: the ack values the REPORT may carry. An ack lands seconds after an owner-DM turn and a report
|
|
1259
|
+
* posted then would time the turn (record_report sets applied_at). So, as the tool-status sender: `open` holds a
|
|
1260
|
+
* change while its 15-minute UTC bucket is open, `closed` what the report carries, and the only post is from one
|
|
1261
|
+
* timer at the next bucket boundary plus jitter.
|
|
1262
|
+
*/
|
|
1263
|
+
private mcpAckOpen;
|
|
1264
|
+
private mcpAckClosed;
|
|
1265
|
+
private mcpAckTimer;
|
|
1243
1266
|
/** PR-B2 follow-up: a failed write is retried on this backoff (the last value repeats) until one lands or close(). */
|
|
1244
1267
|
private readonly mcpPolicyRetryDelaysMs;
|
|
1245
1268
|
private mcpPolicyRetryTimer;
|
|
@@ -3744,6 +3767,45 @@ export declare class BridgeAgent {
|
|
|
3744
3767
|
private readonly mcpClientSockets;
|
|
3745
3768
|
noteMcpClientSocket(token: object, declared: boolean): void;
|
|
3746
3769
|
clearMcpClientSocket(token: object): void;
|
|
3770
|
+
/** Deletion-ack ruling §3: ws-server notes every runtime hello's mcp_deletion_ack_v1 declaration (harness: false). */
|
|
3771
|
+
noteMcpDeletionAckSocket(token: object, declared: boolean): void;
|
|
3772
|
+
/** #1351 round 2 observation: with no ack-capable socket left (detached, or re-hello'd without the feature), the last
|
|
3773
|
+
* credentials value is no longer known: forget it, so a later socket does not inherit a stale value. */
|
|
3774
|
+
private forgetCredentialsIfNoAckSocket;
|
|
3775
|
+
clearMcpDeletionAckSocket(token: object): void;
|
|
3776
|
+
/** #1351 round 2 M-1: ws-server calls this when it sends an owner-DM turn to an ack-capable socket. Bounded. */
|
|
3777
|
+
noteMcpTurnDispatch(token: object, messageId: string): void;
|
|
3778
|
+
/** Ruling §3 / §5.3: the AND over attached sockets, false with none attached (as mcpClientCapable). */
|
|
3779
|
+
private mcpDeletionAckCapable;
|
|
3780
|
+
/**
|
|
3781
|
+
* Deletion-ack ruling (2026-10-31) §3 acceptance. ws-server has validated the frame shape and the socket (declared
|
|
3782
|
+
* mcp_deletion_ack_v1 and mcp_client_v1, no harness). `credentials_present` is taken as is. `applied_sha256` advances
|
|
3783
|
+
* `mcpDeletionsThrough` only when it is the SHA-256 of the bytes THIS bridge last wrote (JSON + newline) and still
|
|
3784
|
+
* holds in force, and that document's top state is one whose deletion set covers the signed removal; then to the
|
|
3785
|
+
* counter recorded at that write, never the live one. Anything else is ignored silently. Never throws.
|
|
3786
|
+
*/
|
|
3787
|
+
noteMcpDeletionAck(token: object, frame: {
|
|
3788
|
+
credentials_present: boolean;
|
|
3789
|
+
message_id?: string;
|
|
3790
|
+
applied_sha256?: string;
|
|
3791
|
+
}): void;
|
|
3792
|
+
/** H-1: a change of the ack values goes into the open bucket; the report sees it only once the bucket has closed. */
|
|
3793
|
+
private noteMcpAckChanged;
|
|
3794
|
+
private promoteMcpAck;
|
|
3795
|
+
/** H-1: one timer, at the first bucket boundary strictly after now plus a jitter in [0, 60 s), as the tool-status sender. */
|
|
3796
|
+
private scheduleMcpAckReport;
|
|
3797
|
+
/**
|
|
3798
|
+
* Ruling §5.3: the three keys. The two ack values come from the CLOSED bucket only (H-1), so no report, whatever
|
|
3799
|
+
* posts it, reveals an owner-DM turn before its 15-minute bucket has closed. `mcp_r3_hold` is not DM timing: live.
|
|
3800
|
+
*/
|
|
3801
|
+
private reportedMcpAck;
|
|
3802
|
+
private mcpAckSignature;
|
|
3803
|
+
/**
|
|
3804
|
+
* A SOCKET change (attach, detach, a hello) can change which keys the report carries; their values are the closed
|
|
3805
|
+
* ones, so this is not owner-DM timing and may use the capability refetch. An ACK never comes here (H-1): it goes
|
|
3806
|
+
* through noteMcpAckChanged and the bucket timer.
|
|
3807
|
+
*/
|
|
3808
|
+
private maybeReportMcpAck;
|
|
3747
3809
|
/** MCP Task 18: ws-server registers (or, with null, removes) a socket's loopback catalog lister. */
|
|
3748
3810
|
noteMcpCatalogLister(token: object, lister: McpCatalogLister | null): void;
|
|
3749
3811
|
/**
|
|
@@ -3790,6 +3852,8 @@ export declare class BridgeAgent {
|
|
|
3790
3852
|
* switches and the new capability, then a debounced refetch (one per burst) only when needed.
|
|
3791
3853
|
*/
|
|
3792
3854
|
private onCapabilityMaybeChanged;
|
|
3855
|
+
/** Debounced, with a max wait: a burst that never pauses still refetches at most maxWait after its first change. */
|
|
3856
|
+
private scheduleCapabilityRefetch;
|
|
3793
3857
|
/**
|
|
3794
3858
|
* A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
|
|
3795
3859
|
* the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
|
|
@@ -33,6 +33,13 @@ import { type ServiceLog } from "../service-log.js";
|
|
|
33
33
|
* backend can write that name (ws-server.test.ts same-build row).
|
|
34
34
|
*/
|
|
35
35
|
export declare const MCP_POLICY_WRITER_FEATURE = "mcp_policy_writer_v1";
|
|
36
|
+
/**
|
|
37
|
+
* Deletion-ack ruling (2026-10-31) §3: declared in the hello reply and by the runtime in its hello. Only a socket that
|
|
38
|
+
* declared it (and mcp_client_v1, no harness) may send `mcp_deletion_ack`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const MCP_DELETION_ACK_FEATURE = "mcp_deletion_ack_v1";
|
|
41
|
+
/** Ruling §3: the top states whose deletion set covers the manifest's signed `removed` (ok), or everything on disk. */
|
|
42
|
+
export declare const MCP_ACK_STATES: ReadonlySet<string>;
|
|
36
43
|
export type McpEntitlementReading = "entitled" | "not_entitled" | "suspended";
|
|
37
44
|
/**
|
|
38
45
|
* OQ-1 ruling (ADR G). Only `true`/"entitled" is in force and only `false`/"not_entitled" is the deleting downgrade.
|
package/dist/index.js
CHANGED
|
@@ -29719,6 +29719,7 @@ function checkGroupLink(v) {
|
|
|
29719
29719
|
}
|
|
29720
29720
|
|
|
29721
29721
|
// src/bridge-agent.ts
|
|
29722
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
29722
29723
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
29723
29724
|
|
|
29724
29725
|
// src/reply-intent.ts
|
|
@@ -36696,6 +36697,8 @@ function ownerDmRelayOf(leafClass, leaves, ids) {
|
|
|
36696
36697
|
|
|
36697
36698
|
// src/grants/mcp-policy.ts
|
|
36698
36699
|
var MCP_POLICY_WRITER_FEATURE = "mcp_policy_writer_v1";
|
|
36700
|
+
var MCP_DELETION_ACK_FEATURE = "mcp_deletion_ack_v1";
|
|
36701
|
+
var MCP_ACK_STATES = /* @__PURE__ */ new Set(["ok", "not_entitled", "kill_switch"]);
|
|
36699
36702
|
function mcpEntitlementReading(mcpEntitled, mcpCause) {
|
|
36700
36703
|
if (mcpEntitled === true && mcpCause === "entitled") return "entitled";
|
|
36701
36704
|
if (mcpEntitled === false && mcpCause === "not_entitled") return "not_entitled";
|
|
@@ -38003,6 +38006,29 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38003
38006
|
*/
|
|
38004
38007
|
mcpPolicyWritten = null;
|
|
38005
38008
|
mcpPolicyWrittenJson = null;
|
|
38009
|
+
/** Deletion-ack ruling §3: the manifest counter in force when `mcpPolicyWrittenJson` was written (never read live). */
|
|
38010
|
+
mcpPolicyWrittenCounter = null;
|
|
38011
|
+
/** Ruling §3: newest counter whose written policy the runtime fully acted on. Memory only, monotonic, starts at 0. */
|
|
38012
|
+
mcpDeletionsThrough = 0;
|
|
38013
|
+
/** Ruling §3: the last `credentials_present` an accepted frame carried; null until one arrives. */
|
|
38014
|
+
mcpCredentialsPresentValue = null;
|
|
38015
|
+
/** Ruling §3: per attached socket, whether its latest hello declared mcp_deletion_ack_v1 (harness noted false). */
|
|
38016
|
+
mcpDeletionAckSockets = /* @__PURE__ */ new Map();
|
|
38017
|
+
/** Ruling §3 rate: the ack-derived report values in the last report posted (null until one is). */
|
|
38018
|
+
lastReportedMcpAck = null;
|
|
38019
|
+
/** #1351 round 2 M-1: bumped on every byte-changing policy write (and on the inert document being put in force). */
|
|
38020
|
+
policyWriteSeq = 0;
|
|
38021
|
+
/** M-1: per ack-capable socket, the policyWriteSeq at the dispatch of each owner-DM turn, by message id (bounded). */
|
|
38022
|
+
mcpTurnSeq = /* @__PURE__ */ new Map();
|
|
38023
|
+
/**
|
|
38024
|
+
* #1351 round 2 H-1: the ack values the REPORT may carry. An ack lands seconds after an owner-DM turn and a report
|
|
38025
|
+
* posted then would time the turn (record_report sets applied_at). So, as the tool-status sender: `open` holds a
|
|
38026
|
+
* change while its 15-minute UTC bucket is open, `closed` what the report carries, and the only post is from one
|
|
38027
|
+
* timer at the next bucket boundary plus jitter.
|
|
38028
|
+
*/
|
|
38029
|
+
mcpAckOpen = null;
|
|
38030
|
+
mcpAckClosed = { through: 0, credentials: null };
|
|
38031
|
+
mcpAckTimer = null;
|
|
38006
38032
|
/** PR-B2 follow-up: a failed write is retried on this backoff (the last value repeats) until one lands or close(). */
|
|
38007
38033
|
mcpPolicyRetryDelaysMs;
|
|
38008
38034
|
mcpPolicyRetryTimer = null;
|
|
@@ -38647,15 +38673,22 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38647
38673
|
recomputeMcpPolicy() {
|
|
38648
38674
|
const doc = mcpPolicyFor(this.mcpPolicyInput());
|
|
38649
38675
|
const json2 = JSON.stringify(doc);
|
|
38650
|
-
if (json2 === this.mcpPolicyWrittenJson)
|
|
38676
|
+
if (json2 === this.mcpPolicyWrittenJson) {
|
|
38677
|
+
if (this.mcpPolicyWritten !== null && this.mcpPolicyWrittenCounter !== null) this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38678
|
+
return;
|
|
38679
|
+
}
|
|
38651
38680
|
if (MCP_INERT_STATES.has(doc.state) && !doc.r3_hold && Object.keys(doc.servers).length === 0 && !existsSync5(join18(runtimeConfigDir(this.home, this.cfg.identityId), MCP_POLICY_FILE))) {
|
|
38652
38681
|
this.mcpPolicyWritten = doc;
|
|
38653
38682
|
this.mcpPolicyWrittenJson = json2;
|
|
38683
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38684
|
+
this.policyWriteSeq++;
|
|
38654
38685
|
this.clearMcpPolicyRetry();
|
|
38655
38686
|
return;
|
|
38656
38687
|
}
|
|
38657
38688
|
this.mcpPolicyWritten = null;
|
|
38658
38689
|
this.mcpPolicyWrittenJson = null;
|
|
38690
|
+
this.mcpPolicyWrittenCounter = null;
|
|
38691
|
+
this.policyWriteSeq++;
|
|
38659
38692
|
try {
|
|
38660
38693
|
this.grantWriters.writeMcpPolicy(this.home, this.cfg.identityId, doc);
|
|
38661
38694
|
} catch (e) {
|
|
@@ -38665,6 +38698,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38665
38698
|
}
|
|
38666
38699
|
this.mcpPolicyWritten = doc;
|
|
38667
38700
|
this.mcpPolicyWrittenJson = json2;
|
|
38701
|
+
this.mcpPolicyWrittenCounter = this.toolGrantCounter;
|
|
38668
38702
|
this.clearMcpPolicyRetry();
|
|
38669
38703
|
}
|
|
38670
38704
|
/** A document is in force: no pending retry, and the next failure starts the backoff from the first delay. */
|
|
@@ -38780,6 +38814,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38780
38814
|
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
38781
38815
|
const reportedSearchProviders = this.searchProvidersCapable();
|
|
38782
38816
|
const reportedUtilityTools = this.utilityToolsCapable();
|
|
38817
|
+
const reportedMcpAck = this.mcpAckSignature();
|
|
38783
38818
|
const carriedOwnerDmTurn = this.toolStatusSender.carry();
|
|
38784
38819
|
const report = {
|
|
38785
38820
|
applied_version: version,
|
|
@@ -38854,6 +38889,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38854
38889
|
// MCP Task 19 (spec §12.3): separate from `capability`; content-free enums and pattern tokens only.
|
|
38855
38890
|
mcp_capability: this.reportedMcpCapability(),
|
|
38856
38891
|
mcp_servers: this.reportedMcpServers(),
|
|
38892
|
+
// Deletion-ack ruling §5.3: content-free scalars. `mcp_deletions_through` only while EVERY attached runtime socket
|
|
38893
|
+
// declared mcp_deletion_ack_v1 (absent = cannot acknowledge); `mcp_credentials_present` only once one reported.
|
|
38894
|
+
...this.reportedMcpAck(),
|
|
38857
38895
|
// Tool status Phase 0 (spec §3.5, round 2 H-5): an updated bridge, with or without a record yet.
|
|
38858
38896
|
tool_status_v1: true
|
|
38859
38897
|
},
|
|
@@ -38882,6 +38920,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
38882
38920
|
this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
|
|
38883
38921
|
this.lastReportedSearchProviders = reportedSearchProviders;
|
|
38884
38922
|
this.lastReportedUtilityTools = reportedUtilityTools;
|
|
38923
|
+
this.lastReportedMcpAck = reportedMcpAck;
|
|
38885
38924
|
if (version) writeServiceLog2("info", "runtime_config.applied", { version });
|
|
38886
38925
|
else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
|
|
38887
38926
|
}
|
|
@@ -43229,7 +43268,12 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
43229
43268
|
return;
|
|
43230
43269
|
}
|
|
43231
43270
|
this.logManifest(v.state);
|
|
43232
|
-
if (v.state === "wrong_owner")
|
|
43271
|
+
if (v.state === "wrong_owner") {
|
|
43272
|
+
this.dropOwnerDmLastTurn();
|
|
43273
|
+
this.mcpDeletionsThrough = 0;
|
|
43274
|
+
this.mcpPolicyWrittenCounter = null;
|
|
43275
|
+
this.noteMcpAckChanged();
|
|
43276
|
+
}
|
|
43233
43277
|
if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
|
|
43234
43278
|
}
|
|
43235
43279
|
/**
|
|
@@ -45704,6 +45748,118 @@ ${channelId}`;
|
|
|
45704
45748
|
clearMcpClientSocket(token) {
|
|
45705
45749
|
if (this.mcpClientSockets.delete(token)) this.onCapabilityMaybeChanged();
|
|
45706
45750
|
}
|
|
45751
|
+
/** Deletion-ack ruling §3: ws-server notes every runtime hello's mcp_deletion_ack_v1 declaration (harness: false). */
|
|
45752
|
+
noteMcpDeletionAckSocket(token, declared) {
|
|
45753
|
+
this.mcpDeletionAckSockets.set(token, declared);
|
|
45754
|
+
if (!declared) this.mcpTurnSeq.delete(token);
|
|
45755
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
45756
|
+
this.maybeReportMcpAck();
|
|
45757
|
+
}
|
|
45758
|
+
/** #1351 round 2 observation: with no ack-capable socket left (detached, or re-hello'd without the feature), the last
|
|
45759
|
+
* credentials value is no longer known: forget it, so a later socket does not inherit a stale value. */
|
|
45760
|
+
forgetCredentialsIfNoAckSocket() {
|
|
45761
|
+
if ([...this.mcpDeletionAckSockets.values()].some(Boolean)) return;
|
|
45762
|
+
this.mcpCredentialsPresentValue = null;
|
|
45763
|
+
this.mcpAckClosed = { ...this.mcpAckClosed, credentials: null };
|
|
45764
|
+
if (this.mcpAckOpen !== null) this.mcpAckOpen = { ...this.mcpAckOpen, credentials: null };
|
|
45765
|
+
}
|
|
45766
|
+
clearMcpDeletionAckSocket(token) {
|
|
45767
|
+
this.mcpTurnSeq.delete(token);
|
|
45768
|
+
if (!this.mcpDeletionAckSockets.delete(token)) return;
|
|
45769
|
+
this.forgetCredentialsIfNoAckSocket();
|
|
45770
|
+
this.maybeReportMcpAck();
|
|
45771
|
+
}
|
|
45772
|
+
/** #1351 round 2 M-1: ws-server calls this when it sends an owner-DM turn to an ack-capable socket. Bounded. */
|
|
45773
|
+
noteMcpTurnDispatch(token, messageId) {
|
|
45774
|
+
let m = this.mcpTurnSeq.get(token);
|
|
45775
|
+
if (m === void 0) this.mcpTurnSeq.set(token, m = /* @__PURE__ */ new Map());
|
|
45776
|
+
m.set(messageId, this.policyWriteSeq);
|
|
45777
|
+
while (m.size > 64) m.delete(m.keys().next().value);
|
|
45778
|
+
}
|
|
45779
|
+
/** Ruling §3 / §5.3: the AND over attached sockets, false with none attached (as mcpClientCapable). */
|
|
45780
|
+
mcpDeletionAckCapable() {
|
|
45781
|
+
if (this.mcpDeletionAckSockets.size === 0) return false;
|
|
45782
|
+
for (const v of this.mcpDeletionAckSockets.values()) if (!v) return false;
|
|
45783
|
+
return true;
|
|
45784
|
+
}
|
|
45785
|
+
/**
|
|
45786
|
+
* Deletion-ack ruling (2026-10-31) §3 acceptance. ws-server has validated the frame shape and the socket (declared
|
|
45787
|
+
* mcp_deletion_ack_v1 and mcp_client_v1, no harness). `credentials_present` is taken as is. `applied_sha256` advances
|
|
45788
|
+
* `mcpDeletionsThrough` only when it is the SHA-256 of the bytes THIS bridge last wrote (JSON + newline) and still
|
|
45789
|
+
* holds in force, and that document's top state is one whose deletion set covers the signed removal; then to the
|
|
45790
|
+
* counter recorded at that write, never the live one. Anything else is ignored silently. Never throws.
|
|
45791
|
+
*/
|
|
45792
|
+
noteMcpDeletionAck(token, frame) {
|
|
45793
|
+
this.mcpCredentialsPresentValue = frame.credentials_present === true;
|
|
45794
|
+
const json2 = this.mcpPolicyWrittenJson;
|
|
45795
|
+
const doc = this.mcpPolicyWritten;
|
|
45796
|
+
const counter = this.mcpPolicyWrittenCounter;
|
|
45797
|
+
const turns = frame.message_id !== void 0 ? this.mcpTurnSeq.get(token) : void 0;
|
|
45798
|
+
const seqAtDispatch = frame.message_id !== void 0 ? turns?.get(frame.message_id) : void 0;
|
|
45799
|
+
if (frame.message_id !== void 0) turns?.delete(frame.message_id);
|
|
45800
|
+
if (frame.applied_sha256 !== void 0 && seqAtDispatch !== void 0 && seqAtDispatch === this.policyWriteSeq && json2 !== null && doc !== null && counter !== null && MCP_ACK_STATES.has(doc.state) && frame.applied_sha256 === createHash6("sha256").update(json2 + "\n", "utf8").digest("hex")) {
|
|
45801
|
+
this.mcpDeletionsThrough = Math.max(this.mcpDeletionsThrough, counter);
|
|
45802
|
+
}
|
|
45803
|
+
this.noteMcpAckChanged();
|
|
45804
|
+
}
|
|
45805
|
+
/** H-1: a change of the ack values goes into the open bucket; the report sees it only once the bucket has closed. */
|
|
45806
|
+
noteMcpAckChanged() {
|
|
45807
|
+
const now = this.toolStatusNow();
|
|
45808
|
+
this.promoteMcpAck(now);
|
|
45809
|
+
const live = { through: this.mcpDeletionsThrough, credentials: this.mcpCredentialsPresentValue };
|
|
45810
|
+
const base = this.mcpAckOpen ?? this.mcpAckClosed;
|
|
45811
|
+
if (base.through === live.through && base.credentials === live.credentials) return;
|
|
45812
|
+
this.mcpAckOpen = { ...live, end: bucketEndMs(bucketStartAt(now)) };
|
|
45813
|
+
this.scheduleMcpAckReport();
|
|
45814
|
+
}
|
|
45815
|
+
promoteMcpAck(now) {
|
|
45816
|
+
if (this.mcpAckOpen !== null && this.mcpAckOpen.end <= now) {
|
|
45817
|
+
this.mcpAckClosed = { through: this.mcpAckOpen.through, credentials: this.mcpAckOpen.credentials };
|
|
45818
|
+
this.mcpAckOpen = null;
|
|
45819
|
+
}
|
|
45820
|
+
}
|
|
45821
|
+
/** H-1: one timer, at the first bucket boundary strictly after now plus a jitter in [0, 60 s), as the tool-status sender. */
|
|
45822
|
+
scheduleMcpAckReport() {
|
|
45823
|
+
if (this.mcpAckTimer !== null || this.closingDown) return;
|
|
45824
|
+
const now = this.toolStatusNow();
|
|
45825
|
+
const boundary = (Math.floor(now / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
|
|
45826
|
+
const at = boundary + Math.floor(this.toolStatusRandom() * TOOL_STATUS_JITTER_MS);
|
|
45827
|
+
this.mcpAckTimer = this.toolStatusSetTimer(() => {
|
|
45828
|
+
this.mcpAckTimer = null;
|
|
45829
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
45830
|
+
if (this.lastReportedMcpAck !== this.mcpAckSignature() && this.bootConfigSettled && !this.closingDown) void this.refetchRuntimeConfig();
|
|
45831
|
+
if (this.mcpAckOpen !== null) this.scheduleMcpAckReport();
|
|
45832
|
+
}, Math.max(0, at - now));
|
|
45833
|
+
}
|
|
45834
|
+
/**
|
|
45835
|
+
* Ruling §5.3: the three keys. The two ack values come from the CLOSED bucket only (H-1), so no report, whatever
|
|
45836
|
+
* posts it, reveals an owner-DM turn before its 15-minute bucket has closed. `mcp_r3_hold` is not DM timing: live.
|
|
45837
|
+
*/
|
|
45838
|
+
reportedMcpAck() {
|
|
45839
|
+
this.promoteMcpAck(this.toolStatusNow());
|
|
45840
|
+
const ackCapable = this.mcpDeletionAckCapable();
|
|
45841
|
+
const anyAck = [...this.mcpDeletionAckSockets.values()].some(Boolean);
|
|
45842
|
+
const closed = this.mcpAckClosed;
|
|
45843
|
+
return {
|
|
45844
|
+
mcp_r3_hold: this.r3Hold(),
|
|
45845
|
+
...ackCapable ? { mcp_deletions_through: closed.through } : {},
|
|
45846
|
+
// `anyAck` is an equivalent mutant today (forgetCredentialsIfNoAckSocket nulls the value whenever no ack socket is
|
|
45847
|
+
// left), kept as defense in depth so the report never depends on that reset alone.
|
|
45848
|
+
...anyAck && closed.credentials !== null ? { mcp_credentials_present: closed.credentials } : {}
|
|
45849
|
+
};
|
|
45850
|
+
}
|
|
45851
|
+
mcpAckSignature() {
|
|
45852
|
+
return JSON.stringify(this.reportedMcpAck());
|
|
45853
|
+
}
|
|
45854
|
+
/**
|
|
45855
|
+
* A SOCKET change (attach, detach, a hello) can change which keys the report carries; their values are the closed
|
|
45856
|
+
* ones, so this is not owner-DM timing and may use the capability refetch. An ACK never comes here (H-1): it goes
|
|
45857
|
+
* through noteMcpAckChanged and the bucket timer.
|
|
45858
|
+
*/
|
|
45859
|
+
maybeReportMcpAck() {
|
|
45860
|
+
if (this.lastReportedMcpAck === this.mcpAckSignature()) return;
|
|
45861
|
+
this.scheduleCapabilityRefetch();
|
|
45862
|
+
}
|
|
45707
45863
|
/** MCP Task 18: ws-server registers (or, with null, removes) a socket's loopback catalog lister. */
|
|
45708
45864
|
noteMcpCatalogLister(token, lister) {
|
|
45709
45865
|
if (lister === null) this.mcpCatalogListers.delete(token);
|
|
@@ -45811,6 +45967,11 @@ ${channelId}`;
|
|
|
45811
45967
|
this.recomputeMcpPolicy();
|
|
45812
45968
|
if (this.closingDown) return;
|
|
45813
45969
|
if (prev === null && !next.search && !next.utility) return;
|
|
45970
|
+
this.scheduleCapabilityRefetch();
|
|
45971
|
+
}
|
|
45972
|
+
/** Debounced, with a max wait: a burst that never pauses still refetches at most maxWait after its first change. */
|
|
45973
|
+
scheduleCapabilityRefetch() {
|
|
45974
|
+
if (this.closingDown) return;
|
|
45814
45975
|
const now = Date.now();
|
|
45815
45976
|
if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
|
|
45816
45977
|
else this.searchCapabilityBurstStart = now;
|
|
@@ -45828,7 +45989,7 @@ ${channelId}`;
|
|
|
45828
45989
|
searchCapabilityRefetchNeeded() {
|
|
45829
45990
|
if (!this.bootConfigSettled || this.closingDown) return false;
|
|
45830
45991
|
const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
|
|
45831
|
-
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable();
|
|
45992
|
+
return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable() || this.lastReportedMcpAck !== this.mcpAckSignature();
|
|
45832
45993
|
}
|
|
45833
45994
|
/**
|
|
45834
45995
|
* D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
|
|
@@ -46055,6 +46216,10 @@ ${channelId}`;
|
|
|
46055
46216
|
this.closingDown = true;
|
|
46056
46217
|
this.statusGate.close();
|
|
46057
46218
|
this.toolStatusSender.close();
|
|
46219
|
+
if (this.mcpAckTimer !== null) {
|
|
46220
|
+
this.toolStatusClearTimer(this.mcpAckTimer);
|
|
46221
|
+
this.mcpAckTimer = null;
|
|
46222
|
+
}
|
|
46058
46223
|
for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
|
|
46059
46224
|
this.pauseRecheckTimers.clear();
|
|
46060
46225
|
for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
|
|
@@ -46974,6 +47139,10 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
46974
47139
|
try {
|
|
46975
47140
|
sock.send(JSON.stringify(frame));
|
|
46976
47141
|
inflight.set(batch[i].message_id, batch[i]);
|
|
47142
|
+
const a = frame.authorization;
|
|
47143
|
+
if (session.mcpDeletionAck === true && (a?.audience === "owner_dm" || a?.relay_speaker === "owner")) {
|
|
47144
|
+
agent.noteMcpTurnDispatch(session, batch[i].message_id);
|
|
47145
|
+
}
|
|
46977
47146
|
session.deliveryToken.set(batch[i].message_id, ++session.deliverySeq);
|
|
46978
47147
|
session.shownRecall.set(batch[i].message_id, { token: session.deliverySeq, ids: recallOut.shown });
|
|
46979
47148
|
session.shownUnified.set(batch[i].message_id, { token: session.deliverySeq, ids: unifiedOut.shown });
|
|
@@ -47022,6 +47191,7 @@ function attachAgentWebSocket(server, agent, opts) {
|
|
|
47022
47191
|
agent.clearSearchProvidersSocket(session);
|
|
47023
47192
|
agent.clearUtilityToolsSocket(session);
|
|
47024
47193
|
agent.clearMcpClientSocket(session);
|
|
47194
|
+
agent.clearMcpDeletionAckSocket(session);
|
|
47025
47195
|
agent.noteMcpCatalogLister(session, null);
|
|
47026
47196
|
for (const settle of session.pendingCatalog.values()) settle(null);
|
|
47027
47197
|
session.pendingCatalog.clear();
|
|
@@ -47160,6 +47330,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
47160
47330
|
session.mcpClient = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(MANIFEST_GRANTS_FEATURE) && m.features.includes(MCP_CLIENT_FEATURE);
|
|
47161
47331
|
agent.noteMcpClientSocket(session, session.mcpClient);
|
|
47162
47332
|
agent.noteMcpCatalogLister(session, session.mcpClient ? mcpCatalogLister(sock, session) : null);
|
|
47333
|
+
session.mcpDeletionAck = session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MCP_DELETION_ACK_FEATURE);
|
|
47334
|
+
agent.noteMcpDeletionAckSocket(session, session.mcpDeletionAck);
|
|
47163
47335
|
if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
|
|
47164
47336
|
agent.noteNotesConsumer();
|
|
47165
47337
|
}
|
|
@@ -47167,7 +47339,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
47167
47339
|
if (session.recallConsumer) agent.noteRecallConsumer();
|
|
47168
47340
|
session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
|
|
47169
47341
|
if (session.unifiedConsumer) agent.noteUnifiedConsumer();
|
|
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] });
|
|
47342
|
+
sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE, REPLY_CREDIT_FEATURE, MCP_POLICY_WRITER_FEATURE, MCP_DELETION_ACK_FEATURE] });
|
|
47171
47343
|
if (!session.deliveryOpen) {
|
|
47172
47344
|
session.deliveryOpen = true;
|
|
47173
47345
|
flush();
|
|
@@ -47398,6 +47570,20 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
47398
47570
|
}
|
|
47399
47571
|
return;
|
|
47400
47572
|
}
|
|
47573
|
+
if (m.type === "mcp_deletion_ack") {
|
|
47574
|
+
const keys = Object.keys(m).sort().join(",");
|
|
47575
|
+
const valid = session.mcpDeletionAck === true && session.mcpClient === true && session.harnessClaimed !== true && (keys === "credentials_present,type" || keys === "credentials_present,message_id,type" || keys === "applied_sha256,credentials_present,message_id,type") && typeof m.credentials_present === "boolean" && (m.message_id === void 0 || typeof m.message_id === "string" && m.message_id.length > 0 && m.message_id.length <= 256) && (m.applied_sha256 === void 0 || typeof m.applied_sha256 === "string" && /^[0-9a-f]{64}$/.test(m.applied_sha256));
|
|
47576
|
+
if (!valid) {
|
|
47577
|
+
sendJson(sock, wsErrorFrame("invalid_request"));
|
|
47578
|
+
return;
|
|
47579
|
+
}
|
|
47580
|
+
agent.noteMcpDeletionAck(session, {
|
|
47581
|
+
credentials_present: m.credentials_present,
|
|
47582
|
+
...typeof m.message_id === "string" ? { message_id: m.message_id } : {},
|
|
47583
|
+
...typeof m.applied_sha256 === "string" ? { applied_sha256: m.applied_sha256 } : {}
|
|
47584
|
+
});
|
|
47585
|
+
return;
|
|
47586
|
+
}
|
|
47401
47587
|
if (m.type === "room_cap_reached") {
|
|
47402
47588
|
if (!session.manifestGrants || typeof m.channel_id !== "string" || !LOWER_UUID.test(m.channel_id)) {
|
|
47403
47589
|
sendJson(sock, wsErrorFrame("invalid_request"));
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BRIDGE_VERSION = "0.
|
|
1
|
+
export declare const BRIDGE_VERSION = "0.37.0";
|
package/dist/ws-server.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export declare const MEMORY_UNIFIED_FEATURE = "memory_unified";
|
|
|
51
51
|
* fake against this list at run time (7b gate fix: a member added here without
|
|
52
52
|
* a fake made three tests throw "is not a function").
|
|
53
53
|
*/
|
|
54
|
-
export declare const WS_AGENT_MEMBERS: readonly ["ackRuntimeMessages", "action", "admitRuntimeDelivery", "applyMemoryNote", "clearHarnessReport", "clearManifestConsumer", "clearPerToolConsumer", "clearSearchProvidersSocket", "clearUtilityToolsSocket", "clearMcpClientSocket", "harnessGrantsWithheld", "signedSearchProvider", "r3Hold", "isRuntimeMessageRetired", "noteConsumerCapability", "noteManifestConsumer", "noteRoomCapReached", "noteSearchProvidersSocket", "noteUtilityToolsSocket", "noteMcpClientSocket", "noteMcpCatalogLister", "noteNotesConsumer", "notePerToolConsumer", "noteRecallConsumer", "noteUnifiedConsumer", "noteSessionClose", "noteSessionEvicted", "noteSessionOpen", "notesAppliedGeneration", "notesForDelivery", "recallForDelivery", "unifiedFeatureOn", "unifiedForDelivery", "recordHarnessReport", "recordRuntimeReply", "reportStatusGated", "requeueFailedRuntimeDeliveries", "requeueFront", "runtimeReplyReceipt", "send", "sendA2A", "subscribe", "takeDeliverable", "wakeListeners"];
|
|
54
|
+
export declare const WS_AGENT_MEMBERS: readonly ["ackRuntimeMessages", "action", "admitRuntimeDelivery", "applyMemoryNote", "clearHarnessReport", "clearManifestConsumer", "clearPerToolConsumer", "clearSearchProvidersSocket", "clearUtilityToolsSocket", "clearMcpClientSocket", "harnessGrantsWithheld", "signedSearchProvider", "r3Hold", "isRuntimeMessageRetired", "noteConsumerCapability", "noteManifestConsumer", "noteRoomCapReached", "noteSearchProvidersSocket", "noteUtilityToolsSocket", "noteMcpClientSocket", "noteMcpCatalogLister", "noteMcpDeletionAckSocket", "clearMcpDeletionAckSocket", "noteMcpDeletionAck", "noteMcpTurnDispatch", "noteNotesConsumer", "notePerToolConsumer", "noteRecallConsumer", "noteUnifiedConsumer", "noteSessionClose", "noteSessionEvicted", "noteSessionOpen", "notesAppliedGeneration", "notesForDelivery", "recallForDelivery", "unifiedFeatureOn", "unifiedForDelivery", "recordHarnessReport", "recordRuntimeReply", "reportStatusGated", "requeueFailedRuntimeDeliveries", "requeueFront", "runtimeReplyReceipt", "send", "sendA2A", "subscribe", "takeDeliverable", "wakeListeners"];
|
|
55
55
|
export type WsAgent = Pick<BridgeAgent, (typeof WS_AGENT_MEMBERS)[number]>;
|
|
56
56
|
export declare function attachAgentWebSocket(server: HttpServer, agent: WsAgent, opts: AgentWebSocketOptions): {
|
|
57
57
|
close(): void;
|