@panguard-ai/panguard-mcp-proxy 1.7.0 → 1.7.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/dist/proxy.d.ts +30 -1
- package/dist/proxy.js +72 -16
- package/package.json +4 -3
package/dist/proxy.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
15
15
|
import type { EvalResult } from './evaluator.js';
|
|
16
16
|
import type { McpGateVerdict } from '@panguard-ai/containment';
|
|
17
|
+
import { type VerifyResult } from '@panguard-ai/panguard-guard/audit';
|
|
17
18
|
export interface ProxyConfig {
|
|
18
19
|
/** Command to start the upstream MCP server */
|
|
19
20
|
readonly upstreamCommand: string;
|
|
@@ -23,6 +24,10 @@ export interface ProxyConfig {
|
|
|
23
24
|
readonly evalTimeout?: number;
|
|
24
25
|
/** Fail mode: 'closed' blocks on error (safer), 'open' allows on error (default for availability) */
|
|
25
26
|
readonly failMode?: 'open' | 'closed';
|
|
27
|
+
/** Stable session id for this proxy run (default: per-process unique). */
|
|
28
|
+
readonly sessionId?: string;
|
|
29
|
+
/** Agent id behind this proxy (default: PANGUARD_AGENT_ID env or 'mcp-agent'). */
|
|
30
|
+
readonly agentId?: string;
|
|
26
31
|
}
|
|
27
32
|
/** The subset of ProxyEvaluator the proxy uses — injectable for testing. */
|
|
28
33
|
export interface ProxyEvaluatorLike {
|
|
@@ -43,10 +48,18 @@ export declare class MCPProxy {
|
|
|
43
48
|
private readonly riskStore;
|
|
44
49
|
/** Confidence at/above which an evaluator deny escalates the whole session. */
|
|
45
50
|
private static readonly ESCALATE_CONFIDENCE;
|
|
46
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* One stdio session per proxy process. Defaults to a per-process unique id
|
|
53
|
+
* (not the old hardcoded constant) so verdict lines from distinct runs are
|
|
54
|
+
* distinguishable; overridable via config for correlation with an orchestrator.
|
|
55
|
+
*/
|
|
47
56
|
private readonly sessionId;
|
|
57
|
+
/** Agent id behind this proxy — written into every verdict line for attribution. */
|
|
58
|
+
private readonly agentId;
|
|
48
59
|
/** Upstream tool names = the Layer 0 capability scope (populated in start()). */
|
|
49
60
|
private upstreamToolNames;
|
|
61
|
+
/** Tamper-evident chain over proxy-verdicts.jsonl (lazily keyed in connect()). */
|
|
62
|
+
private chain;
|
|
50
63
|
constructor(config: ProxyConfig, deps?: {
|
|
51
64
|
evaluator?: ProxyEvaluatorLike;
|
|
52
65
|
});
|
|
@@ -76,5 +89,21 @@ export declare class MCPProxy {
|
|
|
76
89
|
confidence: number;
|
|
77
90
|
matchedRules: readonly string[];
|
|
78
91
|
}): void;
|
|
92
|
+
/**
|
|
93
|
+
* Lazily build the tamper-evident verdict chain. The audit key is resolved
|
|
94
|
+
* keychain-first (file fallback); getAuditKey never throws. Chain construction
|
|
95
|
+
* is best-effort — if it fails, verdict logging silently no-ops rather than
|
|
96
|
+
* bricking the proxy (fail-open on audit).
|
|
97
|
+
*/
|
|
98
|
+
private ensureChain;
|
|
99
|
+
/**
|
|
100
|
+
* Append a verdict to the tamper-evident chain. The REAL sessionId/agentId are
|
|
101
|
+
* carried in actor.agent (the old code dropped them), plus a decisionId and the
|
|
102
|
+
* lifted matched rule id. AuditChain.append is fail-open so a broken audit file
|
|
103
|
+
* never blocks a tool call.
|
|
104
|
+
*/
|
|
105
|
+
private logVerdict;
|
|
106
|
+
/** Verify the durable proxy-verdicts chain end-to-end. */
|
|
107
|
+
verify(): Promise<VerifyResult | null>;
|
|
79
108
|
private registerHandlers;
|
|
80
109
|
}
|
package/dist/proxy.js
CHANGED
|
@@ -16,21 +16,13 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
16
16
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
17
17
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
18
18
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
19
|
-
import {
|
|
19
|
+
import { mkdirSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { ProxyEvaluator } from './evaluator.js';
|
|
23
23
|
import { GuardGate, InlineGate, RiskAnalyzer, InMemoryRiskStore, NoopContainmentController, applyMcpGate, } from '@panguard-ai/containment';
|
|
24
|
+
import { AuditChain, buildActor, newDecisionId, getAuditKey, } from '@panguard-ai/panguard-guard/audit';
|
|
24
25
|
const VERDICT_LOG = join(homedir(), '.panguard-guard', 'proxy-verdicts.jsonl');
|
|
25
|
-
function logVerdict(entry) {
|
|
26
|
-
try {
|
|
27
|
-
mkdirSync(join(homedir(), '.panguard-guard'), { recursive: true });
|
|
28
|
-
appendFileSync(VERDICT_LOG, JSON.stringify({ ...entry, ts: new Date().toISOString() }) + '\n');
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
/* best-effort logging */
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
26
|
export class MCPProxy {
|
|
35
27
|
config;
|
|
36
28
|
evaluator;
|
|
@@ -44,10 +36,18 @@ export class MCPProxy {
|
|
|
44
36
|
riskStore;
|
|
45
37
|
/** Confidence at/above which an evaluator deny escalates the whole session. */
|
|
46
38
|
static ESCALATE_CONFIDENCE = 95;
|
|
47
|
-
/**
|
|
48
|
-
|
|
39
|
+
/**
|
|
40
|
+
* One stdio session per proxy process. Defaults to a per-process unique id
|
|
41
|
+
* (not the old hardcoded constant) so verdict lines from distinct runs are
|
|
42
|
+
* distinguishable; overridable via config for correlation with an orchestrator.
|
|
43
|
+
*/
|
|
44
|
+
sessionId;
|
|
45
|
+
/** Agent id behind this proxy — written into every verdict line for attribution. */
|
|
46
|
+
agentId;
|
|
49
47
|
/** Upstream tool names = the Layer 0 capability scope (populated in start()). */
|
|
50
48
|
upstreamToolNames = new Set();
|
|
49
|
+
/** Tamper-evident chain over proxy-verdicts.jsonl (lazily keyed in connect()). */
|
|
50
|
+
chain = null;
|
|
51
51
|
constructor(config, deps = {}) {
|
|
52
52
|
this.config = config;
|
|
53
53
|
this.evaluator = deps.evaluator ?? new ProxyEvaluator();
|
|
@@ -64,6 +64,9 @@ export class MCPProxy {
|
|
|
64
64
|
config.failMode ??
|
|
65
65
|
(envFailMode === 'open' || envFailMode === 'closed' ? envFailMode : 'closed');
|
|
66
66
|
this.evalTimeout = config.evalTimeout ?? 5000;
|
|
67
|
+
this.sessionId =
|
|
68
|
+
config.sessionId ?? `mcp-proxy-${process.pid}-${Date.now().toString(36)}`;
|
|
69
|
+
this.agentId = config.agentId ?? process.env['PANGUARD_AGENT_ID'] ?? 'mcp-agent';
|
|
67
70
|
// Sync sub-ms pre-check. Runs in front of the async evaluator so the worst
|
|
68
71
|
// payloads (and any session the brain flags) are blocked instantly — and,
|
|
69
72
|
// with fail-closed as the default, an unavailable async evaluator denies.
|
|
@@ -90,6 +93,7 @@ export class MCPProxy {
|
|
|
90
93
|
* in-memory transports without spawning a process.
|
|
91
94
|
*/
|
|
92
95
|
async connect(upstreamTransport, agentTransport) {
|
|
96
|
+
await this.ensureChain();
|
|
93
97
|
const ruleCount = await this.evaluator.loadRules();
|
|
94
98
|
process.stderr.write(`[panguard-proxy] Loaded ${ruleCount} ATR rules\n`);
|
|
95
99
|
this.client = new Client({ name: 'panguard-mcp-proxy', version: '0.1.0' }, { capabilities: {} });
|
|
@@ -133,7 +137,7 @@ export class MCPProxy {
|
|
|
133
137
|
name,
|
|
134
138
|
args: toolArgs,
|
|
135
139
|
sessionId: this.sessionId,
|
|
136
|
-
agentId:
|
|
140
|
+
agentId: this.agentId,
|
|
137
141
|
capabilities: this.upstreamToolNames.size > 0 ? this.upstreamToolNames : new Set([name]),
|
|
138
142
|
});
|
|
139
143
|
}
|
|
@@ -149,6 +153,58 @@ export class MCPProxy {
|
|
|
149
153
|
this.riskStore.set(this.sessionId, { level: 'high', reasons: [...verdict.matchedRules] });
|
|
150
154
|
}
|
|
151
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Lazily build the tamper-evident verdict chain. The audit key is resolved
|
|
158
|
+
* keychain-first (file fallback); getAuditKey never throws. Chain construction
|
|
159
|
+
* is best-effort — if it fails, verdict logging silently no-ops rather than
|
|
160
|
+
* bricking the proxy (fail-open on audit).
|
|
161
|
+
*/
|
|
162
|
+
async ensureChain() {
|
|
163
|
+
if (this.chain)
|
|
164
|
+
return;
|
|
165
|
+
try {
|
|
166
|
+
mkdirSync(join(homedir(), '.panguard-guard'), { recursive: true });
|
|
167
|
+
const key = await getAuditKey();
|
|
168
|
+
// Head-anchor defaults to `<VERDICT_LOG>.head` (per-file) so it never
|
|
169
|
+
// collides with the events / manifest chain anchors in the same dataDir.
|
|
170
|
+
this.chain = new AuditChain(VERDICT_LOG, { key });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
process.stderr.write(`[panguard-audit] proxy chain init failed (verdict logging disabled): ${err instanceof Error ? err.message : String(err)}\n`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Append a verdict to the tamper-evident chain. The REAL sessionId/agentId are
|
|
178
|
+
* carried in actor.agent (the old code dropped them), plus a decisionId and the
|
|
179
|
+
* lifted matched rule id. AuditChain.append is fail-open so a broken audit file
|
|
180
|
+
* never blocks a tool call.
|
|
181
|
+
*/
|
|
182
|
+
logVerdict(entry) {
|
|
183
|
+
if (!this.chain)
|
|
184
|
+
return;
|
|
185
|
+
const rules = entry.rules ?? [];
|
|
186
|
+
this.chain.append({
|
|
187
|
+
phase: entry.phase,
|
|
188
|
+
tool: entry.tool,
|
|
189
|
+
outcome: entry.outcome,
|
|
190
|
+
reason: entry.reason,
|
|
191
|
+
rules: [...rules],
|
|
192
|
+
ms: entry.ms,
|
|
193
|
+
ts: new Date().toISOString(),
|
|
194
|
+
decisionId: newDecisionId(),
|
|
195
|
+
actor: buildActor({
|
|
196
|
+
platform: 'mcp-proxy',
|
|
197
|
+
sessionId: this.sessionId,
|
|
198
|
+
agentId: this.agentId,
|
|
199
|
+
}),
|
|
200
|
+
...(rules.length > 0 ? { rule: { id: rules[0] } } : {}),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** Verify the durable proxy-verdicts chain end-to-end. */
|
|
204
|
+
async verify() {
|
|
205
|
+
await this.ensureChain();
|
|
206
|
+
return this.chain ? this.chain.verify() : null;
|
|
207
|
+
}
|
|
152
208
|
registerHandlers() {
|
|
153
209
|
const client = this.client;
|
|
154
210
|
const server = this.server;
|
|
@@ -166,7 +222,7 @@ export class MCPProxy {
|
|
|
166
222
|
// instantly, even if the async evaluator times out fail-open.
|
|
167
223
|
const gateVerdict = this.gateCheck(name, toolArgs);
|
|
168
224
|
if (!gateVerdict.allow) {
|
|
169
|
-
logVerdict({
|
|
225
|
+
this.logVerdict({
|
|
170
226
|
phase: 'pre-gate',
|
|
171
227
|
tool: name,
|
|
172
228
|
outcome: 'deny',
|
|
@@ -201,7 +257,7 @@ export class MCPProxy {
|
|
|
201
257
|
durationMs: this.evalTimeout,
|
|
202
258
|
};
|
|
203
259
|
}
|
|
204
|
-
logVerdict({
|
|
260
|
+
this.logVerdict({
|
|
205
261
|
phase: 'pre',
|
|
206
262
|
tool: name,
|
|
207
263
|
outcome: preResult.outcome,
|
|
@@ -254,7 +310,7 @@ export class MCPProxy {
|
|
|
254
310
|
durationMs: this.evalTimeout,
|
|
255
311
|
};
|
|
256
312
|
}
|
|
257
|
-
logVerdict({
|
|
313
|
+
this.logVerdict({
|
|
258
314
|
phase: 'post',
|
|
259
315
|
tool: name,
|
|
260
316
|
outcome: postResult.outcome,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panguard-ai/panguard-mcp-proxy",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "MCP Proxy — runtime interception for AI agent tool calls using ATR rules",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
23
23
|
"agent-threat-rules": "^3.5.0",
|
|
24
|
+
"@panguard-ai/atr": "1.7.2",
|
|
24
25
|
"@panguard-ai/containment": "0.1.0",
|
|
25
|
-
"@panguard-ai/
|
|
26
|
+
"@panguard-ai/panguard-guard": "1.7.2"
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
28
|
-
"@panguard-ai/atr": "1.7.
|
|
29
|
+
"@panguard-ai/atr": "1.7.2"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"dist",
|