@attested-intelligence/aga-mcp-server 2.2.0 → 2.2.2
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 +80 -68
- package/dist/adapters/openclaw.d.ts +0 -1
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +0 -1
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/proxy/evaluator.d.ts +0 -1
- package/dist/proxy/evaluator.d.ts.map +1 -1
- package/dist/proxy/evaluator.js +0 -1
- package/dist/proxy/evaluator.js.map +1 -1
- package/dist/proxy/index.d.ts +0 -1
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +0 -1
- package/dist/proxy/index.js.map +1 -1
- package/dist/proxy/profiles.d.ts +0 -1
- package/dist/proxy/profiles.d.ts.map +1 -1
- package/dist/proxy/profiles.js +0 -1
- package/dist/proxy/profiles.js.map +1 -1
- package/dist/proxy/server.d.ts +0 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/server.js +0 -1
- package/dist/proxy/server.js.map +1 -1
- package/dist/proxy/stdio-bridge.d.ts +0 -1
- package/dist/proxy/stdio-bridge.d.ts.map +1 -1
- package/dist/proxy/stdio-bridge.js +0 -1
- package/dist/proxy/stdio-bridge.js.map +1 -1
- package/dist/proxy/types.d.ts +0 -1
- package/dist/proxy/types.d.ts.map +1 -1
- package/dist/proxy/types.js +0 -1
- package/dist/proxy/types.js.map +1 -1
- package/dist/proxy/verify.d.ts +0 -1
- package/dist/proxy/verify.d.ts.map +1 -1
- package/dist/proxy/verify.js +0 -1
- package/dist/proxy/verify.js.map +1 -1
- package/package.json +93 -97
- package/SECURITY.md +0 -59
- package/independent-verifier/README.md +0 -31
- package/independent-verifier/package.json +0 -18
- package/independent-verifier/verify.ts +0 -211
- package/src/adapters/openclaw.ts +0 -125
- package/src/core/artifact.ts +0 -45
- package/src/core/attestation.ts +0 -33
- package/src/core/behavioral.ts +0 -132
- package/src/core/bundle.ts +0 -45
- package/src/core/chain.ts +0 -72
- package/src/core/checkpoint.ts +0 -22
- package/src/core/delegation.ts +0 -146
- package/src/core/disclosure.ts +0 -32
- package/src/core/identity.ts +0 -62
- package/src/core/index.ts +0 -14
- package/src/core/portal.ts +0 -117
- package/src/core/quarantine.ts +0 -16
- package/src/core/receipt.ts +0 -33
- package/src/core/subject.ts +0 -11
- package/src/core/types.ts +0 -285
- package/src/crypto/hash.ts +0 -33
- package/src/crypto/index.ts +0 -5
- package/src/crypto/merkle.ts +0 -43
- package/src/crypto/salt.ts +0 -18
- package/src/crypto/sign.ts +0 -42
- package/src/crypto/types.ts +0 -19
- package/src/index.ts +0 -12
- package/src/middleware/governance.ts +0 -95
- package/src/middleware/index.ts +0 -1
- package/src/proxy/evaluator.ts +0 -176
- package/src/proxy/index.ts +0 -259
- package/src/proxy/profiles.ts +0 -48
- package/src/proxy/server.ts +0 -499
- package/src/proxy/stdio-bridge.ts +0 -171
- package/src/proxy/types.ts +0 -40
- package/src/proxy/verify.ts +0 -202
- package/src/server.ts +0 -435
- package/src/storage/index.ts +0 -3
- package/src/storage/interface.ts +0 -21
- package/src/storage/memory.ts +0 -27
- package/src/storage/sqlite.ts +0 -45
- package/src/tools/README.md +0 -13
- package/src/utils/canonical.ts +0 -14
- package/src/utils/constants.ts +0 -3
- package/src/utils/timestamp.ts +0 -12
- package/src/utils/uuid.ts +0 -2
package/src/proxy/server.ts
DELETED
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AGA Governance Proxy Server
|
|
3
|
-
* TCP proxy that intercepts MCP JSON-RPC 2.0 tool calls,
|
|
4
|
-
* evaluates them against a sealed policy, and produces
|
|
5
|
-
* Ed25519-signed governance receipts.
|
|
6
|
-
*
|
|
7
|
-
* Receipt format: Ed25519-SHA256-JCS (canonical across TS gateway,
|
|
8
|
-
* Python SDK, Go CLI, and browser verifier).
|
|
9
|
-
*
|
|
10
|
-
* Architecture: Client → Proxy (:18800) → Downstream MCP Server
|
|
11
|
-
* The proxy holds ALL signing keys. The client holds NONE.
|
|
12
|
-
*
|
|
13
|
-
* Patent: USPTO App. No. 19/433,835
|
|
14
|
-
* Copyright (c) 2026 Attested Intelligence Holdings LLC
|
|
15
|
-
* SPDX-License-Identifier: MIT
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import * as net from 'node:net';
|
|
19
|
-
import { EventEmitter } from 'node:events';
|
|
20
|
-
import { generateKeyPair, pkToHex, signStr } from '../crypto/sign.js';
|
|
21
|
-
import { bytesToHex, hexToBytes as utilHexToBytes } from '@noble/hashes/utils';
|
|
22
|
-
import { sha256 } from '@noble/hashes/sha256';
|
|
23
|
-
import { sha256Str } from '../crypto/hash.js';
|
|
24
|
-
import { canonicalize } from '../utils/canonical.js';
|
|
25
|
-
import { evaluate, resetRateLimits } from './evaluator.js';
|
|
26
|
-
import { StdioBridge, type StdioBridgeOptions } from './stdio-bridge.js';
|
|
27
|
-
import { PERMISSIVE } from './profiles.js';
|
|
28
|
-
import { utcNow } from '../utils/timestamp.js';
|
|
29
|
-
import { uuid } from '../utils/uuid.js';
|
|
30
|
-
import type { ToolPolicy } from './types.js';
|
|
31
|
-
import type { KeyPair } from '../crypto/types.js';
|
|
32
|
-
|
|
33
|
-
// ── Gateway-format receipt (canonical across all SDKs) ──────
|
|
34
|
-
|
|
35
|
-
export interface GovernanceReceipt {
|
|
36
|
-
receipt_id: string;
|
|
37
|
-
receipt_version: string;
|
|
38
|
-
algorithm: string;
|
|
39
|
-
timestamp: string;
|
|
40
|
-
request_id: string | number | null;
|
|
41
|
-
method: string;
|
|
42
|
-
tool_name: string;
|
|
43
|
-
decision: 'PERMITTED' | 'DENIED';
|
|
44
|
-
reason: string;
|
|
45
|
-
policy_reference: string;
|
|
46
|
-
arguments_hash: string;
|
|
47
|
-
previous_receipt_hash: string;
|
|
48
|
-
gateway_id: string;
|
|
49
|
-
signature: string;
|
|
50
|
-
public_key: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface EvidenceBundle {
|
|
54
|
-
schema_version: string;
|
|
55
|
-
bundle_id: string;
|
|
56
|
-
algorithm: string;
|
|
57
|
-
generated_at: string;
|
|
58
|
-
gateway_id: string;
|
|
59
|
-
public_key: string;
|
|
60
|
-
policy_reference: string;
|
|
61
|
-
receipts: GovernanceReceipt[];
|
|
62
|
-
merkle_root: string;
|
|
63
|
-
merkle_proofs: MerkleProof[];
|
|
64
|
-
offline_capable: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface MerkleProof {
|
|
68
|
-
leaf_hash: string;
|
|
69
|
-
leaf_index: number;
|
|
70
|
-
siblings: string[];
|
|
71
|
-
directions: ('left' | 'right')[];
|
|
72
|
-
merkle_root: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ── Proxy options ───────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
export interface ProxyServerOptions {
|
|
78
|
-
port?: number;
|
|
79
|
-
policy?: ToolPolicy;
|
|
80
|
-
upstream?: StdioBridgeOptions;
|
|
81
|
-
upstreamUrl?: string;
|
|
82
|
-
gatewayId?: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export class GovernanceProxy extends EventEmitter {
|
|
86
|
-
private server: net.Server | null = null;
|
|
87
|
-
private bridge: StdioBridge | null = null;
|
|
88
|
-
|
|
89
|
-
// Crypto key - never leaves this process
|
|
90
|
-
private signingKP: KeyPair;
|
|
91
|
-
|
|
92
|
-
// State
|
|
93
|
-
private policy: ToolPolicy;
|
|
94
|
-
private port: number;
|
|
95
|
-
private started = false;
|
|
96
|
-
private upstreamOptions: StdioBridgeOptions | null;
|
|
97
|
-
private upstreamUrl: string | null;
|
|
98
|
-
private gatewayId: string;
|
|
99
|
-
|
|
100
|
-
// Receipt chain
|
|
101
|
-
private receipts: GovernanceReceipt[] = [];
|
|
102
|
-
private lastReceiptHash: string = '';
|
|
103
|
-
private policyHash: string = '';
|
|
104
|
-
|
|
105
|
-
// Stats
|
|
106
|
-
private stats = { permitted: 0, denied: 0, total: 0, started_at: '' };
|
|
107
|
-
|
|
108
|
-
constructor(options: ProxyServerOptions = {}) {
|
|
109
|
-
super();
|
|
110
|
-
this.port = options.port ?? 18800;
|
|
111
|
-
this.policy = options.policy ?? PERMISSIVE;
|
|
112
|
-
this.upstreamOptions = options.upstream ?? null;
|
|
113
|
-
this.upstreamUrl = options.upstreamUrl ?? null;
|
|
114
|
-
this.gatewayId = options.gatewayId ?? 'aga-proxy';
|
|
115
|
-
this.signingKP = generateKeyPair();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── Start / Stop ───────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
async start(): Promise<void> {
|
|
121
|
-
if (this.started) throw new Error('Proxy already running');
|
|
122
|
-
|
|
123
|
-
this.policyHash = sha256Str(canonicalize(this.policy));
|
|
124
|
-
|
|
125
|
-
// Start downstream bridge if configured
|
|
126
|
-
if (this.upstreamOptions) {
|
|
127
|
-
this.bridge = new StdioBridge(this.upstreamOptions);
|
|
128
|
-
await this.bridge.start();
|
|
129
|
-
this.bridge.on('error', (err) => this.emit('error', err));
|
|
130
|
-
this.bridge.on('exit', (code: number) => {
|
|
131
|
-
process.stderr.write(`[aga-proxy] Downstream exited with code ${code}\n`);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Start TCP server
|
|
136
|
-
this.server = net.createServer((socket) => this.handleConnection(socket));
|
|
137
|
-
await new Promise<void>((resolve, reject) => {
|
|
138
|
-
this.server!.listen(this.port, () => resolve());
|
|
139
|
-
this.server!.on('error', reject);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
this.started = true;
|
|
143
|
-
this.stats.started_at = new Date().toISOString();
|
|
144
|
-
resetRateLimits();
|
|
145
|
-
this.emit('started', { port: this.port });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async stop(): Promise<void> {
|
|
149
|
-
if (!this.started) return;
|
|
150
|
-
|
|
151
|
-
if (this.bridge) {
|
|
152
|
-
await this.bridge.stop();
|
|
153
|
-
this.bridge = null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (this.server) {
|
|
157
|
-
await new Promise<void>((resolve) => {
|
|
158
|
-
this.server!.close(() => resolve());
|
|
159
|
-
});
|
|
160
|
-
this.server = null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
this.started = false;
|
|
164
|
-
this.emit('stopped');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── Connection handler ─────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
private handleConnection(socket: net.Socket): void {
|
|
170
|
-
let buffer = '';
|
|
171
|
-
|
|
172
|
-
socket.on('data', (chunk) => {
|
|
173
|
-
buffer += chunk.toString();
|
|
174
|
-
const lines = buffer.split('\n');
|
|
175
|
-
buffer = lines.pop() || '';
|
|
176
|
-
|
|
177
|
-
for (const line of lines) {
|
|
178
|
-
const trimmed = line.trim();
|
|
179
|
-
if (!trimmed) continue;
|
|
180
|
-
this.handleMessage(trimmed, socket).catch((err) => {
|
|
181
|
-
process.stderr.write(`[aga-proxy] Error handling message: ${err}\n`);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
socket.on('error', () => { /* client disconnected */ });
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private async handleMessage(raw: string, socket: net.Socket): Promise<void> {
|
|
190
|
-
let parsed: Record<string, unknown>;
|
|
191
|
-
try {
|
|
192
|
-
parsed = JSON.parse(raw);
|
|
193
|
-
} catch {
|
|
194
|
-
this.respond(socket, { jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null });
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (parsed.jsonrpc !== '2.0') {
|
|
199
|
-
this.respond(socket, { jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request: missing jsonrpc 2.0' }, id: null });
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const requestId = (parsed.id as string | number | null) ?? null;
|
|
204
|
-
const method = parsed.method as string | undefined;
|
|
205
|
-
|
|
206
|
-
// Non-tools/call methods: forward transparently
|
|
207
|
-
if (method !== 'tools/call') {
|
|
208
|
-
if (this.bridge) {
|
|
209
|
-
try {
|
|
210
|
-
const response = await this.bridge.send(parsed);
|
|
211
|
-
this.respond(socket, response);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
this.respond(socket, {
|
|
214
|
-
jsonrpc: '2.0',
|
|
215
|
-
error: { code: -32603, message: `Downstream error: ${err}` },
|
|
216
|
-
id: requestId,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
} else if (this.upstreamUrl) {
|
|
220
|
-
await this.forwardHttp(raw, socket, requestId);
|
|
221
|
-
} else {
|
|
222
|
-
this.respond(socket, {
|
|
223
|
-
jsonrpc: '2.0',
|
|
224
|
-
error: { code: -32603, message: 'No upstream configured' },
|
|
225
|
-
id: requestId,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// tools/call - governance intercept
|
|
232
|
-
await this.interceptToolCall(parsed, socket, requestId);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ── Tool call interception ─────────────────────────────────
|
|
236
|
-
|
|
237
|
-
private async interceptToolCall(
|
|
238
|
-
parsed: Record<string, unknown>,
|
|
239
|
-
socket: net.Socket,
|
|
240
|
-
requestId: string | number | null,
|
|
241
|
-
): Promise<void> {
|
|
242
|
-
const params = parsed.params as Record<string, unknown> | undefined;
|
|
243
|
-
const toolName = params?.name as string | undefined;
|
|
244
|
-
const toolArgs = params?.arguments as Record<string, unknown> | undefined;
|
|
245
|
-
|
|
246
|
-
this.stats.total++;
|
|
247
|
-
|
|
248
|
-
// Fail-closed: no tool name
|
|
249
|
-
if (!toolName) {
|
|
250
|
-
const receipt = this.generateReceipt('UNKNOWN', 'DENIED', 'tool name extraction failed, fail-closed', requestId, undefined);
|
|
251
|
-
this.stats.denied++;
|
|
252
|
-
this.respond(socket, {
|
|
253
|
-
jsonrpc: '2.0',
|
|
254
|
-
error: {
|
|
255
|
-
code: -32600,
|
|
256
|
-
message: 'Missing tool name',
|
|
257
|
-
data: { receipt_id: receipt.receipt_id, decision: 'DENIED' },
|
|
258
|
-
},
|
|
259
|
-
id: requestId,
|
|
260
|
-
});
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Evaluate against policy
|
|
265
|
-
const decision = evaluate(this.policy, toolName, toolArgs);
|
|
266
|
-
const receipt = this.generateReceipt(
|
|
267
|
-
toolName,
|
|
268
|
-
decision.allowed ? 'PERMITTED' : 'DENIED',
|
|
269
|
-
decision.reason,
|
|
270
|
-
requestId,
|
|
271
|
-
toolArgs,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
if (!decision.allowed) {
|
|
275
|
-
this.stats.denied++;
|
|
276
|
-
this.respond(socket, {
|
|
277
|
-
jsonrpc: '2.0',
|
|
278
|
-
error: {
|
|
279
|
-
code: -32600,
|
|
280
|
-
message: `Tool denied: ${decision.reason}`,
|
|
281
|
-
data: { receipt_id: receipt.receipt_id, decision: 'DENIED', reason: decision.reason },
|
|
282
|
-
},
|
|
283
|
-
id: requestId,
|
|
284
|
-
});
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Permitted - forward to downstream
|
|
289
|
-
this.stats.permitted++;
|
|
290
|
-
|
|
291
|
-
if (this.bridge) {
|
|
292
|
-
try {
|
|
293
|
-
const response = await this.bridge.send(parsed);
|
|
294
|
-
this.respond(socket, response);
|
|
295
|
-
} catch (err) {
|
|
296
|
-
this.respond(socket, {
|
|
297
|
-
jsonrpc: '2.0',
|
|
298
|
-
error: { code: -32603, message: `Downstream error: ${err}` },
|
|
299
|
-
id: requestId,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
} else if (this.upstreamUrl) {
|
|
303
|
-
await this.forwardHttp(JSON.stringify(parsed), socket, requestId);
|
|
304
|
-
} else {
|
|
305
|
-
// No upstream - return success with receipt info
|
|
306
|
-
this.respond(socket, {
|
|
307
|
-
jsonrpc: '2.0',
|
|
308
|
-
result: {
|
|
309
|
-
content: [{ type: 'text', text: JSON.stringify({ permitted: true, receipt_id: receipt.receipt_id, tool: toolName }) }],
|
|
310
|
-
},
|
|
311
|
-
id: requestId,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ── Receipt generation (Ed25519-SHA256-JCS canonical format) ─
|
|
317
|
-
|
|
318
|
-
private generateReceipt(
|
|
319
|
-
toolName: string,
|
|
320
|
-
decision: 'PERMITTED' | 'DENIED',
|
|
321
|
-
reason: string,
|
|
322
|
-
requestId: string | number | null,
|
|
323
|
-
toolArgs: Record<string, unknown> | undefined,
|
|
324
|
-
): GovernanceReceipt {
|
|
325
|
-
const pubKeyHex = pkToHex(this.signingKP.publicKey);
|
|
326
|
-
|
|
327
|
-
// Arguments hash tri-state per spec Section 3.5
|
|
328
|
-
let argumentsHash: string;
|
|
329
|
-
if (toolArgs === undefined) {
|
|
330
|
-
argumentsHash = '';
|
|
331
|
-
} else {
|
|
332
|
-
argumentsHash = sha256Str(canonicalize(toolArgs));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const unsigned = {
|
|
336
|
-
receipt_id: uuid(),
|
|
337
|
-
receipt_version: '1.0',
|
|
338
|
-
algorithm: 'Ed25519-SHA256-JCS',
|
|
339
|
-
timestamp: utcNow(),
|
|
340
|
-
request_id: requestId,
|
|
341
|
-
method: 'tools/call',
|
|
342
|
-
tool_name: toolName,
|
|
343
|
-
decision,
|
|
344
|
-
reason,
|
|
345
|
-
policy_reference: this.policyHash,
|
|
346
|
-
arguments_hash: argumentsHash,
|
|
347
|
-
previous_receipt_hash: this.lastReceiptHash,
|
|
348
|
-
gateway_id: this.gatewayId,
|
|
349
|
-
public_key: pubKeyHex,
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const sig = signStr(canonicalize(unsigned), this.signingKP.secretKey);
|
|
353
|
-
const receipt: GovernanceReceipt = { ...unsigned, signature: bytesToHex(sig) };
|
|
354
|
-
|
|
355
|
-
this.receipts.push(receipt);
|
|
356
|
-
this.lastReceiptHash = sha256Str(canonicalize(receipt));
|
|
357
|
-
|
|
358
|
-
return receipt;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ── Merkle tree (binary, odd-node promotion, binary concat) ─
|
|
362
|
-
|
|
363
|
-
private merkleNodeHash(leftHex: string, rightHex: string): string {
|
|
364
|
-
const left = utilHexToBytes(leftHex);
|
|
365
|
-
const right = utilHexToBytes(rightHex);
|
|
366
|
-
const combined = new Uint8Array(left.length + right.length);
|
|
367
|
-
combined.set(left, 0);
|
|
368
|
-
combined.set(right, left.length);
|
|
369
|
-
return bytesToHex(sha256(combined));
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
private computeMerkleRoot(leaves: string[]): string {
|
|
373
|
-
if (leaves.length === 0) return '';
|
|
374
|
-
if (leaves.length === 1) return leaves[0];
|
|
375
|
-
let level = [...leaves];
|
|
376
|
-
while (level.length > 1) {
|
|
377
|
-
const next: string[] = [];
|
|
378
|
-
for (let i = 0; i < level.length; i += 2) {
|
|
379
|
-
if (i + 1 < level.length) {
|
|
380
|
-
next.push(this.merkleNodeHash(level[i], level[i + 1]));
|
|
381
|
-
} else {
|
|
382
|
-
next.push(level[i]);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
level = next;
|
|
386
|
-
}
|
|
387
|
-
return level[0];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private computeMerkleProof(leaves: string[], leafIndex: number): MerkleProof {
|
|
391
|
-
const siblings: string[] = [];
|
|
392
|
-
const directions: ('left' | 'right')[] = [];
|
|
393
|
-
let level = [...leaves];
|
|
394
|
-
let idx = leafIndex;
|
|
395
|
-
|
|
396
|
-
while (level.length > 1) {
|
|
397
|
-
const next: string[] = [];
|
|
398
|
-
for (let i = 0; i < level.length; i += 2) {
|
|
399
|
-
if (i + 1 < level.length) {
|
|
400
|
-
next.push(this.merkleNodeHash(level[i], level[i + 1]));
|
|
401
|
-
} else {
|
|
402
|
-
next.push(level[i]);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (idx % 2 === 0) {
|
|
406
|
-
if (idx + 1 < level.length) {
|
|
407
|
-
siblings.push(level[idx + 1]);
|
|
408
|
-
directions.push('right');
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
siblings.push(level[idx - 1]);
|
|
412
|
-
directions.push('left');
|
|
413
|
-
}
|
|
414
|
-
idx = Math.floor(idx / 2);
|
|
415
|
-
level = next;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
leaf_hash: leaves[leafIndex],
|
|
420
|
-
leaf_index: leafIndex,
|
|
421
|
-
siblings,
|
|
422
|
-
directions,
|
|
423
|
-
merkle_root: level[0],
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// ── HTTP forwarding ────────────────────────────────────────
|
|
428
|
-
|
|
429
|
-
private async forwardHttp(body: string, socket: net.Socket, requestId: string | number | null): Promise<void> {
|
|
430
|
-
try {
|
|
431
|
-
const resp = await fetch(this.upstreamUrl!, {
|
|
432
|
-
method: 'POST',
|
|
433
|
-
headers: { 'Content-Type': 'application/json' },
|
|
434
|
-
body,
|
|
435
|
-
});
|
|
436
|
-
const data = await resp.json();
|
|
437
|
-
this.respond(socket, data as Record<string, unknown>);
|
|
438
|
-
} catch (err) {
|
|
439
|
-
this.respond(socket, {
|
|
440
|
-
jsonrpc: '2.0',
|
|
441
|
-
error: { code: -32603, message: `HTTP upstream error: ${err}` },
|
|
442
|
-
id: requestId,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ── Response helper ────────────────────────────────────────
|
|
448
|
-
|
|
449
|
-
private respond(socket: net.Socket, msg: Record<string, unknown>): void {
|
|
450
|
-
if (!socket.destroyed) {
|
|
451
|
-
socket.write(JSON.stringify(msg) + '\n');
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// ── Public API ─────────────────────────────────────────────
|
|
456
|
-
|
|
457
|
-
async switchPolicy(newPolicy: ToolPolicy): Promise<void> {
|
|
458
|
-
this.policy = newPolicy;
|
|
459
|
-
this.policyHash = sha256Str(canonicalize(newPolicy));
|
|
460
|
-
resetRateLimits();
|
|
461
|
-
this.emit('policy_switched');
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
exportBundle(): EvidenceBundle {
|
|
465
|
-
if (this.receipts.length === 0) throw new Error('No receipts');
|
|
466
|
-
|
|
467
|
-
const leafHashes = this.receipts.map(r => sha256Str(canonicalize(r)));
|
|
468
|
-
const root = this.computeMerkleRoot(leafHashes);
|
|
469
|
-
const proofs = leafHashes.map((_, i) => this.computeMerkleProof(leafHashes, i));
|
|
470
|
-
|
|
471
|
-
return {
|
|
472
|
-
schema_version: '1.0',
|
|
473
|
-
bundle_id: uuid(),
|
|
474
|
-
algorithm: 'Ed25519-SHA256-JCS',
|
|
475
|
-
generated_at: utcNow(),
|
|
476
|
-
gateway_id: this.gatewayId,
|
|
477
|
-
public_key: pkToHex(this.signingKP.publicKey),
|
|
478
|
-
policy_reference: this.policyHash,
|
|
479
|
-
receipts: this.receipts,
|
|
480
|
-
merkle_root: root,
|
|
481
|
-
merkle_proofs: proofs,
|
|
482
|
-
offline_capable: true,
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
getStatus() {
|
|
487
|
-
return {
|
|
488
|
-
running: this.started,
|
|
489
|
-
port: this.port,
|
|
490
|
-
policy_mode: this.policy.mode,
|
|
491
|
-
receipt_count: this.receipts.length,
|
|
492
|
-
...this.stats,
|
|
493
|
-
public_key: pkToHex(this.signingKP.publicKey),
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
getPublicKey(): string { return pkToHex(this.signingKP.publicKey); }
|
|
498
|
-
getReceipts(): GovernanceReceipt[] { return this.receipts; }
|
|
499
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AGA Governance Proxy - Stdio Bridge
|
|
3
|
-
* Spawns a downstream MCP server as a child process and manages
|
|
4
|
-
* JSON-RPC message framing over stdin/stdout.
|
|
5
|
-
*
|
|
6
|
-
* Patent: USPTO App. No. 19/433,835
|
|
7
|
-
* Copyright (c) 2026 Attested Intelligence Holdings LLC
|
|
8
|
-
* SPDX-License-Identifier: MIT
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { spawn, type ChildProcess } from 'node:child_process';
|
|
12
|
-
import { EventEmitter } from 'node:events';
|
|
13
|
-
|
|
14
|
-
export interface StdioBridgeOptions {
|
|
15
|
-
command: string;
|
|
16
|
-
args?: string[];
|
|
17
|
-
env?: Record<string, string>;
|
|
18
|
-
cwd?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Bridges JSON-RPC messages to/from a child process via stdio.
|
|
23
|
-
* Handles newline-delimited JSON framing.
|
|
24
|
-
*/
|
|
25
|
-
export class StdioBridge extends EventEmitter {
|
|
26
|
-
private child: ChildProcess | null = null;
|
|
27
|
-
private buffer = '';
|
|
28
|
-
private pendingRequests = new Map<string | number, {
|
|
29
|
-
resolve: (response: Record<string, unknown>) => void;
|
|
30
|
-
reject: (error: Error) => void;
|
|
31
|
-
timer: ReturnType<typeof setTimeout>;
|
|
32
|
-
}>();
|
|
33
|
-
|
|
34
|
-
constructor(private options: StdioBridgeOptions) {
|
|
35
|
-
super();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async start(): Promise<void> {
|
|
39
|
-
const { command, args = [], env, cwd } = this.options;
|
|
40
|
-
|
|
41
|
-
this.child = spawn(command, args, {
|
|
42
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
-
env: { ...process.env, ...env },
|
|
44
|
-
cwd,
|
|
45
|
-
shell: process.platform === 'win32',
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
this.child.stdout!.on('data', (chunk: Buffer) => {
|
|
49
|
-
this.buffer += chunk.toString();
|
|
50
|
-
this.processBuffer();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
this.child.stderr!.on('data', (chunk: Buffer) => {
|
|
54
|
-
// Log downstream stderr but don't treat as JSON-RPC
|
|
55
|
-
process.stderr.write(`[downstream] ${chunk.toString()}`);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
this.child.on('exit', (code, signal) => {
|
|
59
|
-
this.emit('exit', code, signal);
|
|
60
|
-
this.rejectAllPending(new Error(`Downstream process exited: code=${code} signal=${signal}`));
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
this.child.on('error', (err) => {
|
|
64
|
-
this.emit('error', err);
|
|
65
|
-
this.rejectAllPending(err);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private processBuffer(): void {
|
|
70
|
-
const lines = this.buffer.split('\n');
|
|
71
|
-
// Keep the last (possibly incomplete) line in the buffer
|
|
72
|
-
this.buffer = lines.pop() || '';
|
|
73
|
-
|
|
74
|
-
for (const line of lines) {
|
|
75
|
-
const trimmed = line.trim();
|
|
76
|
-
if (!trimmed) continue;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const msg = JSON.parse(trimmed) as Record<string, unknown>;
|
|
80
|
-
this.handleMessage(msg);
|
|
81
|
-
} catch {
|
|
82
|
-
// Not valid JSON - skip
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private handleMessage(msg: Record<string, unknown>): void {
|
|
88
|
-
// If it has an id and either result or error, it's a response
|
|
89
|
-
if ('id' in msg && ('result' in msg || 'error' in msg)) {
|
|
90
|
-
const id = msg.id as string | number;
|
|
91
|
-
const pending = this.pendingRequests.get(id);
|
|
92
|
-
if (pending) {
|
|
93
|
-
clearTimeout(pending.timer);
|
|
94
|
-
this.pendingRequests.delete(id);
|
|
95
|
-
pending.resolve(msg);
|
|
96
|
-
}
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Notifications from downstream (no id, or has method) - emit for proxy to handle
|
|
101
|
-
this.emit('notification', msg);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Send a JSON-RPC request to the downstream server and wait for a response.
|
|
106
|
-
*/
|
|
107
|
-
async send(message: Record<string, unknown>, timeoutMs = 30_000): Promise<Record<string, unknown>> {
|
|
108
|
-
if (!this.child?.stdin?.writable) {
|
|
109
|
-
throw new Error('Downstream process not running');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const id = message.id as string | number | undefined;
|
|
113
|
-
|
|
114
|
-
// Notifications (no id) - fire and forget
|
|
115
|
-
if (id === undefined || id === null) {
|
|
116
|
-
this.child.stdin.write(JSON.stringify(message) + '\n');
|
|
117
|
-
return { jsonrpc: '2.0', result: null, id: null };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
121
|
-
const timer = setTimeout(() => {
|
|
122
|
-
this.pendingRequests.delete(id);
|
|
123
|
-
reject(new Error(`Timeout waiting for response to request ${id}`));
|
|
124
|
-
}, timeoutMs);
|
|
125
|
-
|
|
126
|
-
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
127
|
-
this.child!.stdin!.write(JSON.stringify(message) + '\n');
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Send a raw message without waiting for a response.
|
|
133
|
-
*/
|
|
134
|
-
sendRaw(message: Record<string, unknown>): void {
|
|
135
|
-
if (!this.child?.stdin?.writable) {
|
|
136
|
-
throw new Error('Downstream process not running');
|
|
137
|
-
}
|
|
138
|
-
this.child.stdin.write(JSON.stringify(message) + '\n');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async stop(): Promise<void> {
|
|
142
|
-
this.rejectAllPending(new Error('Bridge stopped'));
|
|
143
|
-
if (this.child) {
|
|
144
|
-
this.child.kill('SIGTERM');
|
|
145
|
-
// Give it a moment, then force kill
|
|
146
|
-
await new Promise<void>(resolve => {
|
|
147
|
-
const timer = setTimeout(() => {
|
|
148
|
-
this.child?.kill('SIGKILL');
|
|
149
|
-
resolve();
|
|
150
|
-
}, 3000);
|
|
151
|
-
this.child!.on('exit', () => {
|
|
152
|
-
clearTimeout(timer);
|
|
153
|
-
resolve();
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
this.child = null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
get running(): boolean {
|
|
161
|
-
return this.child !== null && this.child.exitCode === null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private rejectAllPending(err: Error): void {
|
|
165
|
-
for (const [id, pending] of this.pendingRequests) {
|
|
166
|
-
clearTimeout(pending.timer);
|
|
167
|
-
pending.reject(err);
|
|
168
|
-
}
|
|
169
|
-
this.pendingRequests.clear();
|
|
170
|
-
}
|
|
171
|
-
}
|
package/src/proxy/types.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AGA Governance Proxy - Types
|
|
3
|
-
* Adapted from aga-mcp-gateway/src/governance/types.ts
|
|
4
|
-
*
|
|
5
|
-
* Patent: USPTO App. No. 19/433,835
|
|
6
|
-
* Copyright (c) 2026 Attested Intelligence Holdings LLC
|
|
7
|
-
* SPDX-License-Identifier: MIT
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface ToolConstraint {
|
|
11
|
-
name: string;
|
|
12
|
-
allowed: boolean;
|
|
13
|
-
max_calls_per_minute?: number;
|
|
14
|
-
path_prefix?: string;
|
|
15
|
-
path_keys?: string[];
|
|
16
|
-
denied_patterns?: string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ToolPolicy {
|
|
20
|
-
mode: 'allowlist' | 'denylist' | 'audit_only';
|
|
21
|
-
constraints: Record<string, ToolConstraint>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ToolCallDecision {
|
|
25
|
-
allowed: boolean;
|
|
26
|
-
reason: string;
|
|
27
|
-
tool_name: string;
|
|
28
|
-
policy_mode: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ProxyConfig {
|
|
32
|
-
port: number;
|
|
33
|
-
upstream: string;
|
|
34
|
-
upstreamType: 'stdio' | 'http';
|
|
35
|
-
policy: ToolPolicy;
|
|
36
|
-
dataDir: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const DEFAULT_PROXY_PORT = 18800;
|
|
40
|
-
export const DEFAULT_DATA_DIR = '.aga-proxy';
|