diff --git a/.changeset/biome-lint-cleanup.md b/.changeset/biome-lint-cleanup.md new file mode 100644 index 00000000..45340f27 --- /dev/null +++ b/.changeset/biome-lint-cleanup.md @@ -0,0 +1,10 @@ +--- +"@wasmagent/aep": patch +"@wasmagent/compliance": patch +"@wasmagent/core": patch +"@wasmagent/mcp-posture": patch +"@wasmagent/react": patch +--- + +Internal: apply Biome import-ordering and formatting fixes to source files. +No public API, type, or runtime behavior change. diff --git a/packages/aep/src/evidenceCompressor.ts b/packages/aep/src/evidenceCompressor.ts index fc20c3e7..26076c73 100644 --- a/packages/aep/src/evidenceCompressor.ts +++ b/packages/aep/src/evidenceCompressor.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto"; import { canonicalBytes } from "./canonical.js"; -import type { AEPRecord, ActionEvidence } from "./types.js"; +import type { ActionEvidence, AEPRecord } from "./types.js"; /** * evidenceCompressor.ts — auditable summaries and cryptographic fingerprints @@ -215,12 +215,15 @@ export class EvidenceCompressor { const lastRecord = records[records.length - 1]!; // --- tool stats --- - const toolMap = new Map; - outcome_distribution: Record; - }>(); + const toolMap = new Map< + string, + { + total_calls: number; + state_changing_calls: number; + side_effect_distribution: Record; + outcome_distribution: Record; + } + >(); let totalActions = 0; let startedAtMs = firstRecord.created_at_ms; @@ -273,10 +276,18 @@ export class EvidenceCompressor { // aggregate capability decisions for (const dec of record.capability_decisions) { switch (dec.decision) { - case "allow": decAllow++; break; - case "deny": decDeny++; break; - case "ask_user": decAsk++; break; - case "dry_run": decDry++; break; + case "allow": + decAllow++; + break; + case "deny": + decDeny++; + break; + case "ask_user": + decAsk++; + break; + case "dry_run": + decDry++; + break; } } @@ -320,11 +331,15 @@ export class EvidenceCompressor { }; const budgetTotals: CompressedBudgetTotals = {}; - if (tokensSpent !== undefined) (budgetTotals as Record).tokens_spent = tokensSpent; - if (toolCallsSpent !== undefined) (budgetTotals as Record).tool_calls_spent = toolCallsSpent; + if (tokensSpent !== undefined) + (budgetTotals as Record).tokens_spent = tokensSpent; + if (toolCallsSpent !== undefined) + (budgetTotals as Record).tool_calls_spent = toolCallsSpent; if (riskSpent !== undefined) (budgetTotals as Record).risk_spent = riskSpent; - if (retriesSpent !== undefined) (budgetTotals as Record).retries_spent = retriesSpent; - if (humanApprovalsSpent !== undefined) (budgetTotals as Record).human_approvals_spent = humanApprovalsSpent; + if (retriesSpent !== undefined) + (budgetTotals as Record).retries_spent = retriesSpent; + if (humanApprovalsSpent !== undefined) + (budgetTotals as Record).human_approvals_spent = humanApprovalsSpent; const summary: CompressedChainSummary = { chainFingerprint, diff --git a/packages/aep/src/index.test.ts b/packages/aep/src/index.test.ts index 355f9802..e7d386f5 100644 --- a/packages/aep/src/index.test.ts +++ b/packages/aep/src/index.test.ts @@ -53,7 +53,12 @@ import { createLocalSignerFromSeed } from "./signer.js"; import { LocalTimestamper } from "./timestamperLocal.js"; import type { AEPRecord, SideEffectClass } from "./types.js"; import { AEPRecordSchema } from "./types.js"; -import { isStateChangingTool, STATE_CHANGING_PATTERNS, registerStatefulVerbs, clearStatefulVerbs } from "./utils.js"; +import { + clearStatefulVerbs, + isStateChangingTool, + registerStatefulVerbs, + STATE_CHANGING_PATTERNS, +} from "./utils.js"; import { verifyAEPChain, verifyAEPRecord } from "./verify.js"; // Deterministic seed for tests (32 bytes as hex) @@ -540,7 +545,6 @@ describe("isStateChangingTool (#23)", () => { }); }); - describe("registerStatefulVerbs (#304)", () => { afterEach(() => { clearStatefulVerbs(); @@ -5263,8 +5267,12 @@ describe("EvidenceCompressor (#272)", () => { const r2 = emitter2.build(1_700_000_000_001); const summary = compressor.compress([r1, r2], { nowMs: 0 }); - expect(EvidenceCompressor.verifyChainFingerprint([r1, r2], summary.chainFingerprint)).toBe(true); - expect(EvidenceCompressor.verifyChainFingerprint([r2, r1], summary.chainFingerprint)).toBe(false); + expect(EvidenceCompressor.verifyChainFingerprint([r1, r2], summary.chainFingerprint)).toBe( + true + ); + expect(EvidenceCompressor.verifyChainFingerprint([r2, r1], summary.chainFingerprint)).toBe( + false + ); }); it("verifyChainFingerprint returns false for empty records", () => { @@ -5296,8 +5304,18 @@ describe("EvidenceCompressor (#272)", () => { const compressor = new EvidenceCompressor(); const signer = createLocalSignerFromSeed(TEST_SEED, TEST_KEY_ID); const emitter = new AEPEmitter({ run_id: "run-h1", signer }); - emitter.addCapabilityDecision({ capability: "fs:write", subject: "a", resource: "/", decision: "allow" }); - emitter.addCapabilityDecision({ capability: "fs:read", subject: "a", resource: "/", decision: "deny" }); + emitter.addCapabilityDecision({ + capability: "fs:write", + subject: "a", + resource: "/", + decision: "allow", + }); + emitter.addCapabilityDecision({ + capability: "fs:read", + subject: "a", + resource: "/", + decision: "deny", + }); const r = emitter.build(1_700_000_000_000); const summary = compressor.compress([r], { nowMs: 0 }); expect(summary.decisionStats.total).toBe(2); diff --git a/packages/aep/src/index.ts b/packages/aep/src/index.ts index 59e006e6..c85d4990 100644 --- a/packages/aep/src/index.ts +++ b/packages/aep/src/index.ts @@ -1,6 +1,7 @@ export * from "./canonical.js"; export * from "./dsse.js"; export * from "./emitter.js"; +export * from "./evidenceCompressor.js"; export * from "./evidenceMirror.js"; export * from "./evidenceMonitor.js"; export * from "./evidencePublisher.js"; @@ -16,5 +17,3 @@ export * from "./timestamperLocal.js"; export * from "./types.js"; export * from "./utils.js"; export * from "./verify.js"; - -export * from "./evidenceCompressor.js"; diff --git a/packages/aep/src/utils.ts b/packages/aep/src/utils.ts index d7374bb8..97d734d9 100644 --- a/packages/aep/src/utils.ts +++ b/packages/aep/src/utils.ts @@ -123,9 +123,7 @@ export function isStateChangingTool(tool: ToolDescriptor): boolean { if (STATE_CHANGING_PATTERNS.some((p) => p.test(text))) return true; if (customStatefulVerbs.size > 0) { for (const verb of customStatefulVerbs) { - const pattern = new RegExp( - `(?:^|[\\s_-])${escapeRegex(verb)}(?:$|[\\s_-])` - ); + const pattern = new RegExp(`(?:^|[\\s_-])${escapeRegex(verb)}(?:$|[\\s_-])`); if (pattern.test(text)) return true; } } diff --git a/packages/compliance/src/verifier/ComplianceVerifier.ts b/packages/compliance/src/verifier/ComplianceVerifier.ts index afbc96c6..b9194024 100644 --- a/packages/compliance/src/verifier/ComplianceVerifier.ts +++ b/packages/compliance/src/verifier/ComplianceVerifier.ts @@ -31,11 +31,8 @@ * `TaskSpec.priority_hierarchy` + `ConstraintIR.priority`. */ -import { - DeterministicVerifier, - VerificationPipeline, -} from "@wasmagent/core"; import type { Criterion, WorkspaceReader } from "@wasmagent/core"; +import { DeterministicVerifier, VerificationPipeline } from "@wasmagent/core"; import type { ConstraintIR, TaskSpec } from "../ir/ConstraintIR.js"; import { type ConstraintViolation, @@ -204,7 +201,9 @@ export class ComplianceVerifier { const pipeline = new VerificationPipeline({ ws, verifiers: [new DeterministicVerifier()] }); const verifier = new ComplianceVerifier({ pipeline, - ...(opts.evidenceSpanHooks !== undefined ? { evidenceSpanHooks: opts.evidenceSpanHooks } : {}), + ...(opts.evidenceSpanHooks !== undefined + ? { evidenceSpanHooks: opts.evidenceSpanHooks } + : {}), }); return verifier.verify(spec, ...(opts.stage !== undefined ? [{ stage: opts.stage }] : [])); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c7f70e43..c48f7cc5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -226,12 +226,12 @@ export { hybridRetriever, InMemoryStructuredKv, InMemoryVectorStore, + injectHistoryIntoAssembler, KvBackendVectorStore, LazyObservationHandle, MapKvBackend, MemoryBlockSet, MessageAssembler, - injectHistoryIntoAssembler, makeRetrievalTool, ObservationalMemory, StructuredMemory, diff --git a/packages/core/src/memory/index.ts b/packages/core/src/memory/index.ts index ba061ebb..3cb02479 100644 --- a/packages/core/src/memory/index.ts +++ b/packages/core/src/memory/index.ts @@ -3,6 +3,7 @@ export { Bm25Indexer, tokenize as bm25Tokenize } from "./Bm25Indexer.js"; export { FileStructuredKv } from "./fileKv.js"; export type { HybridRetrieverOpts } from "./HybridRetriever.js"; export { HybridRetriever, hybridRetriever } from "./HybridRetriever.js"; +export { injectHistoryIntoAssembler } from "./injectHistory.js"; export { LazyObservationHandle } from "./LazyObservationHandle.js"; export type { MemoryBlock } from "./MemoryBlocks.js"; export { coreMemoryTools, MemoryBlockSet } from "./MemoryBlocks.js"; @@ -37,5 +38,3 @@ export { InMemoryStructuredKv, StructuredMemory, } from "./StructuredMemory.js"; - -export { injectHistoryIntoAssembler } from "./injectHistory.js"; diff --git a/packages/core/src/memory/injectHistory.test.ts b/packages/core/src/memory/injectHistory.test.ts index 7afe0fbb..1dddf888 100644 --- a/packages/core/src/memory/injectHistory.test.ts +++ b/packages/core/src/memory/injectHistory.test.ts @@ -3,9 +3,9 @@ */ import { describe, expect, test } from "bun:test"; -import { MessageAssembler } from "./MessageAssembler.js"; -import { injectHistoryIntoAssembler } from "./injectHistory.js"; import type { ModelMessage } from "../models/types.js"; +import { injectHistoryIntoAssembler } from "./injectHistory.js"; +import { MessageAssembler } from "./MessageAssembler.js"; function makeAssembler(): MessageAssembler { return new MessageAssembler({ diff --git a/packages/core/src/memory/injectHistory.ts b/packages/core/src/memory/injectHistory.ts index 61e977bf..145b8829 100644 --- a/packages/core/src/memory/injectHistory.ts +++ b/packages/core/src/memory/injectHistory.ts @@ -28,8 +28,8 @@ * ``` */ -import type { MessageAssembler } from "./MessageAssembler.js"; import type { ModelMessage } from "../models/types.js"; +import type { MessageAssembler } from "./MessageAssembler.js"; /** * Inject an array of prior `ModelMessage` turns into `assembler` so that diff --git a/packages/mcp-posture/src/index.test.ts b/packages/mcp-posture/src/index.test.ts index aceb4700..6e1ceb72 100644 --- a/packages/mcp-posture/src/index.test.ts +++ b/packages/mcp-posture/src/index.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from 'bun:test'; -import { getSchema } from '@wasmagent/protocol'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from "bun:test"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getSchema } from "@wasmagent/protocol"; import { classifyPostureDriftEvents, createPostureDriftAlert, @@ -12,116 +12,116 @@ import { inspectMCPPosture, RISK_CATEGORIES, validateMCPPosture, -} from './index.js'; +} from "./index.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const VALID_POSTURE = { - posture_version: '0.1', + posture_version: "0.1", identity: { - snapshot_id: 'posture-test-001', - agent_id: 'test-agent-001', - captured_at: '2026-06-28T00:00:00Z', + snapshot_id: "posture-test-001", + agent_id: "test-agent-001", + captured_at: "2026-06-28T00:00:00Z", }, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'test-tool', - tool_name: 'test_tool', + tool_id: "test-tool", + tool_name: "test_tool", }, ], }, ], - attestation: { generator: 'test' }, + attestation: { generator: "test" }, }; -describe('validateMCPPosture', () => { - it('accepts valid MCP Posture', () => { +describe("validateMCPPosture", () => { + it("accepts valid MCP Posture", () => { const result = validateMCPPosture(VALID_POSTURE); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); - it('rejects non-object root', () => { + it("rejects non-object root", () => { expect(validateMCPPosture(null).valid).toBe(false); - expect(validateMCPPosture('string').valid).toBe(false); + expect(validateMCPPosture("string").valid).toBe(false); expect(validateMCPPosture(42).valid).toBe(false); expect(validateMCPPosture([]).valid).toBe(false); }); - it('rejects missing posture_version', () => { + it("rejects missing posture_version", () => { const { posture_version, ...rest } = VALID_POSTURE; const result = validateMCPPosture(rest); expect(result.valid).toBe(false); - expect(result.errors).toContain('missing required: posture_version'); + expect(result.errors).toContain("missing required: posture_version"); }); - it('rejects missing identity', () => { + it("rejects missing identity", () => { const { identity, ...rest } = VALID_POSTURE; const result = validateMCPPosture(rest); expect(result.valid).toBe(false); - expect(result.errors).toContain('missing required: identity'); + expect(result.errors).toContain("missing required: identity"); }); - it('rejects missing servers', () => { + it("rejects missing servers", () => { const { servers, ...rest } = VALID_POSTURE; const result = validateMCPPosture(rest); expect(result.valid).toBe(false); - expect(result.errors).toContain('missing required: servers'); + expect(result.errors).toContain("missing required: servers"); }); - it('rejects missing attestation', () => { + it("rejects missing attestation", () => { const { attestation, ...rest } = VALID_POSTURE; const result = validateMCPPosture(rest); expect(result.valid).toBe(false); - expect(result.errors).toContain('missing required: attestation'); + expect(result.errors).toContain("missing required: attestation"); }); - it('rejects unknown posture_version', () => { - const result = validateMCPPosture({ ...VALID_POSTURE, posture_version: '99.0' }); + it("rejects unknown posture_version", () => { + const result = validateMCPPosture({ ...VALID_POSTURE, posture_version: "99.0" }); expect(result.valid).toBe(false); expect(result.errors).toContain('posture_version must be "0.1"'); }); - describe('identity object', () => { - it('requires snapshot_id', () => { + describe("identity object", () => { + it("requires snapshot_id", () => { const posture = { ...VALID_POSTURE, - identity: { agent_id: 'test-agent', captured_at: '2026-06-28T00:00:00Z' }, + identity: { agent_id: "test-agent", captured_at: "2026-06-28T00:00:00Z" }, }; const result = validateMCPPosture(posture); expect(result.valid).toBe(false); - expect(result.errors).toContain('identity: missing snapshot_id'); + expect(result.errors).toContain("identity: missing snapshot_id"); }); - it('requires agent_id', () => { + it("requires agent_id", () => { const posture = { ...VALID_POSTURE, - identity: { snapshot_id: 'snap-001', captured_at: '2026-06-28T00:00:00Z' }, + identity: { snapshot_id: "snap-001", captured_at: "2026-06-28T00:00:00Z" }, }; const result = validateMCPPosture(posture); expect(result.valid).toBe(false); - expect(result.errors).toContain('identity: missing agent_id'); + expect(result.errors).toContain("identity: missing agent_id"); }); - it('requires captured_at', () => { + it("requires captured_at", () => { const posture = { ...VALID_POSTURE, - identity: { snapshot_id: 'snap-001', agent_id: 'test-agent' }, + identity: { snapshot_id: "snap-001", agent_id: "test-agent" }, }; const result = validateMCPPosture(posture); expect(result.valid).toBe(false); - expect(result.errors).toContain('identity: missing captured_at'); + expect(result.errors).toContain("identity: missing captured_at"); }); }); }); -describe('schema risk categories', () => { - it('includes all 8 risk categories from the taxonomy (7 original + mcp_header_leakage)', () => { - const schema = getSchema('mcp-posture'); +describe("schema risk categories", () => { + it("includes all 8 risk categories from the taxonomy (7 original + mcp_header_leakage)", () => { + const schema = getSchema("mcp-posture"); const toolRiskCategories = (schema.properties?.servers?.items?.properties?.tools?.items?.properties?.risk_categories @@ -132,8 +132,8 @@ describe('schema risk categories', () => { expect(toolRiskCategories).toHaveLength(8); }); - it('risk_summary.category also has all 8 risk categories in the enum', () => { - const schema = getSchema('mcp-posture'); + it("risk_summary.category also has all 8 risk categories in the enum", () => { + const schema = getSchema("mcp-posture"); const summaryCategoryEnum = (schema.properties?.risk_summary?.items?.properties?.category?.enum as string[]) ?? []; @@ -143,59 +143,59 @@ describe('schema risk categories', () => { expect(summaryCategoryEnum).toHaveLength(8); }); - it('RISK_CATEGORIES export includes mcp_header_leakage', () => { - expect(RISK_CATEGORIES).toContain('mcp_header_leakage'); + it("RISK_CATEGORIES export includes mcp_header_leakage", () => { + expect(RISK_CATEGORIES).toContain("mcp_header_leakage"); }); }); -describe('schema covers MCP 2026-07-28 fields', () => { - it('schema has protocol_version field', () => { - const schema = getSchema('mcp-posture'); - expect(schema.properties).toHaveProperty('protocol_version'); +describe("schema covers MCP 2026-07-28 fields", () => { + it("schema has protocol_version field", () => { + const schema = getSchema("mcp-posture"); + expect(schema.properties).toHaveProperty("protocol_version"); }); - it('schema has session_model on servers items', () => { - const schema = getSchema('mcp-posture'); + it("schema has session_model on servers items", () => { + const schema = getSchema("mcp-posture"); const serverProps = schema.properties?.servers?.items?.properties; - expect(serverProps).toHaveProperty('session_model'); - expect(serverProps.session_model.enum).toContain('stateless-handle'); + expect(serverProps).toHaveProperty("session_model"); + expect(serverProps.session_model.enum).toContain("stateless-handle"); }); - it('schema has handle_expiry_policy on servers items', () => { - const schema = getSchema('mcp-posture'); + it("schema has handle_expiry_policy on servers items", () => { + const schema = getSchema("mcp-posture"); const serverProps = schema.properties?.servers?.items?.properties; - expect(serverProps).toHaveProperty('handle_expiry_policy'); + expect(serverProps).toHaveProperty("handle_expiry_policy"); }); - it('schema has attestation.auth with OAuth fields', () => { - const schema = getSchema('mcp-posture'); + it("schema has attestation.auth with OAuth fields", () => { + const schema = getSchema("mcp-posture"); const authProps = schema.properties?.attestation?.properties?.auth?.properties; - expect(authProps).toHaveProperty('audience_bound_token_validated'); - expect(authProps).toHaveProperty('pkce_used'); - expect(authProps).toHaveProperty('per_client_consent_verified'); + expect(authProps).toHaveProperty("audience_bound_token_validated"); + expect(authProps).toHaveProperty("pkce_used"); + expect(authProps).toHaveProperty("per_client_consent_verified"); }); - it('schema has owasp_agentic_ref on risk_summary items', () => { - const schema = getSchema('mcp-posture'); + it("schema has owasp_agentic_ref on risk_summary items", () => { + const schema = getSchema("mcp-posture"); const riskProps = schema.properties?.risk_summary?.items?.properties; - expect(riskProps).toHaveProperty('owasp_agentic_ref'); + expect(riskProps).toHaveProperty("owasp_agentic_ref"); }); - it('posture with stateless-handle session_model is still valid', () => { + it("posture with stateless-handle session_model is still valid", () => { const posture = { ...VALID_POSTURE, - protocol_version: '2026-07-28', + protocol_version: "2026-07-28", servers: [ { - server_id: 'stateless-server', - server_name: 'Stateless MCP Server', - session_model: 'stateless-handle', - handle_expiry_policy: 'short-lived', - tools: [{ tool_id: 't1', tool_name: 'tool_one' }], + server_id: "stateless-server", + server_name: "Stateless MCP Server", + session_model: "stateless-handle", + handle_expiry_policy: "short-lived", + tools: [{ tool_id: "t1", tool_name: "tool_one" }], }, ], attestation: { - generator: 'test', + generator: "test", auth: { audience_bound_token_validated: true, pkce_used: true, @@ -208,92 +208,92 @@ describe('schema covers MCP 2026-07-28 fields', () => { }); }); -describe('verification_endpoint field', () => { - it('accepts valid HTTPS verification_endpoint', () => { +describe("verification_endpoint field", () => { + it("accepts valid HTTPS verification_endpoint", () => { const posture = { ...VALID_POSTURE, - verification_endpoint: 'https://verification.trust.example.com/posture/check', + verification_endpoint: "https://verification.trust.example.com/posture/check", }; const result = validateMCPPosture(posture); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); - it('accepts posture without verification_endpoint (optional)', () => { + it("accepts posture without verification_endpoint (optional)", () => { const result = validateMCPPosture(VALID_POSTURE); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); - it('rejects HTTP verification_endpoint', () => { + it("rejects HTTP verification_endpoint", () => { const posture = { ...VALID_POSTURE, - verification_endpoint: 'http://verification.trust.example.com/posture/check', + verification_endpoint: "http://verification.trust.example.com/posture/check", }; const result = validateMCPPosture(posture); expect(result.valid).toBe(false); - expect(result.errors).toContain('verification_endpoint must use HTTPS scheme'); + expect(result.errors).toContain("verification_endpoint must use HTTPS scheme"); }); - it('rejects malformed URL verification_endpoint', () => { + it("rejects malformed URL verification_endpoint", () => { const posture = { ...VALID_POSTURE, - verification_endpoint: 'not-a-url', + verification_endpoint: "not-a-url", }; const result = validateMCPPosture(posture); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.includes('verification_endpoint'))).toBe(true); + expect(result.errors.some((e) => e.includes("verification_endpoint"))).toBe(true); }); - it('rejects empty string verification_endpoint', () => { + it("rejects empty string verification_endpoint", () => { const posture = { ...VALID_POSTURE, - verification_endpoint: '', + verification_endpoint: "", }; const result = validateMCPPosture(posture); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.includes('verification_endpoint'))).toBe(true); + expect(result.errors.some((e) => e.includes("verification_endpoint"))).toBe(true); }); }); -describe('schema has verification_endpoint field', () => { - it('schema includes verification_endpoint as an optional string property', () => { - const schema = getSchema('mcp-posture'); - expect(schema.properties).toHaveProperty('verification_endpoint'); +describe("schema has verification_endpoint field", () => { + it("schema includes verification_endpoint as an optional string property", () => { + const schema = getSchema("mcp-posture"); + expect(schema.properties).toHaveProperty("verification_endpoint"); const prop = schema.properties.verification_endpoint; - expect(prop.type).toBe('string'); - expect(prop.format).toBe('uri'); - expect(prop.pattern).toBe('^https://.+'); - expect(schema.required).not.toContain('verification_endpoint'); + expect(prop.type).toBe("string"); + expect(prop.format).toBe("uri"); + expect(prop.pattern).toBe("^https://.+"); + expect(schema.required).not.toContain("verification_endpoint"); }); }); -describe('schema covers all fields from posture-model-v0.1.md', () => { - it('has top-level fields: identity, servers, permission_graph, risk_summary, drift, attestation', () => { - const schema = getSchema('mcp-posture'); +describe("schema covers all fields from posture-model-v0.1.md", () => { + it("has top-level fields: identity, servers, permission_graph, risk_summary, drift, attestation", () => { + const schema = getSchema("mcp-posture"); const props = schema.properties; - expect(props).toHaveProperty('identity'); - expect(props).toHaveProperty('servers'); - expect(props).toHaveProperty('permission_graph'); - expect(props).toHaveProperty('risk_summary'); - expect(props).toHaveProperty('drift'); - expect(props).toHaveProperty('attestation'); + expect(props).toHaveProperty("identity"); + expect(props).toHaveProperty("servers"); + expect(props).toHaveProperty("permission_graph"); + expect(props).toHaveProperty("risk_summary"); + expect(props).toHaveProperty("drift"); + expect(props).toHaveProperty("attestation"); }); - it('requires posture_version, identity, servers, and attestation', () => { - const schema = getSchema('mcp-posture'); + it("requires posture_version, identity, servers, and attestation", () => { + const schema = getSchema("mcp-posture"); const required = schema.required as string[]; - expect(required).toContain('posture_version'); - expect(required).toContain('identity'); - expect(required).toContain('servers'); - expect(required).toContain('attestation'); + expect(required).toContain("posture_version"); + expect(required).toContain("identity"); + expect(required).toContain("servers"); + expect(required).toContain("attestation"); }); }); -describe('example file validation', () => { - const examplePath = join(__dirname, '../../../examples/mcp-risk-demo/posture.json'); +describe("example file validation", () => { + const examplePath = join(__dirname, "../../../examples/mcp-risk-demo/posture.json"); interface ExampleTool { risk_severity?: string; } @@ -307,122 +307,122 @@ describe('example file validation', () => { }; function loadExample() { - const raw = readFileSync(examplePath, 'utf-8'); + const raw = readFileSync(examplePath, "utf-8"); exampleData = JSON.parse(raw); } - it('validates examples/mcp-risk-demo/posture.json', () => { + it("validates examples/mcp-risk-demo/posture.json", () => { loadExample(); const result = validateMCPPosture(exampleData); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); - it('has at least 2 MCP servers', () => { + it("has at least 2 MCP servers", () => { loadExample(); expect(exampleData.servers.length).toBeGreaterThanOrEqual(2); }); - it('has at least one tool with high risk severity', () => { + it("has at least one tool with high risk severity", () => { loadExample(); const highRiskTools = exampleData.servers.flatMap( - (server) => server.tools?.filter((t) => t.risk_severity === 'high') ?? [], + (server) => server.tools?.filter((t) => t.risk_severity === "high") ?? [] ); expect(highRiskTools.length).toBeGreaterThanOrEqual(1); }); - it('includes a non-empty permission_graph', () => { + it("includes a non-empty permission_graph", () => { loadExample(); expect(exampleData.permission_graph).toBeDefined(); expect(exampleData.permission_graph).not.toEqual({}); }); - it('includes at least 2 risk_summary entries', () => { + it("includes at least 2 risk_summary entries", () => { loadExample(); expect(exampleData.risk_summary?.length).toBeGreaterThanOrEqual(2); }); }); -describe('inspectMCPPosture', () => { - it('produces human-readable output', () => { +describe("inspectMCPPosture", () => { + it("produces human-readable output", () => { const output = inspectMCPPosture(VALID_POSTURE); - expect(output).toContain('MCP Posture v0.1'); - expect(output).toContain('posture-test-001'); - expect(output).toContain('test-agent-001'); - expect(output).toContain('Servers:'); + expect(output).toContain("MCP Posture v0.1"); + expect(output).toContain("posture-test-001"); + expect(output).toContain("test-agent-001"); + expect(output).toContain("Servers:"); }); - it('shows protocol_version in output', () => { - const posture = { ...VALID_POSTURE, protocol_version: '2026-07-28' }; + it("shows protocol_version in output", () => { + const posture = { ...VALID_POSTURE, protocol_version: "2026-07-28" }; const output = inspectMCPPosture(posture); - expect(output).toContain('2026-07-28'); + expect(output).toContain("2026-07-28"); }); - it('shows owasp_agentic_ref in critical finding output', () => { + it("shows owasp_agentic_ref in critical finding output", () => { const posture = { ...VALID_POSTURE, risk_summary: [ { - finding_id: 'f-001', - severity: 'critical', - category: 'prompt_injection', - description: 'Tool poisoning detected', - owasp_agentic_ref: 'ASI01', + finding_id: "f-001", + severity: "critical", + category: "prompt_injection", + description: "Tool poisoning detected", + owasp_agentic_ref: "ASI01", }, ], }; const output = inspectMCPPosture(posture); - expect(output).toContain('ASI01'); + expect(output).toContain("ASI01"); }); }); -describe('diffMCPPosture', () => { - it('returns empty diff for identical posture snapshots', () => { +describe("diffMCPPosture", () => { + it("returns empty diff for identical posture snapshots", () => { const diff = diffMCPPosture(VALID_POSTURE, { ...VALID_POSTURE }); expect(diff.isEmpty()).toBe(true); }); - it('detects added servers', () => { + it("detects added servers", () => { const newPosture = { ...VALID_POSTURE, servers: [ ...VALID_POSTURE.servers, { - server_id: 'new-server', - server_name: 'New Server', + server_id: "new-server", + server_name: "New Server", tools: [], }, ], }; const diff = diffMCPPosture(VALID_POSTURE, newPosture); expect(diff.isEmpty()).toBe(false); - expect(diff.servers.added).toContain('new-server'); + expect(diff.servers.added).toContain("new-server"); }); - it('detects removed servers', () => { + it("detects removed servers", () => { const newPosture = { ...VALID_POSTURE, servers: [], }; const diff = diffMCPPosture(VALID_POSTURE, newPosture); expect(diff.isEmpty()).toBe(false); - expect(diff.servers.removed).toContain('test-server'); + expect(diff.servers.removed).toContain("test-server"); }); - it('detects added and removed tools', () => { + it("detects added and removed tools", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'new-tool', - tool_name: 'new_tool', - permissions: ['fs:read'], - risk_categories: ['credential_access'], - risk_severity: 'medium', + tool_id: "new-tool", + tool_name: "new_tool", + permissions: ["fs:read"], + risk_categories: ["credential_access"], + risk_severity: "medium", }, ], }, @@ -430,124 +430,124 @@ describe('diffMCPPosture', () => { }; const diff = diffMCPPosture(VALID_POSTURE, newPosture); expect(diff.tools.added).toHaveLength(1); - expect(diff.tools.added[0].tool.tool_id).toBe('new-tool'); + expect(diff.tools.added[0].tool.tool_id).toBe("new-tool"); expect(diff.tools.removed).toHaveLength(1); - expect(diff.tools.removed[0].tool.tool_id).toBe('test-tool'); + expect(diff.tools.removed[0].tool.tool_id).toBe("test-tool"); }); - it('detects permission changes', () => { + it("detects permission changes", () => { const oldPosture = { ...VALID_POSTURE, permission_graph: { - permission_scopes: ['network:outbound'], + permission_scopes: ["network:outbound"], }, }; const newPosture = { ...VALID_POSTURE, permission_graph: { - permission_scopes: ['network:outbound', 'fs:read'], + permission_scopes: ["network:outbound", "fs:read"], }, }; const diff = diffMCPPosture(oldPosture, newPosture); - expect(diff.permissions.added).toContain('fs:read'); + expect(diff.permissions.added).toContain("fs:read"); }); - it('detects risk findings added, removed, and modified', () => { + it("detects risk findings added, removed, and modified", () => { const oldPosture = { ...VALID_POSTURE, risk_summary: [ - { finding_id: 'f-001', severity: 'low', category: 'ssrf', description: 'Old finding' }, + { finding_id: "f-001", severity: "low", category: "ssrf", description: "Old finding" }, ], }; const newPosture = { ...VALID_POSTURE, risk_summary: [ - { finding_id: 'f-001', severity: 'high', category: 'ssrf', description: 'Old finding' }, + { finding_id: "f-001", severity: "high", category: "ssrf", description: "Old finding" }, { - finding_id: 'f-002', - severity: 'medium', - category: 'exfiltration', - description: 'New finding', + finding_id: "f-002", + severity: "medium", + category: "exfiltration", + description: "New finding", }, ], }; const diff = diffMCPPosture(oldPosture, newPosture); expect(diff.risks.modified).toHaveLength(1); - expect(diff.risks.modified[0].finding_id).toBe('f-001'); - expect(diff.risks.modified[0].field).toBe('severity'); - expect(diff.risks.modified[0].old).toBe('low'); - expect(diff.risks.modified[0].new).toBe('high'); + expect(diff.risks.modified[0].finding_id).toBe("f-001"); + expect(diff.risks.modified[0].field).toBe("severity"); + expect(diff.risks.modified[0].old).toBe("low"); + expect(diff.risks.modified[0].new).toBe("high"); expect(diff.risks.added).toHaveLength(1); - expect(diff.risks.added[0].finding_id).toBe('f-002'); + expect(diff.risks.added[0].finding_id).toBe("f-002"); }); }); -describe('formatPostureDiff', () => { - it('produces human-readable diff output', () => { +describe("formatPostureDiff", () => { + it("produces human-readable diff output", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'test-tool', - tool_name: 'test_tool', - permissions: ['network:outbound'], - risk_categories: ['ssrf'], - risk_severity: 'low', + tool_id: "test-tool", + tool_name: "test_tool", + permissions: ["network:outbound"], + risk_categories: ["ssrf"], + risk_severity: "low", }, ], }, { - server_id: 'added-server', - server_name: 'Added Server', + server_id: "added-server", + server_name: "Added Server", tools: [], }, ], }; const diff = diffMCPPosture(VALID_POSTURE, newPosture); const output = formatPostureDiff(diff); - expect(output).toContain('Servers added'); - expect(output).toContain('added-server'); + expect(output).toContain("Servers added"); + expect(output).toContain("added-server"); }); - it('reports no differences for empty diff', () => { + it("reports no differences for empty diff", () => { const diff = diffMCPPosture(VALID_POSTURE, { ...VALID_POSTURE }); const output = formatPostureDiff(diff); - expect(output).toContain('No differences found'); + expect(output).toContain("No differences found"); }); }); -describe('diff with example drift fixtures', () => { - const oldPath = join(__dirname, '../../../examples/mcp-risk-demo/posture-old.json'); - const newPath = join(__dirname, '../../../examples/mcp-risk-demo/posture-new.json'); +describe("diff with example drift fixtures", () => { + const oldPath = join(__dirname, "../../../examples/mcp-risk-demo/posture-old.json"); + const newPath = join(__dirname, "../../../examples/mcp-risk-demo/posture-new.json"); let oldData: Record; let newData: Record; - it('diff detects server addition (local-filesystem)', () => { - oldData = JSON.parse(readFileSync(oldPath, 'utf-8')); - newData = JSON.parse(readFileSync(newPath, 'utf-8')); + it("diff detects server addition (local-filesystem)", () => { + oldData = JSON.parse(readFileSync(oldPath, "utf-8")); + newData = JSON.parse(readFileSync(newPath, "utf-8")); const diff = diffMCPPosture(oldData, newData); - expect(diff.servers.added).toContain('local-filesystem'); + expect(diff.servers.added).toContain("local-filesystem"); }); - it('diff detects tool additions', () => { + it("diff detects tool additions", () => { const diff = diffMCPPosture(oldData, newData); const addedToolIds = diff.tools.added.map((t) => t.tool.tool_id); - expect(addedToolIds).toContain('create-issue'); - expect(addedToolIds).toContain('read-file'); - expect(addedToolIds).toContain('write-file'); + expect(addedToolIds).toContain("create-issue"); + expect(addedToolIds).toContain("read-file"); + expect(addedToolIds).toContain("write-file"); }); - it('diff detects permission expansion', () => { + it("diff detects permission expansion", () => { const diff = diffMCPPosture(oldData, newData); - expect(diff.permissions.added).toContain('fs:read'); - expect(diff.permissions.added).toContain('fs:write'); + expect(diff.permissions.added).toContain("fs:read"); + expect(diff.permissions.added).toContain("fs:write"); }); - it('diff is not empty', () => { + it("diff is not empty", () => { const diff = diffMCPPosture(oldData, newData); expect(diff.isEmpty()).toBe(false); }); @@ -555,49 +555,49 @@ describe('diff with example drift fixtures', () => { // --- Continuous Trust Monitoring tests --- -describe('createPostureDriftAlert', () => { - it('creates an alert with computed hasHighSeverity and isEmpty', () => { +describe("createPostureDriftAlert", () => { + it("creates an alert with computed hasHighSeverity and isEmpty", () => { const alert = createPostureDriftAlert({ - agent_id: 'agent-1', - baseline_at: '2026-01-01T00:00:00Z', - current_at: '2026-01-02T00:00:00Z', + agent_id: "agent-1", + baseline_at: "2026-01-01T00:00:00Z", + current_at: "2026-01-02T00:00:00Z", events: [ { - category: 'server_added', - severity: 'high', - description: 'Server added', - subject: 'srv-1', - detected_at: '2026-01-02T00:00:00Z', + category: "server_added", + severity: "high", + description: "Server added", + subject: "srv-1", + detected_at: "2026-01-02T00:00:00Z", }, ], }); - expect(alert.agent_id).toBe('agent-1'); + expect(alert.agent_id).toBe("agent-1"); expect(alert.isEmpty()).toBe(false); expect(alert.hasHighSeverity()).toBe(true); }); - it('isEmpty returns true for empty events', () => { + it("isEmpty returns true for empty events", () => { const alert = createPostureDriftAlert({ - agent_id: 'agent-1', - baseline_at: '2026-01-01T00:00:00Z', - current_at: '2026-01-02T00:00:00Z', + agent_id: "agent-1", + baseline_at: "2026-01-01T00:00:00Z", + current_at: "2026-01-02T00:00:00Z", events: [], }); expect(alert.isEmpty()).toBe(true); }); - it('hasHighSeverity returns true when a critical event exists', () => { + it("hasHighSeverity returns true when a critical event exists", () => { const alert = createPostureDriftAlert({ - agent_id: 'agent-1', - baseline_at: '2026-01-01T00:00:00Z', - current_at: '2026-01-02T00:00:00Z', + agent_id: "agent-1", + baseline_at: "2026-01-01T00:00:00Z", + current_at: "2026-01-02T00:00:00Z", events: [ { - category: 'risk_finding_introduced', - severity: 'critical', - description: 'Critical finding', - subject: 'f-001', - detected_at: '2026-01-02T00:00:00Z', + category: "risk_finding_introduced", + severity: "critical", + description: "Critical finding", + subject: "f-001", + detected_at: "2026-01-02T00:00:00Z", }, ], }); @@ -605,27 +605,27 @@ describe('createPostureDriftAlert', () => { }); }); -describe('classifyPostureDriftEvents', () => { - it('produces empty alert for identical posture snapshots', () => { +describe("classifyPostureDriftEvents", () => { + it("produces empty alert for identical posture snapshots", () => { const diff = diffMCPPosture(VALID_POSTURE, { ...VALID_POSTURE }); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); expect(alert.isEmpty()).toBe(true); expect(alert.hasHighSeverity()).toBe(false); }); - it('classifies server_added events as high severity', () => { + it("classifies server_added events as high severity", () => { const newPosture = { ...VALID_POSTURE, servers: [ ...VALID_POSTURE.servers, { - server_id: 'new-server', - server_name: 'New Server', + server_id: "new-server", + server_name: "New Server", tools: [], }, ], @@ -633,46 +633,46 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); expect(alert.isEmpty()).toBe(false); - const serverEvents = alert.events.filter((e) => e.category === 'server_added'); + const serverEvents = alert.events.filter((e) => e.category === "server_added"); expect(serverEvents).toHaveLength(1); - expect(serverEvents[0].severity).toBe('high'); - expect(serverEvents[0].subject).toBe('new-server'); + expect(serverEvents[0].severity).toBe("high"); + expect(serverEvents[0].subject).toBe("new-server"); }); - it('classifies server_removed events as info', () => { + it("classifies server_removed events as info", () => { const newPosture = { ...VALID_POSTURE, servers: [] }; const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const removedEvents = alert.events.filter((e) => e.category === 'server_removed'); + const removedEvents = alert.events.filter((e) => e.category === "server_removed"); expect(removedEvents).toHaveLength(1); - expect(removedEvents[0].severity).toBe('info'); - expect(removedEvents[0].subject).toBe('test-server'); + expect(removedEvents[0].severity).toBe("info"); + expect(removedEvents[0].subject).toBe("test-server"); }); - it('classifies tool_added with critical severity for tools with critical risk categories', () => { + it("classifies tool_added with critical severity for tools with critical risk categories", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'new-tool', - tool_name: 'dangerous_tool', - permissions: ['fs:write'], - risk_categories: ['command_execution', 'credential_access'], - risk_severity: 'critical', + tool_id: "new-tool", + tool_name: "dangerous_tool", + permissions: ["fs:write"], + risk_categories: ["command_execution", "credential_access"], + risk_severity: "critical", }, ], }, @@ -681,24 +681,24 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); const toolAddedEvents = alert.events.filter( - (e) => e.category === 'tool_added' && e.severity === 'critical', + (e) => e.category === "tool_added" && e.severity === "critical" ); expect(toolAddedEvents).toHaveLength(1); - expect(toolAddedEvents[0].subject).toBe('new-tool'); + expect(toolAddedEvents[0].subject).toBe("new-tool"); }); - it('classifies tool_removed events as info', () => { + it("classifies tool_removed events as info", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [], }, ], @@ -706,29 +706,29 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const removedToolEvents = alert.events.filter((e) => e.category === 'tool_removed'); + const removedToolEvents = alert.events.filter((e) => e.category === "tool_removed"); expect(removedToolEvents).toHaveLength(1); - expect(removedToolEvents[0].severity).toBe('info'); + expect(removedToolEvents[0].severity).toBe("info"); }); - it('classifies permission_escalation events as high', () => { + it("classifies permission_escalation events as high", () => { const oldPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', - permissions: ['fs:read'], + tool_id: "existing-tool", + tool_name: "existing", + permissions: ["fs:read"], risk_categories: [], - risk_severity: 'low', + risk_severity: "low", }, ], }, @@ -738,15 +738,15 @@ describe('classifyPostureDriftEvents', () => { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', - permissions: ['fs:read', 'fs:write'], + tool_id: "existing-tool", + tool_name: "existing", + permissions: ["fs:read", "fs:write"], risk_categories: [], - risk_severity: 'low', + risk_severity: "low", }, ], }, @@ -755,30 +755,30 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const escalationEvents = alert.events.filter((e) => e.category === 'permission_escalation'); + const escalationEvents = alert.events.filter((e) => e.category === "permission_escalation"); expect(escalationEvents).toHaveLength(1); - expect(escalationEvents[0].severity).toBe('high'); - expect(escalationEvents[0].subject).toBe('existing-tool'); + expect(escalationEvents[0].severity).toBe("high"); + expect(escalationEvents[0].subject).toBe("existing-tool"); }); - it('classifies permission_reduction events as info', () => { + it("classifies permission_reduction events as info", () => { const oldPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', - permissions: ['fs:read'], + tool_id: "existing-tool", + tool_name: "existing", + permissions: ["fs:read"], risk_categories: [], - risk_severity: 'low', + risk_severity: "low", }, ], }, @@ -788,15 +788,15 @@ describe('classifyPostureDriftEvents', () => { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', + tool_id: "existing-tool", + tool_name: "existing", permissions: [], risk_categories: [], - risk_severity: 'low', + risk_severity: "low", }, ], }, @@ -805,29 +805,29 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const reductionEvents = alert.events.filter((e) => e.category === 'permission_reduction'); + const reductionEvents = alert.events.filter((e) => e.category === "permission_reduction"); expect(reductionEvents).toHaveLength(1); - expect(reductionEvents[0].severity).toBe('info'); + expect(reductionEvents[0].severity).toBe("info"); }); - it('classifies risk_category_added events as medium', () => { + it("classifies risk_category_added events as medium", () => { const oldPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', + tool_id: "existing-tool", + tool_name: "existing", permissions: [], risk_categories: [], - risk_severity: 'low', + risk_severity: "low", }, ], }, @@ -837,15 +837,15 @@ describe('classifyPostureDriftEvents', () => { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', + tool_id: "existing-tool", + tool_name: "existing", permissions: [], - risk_categories: ['ssrf'], - risk_severity: 'low', + risk_categories: ["ssrf"], + risk_severity: "low", }, ], }, @@ -854,24 +854,24 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const catAddedEvents = alert.events.filter((e) => e.category === 'risk_category_added'); + const catAddedEvents = alert.events.filter((e) => e.category === "risk_category_added"); expect(catAddedEvents).toHaveLength(1); - expect(catAddedEvents[0].severity).toBe('medium'); + expect(catAddedEvents[0].severity).toBe("medium"); }); - it('classifies risk_finding_introduced events with matching severity', () => { + it("classifies risk_finding_introduced events with matching severity", () => { const newPosture = { ...VALID_POSTURE, risk_summary: [ { - finding_id: 'f-001', - severity: 'critical', - category: 'exfiltration', - description: 'Data exfiltration risk', + finding_id: "f-001", + severity: "critical", + category: "exfiltration", + description: "Data exfiltration risk", }, ], }; @@ -882,150 +882,150 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const findingEvents = alert.events.filter((e) => e.category === 'risk_finding_introduced'); + const findingEvents = alert.events.filter((e) => e.category === "risk_finding_introduced"); expect(findingEvents).toHaveLength(1); - expect(findingEvents[0].severity).toBe('critical'); - expect(findingEvents[0].subject).toBe('f-001'); + expect(findingEvents[0].severity).toBe("critical"); + expect(findingEvents[0].subject).toBe("f-001"); }); - it('classifies risk_finding_resolved events as info', () => { + it("classifies risk_finding_resolved events as info", () => { const oldPosture = { ...VALID_POSTURE, risk_summary: [ - { finding_id: 'f-001', severity: 'high', category: 'ssrf', description: 'Old finding' }, + { finding_id: "f-001", severity: "high", category: "ssrf", description: "Old finding" }, ], }; const newPosture = { ...VALID_POSTURE, risk_summary: [] }; const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const resolvedEvents = alert.events.filter((e) => e.category === 'risk_finding_resolved'); + const resolvedEvents = alert.events.filter((e) => e.category === "risk_finding_resolved"); expect(resolvedEvents).toHaveLength(1); - expect(resolvedEvents[0].severity).toBe('info'); + expect(resolvedEvents[0].severity).toBe("info"); }); - it('classifies risk_finding_escalated when severity increases', () => { + it("classifies risk_finding_escalated when severity increases", () => { const oldPosture = { ...VALID_POSTURE, risk_summary: [ - { finding_id: 'f-001', severity: 'low', category: 'ssrf', description: 'Low finding' }, + { finding_id: "f-001", severity: "low", category: "ssrf", description: "Low finding" }, ], }; const newPosture = { ...VALID_POSTURE, risk_summary: [ - { finding_id: 'f-001', severity: 'critical', category: 'ssrf', description: 'Low finding' }, + { finding_id: "f-001", severity: "critical", category: "ssrf", description: "Low finding" }, ], }; const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const escalatedEvents = alert.events.filter((e) => e.category === 'risk_finding_escalated'); + const escalatedEvents = alert.events.filter((e) => e.category === "risk_finding_escalated"); expect(escalatedEvents).toHaveLength(1); - expect(escalatedEvents[0].severity).toBe('critical'); - expect(escalatedEvents[0].description).toContain('low'); - expect(escalatedEvents[0].description).toContain('critical'); + expect(escalatedEvents[0].severity).toBe("critical"); + expect(escalatedEvents[0].description).toContain("low"); + expect(escalatedEvents[0].description).toContain("critical"); }); - it('does not classify risk_finding_escalated when severity decreases', () => { + it("does not classify risk_finding_escalated when severity decreases", () => { const oldPosture = { ...VALID_POSTURE, risk_summary: [ - { finding_id: 'f-001', severity: 'critical', category: 'ssrf', description: 'Finding' }, + { finding_id: "f-001", severity: "critical", category: "ssrf", description: "Finding" }, ], }; const newPosture = { ...VALID_POSTURE, risk_summary: [ - { finding_id: 'f-001', severity: 'low', category: 'ssrf', description: 'Finding' }, + { finding_id: "f-001", severity: "low", category: "ssrf", description: "Finding" }, ], }; const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const escalatedEvents = alert.events.filter((e) => e.category === 'risk_finding_escalated'); + const escalatedEvents = alert.events.filter((e) => e.category === "risk_finding_escalated"); expect(escalatedEvents).toHaveLength(0); }); - it('classifies scope_expanded events as high', () => { + it("classifies scope_expanded events as high", () => { const oldPosture = { ...VALID_POSTURE, - permission_graph: { permission_scopes: ['network:outbound'] }, + permission_graph: { permission_scopes: ["network:outbound"] }, }; const newPosture = { ...VALID_POSTURE, - permission_graph: { permission_scopes: ['network:outbound', 'fs:read'] }, + permission_graph: { permission_scopes: ["network:outbound", "fs:read"] }, }; const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const expandedEvents = alert.events.filter((e) => e.category === 'scope_expanded'); + const expandedEvents = alert.events.filter((e) => e.category === "scope_expanded"); expect(expandedEvents).toHaveLength(1); - expect(expandedEvents[0].severity).toBe('high'); - expect(expandedEvents[0].subject).toBe('fs:read'); + expect(expandedEvents[0].severity).toBe("high"); + expect(expandedEvents[0].subject).toBe("fs:read"); }); - it('classifies scope_restricted events as info', () => { + it("classifies scope_restricted events as info", () => { const oldPosture = { ...VALID_POSTURE, - permission_graph: { permission_scopes: ['network:outbound', 'fs:read'] }, + permission_graph: { permission_scopes: ["network:outbound", "fs:read"] }, }; const newPosture = { ...VALID_POSTURE, - permission_graph: { permission_scopes: ['network:outbound'] }, + permission_graph: { permission_scopes: ["network:outbound"] }, }; const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - const restrictedEvents = alert.events.filter((e) => e.category === 'scope_restricted'); + const restrictedEvents = alert.events.filter((e) => e.category === "scope_restricted"); expect(restrictedEvents).toHaveLength(1); - expect(restrictedEvents[0].severity).toBe('info'); + expect(restrictedEvents[0].severity).toBe("info"); }); - it('populates agent_id and timestamps on the alert', () => { + it("populates agent_id and timestamps on the alert", () => { const diff = diffMCPPosture(VALID_POSTURE, { ...VALID_POSTURE }); const alert = classifyPostureDriftEvents( diff, - 'my-agent', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "my-agent", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); - expect(alert.agent_id).toBe('my-agent'); - expect(alert.baseline_at).toBe('2026-01-01T00:00:00Z'); - expect(alert.current_at).toBe('2026-01-02T00:00:00Z'); + expect(alert.agent_id).toBe("my-agent"); + expect(alert.baseline_at).toBe("2026-01-01T00:00:00Z"); + expect(alert.current_at).toBe("2026-01-02T00:00:00Z"); }); - it('each event has an ISO 8601 detected_at timestamp', () => { + it("each event has an ISO 8601 detected_at timestamp", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'added-srv', - server_name: 'Added Server', + server_id: "added-srv", + server_name: "Added Server", tools: [], }, ], @@ -1033,105 +1033,105 @@ describe('classifyPostureDriftEvents', () => { const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); for (const event of alert.events) { expect(event.detected_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); } }); - it('detects multiple event types in a single diff', () => { + it("detects multiple event types in a single diff", () => { const oldPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', - permissions: ['fs:read'], + tool_id: "existing-tool", + tool_name: "existing", + permissions: ["fs:read"], risk_categories: [], - risk_severity: 'low', + risk_severity: "low", }, ], }, ], - permission_graph: { permission_scopes: ['fs:read'] }, + permission_graph: { permission_scopes: ["fs:read"] }, risk_summary: [], }; const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'test-server', - server_name: 'Test Server', + server_id: "test-server", + server_name: "Test Server", tools: [ { - tool_id: 'existing-tool', - tool_name: 'existing', - permissions: ['fs:read', 'fs:write'], - risk_categories: ['ssrf'], - risk_severity: 'low', + tool_id: "existing-tool", + tool_name: "existing", + permissions: ["fs:read", "fs:write"], + risk_categories: ["ssrf"], + risk_severity: "low", }, ], }, { - server_id: 'new-srv', - server_name: 'New Server', + server_id: "new-srv", + server_name: "New Server", tools: [], }, ], - permission_graph: { permission_scopes: ['fs:read', 'fs:write', 'network:outbound'] }, + permission_graph: { permission_scopes: ["fs:read", "fs:write", "network:outbound"] }, risk_summary: [ { - finding_id: 'f-001', - severity: 'high', - category: 'exfiltration', - description: 'New finding', + finding_id: "f-001", + severity: "high", + category: "exfiltration", + description: "New finding", }, ], }; const diff = diffMCPPosture(oldPosture, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); const categories = new Set(alert.events.map((e) => e.category)); - expect(categories.has('server_added')).toBe(true); - expect(categories.has('permission_escalation')).toBe(true); - expect(categories.has('risk_category_added')).toBe(true); - expect(categories.has('risk_finding_introduced')).toBe(true); - expect(categories.has('scope_expanded')).toBe(true); + expect(categories.has("server_added")).toBe(true); + expect(categories.has("permission_escalation")).toBe(true); + expect(categories.has("risk_category_added")).toBe(true); + expect(categories.has("risk_finding_introduced")).toBe(true); + expect(categories.has("scope_expanded")).toBe(true); expect(alert.hasHighSeverity()).toBe(true); }); }); -describe('formatPostureDriftAlert', () => { - it('formats empty alert with no drift message', () => { +describe("formatPostureDriftAlert", () => { + it("formats empty alert with no drift message", () => { const alert = classifyPostureDriftEvents( diffMCPPosture(VALID_POSTURE, { ...VALID_POSTURE }), - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); const output = formatPostureDriftAlert(alert); - expect(output).toContain('agent-1'); - expect(output).toContain('No drift events'); + expect(output).toContain("agent-1"); + expect(output).toContain("No drift events"); }); - it('includes agent_id and timestamps in output', () => { + it("includes agent_id and timestamps in output", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'new-srv', - server_name: 'New Server', + server_id: "new-srv", + server_name: "New Server", tools: [], }, ], @@ -1139,56 +1139,56 @@ describe('formatPostureDriftAlert', () => { const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); const output = formatPostureDriftAlert(alert); - expect(output).toContain('agent-1'); - expect(output).toContain('2026-01-01T00:00:00Z'); - expect(output).toContain('2026-01-02T00:00:00Z'); + expect(output).toContain("agent-1"); + expect(output).toContain("2026-01-01T00:00:00Z"); + expect(output).toContain("2026-01-02T00:00:00Z"); }); - it('groups events by severity', () => { + it("groups events by severity", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'new-srv', - server_name: 'New Server', + server_id: "new-srv", + server_name: "New Server", tools: [ { - tool_id: 'critical-tool', - tool_name: 'critical', - permissions: ['fs:write'], - risk_categories: ['privilege_escalation'], - risk_severity: 'critical', + tool_id: "critical-tool", + tool_name: "critical", + permissions: ["fs:write"], + risk_categories: ["privilege_escalation"], + risk_severity: "critical", }, ], }, ], - permission_graph: { permission_scopes: ['network:outbound'] }, + permission_graph: { permission_scopes: ["network:outbound"] }, }; const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); const output = formatPostureDriftAlert(alert); - expect(output).toContain('[CRITICAL]'); - expect(output).toContain('[HIGH]'); - expect(output).toContain('Events:'); + expect(output).toContain("[CRITICAL]"); + expect(output).toContain("[HIGH]"); + expect(output).toContain("Events:"); }); - it('includes event category and description in output', () => { + it("includes event category and description in output", () => { const newPosture = { ...VALID_POSTURE, servers: [ { - server_id: 'new-srv', - server_name: 'New Server', + server_id: "new-srv", + server_name: "New Server", tools: [], }, ], @@ -1196,12 +1196,12 @@ describe('formatPostureDriftAlert', () => { const diff = diffMCPPosture(VALID_POSTURE, newPosture); const alert = classifyPostureDriftEvents( diff, - 'agent-1', - '2026-01-01T00:00:00Z', - '2026-01-02T00:00:00Z', + "agent-1", + "2026-01-01T00:00:00Z", + "2026-01-02T00:00:00Z" ); const output = formatPostureDriftAlert(alert); - expect(output).toContain('server_added'); - expect(output).toContain('new-srv'); + expect(output).toContain("server_added"); + expect(output).toContain("new-srv"); }); }); diff --git a/packages/mcp-posture/src/index.ts b/packages/mcp-posture/src/index.ts index 7ba4fdd6..233dd9ec 100644 --- a/packages/mcp-posture/src/index.ts +++ b/packages/mcp-posture/src/index.ts @@ -3,24 +3,24 @@ export interface ValidationResult { errors: string[]; } -const POSTURE_REQUIRED = ['posture_version', 'identity', 'servers', 'attestation'] as const; -const IDENTITY_REQUIRED = ['snapshot_id', 'agent_id', 'captured_at'] as const; +const POSTURE_REQUIRED = ["posture_version", "identity", "servers", "attestation"] as const; +const IDENTITY_REQUIRED = ["snapshot_id", "agent_id", "captured_at"] as const; export const RISK_CATEGORIES = [ - 'ssrf', - 'exfiltration', - 'command_execution', - 'privilege_escalation', - 'prompt_injection', - 'credential_access', - 'supply_chain', - 'mcp_header_leakage', + "ssrf", + "exfiltration", + "command_execution", + "privilege_escalation", + "prompt_injection", + "credential_access", + "supply_chain", + "mcp_header_leakage", ] as const; export type RiskCategory = (typeof RISK_CATEGORIES)[number]; -export type SessionModel = 'stateful' | 'stateless-handle' | 'unknown'; -export type HandleExpiryPolicy = 'short-lived' | 'long-lived' | 'unset'; +export type SessionModel = "stateful" | "stateless-handle" | "unknown"; +export type HandleExpiryPolicy = "short-lived" | "long-lived" | "unset"; export interface McpPostureAuth { audience_bound_token_validated?: boolean; @@ -73,7 +73,7 @@ export interface PostureDiff { isEmpty(): boolean; } -export function createPostureDiff(partial: Omit): PostureDiff { +export function createPostureDiff(partial: Omit): PostureDiff { const isEmpty = (): boolean => partial.servers.added.length === 0 && partial.servers.removed.length === 0 && @@ -96,27 +96,27 @@ function toArray(val: unknown): unknown[] { function parseServers(servers: unknown): Map { const map = new Map(); for (const item of toArray(servers)) { - if (typeof item === 'object' && item !== null) { + if (typeof item === "object" && item !== null) { const s = item as Record; - if (typeof s.server_id === 'string') { + if (typeof s.server_id === "string") { const tools = new Map(); for (const t of toArray(s.tools)) { - if (typeof t === 'object' && t !== null) { + if (typeof t === "object" && t !== null) { const tool = t as Record; - if (typeof tool.tool_id === 'string') { + if (typeof tool.tool_id === "string") { tools.set(tool.tool_id, { tool_id: tool.tool_id, - tool_name: String(tool.tool_name ?? ''), + tool_name: String(tool.tool_name ?? ""), permissions: toArray(tool.permissions).map(String), risk_categories: toArray(tool.risk_categories).map(String), - risk_severity: String(tool.risk_severity ?? ''), + risk_severity: String(tool.risk_severity ?? ""), }); } } } map.set(s.server_id, { server_id: s.server_id, - server_name: String(s.server_name ?? ''), + server_name: String(s.server_name ?? ""), tools, }); } @@ -128,14 +128,14 @@ function parseServers(servers: unknown): Map { function parseRisks(riskSummary: unknown): Map { const map = new Map(); for (const item of toArray(riskSummary)) { - if (typeof item === 'object' && item !== null) { + if (typeof item === "object" && item !== null) { const r = item as Record; - if (typeof r.finding_id === 'string') { + if (typeof r.finding_id === "string") { map.set(r.finding_id, { finding_id: r.finding_id, - severity: String(r.severity ?? ''), - category: String(r.category ?? ''), - description: String(r.description ?? ''), + severity: String(r.severity ?? ""), + category: String(r.category ?? ""), + description: String(r.description ?? ""), }); } } @@ -145,7 +145,7 @@ function parseRisks(riskSummary: unknown): Map { function diffStringArrays( oldArr: string[], - newArr: string[], + newArr: string[] ): { added: string[]; removed: string[] } { const oldSet = new Set(oldArr); const newSet = new Set(newArr); @@ -157,7 +157,7 @@ function diffStringArrays( export function diffMCPPosture( oldData: Record, - newData: Record, + newData: Record ): PostureDiff { const oldServers = parseServers(oldData.servers); const newServers = parseServers(newData.servers); @@ -213,8 +213,8 @@ export function diffMCPPosture( toolsModified.push({ server_id: id, tool_id: toolId, - field: 'permissions', - old: '', + field: "permissions", + old: "", new: p, }); } @@ -222,9 +222,9 @@ export function diffMCPPosture( toolsModified.push({ server_id: id, tool_id: toolId, - field: 'permissions', + field: "permissions", old: p, - new: '', + new: "", }); } @@ -233,8 +233,8 @@ export function diffMCPPosture( toolsModified.push({ server_id: id, tool_id: toolId, - field: 'risk_category', - old: '', + field: "risk_category", + old: "", new: c, }); } @@ -242,9 +242,9 @@ export function diffMCPPosture( toolsModified.push({ server_id: id, tool_id: toolId, - field: 'risk_category', + field: "risk_category", old: c, - new: '', + new: "", }); } @@ -252,7 +252,7 @@ export function diffMCPPosture( toolsModified.push({ server_id: id, tool_id: toolId, - field: 'risk_severity', + field: "risk_severity", old: oldTool.risk_severity, new: newTool.risk_severity, }); @@ -262,10 +262,10 @@ export function diffMCPPosture( // Permission scope diff from permission_graph const oldScopes = toArray( - (oldData.permission_graph as Record | undefined)?.permission_scopes, + (oldData.permission_graph as Record | undefined)?.permission_scopes ).map(String); const newScopes = toArray( - (newData.permission_graph as Record | undefined)?.permission_scopes, + (newData.permission_graph as Record | undefined)?.permission_scopes ).map(String); const permChanges = diffStringArrays(oldScopes, newScopes); @@ -289,7 +289,7 @@ export function diffMCPPosture( if (oldRisk.severity !== newRisk.severity) { risksModified.push({ finding_id: id, - field: 'severity', + field: "severity", old: oldRisk.severity, new: newRisk.severity, }); @@ -297,7 +297,7 @@ export function diffMCPPosture( if (oldRisk.category !== newRisk.category) { risksModified.push({ finding_id: id, - field: 'category', + field: "category", old: oldRisk.category, new: newRisk.category, }); @@ -305,7 +305,7 @@ export function diffMCPPosture( if (oldRisk.description !== newRisk.description) { risksModified.push({ finding_id: id, - field: 'description', + field: "description", old: oldRisk.description, new: newRisk.description, }); @@ -350,7 +350,7 @@ export function formatPostureDiff(diff: PostureDiff): string { if (diff.tools.modified.length > 0) { lines.push(`Tools changed (${diff.tools.modified.length}):`); for (const m of diff.tools.modified) { - if (m.field === 'permissions' || m.field === 'risk_category') { + if (m.field === "permissions" || m.field === "risk_category") { if (m.new) { lines.push(` ~ ${m.tool_id} (${m.server_id}): ${m.field} added: ${m.new}`); } else { @@ -394,42 +394,42 @@ export function formatPostureDiff(diff: PostureDiff): string { } if (lines.length === 0) { - lines.push('No differences found between the two posture snapshots.'); + lines.push("No differences found between the two posture snapshots."); } - return lines.join('\n'); + return lines.join("\n"); } export function validateMCPPosture(data: unknown): ValidationResult { - if (typeof data !== 'object' || data === null || Array.isArray(data)) { - return { valid: false, errors: ['root must be an object'] }; + if (typeof data !== "object" || data === null || Array.isArray(data)) { + return { valid: false, errors: ["root must be an object"] }; } const d = data as Record; const errors: string[] = []; errors.push(...POSTURE_REQUIRED.filter((k) => !(k in d)).map((k) => `missing required: ${k}`)); - if ('posture_version' in d && d.posture_version !== '0.1') { + if ("posture_version" in d && d.posture_version !== "0.1") { errors.push(`posture_version must be "0.1"`); } - if (d.identity && typeof d.identity === 'object') { + if (d.identity && typeof d.identity === "object") { const id = d.identity as Record; errors.push( - ...IDENTITY_REQUIRED.filter((k) => !(k in id)).map((k) => `identity: missing ${k}`), + ...IDENTITY_REQUIRED.filter((k) => !(k in id)).map((k) => `identity: missing ${k}`) ); } // Validate verification_endpoint if present - if ('verification_endpoint' in d && typeof d.verification_endpoint === 'string') { + if ("verification_endpoint" in d && typeof d.verification_endpoint === "string") { const url = d.verification_endpoint as string; if (!/^https:\/\//.test(url)) { - errors.push('verification_endpoint must use HTTPS scheme'); + errors.push("verification_endpoint must use HTTPS scheme"); } try { new URL(url); } catch { - errors.push('verification_endpoint must be a valid URL'); + errors.push("verification_endpoint must be a valid URL"); } } @@ -441,7 +441,7 @@ export function inspectMCPPosture(data: Record): string { const servers = (data.servers as Record[]) ?? []; const risks = (data.risk_summary as Record[]) ?? []; const permissionGraph = data.permission_graph as Record | undefined; - const protocolVersion = (data.protocol_version as string | undefined) ?? 'pre-2026-07-28'; + const protocolVersion = (data.protocol_version as string | undefined) ?? "pre-2026-07-28"; const totalTools = servers.reduce((sum, s) => sum + ((s.tools as unknown[]) ?? []).length, 0); @@ -451,44 +451,44 @@ export function inspectMCPPosture(data: Record): string { (sum, s) => sum + ((s.tools as Record[]) ?? []).filter( - (t) => t.risk_severity === 'critical' || t.risk_severity === 'high', + (t) => t.risk_severity === "critical" || t.risk_severity === "high" ).length, - 0, + 0 ); const lines: string[] = [ `MCP Posture v${data.posture_version} (protocol: ${protocolVersion})`, - ` Snapshot: ${identity?.snapshot_id ?? '?'}`, - ` Agent: ${identity?.agent_id ?? '?'}`, + ` Snapshot: ${identity?.snapshot_id ?? "?"}`, + ` Agent: ${identity?.agent_id ?? "?"}`, ` Servers: ${servers.length}`, ` Tools: ${totalTools}`, ` High-risk tools: ${highRiskTools}`, ` Risks: ${risks.length}`, ]; - const criticalOrHigh = risks.filter((r) => r.severity === 'critical' || r.severity === 'high'); + const criticalOrHigh = risks.filter((r) => r.severity === "critical" || r.severity === "high"); if (criticalOrHigh.length > 0) { - lines.push(''); + lines.push(""); lines.push(` ⚠ ${criticalOrHigh.length} critical/high finding(s):`); for (const r of criticalOrHigh) { - const agenticRef = r.owasp_agentic_ref ? ` [${r.owasp_agentic_ref}]` : ''; + const agenticRef = r.owasp_agentic_ref ? ` [${r.owasp_agentic_ref}]` : ""; lines.push( - ` [${(r.severity ?? '').toUpperCase()}] ${r.finding_id}: ${r.description}${agenticRef}`, + ` [${(r.severity ?? "").toUpperCase()}] ${r.finding_id}: ${r.description}${agenticRef}` ); } } if (risks.length > 0 && criticalOrHigh.length < risks.length) { - const other = risks.filter((r) => r.severity !== 'critical' && r.severity !== 'high'); - lines.push(''); - lines.push(' Other findings:'); + const other = risks.filter((r) => r.severity !== "critical" && r.severity !== "high"); + lines.push(""); + lines.push(" Other findings:"); for (const r of other) { - lines.push(` [${(r.severity ?? '').toUpperCase()}] ${r.finding_id}: ${r.description}`); + lines.push(` [${(r.severity ?? "").toUpperCase()}] ${r.finding_id}: ${r.description}`); } } - return lines.join('\n'); + return lines.join("\n"); } // --- Semver utilities --- @@ -527,10 +527,10 @@ export function compareSemver(a: string, b: string): number { } export function isVersionInRange(version: string, range: string): boolean { - if (range.startsWith('>=')) return compareSemver(version, range.slice(2)) >= 0; - if (range.startsWith('<=')) return compareSemver(version, range.slice(2)) <= 0; - if (range.startsWith('>')) return compareSemver(version, range.slice(1)) > 0; - if (range.startsWith('<')) return compareSemver(version, range.slice(1)) < 0; + if (range.startsWith(">=")) return compareSemver(version, range.slice(2)) >= 0; + if (range.startsWith("<=")) return compareSemver(version, range.slice(2)) <= 0; + if (range.startsWith(">")) return compareSemver(version, range.slice(1)) > 0; + if (range.startsWith("<")) return compareSemver(version, range.slice(1)) < 0; return version === range; } @@ -559,7 +559,7 @@ export interface MigrationResult { export interface DeprecationNotice { version: string; message: string; - severity: 'info' | 'warn' | 'deprecated'; + severity: "info" | "warn" | "deprecated"; } export interface VersionedValidationResult extends ValidationResult { @@ -568,8 +568,8 @@ export interface VersionedValidationResult extends ValidationResult { } /** Currently supported MCP Posture schema versions. */ -const SUPPORTED_POSTURE_VERSIONS: readonly string[] = ['0.1']; -const LATEST_POSTURE_VERSION = '0.1'; +const SUPPORTED_POSTURE_VERSIONS: readonly string[] = ["0.1"]; +const LATEST_POSTURE_VERSION = "0.1"; const DEPRECATED_POSTURE_VERSIONS: Map = new Map(); const POSTURE_MIGRATION_REGISTRY: MigrationStep[] = []; @@ -618,9 +618,9 @@ export function deprecateVersion(notice: DeprecationNotice): void { /** Migrate an MCP Posture document to a target schema version. */ export function migrateMCPPosture( data: Record, - targetVersion?: string, + targetVersion?: string ): MigrationResult { - const fromVersion = String(data.posture_version ?? 'unknown'); + const fromVersion = String(data.posture_version ?? "unknown"); const target = targetVersion ?? LATEST_POSTURE_VERSION; if (fromVersion === target) { @@ -655,7 +655,7 @@ export function migrateMCPPosture( for (const step of path) { if (step.breaking) { warnings.push( - `Breaking migration: ${step.fromVersion} → ${step.toVersion}: ${step.description}`, + `Breaking migration: ${step.fromVersion} → ${step.toVersion}: ${step.description}` ); } try { @@ -695,8 +695,8 @@ export function detectVersionWarnings(version: string): DeprecationNotice[] { } else if (!SUPPORTED_POSTURE_VERSIONS.includes(version)) { notices.push({ version, - message: `MCP Posture version ${version} is not in the supported set (${SUPPORTED_POSTURE_VERSIONS.join(', ')})`, - severity: 'warn', + message: `MCP Posture version ${version} is not in the supported set (${SUPPORTED_POSTURE_VERSIONS.join(", ")})`, + severity: "warn", }); } return notices; @@ -706,7 +706,7 @@ export function detectVersionWarnings(version: string): DeprecationNotice[] { export function validateMCPPostureWithVersioning(data: unknown): VersionedValidationResult { const raw = data as Record | null; const version = (raw?.posture_version as string | undefined) ?? null; - const versionWarnings = detectVersionWarnings(version ?? 'unknown'); + const versionWarnings = detectVersionWarnings(version ?? "unknown"); const baseResult = validateMCPPosture(data); @@ -720,23 +720,23 @@ export function validateMCPPostureWithVersioning(data: unknown): VersionedValida // --- Continuous Trust Monitoring --- /** Severity levels for posture trust monitoring events. */ -export type PostureEventSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical'; +export type PostureEventSeverity = "info" | "low" | "medium" | "high" | "critical"; /** Categories of posture trust events produced by drift monitoring. */ export type PostureEventCategory = - | 'server_added' - | 'server_removed' - | 'tool_added' - | 'tool_removed' - | 'permission_escalation' - | 'permission_reduction' - | 'risk_category_added' - | 'risk_category_removed' - | 'risk_finding_introduced' - | 'risk_finding_resolved' - | 'risk_finding_escalated' - | 'scope_expanded' - | 'scope_restricted'; + | "server_added" + | "server_removed" + | "tool_added" + | "tool_removed" + | "permission_escalation" + | "permission_reduction" + | "risk_category_added" + | "risk_category_removed" + | "risk_finding_introduced" + | "risk_finding_resolved" + | "risk_finding_escalated" + | "scope_expanded" + | "scope_restricted"; /** A single posture trust event produced by continuous monitoring. */ export interface PostureTrustEvent { @@ -770,10 +770,10 @@ export interface PostureDriftAlert { /** Create a PostureDriftAlert with computed helper methods. */ export function createPostureDriftAlert( - partial: Omit, + partial: Omit ): PostureDriftAlert { const hasHighSeverity = (): boolean => - partial.events.some((e) => e.severity === 'high' || e.severity === 'critical'); + partial.events.some((e) => e.severity === "high" || e.severity === "critical"); const isEmpty = (): boolean => partial.events.length === 0; return { ...partial, hasHighSeverity, isEmpty }; } @@ -781,28 +781,28 @@ export function createPostureDriftAlert( /** Severity for a posture tool based on its risk severity and risk categories. */ function postureToolEventSeverity(tool: ToolEntry): PostureEventSeverity { const criticalCats = [ - 'command_execution', - 'credential_access', - 'exfiltration', - 'privilege_escalation', + "command_execution", + "credential_access", + "exfiltration", + "privilege_escalation", ]; const hasCriticalCat = tool.risk_categories.some((c) => criticalCats.includes(c)); - if (tool.risk_severity === 'critical' || hasCriticalCat) return 'critical'; - if (tool.risk_severity === 'high') return 'high'; - return 'medium'; + if (tool.risk_severity === "critical" || hasCriticalCat) return "critical"; + if (tool.risk_severity === "high") return "high"; + return "medium"; } /** Severity for a risk finding based on its severity field. */ function postureRiskEventSeverity(severity: string): PostureEventSeverity { switch (severity) { - case 'critical': - return 'critical'; - case 'high': - return 'high'; - case 'medium': - return 'medium'; + case "critical": + return "critical"; + case "high": + return "high"; + case "medium": + return "medium"; default: - return 'low'; + return "low"; } } @@ -817,7 +817,7 @@ export function classifyPostureDriftEvents( diff: PostureDiff, agentId: string, baselineAt: string, - currentAt: string, + currentAt: string ): PostureDriftAlert { const now = new Date().toISOString(); const events: PostureTrustEvent[] = []; @@ -825,8 +825,8 @@ export function classifyPostureDriftEvents( // Server additions and removals for (const serverId of diff.servers.added) { events.push({ - category: 'server_added', - severity: 'high', + category: "server_added", + severity: "high", description: `MCP server "${serverId}" added to agent trust boundary`, subject: serverId, detected_at: now, @@ -834,8 +834,8 @@ export function classifyPostureDriftEvents( } for (const serverId of diff.servers.removed) { events.push({ - category: 'server_removed', - severity: 'info', + category: "server_removed", + severity: "info", description: `MCP server "${serverId}" removed from agent trust boundary`, subject: serverId, detected_at: now, @@ -845,7 +845,7 @@ export function classifyPostureDriftEvents( // Tool additions and removals for (const { server_id, tool } of diff.tools.added) { events.push({ - category: 'tool_added', + category: "tool_added", severity: postureToolEventSeverity(tool), description: `Tool "${tool.tool_name}" (${tool.tool_id}) added on server ${server_id}`, subject: tool.tool_id, @@ -854,8 +854,8 @@ export function classifyPostureDriftEvents( } for (const { server_id, tool } of diff.tools.removed) { events.push({ - category: 'tool_removed', - severity: 'info', + category: "tool_removed", + severity: "info", description: `Tool "${tool.tool_name}" (${tool.tool_id}) removed from server ${server_id}`, subject: tool.tool_id, detected_at: now, @@ -864,37 +864,37 @@ export function classifyPostureDriftEvents( // Tool permission and risk_category changes for (const mod of diff.tools.modified) { - if (mod.field === 'permissions') { + if (mod.field === "permissions") { if (mod.new) { events.push({ - category: 'permission_escalation', - severity: 'high', + category: "permission_escalation", + severity: "high", description: `Permission "${mod.new}" added to tool ${mod.tool_id} on ${mod.server_id}`, subject: mod.tool_id, detected_at: now, }); } else if (mod.old) { events.push({ - category: 'permission_reduction', - severity: 'info', + category: "permission_reduction", + severity: "info", description: `Permission "${mod.old}" removed from tool ${mod.tool_id} on ${mod.server_id}`, subject: mod.tool_id, detected_at: now, }); } - } else if (mod.field === 'risk_category') { + } else if (mod.field === "risk_category") { if (mod.new) { events.push({ - category: 'risk_category_added', - severity: 'medium', + category: "risk_category_added", + severity: "medium", description: `Risk category "${mod.new}" added to tool ${mod.tool_id} on ${mod.server_id}`, subject: mod.tool_id, detected_at: now, }); } else if (mod.old) { events.push({ - category: 'risk_category_removed', - severity: 'info', + category: "risk_category_removed", + severity: "info", description: `Risk category "${mod.old}" removed from tool ${mod.tool_id} on ${mod.server_id}`, subject: mod.tool_id, detected_at: now, @@ -906,7 +906,7 @@ export function classifyPostureDriftEvents( // Risk finding changes for (const risk of diff.risks.added) { events.push({ - category: 'risk_finding_introduced', + category: "risk_finding_introduced", severity: postureRiskEventSeverity(risk.severity), description: `New risk finding "${risk.description}" (${risk.severity}/${risk.category})`, subject: risk.finding_id, @@ -915,19 +915,19 @@ export function classifyPostureDriftEvents( } for (const risk of diff.risks.removed) { events.push({ - category: 'risk_finding_resolved', - severity: 'info', + category: "risk_finding_resolved", + severity: "info", description: `Risk finding "${risk.finding_id}" (${risk.description}) removed`, subject: risk.finding_id, detected_at: now, }); } for (const mod of diff.risks.modified) { - if (mod.field === 'severity') { + if (mod.field === "severity") { const severityOrder: Record = { low: 1, medium: 2, high: 3, critical: 4 }; if ((severityOrder[mod.new] ?? 0) > (severityOrder[mod.old] ?? 0)) { events.push({ - category: 'risk_finding_escalated', + category: "risk_finding_escalated", severity: postureRiskEventSeverity(mod.new), description: `Risk ${mod.finding_id} severity escalated from ${mod.old} to ${mod.new}`, subject: mod.finding_id, @@ -940,8 +940,8 @@ export function classifyPostureDriftEvents( // Permission scope changes for (const scope of diff.permissions.added) { events.push({ - category: 'scope_expanded', - severity: 'high', + category: "scope_expanded", + severity: "high", description: `Permission scope "${scope}" granted`, subject: scope, detected_at: now, @@ -949,8 +949,8 @@ export function classifyPostureDriftEvents( } for (const scope of diff.permissions.removed) { events.push({ - category: 'scope_restricted', - severity: 'info', + category: "scope_restricted", + severity: "info", description: `Permission scope "${scope}" removed`, subject: scope, detected_at: now, @@ -981,7 +981,7 @@ export function formatPostureDriftAlert(alert: PostureDriftAlert): string { } for (const [severity, events] of bySeverity) { - lines.push(''); + lines.push(""); lines.push(` [${severity.toUpperCase()}] (${events.length})`); for (const event of events) { lines.push(` • ${event.category}: ${event.description}`); @@ -989,8 +989,8 @@ export function formatPostureDriftAlert(alert: PostureDriftAlert): string { } if (alert.isEmpty()) { - lines.push(' No drift events.'); + lines.push(" No drift events."); } - return lines.join('\n'); + return lines.join("\n"); } diff --git a/packages/react/src/useAgentRun.test.ts b/packages/react/src/useAgentRun.test.ts index 16ce2e3b..a76afe69 100644 --- a/packages/react/src/useAgentRun.test.ts +++ b/packages/react/src/useAgentRun.test.ts @@ -215,11 +215,12 @@ function processEventWithFields( state: MsgState, ev: Record, evField: string, - chField: string | null, + chField: string | null ): MsgState { const s = { ...state, messages: [...state.messages] }; const evType = ev[evField] as string | undefined; - const chanMatches = (expected: string) => chField === null || (ev[chField] as string | undefined) === expected; + const chanMatches = (expected: string) => + chField === null || (ev[chField] as string | undefined) === expected; if (evType === "tool_call" && chanMatches("tool")) { const d = ev.data as { toolName: string }; @@ -252,7 +253,7 @@ describe("useAgentRun eventField/channelField option (D1)", () => { state, { event: "final_answer", channel: "text", data: { answer: "hello" } }, "event", - "channel", + "channel" ); expect(state.finalAnswer).toBe("hello"); expect(state.status).toBe("complete"); @@ -264,7 +265,7 @@ describe("useAgentRun eventField/channelField option (D1)", () => { state, { type: "tool_call", channel: "tool", data: { toolName: "search", callId: "c1" } }, "type", - "channel", + "channel" ); expect(state.messages[0]?.content).toBe("Calling search…"); @@ -272,7 +273,7 @@ describe("useAgentRun eventField/channelField option (D1)", () => { state, { type: "final_answer", channel: "text", data: { answer: "result" } }, "type", - "channel", + "channel" ); expect(state.finalAnswer).toBe("result"); }); @@ -284,7 +285,7 @@ describe("useAgentRun eventField/channelField option (D1)", () => { state, { event: "final_answer", data: { answer: "no-channel" } }, "event", - null, + null ); expect(state.finalAnswer).toBe("no-channel"); }); @@ -296,7 +297,7 @@ describe("useAgentRun eventField/channelField option (D1)", () => { state, { event: "final_answer", channel: "wrong", data: { answer: "ignored" } }, "event", - "channel", + "channel" ); expect(state.finalAnswer).toBeNull(); expect(state.status).toBe("running"); @@ -308,13 +309,17 @@ describe("useAgentRun eventField/channelField option (D1)", () => { state, { type: "tool_call", channel: "tool", data: { toolName: "write", callId: "c2" } }, "type", - "channel", + "channel" ); state = processEventWithFields( state, - { type: "tool_result", channel: "tool", data: { toolName: "write", callId: "c2", error: { message: "boom" } } }, + { + type: "tool_result", + channel: "tool", + data: { toolName: "write", callId: "c2", error: { message: "boom" } }, + }, "type", - "channel", + "channel" ); expect(state.messages[0]?.content).toBe("write failed"); expect(state.messages[0]?.isError).toBe(true); @@ -327,21 +332,27 @@ describe("useAgentRun eventField/channelField option (D1)", () => { // to the hook's built-in handler names. We test the mapping + payload // normalization logic in isolation. -type BuiltinEventName = 'text_delta' | 'tool_call' | 'tool_result' | 'final_answer' | 'thinking_delta' | 'error'; +type BuiltinEventName = + | "text_delta" + | "tool_call" + | "tool_result" + | "final_answer" + | "thinking_delta" + | "error"; function processEventWithMap( state: MsgState, ev: Record, evField: string, chField: string | null, - evMap: Partial>, + evMap: Partial> ): MsgState { const s = { ...state, messages: [...state.messages] }; const rawEvType = ev[evField] as string | undefined; - const evType = (rawEvType !== undefined && evMap[rawEvType] !== undefined) - ? evMap[rawEvType] - : rawEvType; - const chanMatches = (expected: string) => chField === null || (ev[chField] as string | undefined) === expected; + const evType = + rawEvType !== undefined && evMap[rawEvType] !== undefined ? evMap[rawEvType] : rawEvType; + const chanMatches = (expected: string) => + chField === null || (ev[chField] as string | undefined) === expected; // Payload normalization — read both nested data.* and top-level ev.* paths. const evData = (ev.data as Record | undefined) ?? {}; @@ -362,7 +373,12 @@ function processEventWithMap( const d = { toolName: toolCallName, callId: toolCallId }; s.messages.push({ role: "tool", content: `Calling ${d.toolName}…`, toolName: d.toolName }); } else if (evType === "tool_result" && chanMatches("tool")) { - const d = { toolName: toolResultName, callId: toolResultCallId, output: toolResultOutput, error: toolResultError }; + const d = { + toolName: toolResultName, + callId: toolResultCallId, + output: toolResultOutput, + error: toolResultError, + }; const isError = !!d.error; s.messages = s.messages.map((m) => m.toolName === d.toolName && m.content.startsWith("Calling") @@ -375,7 +391,9 @@ function processEventWithMap( s.messages.push({ role: "assistant", content: answer }); s.status = "complete"; } else if (evType === "error") { - const msg = ((ev.data as { error: string } | undefined)?.error ?? ev.error ?? "error") as string; + const msg = ((ev.data as { error: string } | undefined)?.error ?? + ev.error ?? + "error") as string; s.messages.push({ role: "error", content: msg }); s.status = "error"; } @@ -385,20 +403,12 @@ function processEventWithMap( describe("useAgentRun eventMap option (E1)", () => { it("remaps 'text' to 'text_delta' and reads top-level delta field", () => { let state: MsgState = { messages: [], finalAnswer: null, status: "running" }; - state = processEventWithMap( - state, - { type: "text", delta: "Hello " }, - "type", - null, - { text: "text_delta" }, - ); - state = processEventWithMap( - state, - { type: "text", delta: "world" }, - "type", - null, - { text: "text_delta" }, - ); + state = processEventWithMap(state, { type: "text", delta: "Hello " }, "type", null, { + text: "text_delta", + }); + state = processEventWithMap(state, { type: "text", delta: "world" }, "type", null, { + text: "text_delta", + }); // Each delta appended and pushed as assistant message expect(state.messages.some((m) => m.role === "assistant")).toBe(true); }); @@ -410,7 +420,7 @@ describe("useAgentRun eventMap option (E1)", () => { { type: "tool_start", call_id: "c1", name: "search" }, "type", "channel", - { tool_start: "tool_call" }, + { tool_start: "tool_call" } ); // chanMatches("tool") is false (no channel field), so this should NOT match // because chField="channel" and ev.channel is undefined @@ -424,7 +434,7 @@ describe("useAgentRun eventMap option (E1)", () => { { type: "tool_start", call_id: "c1", name: "search" }, "type", null, - { tool_start: "tool_call" }, + { tool_start: "tool_call" } ); expect(state.messages[0]?.content).toBe("Calling search…"); expect(state.messages[0]?.toolName).toBe("search"); @@ -438,14 +448,14 @@ describe("useAgentRun eventMap option (E1)", () => { { type: "tool_start", call_id: "c2", name: "write" }, "type", null, - { tool_start: "tool_call" }, + { tool_start: "tool_call" } ); state = processEventWithMap( state, { type: "tool_end", call_id: "c2", name: "write", count: 3 }, "type", null, - { tool_start: "tool_call", tool_end: "tool_result" }, + { tool_start: "tool_call", tool_end: "tool_result" } ); expect(state.messages[0]?.content).toBe("write done"); expect(state.messages[0]?.isError).toBeFalsy(); @@ -458,14 +468,14 @@ describe("useAgentRun eventMap option (E1)", () => { { type: "tool_start", call_id: "c3", name: "exec" }, "type", null, - { tool_start: "tool_call", tool_end: "tool_result" }, + { tool_start: "tool_call", tool_end: "tool_result" } ); state = processEventWithMap( state, { type: "tool_end", call_id: "c3", name: "exec", error: { message: "timeout" } }, "type", null, - { tool_start: "tool_call", tool_end: "tool_result" }, + { tool_start: "tool_call", tool_end: "tool_result" } ); expect(state.messages[0]?.content).toBe("exec failed"); expect(state.messages[0]?.isError).toBe(true); @@ -474,13 +484,10 @@ describe("useAgentRun eventMap option (E1)", () => { it("passes unmapped events through to onEvent without built-in accumulation", () => { // 'ui_action' is not in the map — should not fire any built-in handler let state: MsgState = { messages: [], finalAnswer: null, status: "running" }; - state = processEventWithMap( - state, - { type: "ui_action", action: "show_panel" }, - "type", - null, - { text: "text_delta", tool_start: "tool_call" }, - ); + state = processEventWithMap(state, { type: "ui_action", action: "show_panel" }, "type", null, { + text: "text_delta", + tool_start: "tool_call", + }); expect(state.messages).toHaveLength(0); expect(state.status).toBe("running"); }); @@ -493,7 +500,7 @@ describe("useAgentRun eventMap option (E1)", () => { { event: "final_answer", channel: "text", data: { answer: "ok" } }, "event", "channel", - {}, + {} ); expect(state.finalAnswer).toBe("ok"); expect(state.status).toBe("complete"); diff --git a/packages/react/src/useAgentRun.ts b/packages/react/src/useAgentRun.ts index 836ff0fb..545b58d5 100644 --- a/packages/react/src/useAgentRun.ts +++ b/packages/react/src/useAgentRun.ts @@ -80,7 +80,12 @@ export interface UseAgentRunOptions { * } * ``` */ - eventMap?: Partial> + eventMap?: Partial< + Record< + string, + "text_delta" | "tool_call" | "tool_result" | "final_answer" | "thinking_delta" | "error" + > + >; /** * A2 — Auto-retry policy for transient SSE disconnects (network blip, * Workers cold-start kick). When enabled the hook reconnects with the @@ -192,7 +197,8 @@ export function useAgentRun( const maxAttempts = resumeOpts.maxAttempts ?? 0; const delayMs = resumeOpts.delayMs ?? 1000; const evField = resolvedOpts.eventField ?? "event"; - const chField = resolvedOpts.channelField === undefined ? "channel" : resolvedOpts.channelField; + const chField = + resolvedOpts.channelField === undefined ? "channel" : resolvedOpts.channelField; const evMap = resolvedOpts.eventMap ?? {}; (async () => { @@ -294,12 +300,15 @@ export function useAgentRun( resolvedOpts.onEvent?.(ev); // Use configured field names for event discriminator and channel filter. - const rawEvType = (ev as unknown as Record)[evField] as string | undefined; + const rawEvType = (ev as unknown as Record)[evField] as + | string + | undefined; // Apply eventMap: if the incoming event name has a mapping, substitute // the built-in handler name so the dispatch switch below fires natively. - const evType = (rawEvType !== undefined && evMap[rawEvType] !== undefined) - ? evMap[rawEvType] - : rawEvType; + const evType = + rawEvType !== undefined && evMap[rawEvType] !== undefined + ? evMap[rawEvType] + : rawEvType; // Payload normalizer: some backends flatten fields onto the top-level // event object instead of nesting them under `data`. We read both paths // so remapped events from any backend shape are handled correctly. @@ -308,14 +317,25 @@ export function useAgentRun( // For text_delta: try ev.data.delta first, fall back to ev.delta const textDelta = (evData.delta ?? evRaw.delta ?? "") as string; // For tool_call: try ev.data.{toolName,callId} first, fall back to ev.{name,call_id} - const toolCallName = (evData.toolName ?? evRaw.name ?? evRaw.toolName ?? "") as string; + const toolCallName = (evData.toolName ?? + evRaw.name ?? + evRaw.toolName ?? + "") as string; const toolCallId = (evData.callId ?? evRaw.call_id ?? evRaw.callId ?? "") as string; // For tool_result: try ev.data.{toolName,callId,output,error}, fall back to top-level - const toolResultName = (evData.toolName ?? evRaw.name ?? evRaw.toolName ?? "") as string; - const toolResultCallId = (evData.callId ?? evRaw.call_id ?? evRaw.callId ?? "") as string; + const toolResultName = (evData.toolName ?? + evRaw.name ?? + evRaw.toolName ?? + "") as string; + const toolResultCallId = (evData.callId ?? + evRaw.call_id ?? + evRaw.callId ?? + "") as string; const toolResultOutput = evData.output ?? evRaw.output; const toolResultError = evData.error ?? evRaw.error; - const evChan = chField ? (ev as unknown as Record)[chField] as string | undefined : null; + const evChan = chField + ? ((ev as unknown as Record)[chField] as string | undefined) + : null; const chanMatches = (expected: string) => chField === null || evChan === expected; if (evType === "text_delta") { @@ -361,7 +381,12 @@ export function useAgentRun( }, ]); } else if (evType === "tool_result" && chanMatches("tool")) { - const d = { toolName: toolResultName, callId: toolResultCallId, output: toolResultOutput, error: toolResultError }; + const d = { + toolName: toolResultName, + callId: toolResultCallId, + output: toolResultOutput, + error: toolResultError, + }; const isError = !!d.error; // Show tool output when available (e.g. "OK: written 371 chars to src/App.tsx") const outputStr = String(d.output ?? "").trim(); @@ -460,7 +485,15 @@ export function useAgentRun( })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, - [endpoint, resolvedOpts.onEvent, resolvedOpts.headers, resolvedOpts.resume, resolvedOpts.eventField, resolvedOpts.channelField, resolvedOpts.eventMap] + [ + endpoint, + resolvedOpts.onEvent, + resolvedOpts.headers, + resolvedOpts.resume, + resolvedOpts.eventField, + resolvedOpts.channelField, + resolvedOpts.eventMap, + ] ); return {