@intutic/mcp-governance-proxy 1.9.0 → 1.10.1

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.
Files changed (123) hide show
  1. package/README.md +28 -0
  2. package/dist/anomaly/detectors.d.ts +129 -0
  3. package/dist/anomaly/detectors.d.ts.map +1 -0
  4. package/dist/anomaly/detectors.js +368 -0
  5. package/dist/anomaly/detectors.js.map +1 -0
  6. package/dist/anomaly/index.d.ts +52 -0
  7. package/dist/anomaly/index.d.ts.map +1 -0
  8. package/dist/anomaly/index.js +71 -0
  9. package/dist/anomaly/index.js.map +1 -0
  10. package/dist/config.d.ts +107 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +129 -6
  13. package/dist/config.js.map +1 -1
  14. package/dist/daemon/healthMonitor.d.ts.map +1 -1
  15. package/dist/daemon/healthMonitor.js +45 -46
  16. package/dist/daemon/healthMonitor.js.map +1 -1
  17. package/dist/daemon/index.d.ts +15 -1
  18. package/dist/daemon/index.d.ts.map +1 -1
  19. package/dist/daemon/index.js +78 -9
  20. package/dist/daemon/index.js.map +1 -1
  21. package/dist/daemon/instrumentation.d.ts +20 -0
  22. package/dist/daemon/instrumentation.d.ts.map +1 -0
  23. package/dist/daemon/instrumentation.js +61 -0
  24. package/dist/daemon/instrumentation.js.map +1 -0
  25. package/dist/daemon/policyCache.d.ts +83 -0
  26. package/dist/daemon/policyCache.d.ts.map +1 -1
  27. package/dist/daemon/policyCache.js +280 -11
  28. package/dist/daemon/policyCache.js.map +1 -1
  29. package/dist/daemon/statusReporter.d.ts +12 -0
  30. package/dist/daemon/statusReporter.d.ts.map +1 -0
  31. package/dist/daemon/statusReporter.js +130 -0
  32. package/dist/daemon/statusReporter.js.map +1 -0
  33. package/dist/daemon/telemetryBatcher.d.ts +3 -1
  34. package/dist/daemon/telemetryBatcher.d.ts.map +1 -1
  35. package/dist/daemon/telemetryBatcher.js +56 -13
  36. package/dist/daemon/telemetryBatcher.js.map +1 -1
  37. package/dist/daemonClient.d.ts +1 -1
  38. package/dist/daemonClient.d.ts.map +1 -1
  39. package/dist/daemonClient.js +0 -2
  40. package/dist/daemonClient.js.map +1 -1
  41. package/dist/dlp.d.ts +19 -0
  42. package/dist/dlp.d.ts.map +1 -1
  43. package/dist/dlp.js +95 -18
  44. package/dist/dlp.js.map +1 -1
  45. package/dist/emitter.d.ts +40 -2
  46. package/dist/emitter.d.ts.map +1 -1
  47. package/dist/emitter.js +27 -5
  48. package/dist/emitter.js.map +1 -1
  49. package/dist/index.js +13 -2
  50. package/dist/index.js.map +1 -1
  51. package/dist/injection.d.ts +46 -0
  52. package/dist/injection.d.ts.map +1 -0
  53. package/dist/injection.js +147 -0
  54. package/dist/injection.js.map +1 -0
  55. package/dist/interceptor.d.ts +131 -1
  56. package/dist/interceptor.d.ts.map +1 -1
  57. package/dist/interceptor.js +301 -5
  58. package/dist/interceptor.js.map +1 -1
  59. package/dist/policy.d.ts +110 -2
  60. package/dist/policy.d.ts.map +1 -1
  61. package/dist/policy.js +206 -5
  62. package/dist/policy.js.map +1 -1
  63. package/dist/proxy.d.ts +205 -0
  64. package/dist/proxy.d.ts.map +1 -1
  65. package/dist/proxy.js +572 -65
  66. package/dist/proxy.js.map +1 -1
  67. package/dist/remoteBridge.d.ts +51 -0
  68. package/dist/remoteBridge.d.ts.map +1 -0
  69. package/dist/remoteBridge.js +182 -0
  70. package/dist/remoteBridge.js.map +1 -0
  71. package/dist/session.d.ts +158 -0
  72. package/dist/session.d.ts.map +1 -0
  73. package/dist/session.js +206 -0
  74. package/dist/session.js.map +1 -0
  75. package/dist/sessionScope.d.ts +51 -0
  76. package/dist/sessionScope.d.ts.map +1 -0
  77. package/dist/sessionScope.js +94 -0
  78. package/dist/sessionScope.js.map +1 -0
  79. package/dist/sessionStore.d.ts +69 -0
  80. package/dist/sessionStore.d.ts.map +1 -0
  81. package/dist/sessionStore.js +143 -0
  82. package/dist/sessionStore.js.map +1 -0
  83. package/dist/tofu.d.ts +112 -0
  84. package/dist/tofu.d.ts.map +1 -0
  85. package/dist/tofu.js +180 -0
  86. package/dist/tofu.js.map +1 -0
  87. package/dist/toolPoison.d.ts +32 -0
  88. package/dist/toolPoison.d.ts.map +1 -0
  89. package/dist/toolPoison.js +81 -0
  90. package/dist/toolPoison.js.map +1 -0
  91. package/dist/valkeyErrors.d.ts +17 -0
  92. package/dist/valkeyErrors.d.ts.map +1 -0
  93. package/dist/valkeyErrors.js +24 -0
  94. package/dist/valkeyErrors.js.map +1 -0
  95. package/dist/wasm/context.d.ts +57 -0
  96. package/dist/wasm/context.d.ts.map +1 -0
  97. package/dist/wasm/context.js +59 -0
  98. package/dist/wasm/context.js.map +1 -0
  99. package/dist/wasm/hostImports.d.ts +43 -0
  100. package/dist/wasm/hostImports.d.ts.map +1 -0
  101. package/dist/wasm/hostImports.js +158 -0
  102. package/dist/wasm/hostImports.js.map +1 -0
  103. package/dist/wasm/loader.d.ts +95 -0
  104. package/dist/wasm/loader.d.ts.map +1 -0
  105. package/dist/wasm/loader.js +198 -0
  106. package/dist/wasm/loader.js.map +1 -0
  107. package/dist/wasm/referencedFiles.d.ts +115 -0
  108. package/dist/wasm/referencedFiles.d.ts.map +1 -0
  109. package/dist/wasm/referencedFiles.js +366 -0
  110. package/dist/wasm/referencedFiles.js.map +1 -0
  111. package/dist/wasm/runner.d.ts +91 -0
  112. package/dist/wasm/runner.d.ts.map +1 -0
  113. package/dist/wasm/runner.js +308 -0
  114. package/dist/wasm/runner.js.map +1 -0
  115. package/dist/wasm/watch.d.ts +5 -0
  116. package/dist/wasm/watch.d.ts.map +1 -0
  117. package/dist/wasm/watch.js +54 -0
  118. package/dist/wasm/watch.js.map +1 -0
  119. package/dist/wasm/worker.d.ts +39 -0
  120. package/dist/wasm/worker.d.ts.map +1 -0
  121. package/dist/wasm/worker.js +245 -0
  122. package/dist/wasm/worker.js.map +1 -0
  123. package/package.json +14 -4
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @intutic/mcp-governance-proxy
2
2
 
