/** * Shared contract suite for harness plugin tests. * * Every harness plugin translates the same core decisions (session gates, * `gateToolCall`, the not-connected short-circuit) onto its own native signals (exit codes, * `{ decision: "block" }`, `{ block: true }`, thrown errors, …). The * *decision semantics* are core-owned and tested once in `packages/core`; * what varies per harness is only the translation. This suite re-runs the * canonical scenario tables through a harness's real entry point via a * small adapter, so a plugin cannot ship with a divergent understanding of * observe / enforce / fail-open / not-connected — without each plugin * re-transcribing the tables. * * A harness test file calls: * * ```ts * runHarnessContractSuite({ * harness: "claude-code", * tool: "Bash", * sessionCanBlock: true, * sessionStart: async ({ client, gates }) => { * const out = await handleHookEvent(sessionInput(), client, gates); * return { blocked: out.decision === "block", reason: out.reason }; * }, * toolBefore: async ({ client, gates }, tool) => { ... }, * }); * ``` * * and then adds only its genuinely harness-specific tests: export shape, * event-name dispatch, block-signal details, input parsing (MCP names, * `subagent_type`), unique activity enrichment, and the install CLI. */ import { OryAgentClient } from "./client.js"; import type { ensureUserAuthenticated } from "./user-login.js"; import type { ensureAgentIdentity, ensureSubAgentIdentity } from "./agent-auth.js"; /** Normalized outcome of driving one lifecycle phase through the plugin. */ export interface ContractOutcome { /** True when the plugin emitted its native block signal. */ blocked: boolean; /** The human-readable reason carried by the block signal, if any. */ reason?: string; } /** * Injectable auth gates handed to the plugin under test. The suite swaps * their behavior per scenario; the adapter forwards them into the plugin's * `deps` parameter (every plugin exposes `userLogin` / `agentGate` / * `subAgentGate` injection points per AGENTS.md Step 5). */ export interface ContractGates { userLogin: typeof ensureUserAuthenticated; agentGate: typeof ensureAgentIdentity; subAgentGate: typeof ensureSubAgentIdentity; } export interface ContractContext { client: OryAgentClient; gates: ContractGates; } export interface HarnessContractAdapter { /** Harness name — used for the describe label and the client. */ harness: string; /** A real, non-interactive tool from this harness's catalog. */ tool: string; /** * @deprecated Ignored. Session start is now uniformly non-blocking on every * harness (authentication never blocks; enforcement is governed by * `permissionMode` at tool-call time). Kept optional so existing adapters * that still pass it type-check. */ sessionCanBlock?: boolean; /** Whether the tool gate can hard-block. Defaults to true. */ toolCanBlock?: boolean; /** Drive the session-start phase through the plugin's real entry point. */ sessionStart(ctx: ContractContext): Promise; /** Drive the pre-tool gate for `tool` through the plugin's real entry point. */ toolBefore(ctx: ContractContext, tool: string): Promise; /** Drive the post-tool phase (must record `tool.complete`). Optional. */ toolAfter?(ctx: ContractContext, tool: string): Promise; /** Feed an event the plugin does not model; return the raw response. Optional. */ unknownEvent?(ctx: ContractContext): Promise; /** Expected raw response for `unknownEvent` (deep-equal). */ unknownEventOutput?: unknown; } /** * Run the canonical harness contract scenarios through the adapter. * Call once per harness test file, then add harness-specific tests. */ export declare function runHarnessContractSuite(adapter: HarnessContractAdapter): void;