deadbyte-mcp 0.15.34 → 0.15.35
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/MANIFEST.SHA256 +67 -22
- package/PROVENANCE.json +57 -30
- package/README.txt +18 -14
- package/RELEASE.json +40 -31
- package/SBOM.cdx.json +25 -3
- package/TEST-MATRIX.json +129 -48
- package/bridge/package.json +11 -0
- package/bridge/src/bridge.mjs +412 -0
- package/bridge/src/node-http-adapter.mjs +56 -0
- package/bridge/src/public-tool-definitions.mjs +14 -0
- package/bridge/src/r40-governor.mjs +160 -0
- package/controller/deadbyte-controller.ps1 +84 -8
- package/controller/deadbyte-process-policy.json +4 -4
- package/package.json +5 -3
- package/scripts/bridge-benchmark-r58.mjs +67 -0
- package/scripts/deploy-r48.mjs +2 -3
- package/scripts/gate-windows-r58.ps1 +33 -0
- package/scripts/owner-probe-r58.mjs +31 -0
- package/scripts/performance-r58.mjs +57 -0
- package/scripts/public-baseline-r58.mjs +72 -0
- package/scripts/public-surface-r58.mjs +84 -0
- package/scripts/r58-cross-plane.ps1 +6 -0
- package/scripts/r58-focused.ps1 +6 -0
- package/scripts/windows-gate-evidence-r58.mjs +93 -0
- package/src/deadbyte-cli.mjs +40 -12
- package/src/doctor-foreground-r58.mjs +44 -0
- package/src/doctor-installation-r58.mjs +63 -0
- package/src/doctor-r45.mjs +21 -4
- package/src/doctor-runtime-r58.mjs +82 -0
- package/src/doctor-transactions-r58.mjs +40 -0
- package/src/foreground-process-identity.mjs +27 -0
- package/src/foreground-session-lock.mjs +20 -0
- package/src/foreground-session-lock.ps1 +15 -0
- package/src/release-generation.mjs +1 -0
- package/src/release-slot.mjs +2 -1
- package/src/remote-session-guardian.mjs +8 -1
- package/src/runtime-update.mjs +85 -2
- package/src/version.mjs +1 -1
- package/test/controller-r211.test.mjs +4 -1
- package/test/controller.test.mjs +2 -1
- package/test/helpers/r58-doctor-fixture.mjs +26 -0
- package/test/r49-windows-gate.test.mjs +2 -2
- package/test/r57-release-contract.test.mjs +5 -3
- package/test/r58-bridge-dependency-binding.test.mjs +62 -0
- package/test/r58-bundled-bridge.test.mjs +22 -0
- package/test/r58-cleanup-retention.test.mjs +45 -0
- package/test/r58-completed-update-receipt.test.mjs +76 -0
- package/test/r58-concurrent-recovery.test.mjs +54 -0
- package/test/r58-controller-core-observation.test.mjs +36 -0
- package/test/r58-controller-json.test.mjs +48 -0
- package/test/r58-controller-live-local.test.mjs +85 -0
- package/test/r58-controller-readonly.test.mjs +23 -0
- package/test/r58-controller-state-coherence.test.mjs +45 -0
- package/test/r58-doctor-cli.test.mjs +20 -0
- package/test/r58-doctor-foreground.test.mjs +50 -0
- package/test/r58-doctor-installation.test.mjs +55 -0
- package/test/r58-doctor-pointer.test.mjs +41 -0
- package/test/r58-doctor-runtime.test.mjs +45 -0
- package/test/r58-doctor-transactions.test.mjs +21 -0
- package/test/r58-guardian-channel.test.mjs +26 -0
- package/test/r58-launcher-shell.test.mjs +28 -0
- package/test/r58-owner-identity.test.mjs +20 -0
- package/test/r58-release-contract.test.mjs +49 -0
- package/test/r58-session-lock.test.mjs +26 -0
- package/test/r58-stale-owner.test.mjs +71 -0
- package/test/r58-windows-gate.test.mjs +31 -0
- package/test/release-generation.test.mjs +4 -1
- package/test/remote-session-cli.test.mjs +55 -2
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { Client } from '@modelcontextprotocol/client';
|
|
7
|
+
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
|
|
8
|
+
import { McpServer, createMcpHandler, createRequestStateCodec, inputRequired, inputResponse } from '@modelcontextprotocol/server';
|
|
9
|
+
import {preparePublicToolDefinitions} from './public-tool-definitions.mjs';
|
|
10
|
+
import {createDeadbyteNodeHandler} from './node-http-adapter.mjs';
|
|
11
|
+
import { bridgeCatalogSha256, bridgeDeadlineForTool, createBridgeGovernor, enforceDesktopBridgeResult, verifyCompactCatalogAttestation } from './r40-governor.mjs';
|
|
12
|
+
|
|
13
|
+
const coreRoot = path.resolve(process.env.DEADBYTE_CORE_ROOT ?? 'C:/Users/DEADBYTE/Downloads/deadbyte-mcp-v0.6.0');
|
|
14
|
+
const coreServer = path.join(coreRoot, 'src', 'mcp-server.mjs');
|
|
15
|
+
const workspaceRoot = path.resolve(process.env.DEADBYTE_WORKSPACE_ROOT ?? path.join(coreRoot, 'workspaces'));
|
|
16
|
+
const port = Number(process.env.DEADBYTE_BRIDGE_PORT ?? '8787');
|
|
17
|
+
const baseTools = ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'workspace_open'];
|
|
18
|
+
const privilegedTools = ['host_exec', 'host_file_read', 'host_file_write'];
|
|
19
|
+
const agentTools = [
|
|
20
|
+
'agent_task_approve', 'agent_task_cancel', 'agent_task_events', 'agent_task_run',
|
|
21
|
+
'agent_task_status', 'agent_task_submit', 'agent_task_verify'
|
|
22
|
+
];
|
|
23
|
+
const codingTools = [
|
|
24
|
+
'coding_benchmark_run', 'coding_command_run', 'coding_file_read', 'coding_file_write',
|
|
25
|
+
'coding_receipt_verify', 'coding_session_status', 'coding_task_approve', 'coding_task_cancel',
|
|
26
|
+
'coding_task_events', 'coding_task_run', 'coding_task_status', 'coding_task_submit',
|
|
27
|
+
'coding_task_verify', 'coding_text_replace', 'coding_tree'
|
|
28
|
+
];
|
|
29
|
+
const autonomousTools = [
|
|
30
|
+
'autonomous_goal_approve', 'autonomous_goal_cancel', 'autonomous_goal_events',
|
|
31
|
+
'autonomous_goal_run', 'autonomous_goal_status', 'autonomous_goal_submit',
|
|
32
|
+
'autonomous_goal_verify', 'autonomous_release_gate', 'autonomous_session_status',
|
|
33
|
+
'native_diff_apply', 'native_diff_preview', 'native_diff_verify'
|
|
34
|
+
];
|
|
35
|
+
const r35FullTools = [
|
|
36
|
+
'agent_task_approve','agent_task_cancel','agent_task_events','agent_task_run','agent_task_status','agent_task_submit','agent_task_verify',
|
|
37
|
+
'autonomous_goal_approve','autonomous_goal_cancel','autonomous_goal_events','autonomous_goal_run','autonomous_goal_status','autonomous_goal_submit','autonomous_goal_verify','autonomous_release_gate','autonomous_session_status',
|
|
38
|
+
'coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read','coding_receipt_verify','coding_search','coding_session_status','coding_stat',
|
|
39
|
+
'coding_symbol_definition','coding_symbol_outline','coding_symbol_references','coding_symbol_replace',
|
|
40
|
+
'coding_task_approve','coding_task_cancel','coding_task_events','coding_task_run','coding_task_status','coding_task_submit','coding_task_verify','coding_text_replace','coding_tree',
|
|
41
|
+
'contained_transform','evidence_verify','file_read',
|
|
42
|
+
'host_exec','host_file_read','host_file_write',
|
|
43
|
+
'machine_fs_delete','machine_fs_edit_exact','machine_fs_list','machine_fs_mkdir','machine_fs_move','machine_fs_read','machine_fs_stat','machine_fs_write',
|
|
44
|
+
'machine_list','machine_ping','machine_process_list','machine_revoke','machine_search_cancel','machine_search_page','machine_search_start','machine_search_status','machine_status','machine_system_status',
|
|
45
|
+
'native_diff_apply','native_diff_preview','native_diff_verify',
|
|
46
|
+
'process_session_action','process_session_list','process_session_read','process_session_start','process_session_status','process_session_terminate',
|
|
47
|
+
'proof_execute','workspace_open'
|
|
48
|
+
].sort();
|
|
49
|
+
const r36CompactTools = [
|
|
50
|
+
'autonomous_goal_approve','autonomous_goal_cancel','autonomous_goal_events','autonomous_goal_run','autonomous_goal_status','autonomous_goal_submit','autonomous_goal_verify','autonomous_release_gate','autonomous_session_status',
|
|
51
|
+
'capability_manifest',
|
|
52
|
+
'coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_inspect','coding_receipt_verify','coding_session_status','coding_symbol_replace',
|
|
53
|
+
'coding_task_approve','coding_task_cancel','coding_task_events','coding_task_run','coding_task_status','coding_task_submit','coding_task_verify','coding_text_replace','coding_tree',
|
|
54
|
+
'evidence_verify',
|
|
55
|
+
'host_exec','host_file_read','host_file_write',
|
|
56
|
+
'machine_fs','machine_list','machine_observe','machine_ping','machine_revoke','machine_status',
|
|
57
|
+
'native_diff_apply','native_diff_preview','native_diff_verify',
|
|
58
|
+
'process_session'
|
|
59
|
+
].sort();
|
|
60
|
+
if (r35FullTools.length !== 73) throw new Error(`R35 production catalog constant drift: ${r35FullTools.length}`);
|
|
61
|
+
if (r36CompactTools.length !== 41) throw new Error(`R36 compact catalog constant drift: ${r36CompactTools.length}`);
|
|
62
|
+
const AUTONOMOUS_DOWNSTREAM_TIMEOUT_MS = 900000;
|
|
63
|
+
const AUTONOMOUS_DOWNSTREAM_TOTAL_MS = 1200000;
|
|
64
|
+
const AUDIT_MAX_STRING_BYTES = 512;
|
|
65
|
+
const AUDIT_MAX_ARRAY_ITEMS = 24;
|
|
66
|
+
const AUDIT_MAX_OBJECT_KEYS = 48;
|
|
67
|
+
const SENSITIVE_AUDIT_KEY = /(token|secret|password|authorization|cookie|recovery|private[_-]?key)/i;
|
|
68
|
+
const PAYLOAD_AUDIT_KEY = /^(content|raw_text|text)$/i;
|
|
69
|
+
let auditSequence = 0;
|
|
70
|
+
const bridgeGovernor = createBridgeGovernor();
|
|
71
|
+
|
|
72
|
+
function auditDigest(text) {
|
|
73
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function auditString(value, key = '') {
|
|
77
|
+
const bytes = Buffer.byteLength(value, 'utf8');
|
|
78
|
+
if (SENSITIVE_AUDIT_KEY.test(key)) return `<redacted bytes=${bytes} sha256=${auditDigest(value)}>`;
|
|
79
|
+
if (PAYLOAD_AUDIT_KEY.test(key)) return `<payload bytes=${bytes} sha256=${auditDigest(value)}>`;
|
|
80
|
+
if (/^(?:Bearer\s+|Basic\s+|sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,})/i.test(value)) {
|
|
81
|
+
return `<redacted bytes=${bytes} sha256=${auditDigest(value)}>`;
|
|
82
|
+
}
|
|
83
|
+
if (bytes <= AUDIT_MAX_STRING_BYTES) return value;
|
|
84
|
+
const prefix = value.slice(0, 256);
|
|
85
|
+
return `${prefix}<truncated bytes=${bytes} sha256=${auditDigest(value)}>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function auditSanitize(value, key = '', depth = 0) {
|
|
89
|
+
if (value === null || value === undefined) return value ?? null;
|
|
90
|
+
if (depth > 6) return '<depth-limit>';
|
|
91
|
+
if (typeof value === 'string') return auditString(value, key);
|
|
92
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
93
|
+
if (typeof value === 'bigint') return value.toString();
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
const out = value.slice(0, AUDIT_MAX_ARRAY_ITEMS).map(item => auditSanitize(item, key, depth + 1));
|
|
96
|
+
if (value.length > AUDIT_MAX_ARRAY_ITEMS) out.push(`<${value.length - AUDIT_MAX_ARRAY_ITEMS} more items>`);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === 'object') {
|
|
100
|
+
const entries = Object.entries(value).slice(0, AUDIT_MAX_OBJECT_KEYS);
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const [childKey, childValue] of entries) out[childKey] = auditSanitize(childValue, childKey, depth + 1);
|
|
103
|
+
if (Object.keys(value).length > AUDIT_MAX_OBJECT_KEYS) out._truncated_keys = Object.keys(value).length - AUDIT_MAX_OBJECT_KEYS;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
return String(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const BRIDGE_LOG_LEVEL = process.env.DEADBYTE_BRIDGE_LOG_LEVEL === 'debug' ? 'debug' : 'human';
|
|
110
|
+
|
|
111
|
+
function auditWrite(kind, call, payload) {
|
|
112
|
+
const stamp = new Date().toISOString();
|
|
113
|
+
const id = String(call.id).padStart(6, '0');
|
|
114
|
+
console.log(`[TOOL ${kind}] ${stamp} #${id} ${call.name} ${JSON.stringify(auditSanitize(payload))}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compactDuration(ms) {
|
|
118
|
+
if (!Number.isFinite(ms) || ms < 0) return '0ms';
|
|
119
|
+
if (ms < 10) return `${ms.toFixed(1)}ms`;
|
|
120
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
121
|
+
return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function auditShortReason(result, error = null) {
|
|
125
|
+
const structured = result?.structuredContent;
|
|
126
|
+
const candidates = error
|
|
127
|
+
? [error?.message ?? String(error)]
|
|
128
|
+
: [
|
|
129
|
+
structured?.error, structured?.message, structured?.reason,
|
|
130
|
+
structured?.result?.error, structured?.result?.message,
|
|
131
|
+
...(Array.isArray(result?.content) ? result.content.filter(item => item?.type === 'text').map(item => item.text) : [])
|
|
132
|
+
];
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
if (typeof candidate !== 'string' || !candidate) continue;
|
|
135
|
+
const sanitized = auditString(candidate);
|
|
136
|
+
const text = String(sanitized).replace(/[\r\n\t]+/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
137
|
+
if (text) return text.length <= 180 ? text : `${text.slice(0, 179)}…`;
|
|
138
|
+
}
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function auditCompactResult(call, { result = null, error = null } = {}) {
|
|
143
|
+
const elapsedMs = Number(process.hrtime.bigint() - call.started) / 1e6;
|
|
144
|
+
const failed = error !== null || result?.isError === true;
|
|
145
|
+
const reason = failed ? auditShortReason(result, error) : '';
|
|
146
|
+
const stamp = new Date().toISOString();
|
|
147
|
+
const id = String(call.id).padStart(6, '0');
|
|
148
|
+
console.log(`[TOOL] ${stamp} #${id} ${failed ? '✗' : '✓'} ${call.name} ${compactDuration(elapsedMs)}${reason ? ` — ${reason}` : ''}`);
|
|
149
|
+
return elapsedMs;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function auditToolStart(name, args) {
|
|
153
|
+
const call = { id: ++auditSequence, name, started: process.hrtime.bigint() };
|
|
154
|
+
if (BRIDGE_LOG_LEVEL === 'debug') auditWrite('IN ', call, args);
|
|
155
|
+
return call;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function auditToolFinish(call, result) {
|
|
159
|
+
const elapsedMs = auditCompactResult(call, { result });
|
|
160
|
+
if (BRIDGE_LOG_LEVEL === 'debug') {
|
|
161
|
+
auditWrite('OUT', call, {
|
|
162
|
+
elapsed_ms: Number(elapsedMs.toFixed(3)),
|
|
163
|
+
is_error: result?.isError === true,
|
|
164
|
+
structured: result?.structuredContent ?? null,
|
|
165
|
+
content: result?.content ?? null
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function auditToolFail(call, error) {
|
|
171
|
+
const elapsedMs = auditCompactResult(call, { error });
|
|
172
|
+
if (BRIDGE_LOG_LEVEL === 'debug') {
|
|
173
|
+
auditWrite('ERR', call, {
|
|
174
|
+
elapsed_ms: Number(elapsedMs.toFixed(3)),
|
|
175
|
+
name: error?.name ?? 'Error',
|
|
176
|
+
message: error?.message ?? String(error)
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const allowedCatalogs = [];
|
|
182
|
+
for (const host of [false, true]) {
|
|
183
|
+
for (const agent of [false, true]) {
|
|
184
|
+
for (const coding of [false, true]) {
|
|
185
|
+
for (const autonomous of [false, true]) {
|
|
186
|
+
if (autonomous && !coding) continue;
|
|
187
|
+
const items = [...baseTools];
|
|
188
|
+
if (host) items.push(...privilegedTools);
|
|
189
|
+
if (agent) items.push(...agentTools);
|
|
190
|
+
if (coding) items.push(...codingTools);
|
|
191
|
+
if (autonomous) items.push(...autonomousTools);
|
|
192
|
+
allowedCatalogs.push(items.sort());
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadToken() {
|
|
199
|
+
const fromEnv = process.env.DEADBYTE_BRIDGE_PATH_TOKEN ?? '';
|
|
200
|
+
if (/^[a-f0-9]{64}$/.test(fromEnv)) return fromEnv;
|
|
201
|
+
const tokenFile = process.env.DEADBYTE_BRIDGE_TOKEN_FILE ?? path.join(os.homedir(), '.deadbyte-mcp', 'bridge', 'path-token.txt');
|
|
202
|
+
const value = readFileSync(tokenFile, 'utf8').trim();
|
|
203
|
+
if (!/^[a-f0-9]{64}$/.test(value)) throw new Error(`invalid bridge path token file: ${tokenFile}`);
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const token = loadToken();
|
|
208
|
+
const requestStateKey = createHash('sha256')
|
|
209
|
+
.update('DEADBYTE-R24-MRTR-V1\0', 'utf8')
|
|
210
|
+
.update(token, 'utf8')
|
|
211
|
+
.digest();
|
|
212
|
+
const requestStateCodec = createRequestStateCodec({ key: requestStateKey, ttlSeconds: 600 });
|
|
213
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('invalid DEADBYTE_BRIDGE_PORT');
|
|
214
|
+
const endpointPath = `/mcp/${token}`;
|
|
215
|
+
|
|
216
|
+
process.on('uncaughtExceptionMonitor', error => {
|
|
217
|
+
console.error(`[bridge-fatal-monitor] ${error?.stack ?? String(error)}`);
|
|
218
|
+
});
|
|
219
|
+
process.on('exit', code => {
|
|
220
|
+
console.error(`[bridge-exit] code=${code}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const downstream = new Client(
|
|
224
|
+
{ name: 'deadbyte-chatgpt-bridge', version: '0.2.0' },
|
|
225
|
+
{ versionNegotiation: { mode: { pin: '2026-07-28' } } }
|
|
226
|
+
);
|
|
227
|
+
const downstreamTransport = new StdioClientTransport({
|
|
228
|
+
command: process.execPath,
|
|
229
|
+
args: [coreServer],
|
|
230
|
+
cwd: coreRoot,
|
|
231
|
+
env: {
|
|
232
|
+
...getDefaultEnvironment(),
|
|
233
|
+
...process.env,
|
|
234
|
+
DEADBYTE_WORKSPACE_ROOT: workspaceRoot
|
|
235
|
+
},
|
|
236
|
+
stderr: 'pipe'
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
downstreamTransport.stderr?.on('data', chunk => process.stderr.write(`[deadbyte-core] ${chunk}`));
|
|
240
|
+
await downstream.connect(downstreamTransport);
|
|
241
|
+
if (downstream.getProtocolEra() !== 'modern') throw new Error('DEADBYTE downstream is not modern MCP');
|
|
242
|
+
|
|
243
|
+
const listed = await downstream.listTools();
|
|
244
|
+
const actualNames = listed.tools.map(tool => tool.name).sort();
|
|
245
|
+
const exactR36Compact = JSON.stringify(actualNames) === JSON.stringify(r36CompactTools);
|
|
246
|
+
let compactCatalogAttestation = null;
|
|
247
|
+
let compactCatalogLegacy = false;
|
|
248
|
+
if (actualNames.includes('capability_manifest')) {
|
|
249
|
+
const manifestResult = await downstream.callTool(
|
|
250
|
+
{ name:'capability_manifest', arguments:{} },
|
|
251
|
+
bridgeDeadlineForTool('capability_manifest')
|
|
252
|
+
);
|
|
253
|
+
const manifestSchema = manifestResult?.structuredContent?.result?.schema ?? null;
|
|
254
|
+
if (manifestSchema === 'deadbyte.capability-manifest.v2' || manifestSchema === 'deadbyte.capability-manifest.v3') {
|
|
255
|
+
compactCatalogAttestation = verifyCompactCatalogAttestation(actualNames, manifestResult);
|
|
256
|
+
} else if (manifestSchema === 'deadbyte.capability-manifest.v1' && exactR36Compact) {
|
|
257
|
+
compactCatalogLegacy = true;
|
|
258
|
+
} else {
|
|
259
|
+
throw new Error(`DEADBYTE compact catalog attestation unavailable or invalid schema: ${String(manifestSchema)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const catalogOk = compactCatalogAttestation?.ok === true || compactCatalogLegacy ||
|
|
263
|
+
allowedCatalogs.some(expected => JSON.stringify(actualNames) === JSON.stringify(expected)) ||
|
|
264
|
+
JSON.stringify(actualNames) === JSON.stringify(r35FullTools);
|
|
265
|
+
if (!catalogOk) throw new Error(`DEADBYTE tool catalog mismatch: ${JSON.stringify(actualNames)}`);
|
|
266
|
+
const hostConfigured = privilegedTools.every(name => actualNames.includes(name));
|
|
267
|
+
const agentConfigured = agentTools.every(name => actualNames.includes(name));
|
|
268
|
+
const codingConfigured = codingTools.every(name => actualNames.includes(name));
|
|
269
|
+
const autonomousConfigured = autonomousTools.every(name => actualNames.includes(name));
|
|
270
|
+
const bridgeCatalogDigest = bridgeCatalogSha256(actualNames);
|
|
271
|
+
|
|
272
|
+
function plannerTextFromInput(ctx) {
|
|
273
|
+
const response = inputResponse(ctx.mcpReq.inputResponses, 'planner');
|
|
274
|
+
if (!response || response.kind !== 'sampling') throw new Error('planner sampling response missing');
|
|
275
|
+
const content = response.result?.content;
|
|
276
|
+
const block = Array.isArray(content) ? content.find(item => item?.type === 'text') : content;
|
|
277
|
+
if (!block || block.type !== 'text' || typeof block.text !== 'string') throw new Error('planner sampling response missing text');
|
|
278
|
+
return { raw_text: block.text, model: typeof response.result?.model === 'string' ? response.result.model.slice(0, 128) : 'client-model' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const publicToolDefinitions=preparePublicToolDefinitions(listed.tools);
|
|
282
|
+
|
|
283
|
+
function buildFrontServer() {
|
|
284
|
+
const front = new McpServer(
|
|
285
|
+
{ name: 'deadbyte-chatgpt-bridge', version: '0.2.0' },
|
|
286
|
+
{ requestState: { verify: requestStateCodec.verify } }
|
|
287
|
+
);
|
|
288
|
+
for (const tool of publicToolDefinitions) {
|
|
289
|
+
const {inputSchema,outputSchema}=tool;
|
|
290
|
+
front.registerTool(tool.name, {
|
|
291
|
+
title: tool.title,
|
|
292
|
+
description: tool.description,
|
|
293
|
+
inputSchema,
|
|
294
|
+
outputSchema,
|
|
295
|
+
annotations: tool.annotations
|
|
296
|
+
}, async (args, ctx) => {
|
|
297
|
+
if (tool.name !== 'autonomous_goal_run') {
|
|
298
|
+
const audit = auditToolStart(tool.name, args);
|
|
299
|
+
const ticket = await bridgeGovernor.acquire(tool.name);
|
|
300
|
+
try {
|
|
301
|
+
const budget = bridgeDeadlineForTool(tool.name);
|
|
302
|
+
const result = await downstream.callTool({ name: tool.name, arguments: args }, budget);
|
|
303
|
+
enforceDesktopBridgeResult(tool.name, result);
|
|
304
|
+
auditToolFinish(audit, result);
|
|
305
|
+
return result;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
auditToolFail(audit, error);
|
|
308
|
+
throw error;
|
|
309
|
+
} finally {
|
|
310
|
+
ticket.release();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const state = ctx.mcpReq.requestState();
|
|
314
|
+
const callArgs = { ...args };
|
|
315
|
+
if (state) {
|
|
316
|
+
if (state.goal_id !== args.goal_id || typeof state.request_id !== 'string') throw new Error('autonomous planner requestState binding mismatch');
|
|
317
|
+
const planner = plannerTextFromInput(ctx);
|
|
318
|
+
callArgs.planner_response = { request_id: state.request_id, raw_text: planner.raw_text, model: planner.model };
|
|
319
|
+
}
|
|
320
|
+
const audit = auditToolStart(tool.name, callArgs);
|
|
321
|
+
const ticket = await bridgeGovernor.acquire(tool.name);
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
const budget = bridgeDeadlineForTool(tool.name);
|
|
325
|
+
result = await downstream.callTool({ name: tool.name, arguments: callArgs }, budget);
|
|
326
|
+
auditToolFinish(audit, result);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
auditToolFail(audit, error);
|
|
329
|
+
throw error;
|
|
330
|
+
} finally {
|
|
331
|
+
ticket.release();
|
|
332
|
+
}
|
|
333
|
+
const plannerRequest = result?.structuredContent?.status === 'input_required' ? result.structuredContent.planner_request : null;
|
|
334
|
+
if (!plannerRequest) return result;
|
|
335
|
+
if (plannerRequest.request_id === undefined || typeof plannerRequest.prompt !== 'string') throw new Error('autonomous planner request malformed');
|
|
336
|
+
return inputRequired({
|
|
337
|
+
inputRequests: {
|
|
338
|
+
planner: inputRequired.createMessage({
|
|
339
|
+
messages: [{ role: 'user', content: { type: 'text', text: plannerRequest.prompt } }],
|
|
340
|
+
maxTokens: 4096,
|
|
341
|
+
temperature: 0,
|
|
342
|
+
includeContext: 'none'
|
|
343
|
+
})
|
|
344
|
+
},
|
|
345
|
+
requestState: await requestStateCodec.mint({ goal_id: args.goal_id, request_id: plannerRequest.request_id })
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return front;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const mcpHandler = createMcpHandler(() => buildFrontServer(), {
|
|
353
|
+
legacy: 'reject',
|
|
354
|
+
responseMode: 'json',
|
|
355
|
+
onerror: error => console.error(`[mcp-http] ${error.stack ?? String(error)}`)
|
|
356
|
+
});
|
|
357
|
+
const nodeMcpHandler = createDeadbyteNodeHandler(mcpHandler);
|
|
358
|
+
const httpServer = createServer(async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
const pathname = new URL(req.url ?? '/', 'http://127.0.0.1').pathname;
|
|
361
|
+
if (pathname === '/healthz') {
|
|
362
|
+
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
|
|
363
|
+
return res.end('ok');
|
|
364
|
+
}
|
|
365
|
+
if (pathname === '/healthz/r40') {
|
|
366
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
367
|
+
return res.end(JSON.stringify({
|
|
368
|
+
status:'ok',catalog_sha256:bridgeCatalogDigest,
|
|
369
|
+
catalog_attestation:compactCatalogAttestation?.ok===true?'verified':compactCatalogLegacy?'legacy-r39':'not-applicable',
|
|
370
|
+
ledger_sha256:compactCatalogAttestation?.ledger_sha256??null,
|
|
371
|
+
governor:bridgeGovernor.snapshot()
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
if (pathname !== endpointPath) {
|
|
375
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
376
|
+
return res.end('not found');
|
|
377
|
+
}
|
|
378
|
+
const contentLength = Number(req.headers['content-length'] ?? '0');
|
|
379
|
+
if (Number.isFinite(contentLength) && contentLength > 1024 * 1024) {
|
|
380
|
+
res.writeHead(413, { 'content-type': 'text/plain; charset=utf-8' });
|
|
381
|
+
return res.end('request too large');
|
|
382
|
+
}
|
|
383
|
+
await nodeMcpHandler(req, res);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
386
|
+
if (!res.writableEnded) res.end('bridge error');
|
|
387
|
+
console.error(error instanceof Error ? error.stack : String(error));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await new Promise((resolve, reject) => {
|
|
392
|
+
httpServer.once('error', reject);
|
|
393
|
+
httpServer.listen(port, '127.0.0.1', resolve);
|
|
394
|
+
});
|
|
395
|
+
console.log(`BRIDGE_READY http://127.0.0.1:${port}${endpointPath}`);
|
|
396
|
+
console.log(`BRIDGE_TOOLS ${actualNames.join(',')}`);
|
|
397
|
+
console.log(`BRIDGE_CATALOG_SHA256 ${bridgeCatalogDigest}`);
|
|
398
|
+
console.log(`BRIDGE_CATALOG_ATTESTATION ${compactCatalogAttestation?.ok===true?'VERIFIED':compactCatalogLegacy?'LEGACY_R39':'NOT_APPLICABLE'}`);
|
|
399
|
+
console.log(`BRIDGE_HOST_PLANE ${hostConfigured ? 'CONFIGURED_DISARMED_OR_ARMED_BY_LOCAL_LEASE' : 'UNCONFIGURED'}`);
|
|
400
|
+
console.log(`BRIDGE_AGENT_RUNTIME ${agentConfigured ? 'ENABLED' : 'DISABLED'}`);
|
|
401
|
+
console.log(`BRIDGE_CODING_RUNTIME ${codingConfigured ? 'ENABLED' : 'DISABLED'}`);
|
|
402
|
+
console.log(`BRIDGE_AUTONOMOUS_RUNTIME ${autonomousConfigured ? 'ENABLED' : 'DISABLED'}`);
|
|
403
|
+
|
|
404
|
+
async function shutdown() {
|
|
405
|
+
httpServer.close();
|
|
406
|
+
await mcpHandler.close().catch(() => {});
|
|
407
|
+
await downstream.close().catch(() => {});
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
process.once('SIGINT', shutdown);
|
|
411
|
+
process.once('SIGTERM', shutdown);
|
|
412
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {once} from 'node:events';
|
|
2
|
+
|
|
3
|
+
function requestHeaders(headers){
|
|
4
|
+
const out=new Headers();
|
|
5
|
+
for(const [name,value] of Object.entries(headers??{})){
|
|
6
|
+
if(value===undefined) continue;
|
|
7
|
+
if(Array.isArray(value)){for(const item of value) out.append(name,String(item));}
|
|
8
|
+
else out.append(name,String(value));
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
async function requestBody(req,maxBodyBytes){
|
|
13
|
+
const method=String(req.method??'GET').toUpperCase();
|
|
14
|
+
if(method==='GET'||method==='HEAD') return undefined;
|
|
15
|
+
const chunks=[];let total=0;
|
|
16
|
+
for await(const chunk of req){
|
|
17
|
+
const bytes=Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
|
|
18
|
+
total+=bytes.length;
|
|
19
|
+
if(total>maxBodyBytes) throw new Error(`bridge request body exceeds ${maxBodyBytes} bytes`);
|
|
20
|
+
chunks.push(bytes);
|
|
21
|
+
}
|
|
22
|
+
return Buffer.concat(chunks,total);
|
|
23
|
+
}
|
|
24
|
+
async function writeResponse(res,response){
|
|
25
|
+
const headers={};
|
|
26
|
+
for(const [name,value] of response.headers.entries()) headers[name]=value;
|
|
27
|
+
res.writeHead(response.status,headers);
|
|
28
|
+
if(!response.body){res.end();return;}
|
|
29
|
+
const reader=response.body.getReader();
|
|
30
|
+
try{
|
|
31
|
+
while(true){
|
|
32
|
+
const {done,value}=await reader.read();
|
|
33
|
+
if(done) break;
|
|
34
|
+
if(res.destroyed){await reader.cancel().catch(()=>{});return;}
|
|
35
|
+
if(!res.write(value)) await once(res,'drain');
|
|
36
|
+
}
|
|
37
|
+
}finally{reader.releaseLock();}
|
|
38
|
+
if(!res.destroyed) res.end();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createDeadbyteNodeHandler(handler,{maxBodyBytes=1024*1024}={}){
|
|
42
|
+
if(!handler||typeof handler.fetch!=='function') throw new Error('bridge node adapter requires fetch-shaped MCP handler');
|
|
43
|
+
if(!Number.isInteger(maxBodyBytes)||maxBodyBytes<1024||maxBodyBytes>4*1024*1024) throw new Error('bridge node adapter maxBodyBytes invalid');
|
|
44
|
+
return async function nodeHandler(req,res){
|
|
45
|
+
const method=String(req.method??'GET').toUpperCase();
|
|
46
|
+
const url=new URL(String(req.url??'/'),'http://127.0.0.1');
|
|
47
|
+
const body=await requestBody(req,maxBodyBytes);
|
|
48
|
+
const init={method,headers:requestHeaders(req.headers)};
|
|
49
|
+
if(body!==undefined) init.body=body;
|
|
50
|
+
const request=new Request(url,init);
|
|
51
|
+
const options=req.auth?{authInfo:req.auth}:undefined;
|
|
52
|
+
const response=await handler.fetch(request,options);
|
|
53
|
+
if(!(response instanceof Response)) throw new Error('bridge MCP handler returned non-Response');
|
|
54
|
+
await writeResponse(res,response);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {fromJsonSchema} from '@modelcontextprotocol/server';
|
|
2
|
+
|
|
3
|
+
// Catalog is fixed by the existing downstream startup attestation. Only schema
|
|
4
|
+
// definitions/validators are shared; servers, callbacks and requestState aren't.
|
|
5
|
+
export function preparePublicToolDefinitions(tools){
|
|
6
|
+
return Object.freeze(tools.map(tool=>{
|
|
7
|
+
const input=structuredClone(tool.inputSchema??{type:'object',properties:{},additionalProperties:false});
|
|
8
|
+
if(tool.name==='autonomous_goal_run'){
|
|
9
|
+
if(input.properties&&typeof input.properties==='object')delete input.properties.planner_response;
|
|
10
|
+
if(Array.isArray(input.required))input.required=input.required.filter(name=>name!=='planner_response');
|
|
11
|
+
}
|
|
12
|
+
return Object.freeze({...tool,inputSchema:fromJsonSchema(input),outputSchema:tool.outputSchema?fromJsonSchema(tool.outputSchema):undefined});
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const DESKTOP_RAW_KEYS=new Set([
|
|
4
|
+
'pixels','pixel_bytes','frame_bytes','raw_bgra','raw_rgba','raw_image','screenshot_bytes','bitmap_bytes'
|
|
5
|
+
]);
|
|
6
|
+
const DEFAULT_LIMITS=Object.freeze({
|
|
7
|
+
normal:Object.freeze({max_active:8,max_queue:32,queue_timeout_ms:30000}),
|
|
8
|
+
autonomous:Object.freeze({max_active:2,max_queue:8,queue_timeout_ms:60000}),
|
|
9
|
+
desktop_observe:Object.freeze({max_active:2,max_queue:4,queue_timeout_ms:10000}),
|
|
10
|
+
desktop_action:Object.freeze({max_active:1,max_queue:2,queue_timeout_ms:5000})
|
|
11
|
+
});
|
|
12
|
+
const DEADLINES=Object.freeze({
|
|
13
|
+
normal:Object.freeze({timeout:120000,maxTotalTimeout:180000}),
|
|
14
|
+
autonomous:Object.freeze({timeout:900000,maxTotalTimeout:1200000}),
|
|
15
|
+
desktop_observe:Object.freeze({timeout:30000,maxTotalTimeout:60000}),
|
|
16
|
+
desktop_action:Object.freeze({timeout:30000,maxTotalTimeout:45000})
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function insist(condition,message){ if(!condition) throw new Error(message); }
|
|
20
|
+
function toolClass(name){
|
|
21
|
+
const value=String(name??'');
|
|
22
|
+
if(value==='autonomous_goal_run') return 'autonomous';
|
|
23
|
+
if(value==='desktop_action') return 'desktop_action';
|
|
24
|
+
if(value.startsWith('desktop_')) return 'desktop_observe';
|
|
25
|
+
return 'normal';
|
|
26
|
+
}
|
|
27
|
+
function estimateBase64Bytes(value){
|
|
28
|
+
if(typeof value!=='string') return 0;
|
|
29
|
+
const clean=value.replace(/\s/g,'');
|
|
30
|
+
if(clean.length===0) return 0;
|
|
31
|
+
const padding=clean.endsWith('==')?2:clean.endsWith('=')?1:0;
|
|
32
|
+
return Math.floor(clean.length*3/4)-padding;
|
|
33
|
+
}
|
|
34
|
+
function scanDesktopPayload(value,{maxPreviewBytes},path='result',seen=new Set()){
|
|
35
|
+
if(value===null||value===undefined) return;
|
|
36
|
+
if(typeof value==='string'||typeof value==='number'||typeof value==='boolean') return;
|
|
37
|
+
if(typeof value!=='object') return;
|
|
38
|
+
if(seen.has(value)) throw new Error('bridge desktop result contains cyclic value');
|
|
39
|
+
seen.add(value);
|
|
40
|
+
if(Array.isArray(value)){
|
|
41
|
+
for(let i=0;i<value.length;i++) scanDesktopPayload(value[i],{maxPreviewBytes},`${path}[${i}]`,seen);
|
|
42
|
+
seen.delete(value); return;
|
|
43
|
+
}
|
|
44
|
+
for(const [key,child] of Object.entries(value)){
|
|
45
|
+
const lower=key.toLowerCase();
|
|
46
|
+
if(DESKTOP_RAW_KEYS.has(lower)) throw new Error(`bridge desktop result contains forbidden raw frame field '${path}.${key}'`);
|
|
47
|
+
if((lower==='preview_png_base64'||lower==='preview_jpeg_base64')&&typeof child==='string'){
|
|
48
|
+
const bytes=estimateBase64Bytes(child);
|
|
49
|
+
if(bytes>maxPreviewBytes) throw new Error(`bridge desktop preview exceeds ${maxPreviewBytes} byte limit`);
|
|
50
|
+
}
|
|
51
|
+
if(lower==='data'&&value.type==='image'&&typeof child==='string'){
|
|
52
|
+
const bytes=estimateBase64Bytes(child);
|
|
53
|
+
if(bytes>maxPreviewBytes) throw new Error(`bridge MCP image content exceeds ${maxPreviewBytes} byte limit`);
|
|
54
|
+
}
|
|
55
|
+
scanDesktopPayload(child,{maxPreviewBytes},`${path}.${key}`,seen);
|
|
56
|
+
}
|
|
57
|
+
seen.delete(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function bridgeToolClass(name){ return toolClass(name); }
|
|
61
|
+
export function bridgeDeadlineForTool(name){ return DEADLINES[toolClass(name)]; }
|
|
62
|
+
export function bridgeCatalogSha256(toolNames){
|
|
63
|
+
insist(Array.isArray(toolNames)&&toolNames.every(x=>typeof x==='string'),'bridge catalog names invalid');
|
|
64
|
+
const sorted=[...new Set(toolNames)].sort();
|
|
65
|
+
insist(sorted.length===toolNames.length,'bridge catalog contains duplicate tool name');
|
|
66
|
+
return createHash('sha256').update(JSON.stringify(sorted),'utf8').digest('hex');
|
|
67
|
+
}
|
|
68
|
+
export function verifyCompactCatalogAttestation(actualNames,toolResult){
|
|
69
|
+
insist(Array.isArray(actualNames)&&actualNames.every(x=>typeof x==='string'&&x.length>0),'actual bridge catalog invalid');
|
|
70
|
+
const actual=[...actualNames].sort();
|
|
71
|
+
insist(new Set(actual).size===actual.length,'actual bridge catalog contains duplicate tool name');
|
|
72
|
+
const envelope=toolResult?.structuredContent;
|
|
73
|
+
insist(envelope?.status==='ok'&&envelope?.op==='manifest','capability manifest tool result invalid');
|
|
74
|
+
const manifest=envelope.result;
|
|
75
|
+
insist(manifest?.schema==='deadbyte.capability-manifest.v2'||manifest?.schema==='deadbyte.capability-manifest.v3','capability manifest schema mismatch');
|
|
76
|
+
insist(manifest.profile==='compact','capability manifest profile mismatch');
|
|
77
|
+
insist(typeof manifest.ledger_sha256==='string'&&/^[0-9a-f]{64}$/.test(manifest.ledger_sha256),'capability manifest ledger hash invalid');
|
|
78
|
+
insist(Array.isArray(manifest.tool_catalog)&&manifest.tool_catalog.every(x=>typeof x==='string'&&x.length>0),'capability manifest tool_catalog invalid');
|
|
79
|
+
const declared=[...manifest.tool_catalog];
|
|
80
|
+
insist(declared.length===manifest.tool_count,'capability manifest tool_count mismatch');
|
|
81
|
+
insist(new Set(declared).size===declared.length,'capability manifest catalog contains duplicate tool name');
|
|
82
|
+
insist(JSON.stringify(declared)===JSON.stringify([...declared].sort()),'capability manifest catalog must be sorted');
|
|
83
|
+
const declaredSha=bridgeCatalogSha256(declared);
|
|
84
|
+
insist(declaredSha===manifest.catalog_sha256,'capability manifest catalog hash mismatch');
|
|
85
|
+
insist(JSON.stringify(actual)===JSON.stringify(declared),'bridge listTools/catalog attestation mismatch');
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
ok:true,profile:'compact',tool_count:declared.length,catalog_sha256:declaredSha,ledger_sha256:manifest.ledger_sha256
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function enforceDesktopBridgeResult(toolName,result,{maxPreviewBytes=1024*1024}={}){
|
|
92
|
+
if(!String(toolName??'').startsWith('desktop_')) return result;
|
|
93
|
+
insist(Number.isInteger(maxPreviewBytes)&&maxPreviewBytes>=16*1024&&maxPreviewBytes<=4*1024*1024,'maxPreviewBytes invalid');
|
|
94
|
+
scanDesktopPayload(result,{maxPreviewBytes});
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createBridgeGovernor({limits=DEFAULT_LIMITS}={}){
|
|
99
|
+
const classes=['normal','autonomous','desktop_observe','desktop_action'];
|
|
100
|
+
for(const name of classes){
|
|
101
|
+
const cfg=limits[name];
|
|
102
|
+
insist(cfg&&Number.isInteger(cfg.max_active)&&cfg.max_active>=1&&cfg.max_active<=64,`bridge limit ${name}.max_active invalid`);
|
|
103
|
+
insist(Number.isInteger(cfg.max_queue)&&cfg.max_queue>=0&&cfg.max_queue<=256,`bridge limit ${name}.max_queue invalid`);
|
|
104
|
+
insist(Number.isInteger(cfg.queue_timeout_ms)&&cfg.queue_timeout_ms>=100&&cfg.queue_timeout_ms<=120000,`bridge limit ${name}.queue_timeout_ms invalid`);
|
|
105
|
+
}
|
|
106
|
+
const state=new Map(classes.map(name=>[name,{active:0,queue:[]}]));
|
|
107
|
+
let sequence=0;
|
|
108
|
+
|
|
109
|
+
function dispatch(name){
|
|
110
|
+
const cfg=limits[name],s=state.get(name);
|
|
111
|
+
while(s.active<cfg.max_active&&s.queue.length){
|
|
112
|
+
const waiter=s.queue.shift();
|
|
113
|
+
if(waiter.done) continue;
|
|
114
|
+
waiter.done=true; clearTimeout(waiter.timer); s.active++;
|
|
115
|
+
waiter.resolve(makeTicket(name,waiter.tool,waiter.id,waiter.queuedAt));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function makeTicket(name,tool,id,queuedAt){
|
|
119
|
+
let released=false;
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
id,class:name,tool,queued_ms:Math.max(0,Date.now()-queuedAt),
|
|
122
|
+
release(){
|
|
123
|
+
if(released) return false;
|
|
124
|
+
released=true;
|
|
125
|
+
const s=state.get(name);
|
|
126
|
+
s.active=Math.max(0,s.active-1);
|
|
127
|
+
dispatch(name);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function acquire(tool){
|
|
133
|
+
const name=toolClass(tool),cfg=limits[name],s=state.get(name),id=++sequence,queuedAt=Date.now();
|
|
134
|
+
if(s.active<cfg.max_active){
|
|
135
|
+
s.active++;
|
|
136
|
+
return makeTicket(name,String(tool),id,queuedAt);
|
|
137
|
+
}
|
|
138
|
+
if(s.queue.length>=cfg.max_queue) throw new Error(`bridge backpressure queue full for ${name}`);
|
|
139
|
+
return await new Promise((resolve,reject)=>{
|
|
140
|
+
const waiter={id,tool:String(tool),queuedAt,resolve,reject,done:false,timer:null};
|
|
141
|
+
waiter.timer=setTimeout(()=>{
|
|
142
|
+
if(waiter.done)return;
|
|
143
|
+
waiter.done=true;
|
|
144
|
+
const index=s.queue.indexOf(waiter); if(index>=0)s.queue.splice(index,1);
|
|
145
|
+
reject(new Error(`bridge backpressure queue timeout for ${name}`));
|
|
146
|
+
},cfg.queue_timeout_ms);
|
|
147
|
+
s.queue.push(waiter);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function snapshot(){
|
|
151
|
+
return Object.freeze(Object.fromEntries(classes.map(name=>{
|
|
152
|
+
const s=state.get(name),cfg=limits[name];
|
|
153
|
+
return [name,Object.freeze({active:s.active,queued:s.queue.filter(x=>!x.done).length,max_active:cfg.max_active,max_queue:cfg.max_queue})];
|
|
154
|
+
})));
|
|
155
|
+
}
|
|
156
|
+
return Object.freeze({acquire,snapshot});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const BRIDGE_R40_DEFAULT_LIMITS=DEFAULT_LIMITS;
|
|
160
|
+
|