3
+ > Three names, one thing: the directory is `packages/mcp-proxy`, the published package is `@intutic/mcp-governance-proxy`, and the docs page is `/integrations/mcp-proxy`.
4
+
3
5
  > Transparent stdio proxy that intercepts MCP `tools/call` JSON-RPC messages and applies workspace SOP policy before forwarding to real MCP servers.
4
6
 
5
7
  ## Overview
@@ -26,6 +28,16 @@ Runs as its own MCP server that exposes governance-aware tool wrappers:
26
28
  Agent ↔ intutic-mcp-daemon (MCP server)
27
29
  ```
28
30
 
31
+ ### Remote Bridge Mode
32
+
33
+ Fronts a remote (HTTP/SSE-transport) MCP server instead of a spawned child process. The harness still spawns this proxy as a normal stdio child — no new listener, no new port — but the proxy's upstream side talks to the remote server over HTTP or Server-Sent Events using the MCP SDK's client transports directly:
34
+
35
+ ```
36
+ Agent ↔ intutic-mcp-proxy ↔ Remote MCP Server (HTTP/SSE)
37
+ ```
38
+
39
+ Every governance check proxy mode applies to a stdio server — allowlists, `tools/list` curation, TOFU pinning, DLP redaction, audit events — applies identically here; only the upstream transport differs. See `--remote-url`/`--remote-transport` below.
40
+
29
41
  ## Features
30
42
 
31
43
  ### DLP Scanning
@@ -78,6 +90,22 @@ npm install @intutic/mcp-governance-proxy
78
90
  intutic-mcp-daemon --port 3100
79
91
  ```
80
92
 
