/** * ATR Quality Standard — Response-action eligibility * * The fifth axis of "clean must never mean never looked at it". The four before * it (fieldCoverage, statusCoverage, skillPathCoverage, wild-measurement) all * ask whether a rule's precision was ever measured. This one asks what a rule is * allowed to DO with the precision it actually demonstrated. * * THE DEFECT THIS MODULE EXISTS TO PREVENT * * `src/action-executor.ts` contains no maturity filter and no lane filter. Grep * it: the words `lane` and `maturity` do not appear. The lane gate in * `src/engine.ts` decides whether a rule may FIRE; once it has fired, * `computeVerdict()` unions `response.actions` across every match and the * executor runs all of them in priority order. So the lane named "alert" — * documented as the analyst/correlation lane — executes `kill_agent` exactly as * readily as the enforce lane does. * * Measured, not reasoned (see docs/RESPONSE-ACTION-ELIGIBILITY.md for the run): * ATR-2026-00062 is `maturity: test`, false-positives on 209 of 5,352 benign * samples (3.91%), and declares `kill_agent`. Driven through * `engine.evaluateWithVerdict()` at `lane: 'alert'` with a recording adapter and * a real benign corpus document, all five of its actions executed — * `kill_agent` first, because it has the highest executor priority. * * WHY THIS IS THE ONE ZERO-COST INTERVENTION * * Every other way to reduce false-positive damage costs recall. Tightening a * regex on this same rule set traded a 64-95% FP reduction for a 64-67% recall * loss. Removing an action a rule never earned costs exactly zero recall: the * rule still fires, still matches, still reports, still alerts. It just stops * destroying a session it was never shown to be right about. * * WHAT IT DOES NOT BUY (measured, so it cannot be overclaimed) * * `response.actions` feeds the ActionExecutor. It does NOT feed the hook * verdict: `src/verdict.ts` computeVerdict derives allow/ask/deny from severity * and confidence alone, and `toClaudeCodePreToolUse` maps that verdict to * `permissionDecision`. Stripping every action off a `severity: critical` rule * leaves `permissionDecision: deny` unchanged. This module governs executed * response actions, not the hook's block decision. * * THE LADDER * * Actions are ranked by blast radius — what survives the action, not how loud it * is. The ranking is deliberately NOT the executor's priority order, which * encodes urgency (kill_agent runs first so it cannot be pre-empted), not harm. * * @module agent-threat-rules/quality/action-eligibility */ /** Blast-radius tiers, ascending. A rule may declare actions at or below its earned tier. */ export declare const ACTION_TIERS: Readonly<{ /** Nothing about the agent's execution changes. Pure observation and routing. */ readonly OBSERVE: 0; /** One operation is denied. The agent survives, retains state, and can retry. */ readonly INTERRUPT: 1; /** Session-wide state is altered for the remainder of the run. The agent survives, degraded. */ readonly DEGRADE: 2; /** The session or the agent is destroyed. Unrecoverable; in-flight work is lost. */ readonly TERMINATE: 3; }>; export type ActionTier = (typeof ACTION_TIERS)[keyof typeof ACTION_TIERS]; /** Human-readable tier names, indexed by tier. */ export declare const TIER_NAMES: readonly string[]; /** Every action name this module assigns a tier to — the dispatchable set. */ export declare const TIERED_ACTIONS: readonly string[]; /** * Blast-radius tier of an action name. * * An unrecognised action is OBSERVE, and that is safe rather than lenient: * `ACTION_METHOD_MAP` in src/action-executor.ts has no entry for it, so the * executor returns `Unknown action` without dispatching. Four such names are on * disk today (`require_human_review`, `quarantine_artifact`, `rate_limit_source`, * `log_alert`); they are inert, not destructive. */ export declare function actionTier(action: string): ActionTier; /** Highest blast-radius tier present in a list of actions. */ export declare function highestTier(actions: readonly string[]): ActionTier; /** * Percentage false-positive rate at or below which the published standard says a * rule is production-grade. docs/QUALITY-STANDARD.md, STABLE promotion criteria: * "Wild FP rate <= 0.5%". docs/proposals/001-atr-quality-standard-rfc.md §3 * repeats it as a hard requirement for `stable`. */ export declare const STABLE_FP_CEILING_PCT = 0.5; /** * Percentage false-positive rate at which the published standard withdraws * production status. docs/QUALITY-STANDARD.md, STABLE demotion criteria: * "Wild FP rate > 2% -> Automatic demotion". A rule the standard would demote * out of the blocking tier must not keep blocking actions while it sits there. */ export declare const DEMOTION_FP_PCT = 2; /** * Stable fingerprint of everything that decides whether a rule fires. * * A false-positive count is a measurement OF a detection, not of a rule id. Edit * the conditions and the number on file describes a rule that no longer exists — * the same way a green CI run expires when the corpus or the measuring tool moves * underneath it. Consumers store this alongside the count and treat a mismatch as * "unmeasured", which by the policy below caps the rule at OBSERVE until someone * re-measures it. * * Keys are sorted recursively so that YAML reordering, which changes nothing the * engine sees, does not invalidate a measurement. `scan_target` is folded in * because it selects the skill-path compound gate and therefore changes which * inputs can reach the rule at all. */ export declare function detectionFingerprint(rule: { detection?: unknown; tags?: { scan_target?: unknown; }; agent_source?: { type?: unknown; }; }): string; /** A false-positive measurement, or the explicit absence of one. */ export interface FpEvidence { /** * False positives observed. `undefined` means no measurement exists — which is * NOT the same as zero and must never be coerced to it (see wild-measurement.ts * for the `?? 0` collapse that put `wild_fp_rate: 0` on 230 never-measured rules). */ readonly fpCount?: number; /** Benign samples the count was observed over. Required to interpret fpCount. */ readonly sampleCount?: number; /** * True when the harness reported that some of the rule's declared conditions * could not be exercised at all (fieldCoverage: a field no sample fills, or a * method the synchronous engine cannot run — trace, behavioral, signature, or * semantic without a pattern fallback). A partial measurement that found * nothing bounds nothing, so it is treated as no measurement. */ readonly partial?: boolean; /** * Maturity as recorded at measurement time. `stable` is the only maturity the * published ladder calls "safe for blocking actions in enterprise deployments" * (docs/QUALITY-STANDARD.md) and the only one the enforce lane admits. */ readonly maturity?: string; } /** Why a given tier ceiling was assigned. Carried into rule YAML and gate output. */ export interface EligibilityDecision { readonly maxTier: ActionTier; readonly fpRatePct: number | null; readonly reason: string; } /** * True when a count and its denominator are both present and usable. * * Says nothing about completeness — `partial` is handled separately, because a * partial measurement that already found false positives is real evidence of * imprecision (the true rate can only be higher), while a partial measurement * that found none is evidence of nothing. */ export declare function hasCount(ev: FpEvidence): boolean; /** * True when the evidence can support raising a rule above the observe tier. * * A partial run with zero hits cannot: the harness said out loud that it never * exercised some of the rule's conditions, so its silence is an unasked question, * not an answer. A partial run WITH hits can, and does — the hits happened. */ export declare function isMeasured(ev: FpEvidence): boolean; /** * The policy: measured false-positive evidence -> highest action tier a rule may * declare. * * The two numeric lines are NOT chosen here. They are the two the project already * published, lifted unchanged: * * <= 0.5% the STABLE promotion ceiling (QUALITY-STANDARD.md, RFC-001 §3) * > 2.0% the STABLE automatic-demotion trigger (QUALITY-STANDARD.md) * * Mapping them onto blast radius: * * no measurement -> OBSERVE absence of evidence is not evidence; a * gate that cannot read a measurement must * refuse, not pass. A PARTIAL run that found * nothing lands here too — the harness said * it never exercised some conditions, so its * silence is an unasked question. A partial * run that DID find hits is graded on them: * the hits happened, and the true rate can * only be higher * > 2% -> OBSERVE the standard's own demotion line: at this * rate it would be pulled out of the * blocking tier, so it does not keep * blocking actions while it sits there * 0.5% - 2% -> INTERRUPT below the demotion line but above the * production ceiling: a recoverable, * per-operation denial is proportionate; * session-wide state changes are not * > 0 and <= 0.5% -> DEGRADE meets the production FP ceiling, yet * demonstrably still wrong sometimes — a * recoverable degradation is earned, an * unrecoverable termination is not * 0 FP and maturity -> TERMINATE the only evidence level that earns an * stable irreversible action; see below * * WHY TERMINATE NEEDS MORE THAN 0 FP ON THIS CORPUS * * Zero observed events in n trials does not mean zero rate. The rule of three * puts the 95% upper bound at 3/n: on 5,352 benign samples, a clean sweep only * certifies a true FP rate below roughly 0.056%. At an agent fleet's event * volume that is still a real number of destroyed sessions. The published ladder * already names the extra evidence needed to close that gap — `stable` requires * `wild_samples >= 1000`, `wild_fp_rate <= 0.5%`, `confidence >= 80` and two * maintainer approvals — and it is the same ladder that says STABLE is the tier * "safe for blocking actions in enterprise deployments" while EXPERIMENTAL is * "safe for evaluation and non-blocking alerting". So TERMINATE requires both: a * clean sweep of this corpus AND the maturity the standard reserves for * production blocking. */ export declare function maxTierFor(ev: FpEvidence): EligibilityDecision; /** Actions in `actions` whose tier exceeds `maxTier`, in declaration order. */ export declare function ineligibleActions(actions: readonly string[], maxTier: ActionTier): readonly string[]; /** * The eligible subset, order preserved. * * `alert` is appended when the filter would otherwise leave a rule with nothing * to do: a detection that reports nothing is worse than one that over-reports, * and OBSERVE-tier reporting is available at every evidence level by definition. */ export declare function eligibleActions(actions: readonly string[], maxTier: ActionTier): readonly string[]; /** * Content digest of the benign corpora a measurement was taken on. * * `detectionFingerprint` stops a rule drifting out from under its number. It * says nothing about the number, and the measurement file is committed — so in * review, editing `fp_count` from 2814 to 0 plus one `maturity` line handed a * rule that false-positives on 52.58% of the corpus a `kill_agent`, with the * whole suite still green. "Do not hand-edit" is a convention, not a control. * * This lives here, exported, so the producer and the gate cannot drift apart: * two copies of a hashing routine is exactly the shape of defect this policy * exists to close. * * WHAT THIS DOES NOT CLOSE, stated plainly because the gap is easy to miss: * editing `fp_count` does not touch the corpus, so the digest still matches and * the gate still passes. Verified by attack — 2814 -> 0 sails through. The * digest closes corpus drift (the measurement describing a corpus that has * since moved); it does nothing about number drift. Only regeneration closes * that, which is why .github/workflows/action-eligibility.yml re-measures on a * schedule and diffs the per-rule counts. Per-PR regeneration was considered * and rejected: 429 of 776 rules carry load-bearing numbers, so verifying them * is a full ~45-minute gate run, and a contributor cannot forge the file * without the scheduled diff surfacing it. */ export declare function corpusDigest(repoRoot: string, corpora: readonly string[], fs: { readonly existsSync: (p: string) => boolean; readonly readdirSync: (p: string) => string[]; readonly statSync: (p: string) => { isDirectory(): boolean; }; readonly readFileSync: (p: string) => Buffer; }, path: { readonly join: (...p: string[]) => string; readonly relative: (a: string, b: string) => string; }, createHash: (alg: string) => { update(d: string | Buffer): unknown; digest(enc: string): string; }): string; export declare const MEASUREMENT_CORPORA: readonly string[]; //# sourceMappingURL=action-eligibility.d.ts.map