@panguard-ai/panguard-mcp-proxy 1.7.1 → 1.7.3

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 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
- /** One stdio session per proxy process. */
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 { appendFileSync, mkdirSync } from 'node:fs';
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
- /** One stdio session per proxy process. */
48
- sessionId = 'mcp-proxy-session';
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: 'mcp-agent',
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.1",
3
+ "version": "1.7.3",
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.1",
25
- "@panguard-ai/containment": "0.1.0"
24
+ "@panguard-ai/atr": "1.7.3",
25
+ "@panguard-ai/containment": "0.1.0",
26
+ "@panguard-ai/panguard-guard": "1.7.3"
26
27
  },
27
28
  "peerDependencies": {
28
- "@panguard-ai/atr": "1.7.1"
29
+ "@panguard-ai/atr": "1.7.3"
29
30
  },
30
31
  "files": [
31
32
  "dist",