@exfil/canary 1.0.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/LICENSE +21 -0
- package/README.md +387 -0
- package/SECURITY.md +50 -0
- package/dist/entities.d.ts +43 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +218 -0
- package/dist/entities.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +50 -0
- package/dist/logger.js.map +1 -0
- package/dist/persistence.d.ts +48 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +296 -0
- package/dist/persistence.js.map +1 -0
- package/dist/proxy/DownstreamManager.d.ts +55 -0
- package/dist/proxy/DownstreamManager.d.ts.map +1 -0
- package/dist/proxy/DownstreamManager.js +110 -0
- package/dist/proxy/DownstreamManager.js.map +1 -0
- package/dist/proxy/ProxyServer.d.ts +60 -0
- package/dist/proxy/ProxyServer.d.ts.map +1 -0
- package/dist/proxy/ProxyServer.js +480 -0
- package/dist/proxy/ProxyServer.js.map +1 -0
- package/dist/proxy/auditor/DualAuditor.d.ts +27 -0
- package/dist/proxy/auditor/DualAuditor.d.ts.map +1 -0
- package/dist/proxy/auditor/DualAuditor.js +44 -0
- package/dist/proxy/auditor/DualAuditor.js.map +1 -0
- package/dist/proxy/auditor/LLMAuditor.d.ts +16 -0
- package/dist/proxy/auditor/LLMAuditor.d.ts.map +1 -0
- package/dist/proxy/auditor/LLMAuditor.js +221 -0
- package/dist/proxy/auditor/LLMAuditor.js.map +1 -0
- package/dist/proxy/auditor/types.d.ts +54 -0
- package/dist/proxy/auditor/types.d.ts.map +1 -0
- package/dist/proxy/auditor/types.js +11 -0
- package/dist/proxy/auditor/types.js.map +1 -0
- package/dist/proxy/types.d.ts +71 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +8 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/scanner.d.ts +37 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +57 -0
- package/dist/scanner.js.map +1 -0
- package/dist/server.d.ts +59 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +711 -0
- package/dist/server.js.map +1 -0
- package/dist/simhash.d.ts +65 -0
- package/dist/simhash.d.ts.map +1 -0
- package/dist/simhash.js +151 -0
- package/dist/simhash.js.map +1 -0
- package/dist/state.d.ts +86 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +136 -0
- package/dist/state.js.map +1 -0
- package/dist/token.d.ts +70 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +146 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/proxy.example.json +53 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State persistence — atomic writes, 0o600 file mode, no sequence storage.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY (RC-1): The `sequence` field is intentionally omitted from the
|
|
5
|
+
* persisted representation. After a server restart, existing token metadata
|
|
6
|
+
* (ID, expiry, leakage history) is recovered, but the invisible Unicode
|
|
7
|
+
* payload is lost. Tokens recovered from disk cannot re-detect their
|
|
8
|
+
* embedded sequence in new data; they are useful only for historical
|
|
9
|
+
* reporting. This is a known limitation — see SECURITY.md.
|
|
10
|
+
*
|
|
11
|
+
* SECURITY (RC-7): All recovered records are validated through manual type
|
|
12
|
+
* guards. Invalid records are discarded with a warning; the server never
|
|
13
|
+
* throws on malformed persistence data.
|
|
14
|
+
*/
|
|
15
|
+
import { promises as fs } from 'fs';
|
|
16
|
+
import { isValidTokenId } from './scanner.js';
|
|
17
|
+
import { log } from './logger.js';
|
|
18
|
+
const CURRENT_VERSION = 1;
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Persistence entry point
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Atomically persists session state to disk (sequence field omitted).
|
|
24
|
+
*
|
|
25
|
+
* Writes to a `.tmp` sibling file first, then renames to the target path
|
|
26
|
+
* so the on-disk file is never observed in a partial state. File mode is
|
|
27
|
+
* set to 0o600 (owner read/write only).
|
|
28
|
+
*
|
|
29
|
+
* @param state Current session state.
|
|
30
|
+
* @param config Server configuration (provides `persist_path`).
|
|
31
|
+
*/
|
|
32
|
+
export async function persistState(state, config) {
|
|
33
|
+
if (!config.persist_path)
|
|
34
|
+
return;
|
|
35
|
+
const persistedTokens = [];
|
|
36
|
+
for (const token of state.tokens.values()) {
|
|
37
|
+
// RC-1: sequence intentionally excluded.
|
|
38
|
+
// v1.1: entity values intentionally excluded — same RC-1 treatment.
|
|
39
|
+
const persistedEntities = token.entity_canaries.map((ec) => ({
|
|
40
|
+
entity_type: ec.entity_type,
|
|
41
|
+
context_hint: ec.context_hint,
|
|
42
|
+
}));
|
|
43
|
+
persistedTokens.push({
|
|
44
|
+
token_id: token.token_id,
|
|
45
|
+
source_type: token.source_type,
|
|
46
|
+
source_server: token.source_server,
|
|
47
|
+
source_tool: token.source_tool,
|
|
48
|
+
source_call_id: token.source_call_id,
|
|
49
|
+
embed_position: token.embed_position,
|
|
50
|
+
created_at: token.created_at,
|
|
51
|
+
expires_at: token.expires_at,
|
|
52
|
+
leaked: token.leaked,
|
|
53
|
+
leakage_event_ids: [...token.leakage_event_ids],
|
|
54
|
+
entity_canaries: persistedEntities,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const leakageEvents = [...state.leakage_events.values()];
|
|
58
|
+
const payload = {
|
|
59
|
+
version: CURRENT_VERSION,
|
|
60
|
+
session_id: state.session_id,
|
|
61
|
+
created_at: state.created_at,
|
|
62
|
+
token_counter: state.token_counter,
|
|
63
|
+
tokens: persistedTokens,
|
|
64
|
+
leakage_events: leakageEvents,
|
|
65
|
+
};
|
|
66
|
+
const json = JSON.stringify(payload, null, 2);
|
|
67
|
+
const tmpPath = config.persist_path + '.tmp';
|
|
68
|
+
try {
|
|
69
|
+
await fs.writeFile(tmpPath, json, { encoding: 'utf8', mode: 0o600 });
|
|
70
|
+
// chmod the tmp file BEFORE rename so the secure mode is set atomically —
|
|
71
|
+
// there is no window where the target file exists with looser permissions.
|
|
72
|
+
await fs.chmod(tmpPath, 0o600);
|
|
73
|
+
await fs.rename(tmpPath, config.persist_path);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
log('warn', `Persistence write failed: ${err.message}`);
|
|
77
|
+
// Clean up orphaned tmp file if rename failed.
|
|
78
|
+
try {
|
|
79
|
+
await fs.unlink(tmpPath);
|
|
80
|
+
}
|
|
81
|
+
catch { /* ignore */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Attempts to recover session state from the persistence file.
|
|
86
|
+
*
|
|
87
|
+
* Returns `null` when:
|
|
88
|
+
* - No path is configured.
|
|
89
|
+
* - The file does not exist.
|
|
90
|
+
* - Top-level validation fails (the whole file is malformed).
|
|
91
|
+
*
|
|
92
|
+
* Individual token or event records that fail validation are discarded with
|
|
93
|
+
* a warning rather than crashing the server (RC-7).
|
|
94
|
+
*
|
|
95
|
+
* @param config Server configuration.
|
|
96
|
+
* @returns Recovered `SessionState`, or `null` to start fresh.
|
|
97
|
+
*/
|
|
98
|
+
export async function recoverState(config) {
|
|
99
|
+
if (!config.persist_path)
|
|
100
|
+
return null;
|
|
101
|
+
let raw;
|
|
102
|
+
try {
|
|
103
|
+
const json = await fs.readFile(config.persist_path, 'utf8');
|
|
104
|
+
raw = JSON.parse(json);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const msg = err.code === 'ENOENT'
|
|
108
|
+
? 'No persistence file found — starting fresh.'
|
|
109
|
+
: `Failed to read/parse persistence file: ${err.message}`;
|
|
110
|
+
log('info', msg);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!validatePersistedState(raw)) {
|
|
114
|
+
log('warn', 'Persistence file failed top-level validation — starting fresh.');
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const persisted = raw;
|
|
118
|
+
const state = {
|
|
119
|
+
session_id: persisted.session_id,
|
|
120
|
+
created_at: persisted.created_at,
|
|
121
|
+
tokens: new Map(),
|
|
122
|
+
leakage_events: new Map(),
|
|
123
|
+
token_counter: persisted.token_counter,
|
|
124
|
+
};
|
|
125
|
+
let skippedTokens = 0;
|
|
126
|
+
for (const raw_token of persisted.tokens) {
|
|
127
|
+
if (!validatePersistedToken(raw_token)) {
|
|
128
|
+
skippedTokens += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Recovered tokens have no sequence or entity values — cannot re-detect.
|
|
132
|
+
const token = {
|
|
133
|
+
token_id: raw_token.token_id,
|
|
134
|
+
sequence: '', // RC-1: sequence not stored; cannot re-detect.
|
|
135
|
+
source_type: raw_token.source_type,
|
|
136
|
+
source_server: raw_token.source_server,
|
|
137
|
+
source_tool: raw_token.source_tool,
|
|
138
|
+
source_call_id: raw_token.source_call_id,
|
|
139
|
+
embed_position: raw_token.embed_position,
|
|
140
|
+
created_at: raw_token.created_at,
|
|
141
|
+
expires_at: raw_token.expires_at,
|
|
142
|
+
leaked: raw_token.leaked,
|
|
143
|
+
leakage_event_ids: [...raw_token.leakage_event_ids],
|
|
144
|
+
// v1.1: values not stored; null signals "cannot re-detect after restart".
|
|
145
|
+
entity_canaries: (raw_token.entity_canaries ?? []).map((pe) => ({
|
|
146
|
+
entity_type: pe.entity_type,
|
|
147
|
+
value: null, // RC-1: not stored; cannot re-detect after restart
|
|
148
|
+
context_hint: pe.context_hint,
|
|
149
|
+
})),
|
|
150
|
+
simhash: null, // v1.6: not stored; cannot fingerprint after restart
|
|
151
|
+
};
|
|
152
|
+
state.tokens.set(token.token_id, token);
|
|
153
|
+
}
|
|
154
|
+
let skippedEvents = 0;
|
|
155
|
+
for (const raw_event of persisted.leakage_events) {
|
|
156
|
+
if (!validateLeakageEvent(raw_event)) {
|
|
157
|
+
skippedEvents += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
state.leakage_events.set(raw_event.event_id, raw_event);
|
|
161
|
+
}
|
|
162
|
+
if (skippedTokens > 0 || skippedEvents > 0) {
|
|
163
|
+
log('warn', `State recovery: discarded ${skippedTokens} invalid token(s) and ${skippedEvents} invalid event(s).`);
|
|
164
|
+
}
|
|
165
|
+
log('info', `Recovered ${state.tokens.size} token(s) and ${state.leakage_events.size} event(s) from persistence.`);
|
|
166
|
+
return state;
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Runtime type guards (RC-7)
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
const SOURCE_TYPES = new Set([
|
|
172
|
+
'tool_result', 'file_read', 'api_response', 'database_row', 'user_message', 'other',
|
|
173
|
+
]);
|
|
174
|
+
const EMBED_POSITIONS = new Set([
|
|
175
|
+
'prefix', 'suffix', 'both', 'random_word_boundary',
|
|
176
|
+
]);
|
|
177
|
+
const DETECTION_METHODS = new Set(['check_leakage', 'scan_outbound']);
|
|
178
|
+
const ACTIONS_TAKEN = new Set(['logged', 'halted', 'alerted', 'none']);
|
|
179
|
+
function isString(v) {
|
|
180
|
+
return typeof v === 'string';
|
|
181
|
+
}
|
|
182
|
+
function isNumber(v) {
|
|
183
|
+
return typeof v === 'number' && Number.isFinite(v);
|
|
184
|
+
}
|
|
185
|
+
function isBoolean(v) {
|
|
186
|
+
return typeof v === 'boolean';
|
|
187
|
+
}
|
|
188
|
+
function isObject(v) {
|
|
189
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
190
|
+
}
|
|
191
|
+
function isArray(v) {
|
|
192
|
+
return Array.isArray(v);
|
|
193
|
+
}
|
|
194
|
+
function isStringArray(v) {
|
|
195
|
+
return isArray(v) && v.every(isString);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Top-level validation of the raw parsed JSON from the persistence file.
|
|
199
|
+
*
|
|
200
|
+
* @param raw Parsed JSON value.
|
|
201
|
+
*/
|
|
202
|
+
export function validatePersistedState(raw) {
|
|
203
|
+
if (!isObject(raw))
|
|
204
|
+
return false;
|
|
205
|
+
if (raw['version'] !== CURRENT_VERSION)
|
|
206
|
+
return false;
|
|
207
|
+
if (!isString(raw['session_id']) || !isValidTokenId(raw['session_id']))
|
|
208
|
+
return false;
|
|
209
|
+
if (!isNumber(raw['created_at']) || raw['created_at'] <= 0)
|
|
210
|
+
return false;
|
|
211
|
+
if (!isNumber(raw['token_counter']) || raw['token_counter'] < 0)
|
|
212
|
+
return false;
|
|
213
|
+
if (!isArray(raw['tokens']))
|
|
214
|
+
return false;
|
|
215
|
+
if (!isArray(raw['leakage_events']))
|
|
216
|
+
return false;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Validates a single persisted token record (RC-7).
|
|
221
|
+
*
|
|
222
|
+
* @param raw Candidate token object.
|
|
223
|
+
*/
|
|
224
|
+
function validatePersistedToken(raw) {
|
|
225
|
+
if (!isObject(raw))
|
|
226
|
+
return false;
|
|
227
|
+
if (!isString(raw['token_id']) || !isValidTokenId(raw['token_id']))
|
|
228
|
+
return false;
|
|
229
|
+
if (!isString(raw['source_type']) || !SOURCE_TYPES.has(raw['source_type']))
|
|
230
|
+
return false;
|
|
231
|
+
if (!isString(raw['embed_position']) || !EMBED_POSITIONS.has(raw['embed_position']))
|
|
232
|
+
return false;
|
|
233
|
+
if (!isNumber(raw['created_at']) || raw['created_at'] <= 0)
|
|
234
|
+
return false;
|
|
235
|
+
if (!isNumber(raw['expires_at']) || raw['expires_at'] <= 0)
|
|
236
|
+
return false;
|
|
237
|
+
if (!isBoolean(raw['leaked']))
|
|
238
|
+
return false;
|
|
239
|
+
if (!isStringArray(raw['leakage_event_ids']))
|
|
240
|
+
return false;
|
|
241
|
+
// Optional string fields
|
|
242
|
+
if ('source_server' in raw && raw['source_server'] !== undefined && !isString(raw['source_server']))
|
|
243
|
+
return false;
|
|
244
|
+
if ('source_tool' in raw && raw['source_tool'] !== undefined && !isString(raw['source_tool']))
|
|
245
|
+
return false;
|
|
246
|
+
if ('source_call_id' in raw && raw['source_call_id'] !== undefined && !isString(raw['source_call_id']))
|
|
247
|
+
return false;
|
|
248
|
+
// v1.1: entity_canaries is optional (absent in files written by v1.0)
|
|
249
|
+
if ('entity_canaries' in raw && raw['entity_canaries'] !== undefined) {
|
|
250
|
+
if (!isArray(raw['entity_canaries']))
|
|
251
|
+
return false;
|
|
252
|
+
for (const ec of raw['entity_canaries']) {
|
|
253
|
+
if (!isObject(ec))
|
|
254
|
+
return false;
|
|
255
|
+
if (!isString(ec['entity_type']))
|
|
256
|
+
return false;
|
|
257
|
+
if (!isString(ec['context_hint']))
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Validates a single leakage event record (RC-7).
|
|
265
|
+
*
|
|
266
|
+
* @param raw Candidate event object.
|
|
267
|
+
*/
|
|
268
|
+
function validateLeakageEvent(raw) {
|
|
269
|
+
if (!isObject(raw))
|
|
270
|
+
return false;
|
|
271
|
+
if (!isString(raw['event_id']) || !isValidTokenId(raw['event_id']))
|
|
272
|
+
return false;
|
|
273
|
+
if (!isString(raw['token_id']) || !isValidTokenId(raw['token_id']))
|
|
274
|
+
return false;
|
|
275
|
+
if (!isNumber(raw['detected_at']) || raw['detected_at'] <= 0)
|
|
276
|
+
return false;
|
|
277
|
+
if (!isString(raw['detection_method']) || !DETECTION_METHODS.has(raw['detection_method']))
|
|
278
|
+
return false;
|
|
279
|
+
if (!isString(raw['action_taken']) || !ACTIONS_TAKEN.has(raw['action_taken']))
|
|
280
|
+
return false;
|
|
281
|
+
if (!isBoolean(raw['webhook_attempted']))
|
|
282
|
+
return false;
|
|
283
|
+
if (raw['webhook_delivered'] !== null && !isBoolean(raw['webhook_delivered']))
|
|
284
|
+
return false;
|
|
285
|
+
// Optional string fields
|
|
286
|
+
if ('target_server' in raw && raw['target_server'] !== undefined && !isString(raw['target_server']))
|
|
287
|
+
return false;
|
|
288
|
+
if ('target_tool' in raw && raw['target_tool'] !== undefined && !isString(raw['target_tool']))
|
|
289
|
+
return false;
|
|
290
|
+
if ('target_call_id' in raw && raw['target_call_id'] !== undefined && !isString(raw['target_call_id']))
|
|
291
|
+
return false;
|
|
292
|
+
if ('turn_number' in raw && raw['turn_number'] !== undefined && !isNumber(raw['turn_number']))
|
|
293
|
+
return false;
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DownstreamManager — connects to and manages all downstream MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Each downstream server is spawned as a subprocess via StdioClientTransport.
|
|
5
|
+
* Tools from every server are aggregated and namespaced as `{server_id}__{tool_name}`
|
|
6
|
+
* so the agent sees a flat, unambiguous tool list even when multiple servers
|
|
7
|
+
* expose tools with the same name.
|
|
8
|
+
*/
|
|
9
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import type { ProxyConfig } from './types.js';
|
|
11
|
+
/** A resolved tool entry: the namespaced name, original name, and owning server. */
|
|
12
|
+
export interface DownstreamTool {
|
|
13
|
+
/** Namespaced name exposed to the agent: `{server_id}__{original_name}`. */
|
|
14
|
+
namespaced_name: string;
|
|
15
|
+
/** Original tool name on the downstream server. */
|
|
16
|
+
original_name: string;
|
|
17
|
+
/** Server that owns this tool. */
|
|
18
|
+
server_id: string;
|
|
19
|
+
/** Full SDK Tool definition with the namespaced name substituted in. */
|
|
20
|
+
tool: Tool;
|
|
21
|
+
}
|
|
22
|
+
export declare class DownstreamManager {
|
|
23
|
+
private servers;
|
|
24
|
+
/**
|
|
25
|
+
* Connects to every downstream server listed in `proxyConfig`.
|
|
26
|
+
* Failures on individual servers are logged but do not abort the others.
|
|
27
|
+
*/
|
|
28
|
+
connect(proxyConfig: ProxyConfig): Promise<void>;
|
|
29
|
+
private connectOne;
|
|
30
|
+
/** Returns all namespaced tools across all connected downstream servers. */
|
|
31
|
+
listAllTools(): DownstreamTool[];
|
|
32
|
+
/**
|
|
33
|
+
* Looks up which server and original tool name correspond to a namespaced name.
|
|
34
|
+
* Returns null if not found.
|
|
35
|
+
*/
|
|
36
|
+
resolveTool(namespacedName: string): {
|
|
37
|
+
server_id: string;
|
|
38
|
+
original_name: string;
|
|
39
|
+
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Forwards a tool call to the appropriate downstream server.
|
|
42
|
+
*
|
|
43
|
+
* @param server_id The downstream server id.
|
|
44
|
+
* @param original_name The original (non-namespaced) tool name.
|
|
45
|
+
* @param args Arguments to forward.
|
|
46
|
+
* @returns The raw SDK response.
|
|
47
|
+
*/
|
|
48
|
+
callTool(server_id: string, original_name: string, args: Record<string, unknown>): Promise<{
|
|
49
|
+
content: unknown[];
|
|
50
|
+
isError?: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
/** Disconnects all downstream clients. */
|
|
53
|
+
disconnect(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=DownstreamManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DownstreamManager.d.ts","sourceRoot":"","sources":["../../src/proxy/DownstreamManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAC/D,OAAO,KAAK,EAA0B,WAAW,EAAE,MAAM,YAAY,CAAC;AAGtE,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,eAAe,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,IAAI,EAAE,IAAI,CAAC;CACZ;AASD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAkC;IAEjD;;;OAGG;IACG,OAAO,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;YAOxC,UAAU;IA+CxB,4EAA4E;IAC5E,YAAY,IAAI,cAAc,EAAE;IAQhC;;;OAGG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAQxF;;;;;;;OAOG;IACG,QAAQ,CACZ,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IASrD,0CAA0C;IACpC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAWlC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DownstreamManager — connects to and manages all downstream MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Each downstream server is spawned as a subprocess via StdioClientTransport.
|
|
5
|
+
* Tools from every server are aggregated and namespaced as `{server_id}__{tool_name}`
|
|
6
|
+
* so the agent sees a flat, unambiguous tool list even when multiple servers
|
|
7
|
+
* expose tools with the same name.
|
|
8
|
+
*/
|
|
9
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
10
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
11
|
+
import { log } from '../logger.js';
|
|
12
|
+
export class DownstreamManager {
|
|
13
|
+
servers = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Connects to every downstream server listed in `proxyConfig`.
|
|
16
|
+
* Failures on individual servers are logged but do not abort the others.
|
|
17
|
+
*/
|
|
18
|
+
async connect(proxyConfig) {
|
|
19
|
+
for (const serverConfig of proxyConfig.servers) {
|
|
20
|
+
await this.connectOne(serverConfig);
|
|
21
|
+
}
|
|
22
|
+
log('info', `DownstreamManager: connected to ${this.servers.size}/${proxyConfig.servers.length} server(s).`);
|
|
23
|
+
}
|
|
24
|
+
async connectOne(serverConfig) {
|
|
25
|
+
const { id, command, args = [], env } = serverConfig;
|
|
26
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(id)) {
|
|
27
|
+
throw new Error(`Downstream server id "${id}" is invalid. ` +
|
|
28
|
+
'Must match /^[a-z][a-z0-9_-]*$/ (lowercase, start with a letter, no spaces).');
|
|
29
|
+
}
|
|
30
|
+
const transport = new StdioClientTransport({
|
|
31
|
+
command,
|
|
32
|
+
args,
|
|
33
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
34
|
+
});
|
|
35
|
+
const client = new Client({ name: 'exfil/canary-proxy', version: '1.0.0' }, { capabilities: {} });
|
|
36
|
+
try {
|
|
37
|
+
await client.connect(transport);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
log('warn', `DownstreamManager: failed to connect to "${id}": ${err.message}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let sdkTools = [];
|
|
44
|
+
try {
|
|
45
|
+
const response = await client.listTools();
|
|
46
|
+
sdkTools = response.tools ?? [];
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
log('warn', `DownstreamManager: listTools failed for "${id}": ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
const tools = sdkTools.map((t) => ({
|
|
52
|
+
namespaced_name: `${id}__${t.name}`,
|
|
53
|
+
original_name: t.name,
|
|
54
|
+
server_id: id,
|
|
55
|
+
tool: { ...t, name: `${id}__${t.name}` },
|
|
56
|
+
}));
|
|
57
|
+
this.servers.set(id, { config: serverConfig, client, tools });
|
|
58
|
+
log('info', `DownstreamManager: "${id}" ready — ${tools.length} tool(s).`);
|
|
59
|
+
}
|
|
60
|
+
/** Returns all namespaced tools across all connected downstream servers. */
|
|
61
|
+
listAllTools() {
|
|
62
|
+
const all = [];
|
|
63
|
+
for (const entry of this.servers.values()) {
|
|
64
|
+
all.push(...entry.tools);
|
|
65
|
+
}
|
|
66
|
+
return all;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Looks up which server and original tool name correspond to a namespaced name.
|
|
70
|
+
* Returns null if not found.
|
|
71
|
+
*/
|
|
72
|
+
resolveTool(namespacedName) {
|
|
73
|
+
for (const [server_id, entry] of this.servers.entries()) {
|
|
74
|
+
const dt = entry.tools.find((t) => t.namespaced_name === namespacedName);
|
|
75
|
+
if (dt)
|
|
76
|
+
return { server_id, original_name: dt.original_name };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Forwards a tool call to the appropriate downstream server.
|
|
82
|
+
*
|
|
83
|
+
* @param server_id The downstream server id.
|
|
84
|
+
* @param original_name The original (non-namespaced) tool name.
|
|
85
|
+
* @param args Arguments to forward.
|
|
86
|
+
* @returns The raw SDK response.
|
|
87
|
+
*/
|
|
88
|
+
async callTool(server_id, original_name, args) {
|
|
89
|
+
const entry = this.servers.get(server_id);
|
|
90
|
+
if (!entry) {
|
|
91
|
+
throw new Error(`No connected downstream server with id "${server_id}".`);
|
|
92
|
+
}
|
|
93
|
+
const result = await entry.client.callTool({ name: original_name, arguments: args });
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/** Disconnects all downstream clients. */
|
|
97
|
+
async disconnect() {
|
|
98
|
+
for (const [id, entry] of this.servers.entries()) {
|
|
99
|
+
try {
|
|
100
|
+
await entry.client.close();
|
|
101
|
+
log('debug', `DownstreamManager: closed "${id}".`);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
log('warn', `DownstreamManager: error closing "${id}": ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.servers.clear();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=DownstreamManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DownstreamManager.js","sourceRoot":"","sources":["../../src/proxy/DownstreamManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProxyServer — transparent canary-token proxy for MCP tool calls.
|
|
3
|
+
*
|
|
4
|
+
* The agent connects to ProxyServer as its only MCP server. ProxyServer:
|
|
5
|
+
*
|
|
6
|
+
* 1. Aggregates all tools from downstream servers (namespaced as `{id}__{tool}`).
|
|
7
|
+
* 2. On every tool call:
|
|
8
|
+
* a. Serialises the arguments and scans for active canary sequences.
|
|
9
|
+
* → Blocks (or logs/alerts) if a token is found in outbound args.
|
|
10
|
+
* b. Forwards the call to the downstream server.
|
|
11
|
+
* c. Walks the response, wrapping every text string ≥ MIN_WRAP_CHARS
|
|
12
|
+
* with a freshly generated canary token registered in session state.
|
|
13
|
+
* 3. Exposes a single built-in tool `canary__get_report` for operator use.
|
|
14
|
+
*
|
|
15
|
+
* The agent never needs to explicitly call wrap_content or scan_outbound —
|
|
16
|
+
* interception happens transparently on every I/O crossing.
|
|
17
|
+
*/
|
|
18
|
+
import type { CanaryConfig, SessionState } from '../types.js';
|
|
19
|
+
import type { ProxyConfig } from './types.js';
|
|
20
|
+
export declare class ProxyServer {
|
|
21
|
+
private readonly upstream;
|
|
22
|
+
private readonly downstream;
|
|
23
|
+
private readonly config;
|
|
24
|
+
private state;
|
|
25
|
+
/** Accumulated tool results for auditor context (ring-buffered to 50). */
|
|
26
|
+
private storedResults;
|
|
27
|
+
private resultCounter;
|
|
28
|
+
/** Dual-auditor instance, or null if not configured. */
|
|
29
|
+
private readonly auditor;
|
|
30
|
+
/** Number of detections made by the LLM auditor layer (for report). */
|
|
31
|
+
private auditorDetections;
|
|
32
|
+
constructor(state: SessionState, config: CanaryConfig, proxyConfig: ProxyConfig);
|
|
33
|
+
private _proxyConfig;
|
|
34
|
+
start(): Promise<void>;
|
|
35
|
+
shutdown(): Promise<void>;
|
|
36
|
+
sweepExpiredTokens(): void;
|
|
37
|
+
private registerHandlers;
|
|
38
|
+
/**
|
|
39
|
+
* Scans serialised tool arguments for any active canary tokens.
|
|
40
|
+
* Fires resolveAction (which may throw McpError to halt) if a token is found.
|
|
41
|
+
*/
|
|
42
|
+
private scanInboundArgs;
|
|
43
|
+
/**
|
|
44
|
+
* Walks the content array from a downstream response.
|
|
45
|
+
* For every text item with ≥ MIN_WRAP_CHARS characters, embeds a new
|
|
46
|
+
* canary token and registers it in session state.
|
|
47
|
+
*
|
|
48
|
+
* Non-text content (images, etc.) is passed through unchanged.
|
|
49
|
+
*/
|
|
50
|
+
private wrapResponseContent;
|
|
51
|
+
private handleGetReport;
|
|
52
|
+
/**
|
|
53
|
+
* Fires response_mode action for an auditor DERIVED verdict.
|
|
54
|
+
* Unlike resolveAction, there may be no specific token_id — the auditor
|
|
55
|
+
* detected derived content without matching a canary marker.
|
|
56
|
+
*/
|
|
57
|
+
private resolveAuditorAction;
|
|
58
|
+
private resolveAction;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=ProxyServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProxyServer.d.ts","sourceRoot":"","sources":["../../src/proxy/ProxyServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAWH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAe,MAAM,aAAa,CAAC;AAkB3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgF9C,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,KAAK,CAAe;IAC5B,0EAA0E;IAC1E,OAAO,CAAC,aAAa,CAA0B;IAC/C,OAAO,CAAC,aAAa,CAAK;IAC1B,wDAAwD;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,uEAAuE;IACvE,OAAO,CAAC,iBAAiB,CAAK;gBAElB,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW;IAyB/E,OAAO,CAAC,YAAY,CAAc;IAM5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,kBAAkB,IAAI,IAAI;IAY1B,OAAO,CAAC,gBAAgB;IAoFxB;;;OAGG;YACW,eAAe;IAgH7B;;;;;;OAMG;YACW,mBAAmB;IAgEjC,OAAO,CAAC,eAAe;IAgEvB;;;;OAIG;YACW,oBAAoB;YAiCpB,aAAa;CAkC5B"}
|