93
+ ### Remote Bridge Mode
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "linear": {
99
+ "command": "intutic-mcp-proxy",
100
+ "args": ["--remote-url", "https://mcp.linear.app/sse", "--remote-transport", "sse"],
101
+ "env": { "INTUTIC_REMOTE_HEADERS": "{\"Authorization\":\"Bearer <token>\"}" }
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ `--remote-transport` accepts `sse` or `http` (default `http`, the SDK's current non-deprecated `StreamableHTTPClientTransport`). `--remote-url` and a positional stdio command (`--` followed by a command) are mutually exclusive — pass one or the other. Auth headers ride via the `INTUTIC_REMOTE_HEADERS` environment variable (a JSON object string), never as a CLI argument — argv is visible to any local process via `ps`.
108
+
81
109
  ## Part of Intutic
82
110
 
83
111
  This package is part of the [Intutic](https://github.com/intutic/intutic) monorepo — an open-core AI governance control plane for developer teams.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * anomaly/detectors.ts — The 7 anomaly detectors ported from
3
+ * `packages/proxy/src/plugins/anomaly/detectors.rs`, confirmed applicable to
4
+ * MCP tool-call traffic.
5
+ *
6
+ * Every threshold, window, marker list, and disposition below was read
7
+ * directly from `detectors.rs` (and, for `tool_poisoning`, from
8
+ * `tool_poison.rs`) — none invented. See this package's Phase 2 report for
9
+ * the verbatim citations.
10
+ *
11
+ * # Which 26 were excluded, and why
12
+ *
13
+ * `detectors.rs` registers ~26 detectors; this file ports exactly 7. The
14
+ * rest are excluded for reasons specific to what an MCP stdio proxy can
15
+ * honestly observe — see the Phase 2 report for the itemized list — not
16
+ * because they were skipped for time.
17
+ *
18
+ * @module
19
+ */
20
+ import type { ToolsListEntry } from '../session.js';
21
+ export type Disposition = 'steer' | 'reask' | 'kill';
22
+ /** Severity order for sorting/clamping — mirrors `Disposition`'s derived
23
+ * `Ord` in mod.rs (`Steer < Reask < Ask < Kill`); this package never uses
24
+ * `Ask`, so the four-way order collapses to three. */
25
+ export declare const DISPOSITION_SEVERITY: Readonly<Record<Disposition, number>>;
26
+ export interface AnomalyFinding {
27
+ /** Stable detector identity — same convention as `AnomalyDetector::id()`. */
28
+ detectorId: string;
29
+ /** A taxonomy label, matching (a subset of) `AnomalyKind::as_str()`'s wire values. */
30
+ kind: string;
31
+ reason: string;
32
+ /** 0.0–1.0. */
33
+ confidence: number;
34
+ /** The disposition this DETECTOR declares — the Rust-source-verified
35
+ * ceiling `mcpAnomalyMode`/per-detector overrides may only ever demote
36
+ * from (see `anomaly/index.ts`'s `resolveEffectiveDisposition`). */
37
+ disposition: Disposition;
38
+ }
39
+ /**
40
+ * A node calling the same tool over and over — the most common shape of a
41
+ * graph that has stopped making progress. `Reask` disposition (verified:
42
+ * `AnomalyFinding::reask` in `ConsecutiveRepeatDetector::detect`).
43
+ */
44
+ export declare function consecutiveRepeat(seq: readonly string[]): AnomalyFinding | null;
45
+ /**
46
+ * Two tools handing work back and forth — invisible to `consecutiveRepeat`
47
+ * because no tool ever repeats twice in a row. `Reask` disposition
48
+ * (verified).
49
+ */
50
+ export declare function pingPongCycle(seq: readonly string[]): AnomalyFinding | null;
51
+ /**
52
+ * Case-folded interning of the tool-name sequence into small integer ids.
53
+ *
54
+ * The Rust `anchor_projection` also strips synthesised `action:` tokens
55
+ * before interning — this proxy has no equivalent token-synthesis machinery
56
+ * (MCP's `tool_sequence` is recorded tool names only, see `session.ts`), so
57
+ * every entry here is already a concrete anchor; this function's only job is
58
+ * the interning itself.
59
+ */
60
+ export declare function anchorProjection(seq: readonly string[]): {
61
+ anchors: number[];
62
+ symbols: string[];
63
+ };
64
+ /**
65
+ * Repetition that survives one stray call — the gap-tolerant counterpart to
66
+ * `consecutiveRepeat`/`pingPongCycle`. `Steer` disposition (verified: this
67
+ * detector "ships steer and may never earn kill under the promotion rule,"
68
+ * per `detectors.rs`'s own doc comment — `Read → View → Write` repeated
69
+ * eight times is both a perfect period-3 cycle AND a productive agent
70
+ * editing eight files, a category error in the signal no threshold fixes).
71
+ */
72
+ export declare function landmarkCycle(seq: readonly string[]): AnomalyFinding | null;
73
+ /**
74
+ * A long run drawing on a single tool, possibly interleaved with nothing
75
+ * else, that still makes no progress. `Steer` disposition, confidence 0.8
76
+ * (both verified from `ToolDiversityCollapseDetector::detect`).
77
+ */
78
+ export declare function toolDiversityCollapse(seq: readonly string[]): AnomalyFinding | null;
79
+ /**
80
+ * One code-execution call bundling what per-call gates would have caught
81
+ * apart — the in-blob analogue of `secret_read` followed by `http_post`.
82
+ * Substring markers, deliberately (see `detectors.rs`'s doc comment): an AST
83
+ * per language is neither cheap nor honestly complete, and a marker's
84
+ * failure mode (an obfuscated payload slips through) is strictly better than
85
+ * claiming AST-grade coverage and delivering it for two languages of ten.
86
+ * `Kill` disposition on both shapes (verified: both branches return
87
+ * `AnomalyFinding::kill` in `CodeAsActionDetector::detect`).
88
+ */
89
+ export declare function codeAsAction(toolName: string, toolArguments: unknown): AnomalyFinding | null;
90
+ /**
91
+ * Runs the five sequence/call-based detectors above against one prospective
92
+ * request and returns every finding, sorted most-severe-first — mirroring
93
+ * `DetectorRegistry::evaluate_all`'s own sort (disposition severity;
94
+ * `AnomalyKind` severity is not ported, since this proxy does not carry that
95
+ * taxonomy-wide ranking table).
96
+ */
97
+ export declare function evaluateSequenceDetectors(toolSequence: readonly string[], toolName: string, toolArguments: unknown): AnomalyFinding[];
98
+ /**
99
+ * Instructions hidden in a tool's DESCRIPTION rather than in conversation
100
+ * text — a different attack surface from `injection.ts`'s response-direction
101
+ * scan (which reuses the same 5 conversational patterns Phase 1 ported).
102
+ * Reads `toolPoison.ts`'s 7-pattern scanner. `Steer` disposition (verified:
103
+ * `AnomalyFinding::steer` in `ToolPoisoningDetector::detect`) — the Rust
104
+ * doc's own reasoning for why this steers rather than reasks applies
105
+ * identically here: the poisoned text belongs to a third-party server's
106
+ * static tool array, identical on every retry, so a reask would be a
107
+ * guaranteed three-strike block rather than a correction loop.
108
+ */
109
+ export declare function toolPoisoning(tools: readonly ToolsListEntry[]): AnomalyFinding | null;
110
+ /**
111
+ * ≥3 distinct DLP-redacted pattern types in one response is a credential
112
+ * sweep, not a single mistake. `Kill` in the Rust source (verified:
113
+ * `AnomalyFinding::kill` in `DlpEscalationDetector::detect`) — but this
114
+ * proxy deliberately never converts that into a NEW block decision: unlike
115
+ * the Rust proxy's request-direction detector, this MCP proxy's own DLP scan
116
+ * ALREADY blocks a request carrying any single finding (interceptor.ts step
117
+ * 1, unconditionally), so a request-direction escalation gate here would be
118
+ * unreachable dead code. The place this actually adds value is the RESPONSE
119
+ * direction, where DLP redacts rather than blocks (the call already ran) —
120
+ * there, blocking after redaction protects nothing further (the same
121
+ * reasoning `dlp.ts`'s own module doc gives for why redaction, not
122
+ * blocking, is correct on results), so this function is consulted for EVENT
123
+ * SEVERITY escalation only. Declaring its disposition as `'kill'` here is
124
+ * about honestly recording what the Rust source declares for the
125
+ * promotion-invariant bookkeeping, not about this proxy ever enforcing it as
126
+ * a block.
127
+ */
128
+ export declare function dlpEscalation(distinctPatternDescriptions: readonly string[]): AnomalyFinding | null;
129
+ //# sourceMappingURL=detectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectors.d.ts","sourceRoot":"","sources":["../../src/anomaly/detectors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AAEpD;;uDAEuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAItE,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,eAAe;IACf,UAAU,EAAE,MAAM,CAAA;IAClB;;yEAEqE;IACrE,WAAW,EAAE,WAAW,CAAA;CACzB;AAkBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,GAAG,IAAI,CAmB/E;AAOD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,GAAG,IAAI,CAgB3E;AAeD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAajG;AAmCD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,GAAG,IAAI,CA6B3E;AAQD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,GAAG,IAAI,CAYnF;AAsBD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAuC5F;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,OAAO,GACrB,cAAc,EAAE,CAUlB;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,GAAG,IAAI,CAmBrF;AAOD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,2BAA2B,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,GAAG,IAAI,CAUnG"}
@@ -0,0 +1,368 @@
1
+ /**
2
+ * anomaly/detectors.ts — The 7 anomaly detectors ported from
3
+ * `packages/proxy/src/plugins/anomaly/detectors.rs`, confirmed applicable to
4
+ * MCP tool-call traffic.
5
+ *
6
+ * Every threshold, window, marker list, and disposition below was read
7
+ * directly from `detectors.rs` (and, for `tool_poisoning`, from
8
+ * `tool_poison.rs`) — none invented. See this package's Phase 2 report for
9
+ * the verbatim citations.
10
+ *
11
+ * # Which 26 were excluded, and why
12
+ *
13
+ * `detectors.rs` registers ~26 detectors; this file ports exactly 7. The
14
+ * rest are excluded for reasons specific to what an MCP stdio proxy can
15
+ * honestly observe — see the Phase 2 report for the itemized list — not
16
+ * because they were skipped for time.
17
+ *
18
+ * @module
19
+ */
20
+ import { scanToolDescription } from '../toolPoison.js';
21
+ /** Severity order for sorting/clamping — mirrors `Disposition`'s derived
22
+ * `Ord` in mod.rs (`Steer < Reask < Ask < Kill`); this package never uses
23
+ * `Ask`, so the four-way order collapses to three. */
24
+ export const DISPOSITION_SEVERITY = {
25
+ steer: 1,
26
+ reask: 2,
27
+ kill: 3,
28
+ };
29
+ /**
30
+ * Confidence for a threshold detector, from how far past its threshold it
31
+ * is. Ported verbatim from `detectors.rs`'s `threshold_confidence`: never
32
+ * reaches 1.0 (an unmeasured structural threshold has not earned certainty),
33
+ * starts at 0.55 at the threshold, +0.1 per unit past it, capped at 0.95.
34
+ */
35
+ function thresholdConfidence(observed, threshold) {
36
+ const excess = Math.max(0, observed - threshold);
37
+ return Math.min(0.55 + 0.1 * excess, 0.95);
38
+ }
39
+ // ── consecutive_repeat ──────────────────────────────────────────────────────
40
+ /** Ported from `detectors.rs`'s `REPETITION_THRESHOLD`. */
41
+ const REPETITION_THRESHOLD = 5;
42
+ /**
43
+ * A node calling the same tool over and over — the most common shape of a
44
+ * graph that has stopped making progress. `Reask` disposition (verified:
45
+ * `AnomalyFinding::reask` in `ConsecutiveRepeatDetector::detect`).
46
+ */
47
+ export function consecutiveRepeat(seq) {
48
+ let run = 1;
49
+ for (let i = 1; i < seq.length; i++) {
50
+ if (seq[i] === seq[i - 1]) {
51
+ run += 1;
52
+ if (run >= REPETITION_THRESHOLD) {
53
+ return {
54
+ detectorId: 'consecutive_repeat',
55
+ kind: 'loop_detected',
56
+ reason: `Loop detected: '${seq[i]}' called ${run} times consecutively without progress`,
57
+ confidence: thresholdConfidence(run, REPETITION_THRESHOLD),
58
+ disposition: 'reask',
59
+ };
60
+ }
61
+ }
62
+ else {
63
+ run = 1;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ // ── ping_pong_cycle ──────────────────────────────────────────────────────
69
+ /** Ported from `detectors.rs`'s `PING_PONG_CYCLES`. */
70
+ const PING_PONG_CYCLES = 3;
71
+ /**
72
+ * Two tools handing work back and forth — invisible to `consecutiveRepeat`
73
+ * because no tool ever repeats twice in a row. `Reask` disposition
74
+ * (verified).
75
+ */
76
+ export function pingPongCycle(seq) {
77
+ const needed = PING_PONG_CYCLES * 2;
78
+ if (seq.length < needed)
79
+ return null;
80
+ const tail = seq.slice(seq.length - needed);
81
+ const a = tail[0];
82
+ const b = tail[1];
83
+ if (a === b)
84
+ return null; // that is a spin, not an alternation
85
+ const alternating = tail.every((t, i) => (i % 2 === 0 ? t === a : t === b));
86
+ if (!alternating)
87
+ return null;
88
+ return {
89
+ detectorId: 'ping_pong_cycle',
90
+ kind: 'loop_detected',
91
+ reason: `Loop detected: '${a}' and '${b}' alternating for ${PING_PONG_CYCLES} cycles with no other activity`,
92
+ confidence: thresholdConfidence(PING_PONG_CYCLES, PING_PONG_CYCLES),
93
+ disposition: 'reask',
94
+ };
95
+ }
96
+ // ── landmark_cycle ──────────────────────────────────────────────────────
97
+ // All six constants below are ported verbatim from `detectors.rs` — see that
98
+ // file's own extensive doc comments (LANDMARK_WINDOW/MAX_CYCLE_PERIOD are
99
+ // deliberately coupled: 6 * MIN_CYCLE_REPEATS(4) == 24 == LANDMARK_WINDOW).
100
+ const LANDMARK_WINDOW = 24;
101
+ const MIN_LANDMARK_ANCHORS = 8;
102
+ const MAX_CYCLE_PERIOD = 6;
103
+ const MIN_CYCLE_REPEATS = 4;
104
+ const CYCLE_COVERAGE_FLOOR = 0.75;
105
+ const CYCLE_MATCH_RATIO = 0.75;
106
+ const ANCHOR_FREQ_SLOTS = 256;
107
+ /**
108
+ * Case-folded interning of the tool-name sequence into small integer ids.
109
+ *
110
+ * The Rust `anchor_projection` also strips synthesised `action:` tokens
111
+ * before interning — this proxy has no equivalent token-synthesis machinery
112
+ * (MCP's `tool_sequence` is recorded tool names only, see `session.ts`), so
113
+ * every entry here is already a concrete anchor; this function's only job is
114
+ * the interning itself.
115
+ */
116
+ export function anchorProjection(seq) {
117
+ const symbols = [];
118
+ const anchors = [];
119
+ for (const tok of seq) {
120
+ const lower = tok.toLowerCase();
121
+ let id = symbols.findIndex((s) => s.toLowerCase() === lower);
122
+ if (id === -1) {
123
+ symbols.push(tok);
124
+ id = symbols.length - 1;
125
+ }
126
+ anchors.push(Math.min(id, 0xffff));
127
+ }
128
+ return { anchors, symbols };
129
+ }
130
+ /** Ported from `detectors.rs`'s `landmark_cycle_coverage`. */
131
+ function landmarkCycleCoverage(seq) {
132
+ const { anchors, symbols } = anchorProjection(seq);
133
+ if (anchors.length < MIN_LANDMARK_ANCHORS)
134
+ return null;
135
+ const window = anchors.slice(Math.max(0, anchors.length - LANDMARK_WINDOW));
136
+ const freq = new Array(ANCHOR_FREQ_SLOTS).fill(0);
137
+ for (const id of window) {
138
+ const slot = id % ANCHOR_FREQ_SLOTS;
139
+ freq[slot] = (freq[slot] ?? 0) + 1;
140
+ }
141
+ const survivors = window.filter((id) => (freq[id % ANCHOR_FREQ_SLOTS] ?? 0) > 1);
142
+ if (survivors.length < MIN_LANDMARK_ANCHORS)
143
+ return null;
144
+ return { coverage: survivors.length / window.length, windowLen: window.length, survivors, symbols };
145
+ }
146
+ /** Ported from `detectors.rs`'s `describe_cycle`. */
147
+ function describeCycle(symbols, survivors, period) {
148
+ if (survivors.length < period)
149
+ return '';
150
+ return survivors
151
+ .slice(survivors.length - period)
152
+ .map((id) => symbols[id] ?? '?')
153
+ .join(' → ');
154
+ }
155
+ /**
156
+ * Repetition that survives one stray call — the gap-tolerant counterpart to
157
+ * `consecutiveRepeat`/`pingPongCycle`. `Steer` disposition (verified: this
158
+ * detector "ships steer and may never earn kill under the promotion rule,"
159
+ * per `detectors.rs`'s own doc comment — `Read → View → Write` repeated
160
+ * eight times is both a perfect period-3 cycle AND a productive agent
161
+ * editing eight files, a category error in the signal no threshold fixes).
162
+ */
163
+ export function landmarkCycle(seq) {
164
+ const sample = landmarkCycleCoverage(seq);
165
+ if (!sample || sample.coverage < CYCLE_COVERAGE_FLOOR)
166
+ return null;
167
+ const { windowLen, survivors, symbols } = sample;
168
+ const n = survivors.length;
169
+ for (let p = 1; p <= MAX_CYCLE_PERIOD; p++) {
170
+ if (n < Math.max(MIN_LANDMARK_ANCHORS, p * MIN_CYCLE_REPEATS))
171
+ continue;
172
+ const comparable = n - p;
173
+ if (comparable === 0)
174
+ continue;
175
+ let matches = 0;
176
+ for (let i = p; i < n; i++) {
177
+ if (survivors[i] === survivors[i - p])
178
+ matches += 1;
179
+ }
180
+ const score = matches / comparable;
181
+ if (score < CYCLE_MATCH_RATIO)
182
+ continue;
183
+ const cycle = describeCycle(symbols, survivors, p);
184
+ return {
185
+ detectorId: 'landmark_cycle',
186
+ kind: 'loop_detected',
187
+ reason: `Loop detected: ${survivors.length} of the last ${windowLen} concrete tool calls are ` +
188
+ `repeating on a period of ${p} (${cycle}), matching ${(score * 100).toFixed(0)}% of the time`,
189
+ confidence: score,
190
+ disposition: 'steer',
191
+ };
192
+ }
193
+ return null;
194
+ }
195
+ // ── tool_diversity_collapse ──────────────────────────────────────────────
196
+ /** Ported from `detectors.rs`'s `DIVERSITY_WINDOW` / `DIVERSITY_MIN_DISTINCT`. */
197
+ const DIVERSITY_WINDOW = 10;
198
+ const DIVERSITY_MIN_DISTINCT = 2;
199
+ /**
200
+ * A long run drawing on a single tool, possibly interleaved with nothing
201
+ * else, that still makes no progress. `Steer` disposition, confidence 0.8
202
+ * (both verified from `ToolDiversityCollapseDetector::detect`).
203
+ */
204
+ export function toolDiversityCollapse(seq) {
205
+ if (seq.length < DIVERSITY_WINDOW)
206
+ return null;
207
+ const tail = seq.slice(seq.length - DIVERSITY_WINDOW);
208
+ const distinct = new Set(tail);
209
+ if (distinct.size >= DIVERSITY_MIN_DISTINCT)
210
+ return null;
211
+ return {
212
+ detectorId: 'tool_diversity_collapse',
213
+ kind: 'token_waste',
214
+ reason: `No progress: the last ${DIVERSITY_WINDOW} tool calls used only '${tail[0]}'`,
215
+ confidence: 0.8,
216
+ disposition: 'steer',
217
+ };
218
+ }
219
+ // ── code_as_action ──────────────────────────────────────────────────────
220
+ // Ported verbatim from `detectors.rs`'s four marker arrays.
221
+ const CODE_EXECUTION_TOOL_MARKERS = [
222
+ 'bash', 'shell', 'exec', 'python', 'repl', 'run_code', 'code_interpreter', 'jupyter', 'terminal', 'eval',
223
+ ];
224
+ const CODE_SECRET_ACCESS_MARKERS = [
225
+ '.aws/credentials', 'id_rsa', '.ssh/', '.env', 'aws_secret_access_key',
226
+ 'getenv("anthropic', 'getenv("openai', 'getenv("aws',
227
+ 'process.env.anthropic', 'process.env.openai', 'process.env.aws',
228
+ 'keychain', 'secretsmanager',
229
+ ];
230
+ const CODE_EGRESS_MARKERS = [
231
+ 'curl ', 'wget ', 'http.post', 'requests.post', 'requests.put', 'fetch(', 'urlopen',
232
+ 'httpx.', 'nc ', 'netcat', 'scp ', 'rsync ',
233
+ ];
234
+ const CODE_DESTRUCTIVE_MARKERS = [
235
+ 'rm -rf /', 'rm -fr /', 'mkfs', 'dd if=', ' of=/dev/', ':(){', '> /dev/sda',
236
+ ];
237
+ /**
238
+ * One code-execution call bundling what per-call gates would have caught
239
+ * apart — the in-blob analogue of `secret_read` followed by `http_post`.
240
+ * Substring markers, deliberately (see `detectors.rs`'s doc comment): an AST
241
+ * per language is neither cheap nor honestly complete, and a marker's
242
+ * failure mode (an obfuscated payload slips through) is strictly better than
243
+ * claiming AST-grade coverage and delivering it for two languages of ten.
244
+ * `Kill` disposition on both shapes (verified: both branches return
245
+ * `AnomalyFinding::kill` in `CodeAsActionDetector::detect`).
246
+ */
247
+ export function codeAsAction(toolName, toolArguments) {
248
+ const name = toolName.toLowerCase();
249
+ if (!CODE_EXECUTION_TOOL_MARKERS.some((m) => name.includes(m)))
250
+ return null;
251
+ let code;
252
+ try {
253
+ code = JSON.stringify(toolArguments ?? {}).toLowerCase();
254
+ }
255
+ catch {
256
+ return null; // circular/unserializable arguments — nothing to scan
257
+ }
258
+ const destructive = CODE_DESTRUCTIVE_MARKERS.find((m) => code.includes(m));
259
+ if (destructive) {
260
+ return {
261
+ detectorId: 'code_as_action',
262
+ kind: 'tool_abuse',
263
+ reason: `Code-execution call '${toolName}' contains a destructive operation ('${destructive.trim()}'). ` +
264
+ `Per-call gates cannot see inside a code blob; this one was read.`,
265
+ confidence: 1.0,
266
+ disposition: 'kill',
267
+ };
268
+ }
269
+ const secretAccess = CODE_SECRET_ACCESS_MARKERS.find((m) => code.includes(m));
270
+ const egress = CODE_EGRESS_MARKERS.find((m) => code.includes(m));
271
+ if (secretAccess && egress) {
272
+ return {
273
+ detectorId: 'code_as_action',
274
+ kind: 'data_exfiltration',
275
+ reason: `Code-execution call '${toolName}' bundles credential access ('${secretAccess}') with network ` +
276
+ `egress ('${egress.trim()}') in one blob — the in-code shape of secret_read -> http_post, which ` +
277
+ `the succession detector cannot see because both halves are inside a single call.`,
278
+ confidence: 1.0,
279
+ disposition: 'kill',
280
+ };
281
+ }
282
+ return null;
283
+ }
284
+ /**
285
+ * Runs the five sequence/call-based detectors above against one prospective
286
+ * request and returns every finding, sorted most-severe-first — mirroring
287
+ * `DetectorRegistry::evaluate_all`'s own sort (disposition severity;
288
+ * `AnomalyKind` severity is not ported, since this proxy does not carry that
289
+ * taxonomy-wide ranking table).
290
+ */
291
+ export function evaluateSequenceDetectors(toolSequence, toolName, toolArguments) {
292
+ const findings = [
293
+ consecutiveRepeat(toolSequence),
294
+ pingPongCycle(toolSequence),
295
+ landmarkCycle(toolSequence),
296
+ toolDiversityCollapse(toolSequence),
297
+ codeAsAction(toolName, toolArguments),
298
+ ].filter((f) => f !== null);
299
+ findings.sort((a, b) => DISPOSITION_SEVERITY[b.disposition] - DISPOSITION_SEVERITY[a.disposition]);
300
+ return findings;
301
+ }
302
+ // ── tool_poisoning ──────────────────────────────────────────────────────
303
+ /**
304
+ * Instructions hidden in a tool's DESCRIPTION rather than in conversation
305
+ * text — a different attack surface from `injection.ts`'s response-direction
306
+ * scan (which reuses the same 5 conversational patterns Phase 1 ported).
307
+ * Reads `toolPoison.ts`'s 7-pattern scanner. `Steer` disposition (verified:
308
+ * `AnomalyFinding::steer` in `ToolPoisoningDetector::detect`) — the Rust
309
+ * doc's own reasoning for why this steers rather than reasks applies
310
+ * identically here: the poisoned text belongs to a third-party server's
311
+ * static tool array, identical on every retry, so a reask would be a
312
+ * guaranteed three-strike block rather than a correction loop.
313
+ */
314
+ export function toolPoisoning(tools) {
315
+ const hits = [];
316
+ for (const tool of tools) {
317
+ if (!tool.description)
318
+ continue;
319
+ for (const technique of scanToolDescription(tool.description)) {
320
+ hits.push(`${tool.name}: ${technique}`);
321
+ }
322
+ }
323
+ if (hits.length === 0)
324
+ return null;
325
+ const deduped = Array.from(new Set(hits)).sort();
326
+ return {
327
+ detectorId: 'tool_poisoning',
328
+ kind: 'prompt_injection',
329
+ reason: `Tool description carries model-directed instructions (${deduped.join('; ')}). The description ` +
330
+ `reaches the model unchanged — this is a report, not a block.`,
331
+ confidence: 0.6,
332
+ disposition: 'steer',
333
+ };
334
+ }
335
+ // ── dlp_escalation ──────────────────────────────────────────────────────
336
+ /** Ported from `detectors.rs`'s `DLP_ESCALATION_THRESHOLD`. */
337
+ const DLP_ESCALATION_THRESHOLD = 3;
338
+ /**
339
+ * ≥3 distinct DLP-redacted pattern types in one response is a credential
340
+ * sweep, not a single mistake. `Kill` in the Rust source (verified:
341
+ * `AnomalyFinding::kill` in `DlpEscalationDetector::detect`) — but this
342
+ * proxy deliberately never converts that into a NEW block decision: unlike
343
+ * the Rust proxy's request-direction detector, this MCP proxy's own DLP scan
344
+ * ALREADY blocks a request carrying any single finding (interceptor.ts step
345
+ * 1, unconditionally), so a request-direction escalation gate here would be
346
+ * unreachable dead code. The place this actually adds value is the RESPONSE
347
+ * direction, where DLP redacts rather than blocks (the call already ran) —
348
+ * there, blocking after redaction protects nothing further (the same
349
+ * reasoning `dlp.ts`'s own module doc gives for why redaction, not
350
+ * blocking, is correct on results), so this function is consulted for EVENT
351
+ * SEVERITY escalation only. Declaring its disposition as `'kill'` here is
352
+ * about honestly recording what the Rust source declares for the
353
+ * promotion-invariant bookkeeping, not about this proxy ever enforcing it as
354
+ * a block.
355
+ */
356
+ export function dlpEscalation(distinctPatternDescriptions) {
357
+ const distinct = Array.from(new Set(distinctPatternDescriptions)).sort();
358
+ if (distinct.length < DLP_ESCALATION_THRESHOLD)
359
+ return null;
360
+ return {
361
+ detectorId: 'dlp_escalation',
362
+ kind: 'data_exfiltration',
363
+ reason: `Credential sweep: ${distinct.length} distinct sensitive patterns in one response (${distinct.join(', ')})`,
364
+ confidence: 1.0,
365
+ disposition: 'kill',
366
+ };
367
+ }
368
+ //# sourceMappingURL=detectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectors.js","sourceRoot":"","sources":["../../src/anomaly/detectors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * anomaly/index.ts — Public entry point for Phase 2 anomaly detection:
3
+ * disposition resolution (the config promotion-invariant) and the reask
4
+ * escalation constant, layered on top of `detectors.ts`'s pure detector
5
+ * functions.
6
+ *
7
+ * @module
8
+ */
9
+ import type { AnomalyFinding, Disposition } from './detectors.js';
10
+ export * from './detectors.js';
11
+ /**
12
+ * Ported from `packages/proxy/src/plugins/anomaly/mod.rs`'s
13
+ * `REASK_MAX_ATTEMPTS` (confirmed: `pub const REASK_MAX_ATTEMPTS: u32 = 3;`).
14
+ * How many times one session may trip the same reask finding before it
15
+ * hardens into an unconditional block.
16
+ */
17
+ export declare const REASK_MAX_ATTEMPTS = 3;
18
+ /**
19
+ * The Rust-source-declared disposition CEILING for each of the 7 ported
20
+ * detectors — copied from the `disposition` field each `detectors.ts`
21
+ * function's own finding already carries, restated here as a lookup table
22
+ * so `resolveEffectiveDisposition` can validate a config override against it
23
+ * even when no finding fired yet (an `'off'` override skips evaluation
24
+ * entirely — see `resolveEffectiveDisposition`).
25
+ */
26
+ export declare const DETECTOR_BASE_DISPOSITION: Readonly<Record<string, Disposition>>;
27
+ export type AnomalyMode = 'enforce' | 'warn' | 'off';
28
+ /**
29
+ * Resolves the EFFECTIVE disposition for one finding, honoring:
30
+ *
31
+ * 1. `mode: 'off'` — this detector class is disabled outright (caller should
32
+ * skip evaluation before this is even called, but `'off'` is handled here
33
+ * too as a defensive floor).
34
+ * 2. `mode: 'warn'` — every finding is capped at `'steer'` (report, never
35
+ * block), regardless of what the detector itself declared.
36
+ * 3. A per-detector override (`mcpAnomalyOverrides`) — capped at
37
+ * `min(override, the detector's own Rust-declared disposition)`.
38
+ *
39
+ * **The invariant this function exists to enforce, mirrored from
40
+ * `plugins/anomaly/mod.rs`'s promotion rule**: config may only ever DEMOTE a
41
+ * finding toward `steer`/`off`, never promote it past what
42
+ * `DETECTOR_BASE_DISPOSITION` (or, equivalently, `finding.disposition`
43
+ * itself — the two must always agree, since `detectors.ts` never emits a
44
+ * disposition other than each function's declared one) already declares. An
45
+ * override of `'kill'` on `landmark_cycle` (whose Rust source only ever
46
+ * disposes `steer`) has no effect — `min(kill, steer) = steer` — because the
47
+ * Rust proxy has never measured `landmark_cycle`'s false-positive rate at
48
+ * `kill` severity, and this proxy inherits that same unmeasured status
49
+ * rather than re-deciding it locally.
50
+ */
51
+ export declare function resolveEffectiveDisposition(finding: AnomalyFinding, mode: AnomalyMode, overrides: Readonly<Record<string, Disposition | 'off'>>): Disposition | 'off';
52
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/anomaly/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjE,cAAc,gBAAgB,CAAA;AAE9B;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAQ3E,CAAA;AAID,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAAC,CAAC,GACvD,WAAW,GAAG,KAAK,CAarB"}