@intutic/mcp-governance-proxy 1.10.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.
- package/README.md +28 -0
- package/dist/anomaly/detectors.d.ts +129 -0
- package/dist/anomaly/detectors.d.ts.map +1 -0
- package/dist/anomaly/detectors.js +368 -0
- package/dist/anomaly/detectors.js.map +1 -0
- package/dist/anomaly/index.d.ts +52 -0
- package/dist/anomaly/index.d.ts.map +1 -0
- package/dist/anomaly/index.js +71 -0
- package/dist/anomaly/index.js.map +1 -0
- package/dist/config.d.ts +107 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +129 -6
- package/dist/config.js.map +1 -1
- package/dist/daemon/healthMonitor.d.ts.map +1 -1
- package/dist/daemon/healthMonitor.js +45 -46
- package/dist/daemon/healthMonitor.js.map +1 -1
- package/dist/daemon/index.d.ts +15 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +78 -9
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/instrumentation.d.ts +20 -0
- package/dist/daemon/instrumentation.d.ts.map +1 -0
- package/dist/daemon/instrumentation.js +61 -0
- package/dist/daemon/instrumentation.js.map +1 -0
- package/dist/daemon/policyCache.d.ts +83 -0
- package/dist/daemon/policyCache.d.ts.map +1 -1
- package/dist/daemon/policyCache.js +280 -11
- package/dist/daemon/policyCache.js.map +1 -1
- package/dist/daemon/statusReporter.d.ts +12 -0
- package/dist/daemon/statusReporter.d.ts.map +1 -0
- package/dist/daemon/statusReporter.js +130 -0
- package/dist/daemon/statusReporter.js.map +1 -0
- package/dist/daemon/telemetryBatcher.d.ts +3 -1
- package/dist/daemon/telemetryBatcher.d.ts.map +1 -1
- package/dist/daemon/telemetryBatcher.js +56 -13
- package/dist/daemon/telemetryBatcher.js.map +1 -1
- package/dist/daemonClient.d.ts +1 -1
- package/dist/daemonClient.d.ts.map +1 -1
- package/dist/daemonClient.js +0 -2
- package/dist/daemonClient.js.map +1 -1
- package/dist/dlp.d.ts +19 -0
- package/dist/dlp.d.ts.map +1 -1
- package/dist/dlp.js +95 -18
- package/dist/dlp.js.map +1 -1
- package/dist/emitter.d.ts +40 -2
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +27 -5
- package/dist/emitter.js.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/injection.d.ts +46 -0
- package/dist/injection.d.ts.map +1 -0
- package/dist/injection.js +147 -0
- package/dist/injection.js.map +1 -0
- package/dist/interceptor.d.ts +131 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +301 -5
- package/dist/interceptor.js.map +1 -1
- package/dist/policy.d.ts +110 -2
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +206 -5
- package/dist/policy.js.map +1 -1
- package/dist/proxy.d.ts +205 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +572 -65
- package/dist/proxy.js.map +1 -1
- package/dist/remoteBridge.d.ts +51 -0
- package/dist/remoteBridge.d.ts.map +1 -0
- package/dist/remoteBridge.js +182 -0
- package/dist/remoteBridge.js.map +1 -0
- package/dist/session.d.ts +158 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +206 -0
- package/dist/session.js.map +1 -0
- package/dist/sessionScope.d.ts +51 -0
- package/dist/sessionScope.d.ts.map +1 -0
- package/dist/sessionScope.js +94 -0
- package/dist/sessionScope.js.map +1 -0
- package/dist/sessionStore.d.ts +69 -0
- package/dist/sessionStore.d.ts.map +1 -0
- package/dist/sessionStore.js +143 -0
- package/dist/sessionStore.js.map +1 -0
- package/dist/tofu.d.ts +112 -0
- package/dist/tofu.d.ts.map +1 -0
- package/dist/tofu.js +180 -0
- package/dist/tofu.js.map +1 -0
- package/dist/toolPoison.d.ts +32 -0
- package/dist/toolPoison.d.ts.map +1 -0
- package/dist/toolPoison.js +81 -0
- package/dist/toolPoison.js.map +1 -0
- package/dist/valkeyErrors.d.ts +17 -0
- package/dist/valkeyErrors.d.ts.map +1 -0
- package/dist/valkeyErrors.js +24 -0
- package/dist/valkeyErrors.js.map +1 -0
- package/dist/wasm/context.d.ts +57 -0
- package/dist/wasm/context.d.ts.map +1 -0
- package/dist/wasm/context.js +59 -0
- package/dist/wasm/context.js.map +1 -0
- package/dist/wasm/hostImports.d.ts +43 -0
- package/dist/wasm/hostImports.d.ts.map +1 -0
- package/dist/wasm/hostImports.js +158 -0
- package/dist/wasm/hostImports.js.map +1 -0
- package/dist/wasm/loader.d.ts +95 -0
- package/dist/wasm/loader.d.ts.map +1 -0
- package/dist/wasm/loader.js +198 -0
- package/dist/wasm/loader.js.map +1 -0
- package/dist/wasm/referencedFiles.d.ts +115 -0
- package/dist/wasm/referencedFiles.d.ts.map +1 -0
- package/dist/wasm/referencedFiles.js +366 -0
- package/dist/wasm/referencedFiles.js.map +1 -0
- package/dist/wasm/runner.d.ts +91 -0
- package/dist/wasm/runner.d.ts.map +1 -0
- package/dist/wasm/runner.js +308 -0
- package/dist/wasm/runner.js.map +1 -0
- package/dist/wasm/watch.d.ts +5 -0
- package/dist/wasm/watch.d.ts.map +1 -0
- package/dist/wasm/watch.js +54 -0
- package/dist/wasm/watch.js.map +1 -0
- package/dist/wasm/worker.d.ts +39 -0
- package/dist/wasm/worker.d.ts.map +1 -0
- package/dist/wasm/worker.js +245 -0
- package/dist/wasm/worker.js.map +1 -0
- 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"}
|