feat(governance): add policy-as-code — .llm-security/policy.json for distributable hook configuration
New policy-loader.mjs reads .llm-security/policy.json with deep-merge against defaults that exactly match existing hardcoded values. Integrated into all 7 hooks: - pre-prompt-inject-scan: injection.mode (env var still takes precedence) - post-session-guard: trifecta.mode, window_size, long_horizon_window - pre-edit-secrets: secrets.additional_patterns - pre-bash-destructive: destructive.additional_blocked - pre-write-pathguard: pathguard.additional_protected - pre-install-supply-chain: supply_chain.additional_blocked_packages - post-mcp-verify: mcp.volume_threshold_bytes, mcp.trusted_servers Backward compatible: no policy file = identical behavior to v5.1.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0439e0f650
commit
8ec320f40c
9 changed files with 300 additions and 13 deletions
|
|
@ -20,6 +20,7 @@ import { join } from 'node:path';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { scanForInjection } from '../../scanners/lib/injection-patterns.mjs';
|
||||
import { checkDescriptionDrift } from '../../scanners/lib/mcp-description-cache.mjs';
|
||||
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Secret patterns — same set as pre-edit-secrets.mjs so any secret that
|
||||
|
|
@ -73,7 +74,7 @@ const MIN_INJECTION_SCAN_LENGTH = 100;
|
|||
// a session. Warns when a single tool produces disproportionate output.
|
||||
// State file: ${os.tmpdir()}/llm-security-mcp-volume-${ppid}.json
|
||||
// ---------------------------------------------------------------------------
|
||||
const MCP_TOOL_VOLUME_THRESHOLD = 100_000; // 100 KB from a single MCP tool
|
||||
const MCP_TOOL_VOLUME_THRESHOLD = getPolicyValue('mcp', 'volume_threshold_bytes', 100_000);
|
||||
const VOLUME_STATE_FILE = join(tmpdir(), `llm-security-mcp-volume-${process.ppid}.json`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -199,6 +200,11 @@ if (!outputText.trim()) {
|
|||
const advisories = [];
|
||||
const isBash = toolName === 'Bash';
|
||||
|
||||
// Policy: trusted MCP servers are exempt from volume tracking and drift checks
|
||||
const trustedServers = new Set(getPolicyValue('mcp', 'trusted_servers', []));
|
||||
const mcpServerName = toolName.includes('mcp__') ? toolName.split('__')[1] : null;
|
||||
const isTrustedMcp = mcpServerName && trustedServers.has(mcpServerName);
|
||||
|
||||
// =========================================================================
|
||||
// Bash-specific checks: secrets, external URLs, large MCP output
|
||||
// These checks are only relevant for shell command output.
|
||||
|
|
@ -322,7 +328,7 @@ if (isHtmlSource && outputText.length >= MIN_INJECTION_SCAN_LENGTH) {
|
|||
// Only relevant for MCP tools that provide a description in tool_input.
|
||||
// =========================================================================
|
||||
const isMcpTool = toolName?.startsWith('mcp__');
|
||||
if (isMcpTool) {
|
||||
if (isMcpTool && !isTrustedMcp) {
|
||||
const description = toolInput?.description || toolInput?.tool_description || '';
|
||||
if (description && typeof description === 'string' && description.length > 10) {
|
||||
try {
|
||||
|
|
@ -345,7 +351,7 @@ if (isMcpTool) {
|
|||
// Tracks cumulative output size per MCP tool within a session. Warns when
|
||||
// a single tool produces disproportionate output (>100 KB cumulative).
|
||||
// =========================================================================
|
||||
if (isMcpTool && outputText.length > 0) {
|
||||
if (isMcpTool && !isTrustedMcp && outputText.length > 0) {
|
||||
const volState = loadVolumeState();
|
||||
volState.volumes[toolName] = (volState.volumes[toolName] || 0) + outputText.length;
|
||||
const toolTotal = volState.volumes[toolName];
|
||||
|
|
|
|||
|
|
@ -43,18 +43,19 @@ import { createHash } from 'node:crypto';
|
|||
import { extractMcpServer } from '../../scanners/lib/mcp-description-cache.mjs';
|
||||
import { jensenShannonDivergence, buildDistribution } from '../../scanners/lib/distribution-stats.mjs';
|
||||
import { writeAuditEvent } from '../../scanners/lib/audit-trail.mjs';
|
||||
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const WINDOW_SIZE = 20;
|
||||
const WINDOW_SIZE = getPolicyValue('trifecta', 'window_size', 20);
|
||||
const STATE_PREFIX = 'llm-security-session-';
|
||||
const STATE_DIR = tmpdir();
|
||||
const CLEANUP_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
// Long-horizon monitoring (OpenAI Atlas, Dec 2025)
|
||||
const LONG_HORIZON_WINDOW = 100;
|
||||
const LONG_HORIZON_WINDOW = getPolicyValue('trifecta', 'long_horizon_window', 100);
|
||||
const SLOW_BURN_MIN_SPREAD = 50;
|
||||
const DRIFT_THRESHOLD = 0.25;
|
||||
const DRIFT_SAMPLE_SIZE = 20;
|
||||
|
|
@ -62,8 +63,9 @@ const DRIFT_SAMPLE_SIZE = 20;
|
|||
// Sub-agent delegation tracking (DeepMind Agent Traps kat. 4, v5.0 S4)
|
||||
const DELEGATION_ESCALATION_WINDOW = 5; // calls after input_source
|
||||
|
||||
// Rule of Two enforcement mode: block | warn | off (default: warn)
|
||||
const TRIFECTA_MODE = (process.env.LLM_SECURITY_TRIFECTA_MODE || 'warn').toLowerCase();
|
||||
// Rule of Two enforcement mode: block | warn | off (env var takes precedence over policy)
|
||||
const policyTrifectaMode = getPolicyValue('trifecta', 'mode', 'warn');
|
||||
const TRIFECTA_MODE = (process.env.LLM_SECURITY_TRIFECTA_MODE || policyTrifectaMode).toLowerCase();
|
||||
|
||||
// Volume tracking thresholds (cumulative bytes per session)
|
||||
const VOLUME_THRESHOLDS = [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { normalizeBashExpansion } from '../../scanners/lib/bash-normalize.mjs';
|
||||
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BLOCK rules — exit 2, command is not executed.
|
||||
|
|
@ -78,6 +79,12 @@ const BLOCK_RULES = [
|
|||
'strings, which is a common code injection vector. Blocked. ' +
|
||||
'Refactor to use explicit commands instead.',
|
||||
},
|
||||
// Policy-defined additional blocked patterns
|
||||
...getPolicyValue('destructive', 'additional_blocked', []).map(entry => ({
|
||||
name: entry.name || 'Custom blocked pattern',
|
||||
pattern: new RegExp(entry.pattern),
|
||||
description: entry.description || 'Blocked by policy.',
|
||||
})),
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { normalize } from 'node:path';
|
||||
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Secret detection patterns (union of global, kiur, llm-security, ms-ai-architect)
|
||||
|
|
@ -32,6 +33,11 @@ const SECRET_PATTERNS = [
|
|||
{ name: 'Generic credential assignment', pattern: /(?:password|passwd|secret|token|api[_-]?key)\s*[=:]\s*['"][^'"]{8,}['"]/i },
|
||||
{ name: 'Authorization header with token', pattern: /[Bb]earer [A-Za-z0-9\-._~+/]{20,}/ },
|
||||
{ name: 'Database connection string', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^\s]+@[^\s]+/i },
|
||||
// Policy-defined additional patterns
|
||||
...getPolicyValue('secrets', 'additional_patterns', []).map((p, i) => ({
|
||||
name: `Custom pattern ${i + 1}`,
|
||||
pattern: new RegExp(p),
|
||||
})),
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ import {
|
|||
queryOSV, extractOSVSeverity,
|
||||
} from '../../scanners/lib/supply-chain-data.mjs';
|
||||
import { normalizeBashExpansion } from '../../scanners/lib/bash-normalize.mjs';
|
||||
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
// Policy-defined additional blocked packages (merged with built-in lists)
|
||||
const POLICY_BLOCKED = new Set(getPolicyValue('supply_chain', 'additional_blocked_packages', []));
|
||||
|
||||
// ===========================================================================
|
||||
// Read stdin
|
||||
|
|
@ -119,10 +123,10 @@ async function checkNpm() {
|
|||
for (const spec of packages) {
|
||||
const { name, version } = parseSpec(spec);
|
||||
|
||||
if (isCompromised(NPM_COMPROMISED, name, version)) {
|
||||
if (isCompromised(NPM_COMPROMISED, name, version) || POLICY_BLOCKED.has(name)) {
|
||||
blocks.push(
|
||||
`COMPROMISED: ${name}${version ? '@' + version : ''}\n` +
|
||||
` Known supply chain attack. See: https://socket.dev/npm/package/${name}`
|
||||
` ${POLICY_BLOCKED.has(name) ? 'Blocked by policy.' : 'Known supply chain attack.'} See: https://socket.dev/npm/package/${name}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -325,10 +329,10 @@ async function checkPip() {
|
|||
for (const spec of packages) {
|
||||
const { name, version } = parsePipSpec(spec);
|
||||
|
||||
if (isCompromised(PIP_COMPROMISED, name, version)) {
|
||||
if (isCompromised(PIP_COMPROMISED, name, version) || POLICY_BLOCKED.has(name)) {
|
||||
blocks.push(
|
||||
`COMPROMISED: ${name} (PyPI)\n` +
|
||||
` Known malicious package (likely typosquat).\n` +
|
||||
` ${POLICY_BLOCKED.has(name) ? 'Blocked by policy.' : 'Known malicious package (likely typosquat).'}\n` +
|
||||
` See: https://pypi.org/project/${name}/`
|
||||
);
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -21,14 +21,16 @@
|
|||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { scanForInjection } from '../../scanners/lib/injection-patterns.mjs';
|
||||
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mode configuration
|
||||
// Mode configuration (env var takes precedence over policy file)
|
||||
// ---------------------------------------------------------------------------
|
||||
const VALID_MODES = new Set(['block', 'warn', 'off']);
|
||||
const policyMode = getPolicyValue('injection', 'mode', 'block');
|
||||
const mode = VALID_MODES.has(process.env.LLM_SECURITY_INJECTION_MODE)
|
||||
? process.env.LLM_SECURITY_INJECTION_MODE
|
||||
: 'block';
|
||||
: VALID_MODES.has(policyMode) ? policyMode : 'block';
|
||||
|
||||
// Off mode: skip scanning entirely
|
||||
if (mode === 'off') {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { basename, normalize, resolve } from 'node:path';
|
||||
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sensitive path patterns — 8 categories
|
||||
|
|
@ -68,6 +69,9 @@ const SETTINGS_FILES = [
|
|||
'settings.local.json',
|
||||
];
|
||||
|
||||
/** Category 9: Policy-defined additional protected paths */
|
||||
const POLICY_PATTERNS = getPolicyValue('pathguard', 'additional_protected', []).map(p => new RegExp(p));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path classification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -142,6 +146,13 @@ function classifyPath(filePath) {
|
|||
}
|
||||
}
|
||||
|
||||
// Category 9: Policy-defined additional protected paths
|
||||
for (const pat of POLICY_PATTERNS) {
|
||||
if (pat.test(norm)) {
|
||||
return { blocked: true, category: 'policy', reason: `Policy-protected path: ${norm}` };
|
||||
}
|
||||
}
|
||||
|
||||
return { blocked: false, category: '', reason: '' };
|
||||
}
|
||||
|
||||
|
|
|
|||
146
plugins/llm-security/scanners/lib/policy-loader.mjs
Normal file
146
plugins/llm-security/scanners/lib/policy-loader.mjs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// policy-loader.mjs — Central policy file reader for distributable hook configuration
|
||||
// Reads .llm-security/policy.json from project root. Falls back to defaults
|
||||
// matching existing hardcoded behavior when no policy file exists.
|
||||
// Zero external dependencies.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default policy — matches all existing hardcoded values exactly
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_POLICY = Object.freeze({
|
||||
version: '1.0',
|
||||
injection: {
|
||||
mode: 'block',
|
||||
medium_advisory: true,
|
||||
custom_patterns: [],
|
||||
},
|
||||
trifecta: {
|
||||
mode: 'warn',
|
||||
window_size: 20,
|
||||
long_horizon_window: 100,
|
||||
},
|
||||
secrets: {
|
||||
additional_patterns: [],
|
||||
allowed_paths: [],
|
||||
},
|
||||
destructive: {
|
||||
additional_blocked: [],
|
||||
allowed_commands: [],
|
||||
},
|
||||
pathguard: {
|
||||
additional_protected: [],
|
||||
allowed_paths: [],
|
||||
},
|
||||
supply_chain: {
|
||||
additional_blocked_packages: [],
|
||||
trusted_registries: [],
|
||||
},
|
||||
mcp: {
|
||||
trusted_servers: [],
|
||||
volume_threshold_bytes: 100_000,
|
||||
},
|
||||
audit: {
|
||||
log_path: null,
|
||||
events: ['trifecta', 'injection', 'secrets', 'destructive'],
|
||||
},
|
||||
});
|
||||
|
||||
// Cache loaded policy per project root
|
||||
const cache = new Map();
|
||||
|
||||
/**
|
||||
* Resolve project root from env or cwd.
|
||||
* @param {string} [explicitRoot]
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveRoot(explicitRoot) {
|
||||
return explicitRoot || process.env.CLAUDE_PROJECT_ROOT || process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects (source overrides target).
|
||||
* @param {object} target
|
||||
* @param {object} source
|
||||
* @returns {object}
|
||||
*/
|
||||
function deepMerge(target, source) {
|
||||
const result = { ...target };
|
||||
for (const key of Object.keys(source)) {
|
||||
if (
|
||||
source[key] !== null &&
|
||||
typeof source[key] === 'object' &&
|
||||
!Array.isArray(source[key]) &&
|
||||
typeof target[key] === 'object' &&
|
||||
!Array.isArray(target[key])
|
||||
) {
|
||||
result[key] = deepMerge(target[key], source[key]);
|
||||
} else {
|
||||
result[key] = source[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load policy from .llm-security/policy.json.
|
||||
* Returns defaults if no policy file exists or if parsing fails.
|
||||
* Cached per project root (per process).
|
||||
*
|
||||
* @param {string} [projectRoot] - Explicit root, or derived from env/cwd
|
||||
* @returns {object} Merged policy with defaults
|
||||
*/
|
||||
export function loadPolicy(projectRoot) {
|
||||
const root = resolveRoot(projectRoot);
|
||||
|
||||
if (cache.has(root)) return cache.get(root);
|
||||
|
||||
const policyPath = join(root, '.llm-security', 'policy.json');
|
||||
let policy;
|
||||
|
||||
try {
|
||||
const raw = readFileSync(policyPath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
policy = deepMerge(DEFAULT_POLICY, parsed);
|
||||
} catch {
|
||||
// No policy file or invalid JSON — use defaults
|
||||
policy = { ...DEFAULT_POLICY };
|
||||
}
|
||||
|
||||
cache.set(root, policy);
|
||||
return policy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific policy value with fallback.
|
||||
* Environment variables ALWAYS take precedence over policy file values.
|
||||
*
|
||||
* @param {string} section - Policy section (e.g. 'injection', 'trifecta')
|
||||
* @param {string} key - Key within section (e.g. 'mode', 'window_size')
|
||||
* @param {*} defaultValue - Fallback if neither policy nor default has the value
|
||||
* @param {string} [projectRoot] - Explicit root
|
||||
* @returns {*}
|
||||
*/
|
||||
export function getPolicyValue(section, key, defaultValue, projectRoot) {
|
||||
const policy = loadPolicy(projectRoot);
|
||||
const sectionObj = policy[section];
|
||||
if (sectionObj && key in sectionObj) return sectionObj[key];
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full default policy (for documentation/example generation).
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getDefaultPolicy() {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_POLICY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cache (for testing only).
|
||||
*/
|
||||
export function _resetCacheForTest() {
|
||||
cache.clear();
|
||||
}
|
||||
103
plugins/llm-security/tests/lib/policy-loader.test.mjs
Normal file
103
plugins/llm-security/tests/lib/policy-loader.test.mjs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// policy-loader.test.mjs — Tests for policy-as-code loader
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { loadPolicy, getPolicyValue, getDefaultPolicy, _resetCacheForTest } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
const TEST_ROOT = join(tmpdir(), `llm-security-policy-test-${Date.now()}`);
|
||||
const POLICY_DIR = join(TEST_ROOT, '.llm-security');
|
||||
const POLICY_FILE = join(POLICY_DIR, 'policy.json');
|
||||
|
||||
describe('policy-loader', () => {
|
||||
beforeEach(() => {
|
||||
_resetCacheForTest();
|
||||
mkdirSync(POLICY_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_resetCacheForTest();
|
||||
try { rmSync(TEST_ROOT, { recursive: true }); } catch {}
|
||||
});
|
||||
|
||||
it('returns defaults when no policy file exists', () => {
|
||||
rmSync(POLICY_FILE, { force: true });
|
||||
const policy = loadPolicy(TEST_ROOT);
|
||||
assert.equal(policy.version, '1.0');
|
||||
assert.equal(policy.injection.mode, 'block');
|
||||
assert.equal(policy.trifecta.mode, 'warn');
|
||||
assert.equal(policy.trifecta.window_size, 20);
|
||||
});
|
||||
|
||||
it('reads and merges valid policy file', () => {
|
||||
writeFileSync(POLICY_FILE, JSON.stringify({
|
||||
version: '1.0',
|
||||
trifecta: { mode: 'off' },
|
||||
}));
|
||||
const policy = loadPolicy(TEST_ROOT);
|
||||
assert.equal(policy.trifecta.mode, 'off');
|
||||
// Other defaults preserved
|
||||
assert.equal(policy.trifecta.window_size, 20);
|
||||
assert.equal(policy.injection.mode, 'block');
|
||||
});
|
||||
|
||||
it('handles partial policy (deep merge preserves defaults)', () => {
|
||||
writeFileSync(POLICY_FILE, JSON.stringify({
|
||||
secrets: { additional_patterns: ['CUSTOM_SECRET=\\w+'] },
|
||||
}));
|
||||
const policy = loadPolicy(TEST_ROOT);
|
||||
assert.deepEqual(policy.secrets.additional_patterns, ['CUSTOM_SECRET=\\w+']);
|
||||
assert.deepEqual(policy.secrets.allowed_paths, []); // default preserved
|
||||
});
|
||||
|
||||
it('caches policy per root', () => {
|
||||
writeFileSync(POLICY_FILE, JSON.stringify({ trifecta: { mode: 'block' } }));
|
||||
const p1 = loadPolicy(TEST_ROOT);
|
||||
// Modify file — should still return cached
|
||||
writeFileSync(POLICY_FILE, JSON.stringify({ trifecta: { mode: 'off' } }));
|
||||
const p2 = loadPolicy(TEST_ROOT);
|
||||
assert.equal(p1, p2); // same reference (cached)
|
||||
assert.equal(p2.trifecta.mode, 'block'); // original value
|
||||
});
|
||||
|
||||
it('getPolicyValue returns correct values', () => {
|
||||
writeFileSync(POLICY_FILE, JSON.stringify({
|
||||
mcp: { volume_threshold_bytes: 500_000 },
|
||||
}));
|
||||
const val = getPolicyValue('mcp', 'volume_threshold_bytes', 100_000, TEST_ROOT);
|
||||
assert.equal(val, 500_000);
|
||||
});
|
||||
|
||||
it('getPolicyValue returns default when key not in policy', () => {
|
||||
writeFileSync(POLICY_FILE, JSON.stringify({ version: '1.0' }));
|
||||
const val = getPolicyValue('mcp', 'nonexistent_key', 42, TEST_ROOT);
|
||||
assert.equal(val, 42);
|
||||
});
|
||||
|
||||
it('handles invalid JSON gracefully', () => {
|
||||
writeFileSync(POLICY_FILE, 'not valid json!!!');
|
||||
const policy = loadPolicy(TEST_ROOT);
|
||||
// Should return defaults without crashing
|
||||
assert.equal(policy.version, '1.0');
|
||||
assert.equal(policy.injection.mode, 'block');
|
||||
});
|
||||
|
||||
it('getDefaultPolicy returns a copy', () => {
|
||||
const d1 = getDefaultPolicy();
|
||||
const d2 = getDefaultPolicy();
|
||||
assert.deepEqual(d1, d2);
|
||||
assert.notEqual(d1, d2); // different references
|
||||
});
|
||||
|
||||
it('default policy matches existing hardcoded values', () => {
|
||||
const defaults = getDefaultPolicy();
|
||||
// These must match the hardcoded values in hooks
|
||||
assert.equal(defaults.injection.mode, 'block');
|
||||
assert.equal(defaults.trifecta.mode, 'warn');
|
||||
assert.equal(defaults.trifecta.window_size, 20);
|
||||
assert.equal(defaults.trifecta.long_horizon_window, 100);
|
||||
assert.equal(defaults.mcp.volume_threshold_bytes, 100_000);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue