@agenttrail/guardrails 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/dist/{chunk-CDYD5WBH.js → chunk-AUGYGUPX.js} +2 -2
- package/dist/chunk-AUGYGUPX.js.map +1 -0
- package/dist/{chunk-DKLS2UEL.js → chunk-IQBU26ZF.js} +3 -3
- package/dist/chunk-IQBU26ZF.js.map +1 -0
- package/dist/guardrails.cjs +2 -2
- package/dist/guardrails.cjs.map +1 -1
- package/dist/guardrails.d.cts +51 -85
- package/dist/guardrails.d.ts +51 -85
- package/dist/guardrails.js +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/schema.cjs +1 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +35 -92
- package/dist/schema.d.ts +35 -92
- package/dist/schema.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-CDYD5WBH.js.map +0 -1
- package/dist/chunk-DKLS2UEL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
mentions,
|
|
21
21
|
pwsh,
|
|
22
22
|
rulesForPack
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-IQBU26ZF.js";
|
|
24
24
|
import {
|
|
25
25
|
ACTIONS,
|
|
26
26
|
ActionSchema,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
hasNestedUnboundedQuantifier,
|
|
38
38
|
parseRule,
|
|
39
39
|
parseRuleOrThrow
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-AUGYGUPX.js";
|
|
41
41
|
export {
|
|
42
42
|
ACTIONS,
|
|
43
43
|
ActionSchema,
|
package/dist/schema.cjs
CHANGED
|
@@ -154,7 +154,7 @@ var FixturesSchema = import_zod.z.object({
|
|
|
154
154
|
allow: import_zod.z.array(FixtureSchema).min(1)
|
|
155
155
|
}).strict();
|
|
156
156
|
var RuleSchema = import_zod.z.object({
|
|
157
|
-
/** Stable
|
|
157
|
+
/** Stable: a rule id is never renamed. */
|
|
158
158
|
id: import_zod.z.string().min(1),
|
|
159
159
|
/** The pack this rule belongs to. Validated against `PACKS` by the registry. */
|
|
160
160
|
category: import_zod.z.string().min(1),
|
package/dist/schema.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema.ts"],"sourcesContent":["/**\n * The guardrails rule format — self-contained, and deliberately so.\n *\n * ── Why this file imports nothing from the monorepo ──────────────────────────\n *\n * `@agenttrail/shared` and `@agenttrail/policy-engine` are both `private: true`.\n * A public package whose source reads `import { … } from \"@agenttrail/shared\"`\n * does not build for anyone outside this repo, and open source whose source does\n * not compile is the first thing this audience checks\n * (AGENTTRAIL-GUARD-SPEC.md 2.1).\n *\n * So this file mirrors the engine's `MatchCondition` / `Match` rather than\n * importing them, and `@agenttrail/guardrails` declares NO dependency on either\n * package — not even a dev one. That is not a convention anyone has to remember:\n * pnpm does not hoist workspace packages, so a stray import here fails to\n * resolve at typecheck and build time. A `MODULE_NOT_FOUND` cannot be argued\n * with, and a lint rule over source text can — this repo has already had comment\n * prose parsed as an import.\n *\n * The price of owning a second copy is drift, so a parity test that lives with\n * the policy engine pins this schema to the engine's own:\n * `guardrails-schema-parity.test.ts`.\n * It is not optional and not redundant. Read it before changing anything here.\n *\n * ── What this is NOT ─────────────────────────────────────────────────────────\n *\n * This is not the engine's `PolicyPredicate`. That schema is `.strict()` with a\n * required `version` and `action`, so it rejects 100% of this rule format — it\n * is the schema of the `match` SUB-OBJECT, not of a rule. A rule carries\n * `defaultAction` (what the guard does when it matches) and no `version`; the\n * package version is the corpus version.\n */\n\nimport { z } from \"zod\";\n\n// ── Severity ────────────────────────────────────────────────────────────────\n\n/**\n * All five product severities, in order, most→least severe.\n *\n * This tuple mirrors `shared/src/types/common.ts` `Severity` and `risk.ts`\n * `RISK_SEVERITIES` exactly, and the parity test pins them together so they\n * cannot drift. Matching the product means the hosted catalog layers its own\n * metadata by rule id with no translation table in between (spec 8.3).\n *\n * `info` earns its place: five of the eight packs default to `ask`/`warn`, and a\n * warn-only rule labelled `low` overstates itself.\n *\n * ── Sharing a vocabulary with a pricing table is not permission to price ─────\n *\n * These same five values key `DEFAULT_PER_INCIDENT_CENTS` in the private\n * `risk.ts` — `critical` is $160,000. Because the tuples now line up perfectly,\n * joining severity to money is one line. **Do not.** The guard prints no\n * currency anywhere (Appendix 0 #12, spec 9.1), and this package therefore ships\n * NO mapping from severity to money. The reason is on the record: a slightly\n * loose rule once produced the headline \"rm -rf seen 17 times, $5,070,000\",\n * built entirely out of a test suite deleting its own scratch folders.\n */\nexport const SEVERITIES = [\"critical\", \"high\", \"medium\", \"low\", \"info\"] as const;\n\n/** How dangerous the thing a rule catches is. An authoring signal, not a price. */\nexport const SeveritySchema = z.enum(SEVERITIES);\n\n// ── Actions ─────────────────────────────────────────────────────────────────\n\n/**\n * What the guard does when a rule matches.\n *\n * The guard maps these onto Claude Code's `PreToolUse` verdicts:\n * `block` → deny, `require_approval` → ask, `warn` → allow-but-recorded.\n */\nexport const ACTIONS = [\"block\", \"require_approval\", \"warn\"] as const;\n\n/** The action a rule takes by default, before any user override. */\nexport const ActionSchema = z.enum(ACTIONS);\n\n// ── detail_matches: regex limits, mirrored from the engine ───────────────────\n\n/** Longest accepted `detail_matches` pattern. Long enough for any real rule. */\nexport const DETAIL_MATCHES_MAX_PATTERN_LENGTH = 200;\n\n/** Most `detail_matches` patterns allowed on one condition (they are ORed). */\nexport const DETAIL_MATCHES_MAX_PATTERNS = 10;\n\n/**\n * Reject a regex that can backtrack catastrophically, at PARSE time.\n *\n * Mirrored from the engine's `hasNestedUnboundedQuantifier`, and pinned to it by\n * the parity test — including behavior over a shared pattern corpus, not just\n * the fact that a function exists.\n *\n * Node has no regex timeout, so the only real bounds are a non-backtracking\n * engine (RE2 — a runtime dependency deliberately not taken) or a syntactic\n * restriction. This is the syntactic one: it rejects **star height >= 2**, an\n * unbounded quantifier applied to a group that itself contains an unbounded\n * quantifier — `(a+)+`, `(a*)*`, `(\\s+x?)+` — the shape behind essentially every\n * practical ReDoS.\n *\n * This matters more here than it does on the server. The guard's hook has a 10s\n * Claude Code timeout, no internal watchdog, and it **fails open**: a pattern\n * that backtracks does not merely make enforcement slow, it lets the action\n * through. A slow rule on the hosted PDP is a slow rule; a slow rule in the\n * guard is a disabled rule.\n *\n * HONEST LIMIT: this is a bound, not a proof. Star height 1 can still be\n * quadratic (`(a|a)*`), which is slow but not exponential, and is acceptable\n * against the length cap above. A hard guarantee needs RE2.\n *\n * @param source - The regex body (no delimiters, no flags).\n * @returns `true` when the pattern nests unbounded quantifiers.\n */\nexport function hasNestedUnboundedQuantifier(source: string): boolean {\n /** Is `source[i]` an unbounded quantifier (`*`, `+`, or `{n,}`)? */\n const unboundedQuantifierAt = (s: string, i: number): boolean => {\n const ch = s[i];\n if (ch === \"*\" || ch === \"+\") return true;\n if (ch !== \"{\") return false;\n const close = s.indexOf(\"}\", i);\n // `{n,}` is unbounded; `{n}` and `{n,m}` are not.\n return close !== -1 && /^\\{\\d*,\\}$/.test(s.slice(i, close + 1));\n };\n\n /** Does `body` contain an unbounded quantifier outside a character class? */\n const containsUnbounded = (body: string): boolean => {\n let inClass = false;\n for (let i = 0; i < body.length; i++) {\n const ch = body[i];\n if (ch === \"\\\\\") {\n i++;\n continue;\n }\n if (inClass) {\n if (ch === \"]\") inClass = false;\n continue;\n }\n if (ch === \"[\") {\n inClass = true;\n continue;\n }\n if (unboundedQuantifierAt(body, i)) return true;\n }\n return false;\n };\n\n const groupStarts: number[] = [];\n let inClass = false;\n for (let i = 0; i < source.length; i++) {\n const ch = source[i];\n if (ch === \"\\\\\") {\n i++; // skip the escaped char — `\\(` is a literal, not a group\n continue;\n }\n if (inClass) {\n if (ch === \"]\") inClass = false;\n continue;\n }\n if (ch === \"[\") {\n inClass = true;\n continue;\n }\n if (ch === \"(\") {\n groupStarts.push(i);\n continue;\n }\n if (ch === \")\") {\n const start = groupStarts.pop();\n if (start === undefined) continue; // unbalanced — the RegExp compile catches it\n // Quantified group? Then its body must not itself repeat unboundedly.\n if (unboundedQuantifierAt(source, i + 1) && containsUnbounded(source.slice(start + 1, i))) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * One `detail_matches` pattern: compilable, length-capped, and free of nested\n * unbounded quantifiers. The engine compiles these with `i` and never `g`/`y` —\n * a global regex carries `lastIndex` between calls and would intermittently miss.\n */\nconst DetailMatchPatternSchema = z\n .string()\n .min(1)\n .max(DETAIL_MATCHES_MAX_PATTERN_LENGTH)\n .superRefine((pattern, ctx) => {\n try {\n new RegExp(pattern, \"i\");\n } catch (error) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `detail_matches: not a valid regular expression (${\n error instanceof Error ? error.message : \"unknown error\"\n })`,\n });\n return;\n }\n if (hasNestedUnboundedQuantifier(pattern)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"detail_matches: nested unbounded quantifier (e.g. `(a+)+`) can backtrack catastrophically; rewrite without repeating a repeating group\",\n });\n }\n });\n\n// ── Match conditions ────────────────────────────────────────────────────────\n\n/**\n * One condition matched against a tool call.\n *\n * - `kind`: required. The span kind, e.g. `execute_tool`. Case-insensitive.\n * - `label`: optional. The tool name — a case-insensitive GLOB, so\n * `mcp__chrome__*` and `{Bash,PowerShell}` are one condition rather than a\n * list that rots. A plain label has no glob character and matches exactly.\n * - `detail_contains`: optional. Substrings that must ALL appear (AND).\n * **CASE-SENSITIVE**, and deliberately: `AKIA`, `ghp_`, `sk_live_` and the\n * upper-case `TRUNCATE` arm are rules where case IS the signal. Author\n * case-insensitive text matches with `detail_matches` instead.\n * - `detail_matches`: optional. Regex patterns against the same text; at least\n * one must match (OR, unlike `detail_contains`). Case-insensitive.\n * - `file_glob`: optional. Case-insensitive glob against the file path.\n *\n * ── Two engine fields are deliberately absent ────────────────────────────────\n *\n * `numeric` is BANNED (spec 7.4). At guard decision time `tokens`,\n * `cachedTokens` and `durationMs` are all 0, because the action has not run yet.\n * `gt` is therefore always false — the rule can never fire — and, worse, **`lt`\n * is always true, so the rule fires on every single command.** Omitting the key\n * from a `.strict()` object makes that a parse error rather than a lint finding.\n *\n * `scope` is BANNED for the same class of reason and is absent from the rule\n * envelope below: `agent_in` compares literally against an agent id that is\n * always a UUID, never the string `\"claude-code\"`, so a scoped rule matches\n * nothing, forever, silently.\n *\n * ── The third ban needs an actual check ──────────────────────────────────────\n *\n * A command matcher (`detail_contains` / `detail_matches`) and a file matcher\n * (`file_glob`) in ONE condition can never both be satisfied: the guard's mapper\n * is an if/else chain, so no real tool call ever carries both a command and a\n * file path. The engine's schema permits the combination; this one refuses it,\n * which is a deliberate divergence the parity test knows about.\n */\nexport const MatchConditionSchema = z\n .object({\n kind: z.string().min(1),\n label: z.string().min(1).optional(),\n detail_contains: z.array(z.string().min(1)).min(1).optional(),\n detail_matches: z\n .array(DetailMatchPatternSchema)\n .min(1)\n .max(DETAIL_MATCHES_MAX_PATTERNS)\n .optional(),\n file_glob: z.string().min(1).optional(),\n })\n .strict()\n .superRefine((condition, ctx) => {\n const hasCommandMatcher =\n condition.detail_contains !== undefined || condition.detail_matches !== undefined;\n if (hasCommandMatcher && condition.file_glob !== undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"a condition may not combine a command matcher (detail_contains / detail_matches) with a file matcher (file_glob): no real tool call carries both, so the condition can never match. Split it into two conditions under any_of.\",\n });\n }\n });\n\n/**\n * Match composition — conditions combined with `any_of` (OR), `all_of` (AND)\n * and/or `none_of` (negation).\n *\n * At least one of `any_of` or `all_of` must be present. `none_of` is an optional\n * refinement that CANNOT stand alone: a match must positively select calls\n * before excluding some. A pure-negation match is satisfied by every\n * non-matching call and, at `block`, would deny everything the agent does.\n */\nexport const MatchSchema = z\n .object({\n any_of: z.array(MatchConditionSchema).min(1).optional(),\n all_of: z.array(MatchConditionSchema).min(1).optional(),\n none_of: z.array(MatchConditionSchema).min(1).optional(),\n })\n .strict()\n .refine((data) => data.any_of !== undefined || data.all_of !== undefined, {\n message: \"At least one of 'any_of' or 'all_of' must be provided\",\n });\n\n// ── Fixtures ────────────────────────────────────────────────────────────────\n\n/**\n * The tools a fixture may name. Free-form on purpose — the guard's matcher grows\n * (`mcp__*` alone is unbounded) and a closed enum here would reject a valid rule\n * for a tool this package has not heard of yet.\n */\nconst FixtureToolSchema = z.string().min(1);\n\n/**\n * A fixture on the COMMAND channel: `Bash`, `PowerShell`, `WebSearch` (its\n * query is ordinary text), and `mcp__*` (its serialized input).\n */\nconst CommandFixtureSchema = z\n .object({ tool: FixtureToolSchema, command: z.string().min(1) })\n .strict();\n\n/** A fixture on the FILE channel: `Edit`, `Write`, `Read`, `NotebookEdit`. */\nconst FileFixtureSchema = z\n .object({ tool: FixtureToolSchema, file_path: z.string().min(1) })\n .strict();\n\n/**\n * One fixture: a tool call this rule must match, or must not.\n *\n * A bare string is shorthand for `{ tool: \"Bash\", command: \"<string>\" }`, which\n * covers most rules and keeps them terse. Anything else is an object with a\n * `tool` plus **exactly one** of `command` or `file_path` — the two channels in\n * spec 4.1. `.strict()` on each variant is what makes \"exactly one\" true: a\n * `command` object carrying a `file_path` matches neither variant.\n *\n * The parse output is always normalized to the object form, so the harness has\n * exactly one shape to feed the evaluator.\n *\n * ── There is no `url` channel, and that is load-bearing ──────────────────────\n *\n * Spec 7.2 permitted `{ tool: \"WebFetch\", url: … }` until 2026-09-07. It was\n * removed (PR #532) because **the engine cannot see a URL**: the only two\n * attribute keys any matcher reads are `detail` and `file_path`, and there is no\n * `url` field in the condition schema. A value written to `url` is read by\n * nothing, so a `WebFetch` rule would have matched nothing, forever, silently —\n * exactly the failure `scope` is banned for.\n *\n * Aliasing the URL onto the command channel (which the paid PEP does) was\n * rejected: it ships a channel whose rules read as commands, and the guard's\n * honesty about its own coverage is the product. **v1 ships no website rules**,\n * `WebFetch` is not intercepted, and the README says so rather than papering\n * over it. `WebSearch` stays, because its query really is text on the command\n * channel.\n *\n * A `url` key therefore fails to parse — it does not silently become a fixture\n * that tests nothing. `__tests__/schema.test.ts` pins that, because someone\n * working from a cached reading of the old spec will write exactly it.\n */\nexport const FixtureSchema = z.union([\n z\n .string()\n .min(1)\n .transform((command) => ({ tool: \"Bash\" as const, command })),\n CommandFixtureSchema,\n FileFixtureSchema,\n]);\n\n/**\n * The two directions every rule must prove, through the real evaluator.\n *\n * **`block` means \"this rule MUST match\"; `allow` means \"this rule MUST NOT\n * match\".** They are not verdicts. Five of the eight packs default to `ask` or\n * `warn`, and `warn` folds into an allow verdict, so reading `block` as \"the\n * guard denies\" would fail every `warn` rule and make the negative fixture\n * vacuous for them.\n *\n * Both are `.min(1)`: the spec only requires CI to fail on a rule missing a\n * negative fixture, but requiring it here moves that failure to authoring time,\n * where it costs a keystroke instead of a pull-request round trip.\n */\nexport const FixturesSchema = z\n .object({\n block: z.array(FixtureSchema).min(1),\n allow: z.array(FixtureSchema).min(1),\n })\n .strict();\n\n// ── The rule ────────────────────────────────────────────────────────────────\n\n/**\n * One published rule.\n *\n * `minTier` and `installCount` are absent on purpose. Both are hosted-only\n * metadata, layered on by rule id at serve time, and `installCount` is authored\n * fiction by its own docstring — shipping invented adoption numbers in an\n * open-source security tool becomes the story. `.strict()` turns either one into\n * a parse error rather than a field that quietly rides along into the public\n * package.\n *\n * `description` carries the rule's HONEST COVERAGE LIMITS, verbatim — e.g. that\n * an `rm -rf` rule misses `rm -fr`. That is the difference between a credible\n * rule set and a marketing one, and it is the first thing a technical evaluator\n * checks. It is a convention rather than a schema constraint, because no\n * validator can tell an honest limit from a sentence shaped like one.\n */\nexport const RuleSchema = z\n .object({\n /** Stable forever. Never renamed — it is the join key for hosted metadata. */\n id: z.string().min(1),\n /** The pack this rule belongs to. Validated against `PACKS` by the registry. */\n category: z.string().min(1),\n severity: SeveritySchema,\n defaultAction: ActionSchema,\n title: z.string().min(1),\n description: z.string().min(1),\n match: MatchSchema,\n fixtures: FixturesSchema,\n })\n .strict();\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\n/** How dangerous the caught action is. Never a price. */\nexport type Severity = (typeof SEVERITIES)[number];\n\n/** What the guard does on a match. */\nexport type Action = (typeof ACTIONS)[number];\n\n/** One condition matched against a tool call. */\nexport type MatchCondition = z.infer<typeof MatchConditionSchema>;\n\n/** A composed match — `any_of` / `all_of` / `none_of`. */\nexport type Match = z.infer<typeof MatchSchema>;\n\n/** A fixture as authored: a bare command string, or a tagged single-channel object. */\nexport type FixtureInput = z.input<typeof FixtureSchema>;\n\n/** A fixture after parsing — always the tagged object form. */\nexport type Fixture = z.output<typeof FixtureSchema>;\n\n/** The must-match / must-not-match pair every rule ships. */\nexport type Fixtures = z.infer<typeof FixturesSchema>;\n\n/** One published rule, as authored. */\nexport type RuleInput = z.input<typeof RuleSchema>;\n\n/** One published rule, parsed and normalized. */\nexport type Rule = z.output<typeof RuleSchema>;\n\n// ── Parsing ─────────────────────────────────────────────────────────────────\n\n/**\n * Validate a rule from unknown input.\n *\n * This is the entry point a contributor gets locally. It answers \"is this a\n * well-formed rule?\" — it does NOT answer \"does this rule actually fire on the\n * command you think it does.\" That needs the real evaluator, which lives in a\n * private package, so it runs in our CI on the pull request. A contributor gets\n * the schema here and the real answer there, which is where it has to run to be\n * trusted anyway.\n *\n * @param input - Unknown input, e.g. parsed JSON or a rule module's default export.\n * @returns A Zod SafeParseReturnType — check `.success` before reading `.data`.\n */\nexport function parseRule(input: unknown): z.SafeParseReturnType<unknown, Rule> {\n return RuleSchema.safeParse(input);\n}\n\n/**\n * Parse a rule, or throw with the reason.\n *\n * Used by rule modules themselves via {@link defineRule}, so an authoring\n * mistake fails at import — which is to say, at test time — rather than\n * surviving into a corpus that only validates what it remembers to check.\n */\nexport function parseRuleOrThrow(input: unknown): Rule {\n const result = parseRule(input);\n if (!result.success) {\n const id =\n typeof input === \"object\" && input !== null && \"id\" in input\n ? String((input as { id: unknown }).id)\n : \"<unknown id>\";\n throw new Error(`invalid guardrails rule ${id}: ${result.error.message}`);\n }\n return result.data;\n}\n\n/**\n * Declare a rule. Validates at module load, so a malformed rule cannot reach the\n * registry — the corpus is only as trustworthy as its weakest unvalidated entry.\n */\nexport function defineRule(rule: RuleInput): Rule {\n return parseRuleOrThrow(rule);\n}\n"],"mappings":"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","names":["inClass"]}
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts"],"sourcesContent":["/**\n * The guardrails rule format — self-contained, and deliberately so.\n *\n * This file defines the rule format and imports nothing but zod, so the package\n * builds on its own. The condition and match shapes mirror the evaluator's rather\n * than importing them; a change to either has to be made in both places.\n *\n * A rule carries `defaultAction` (what the guard does when it matches) and no\n * `version`; the package version is the corpus version.\n */\n\nimport { z } from \"zod\";\n\n// ── Severity ────────────────────────────────────────────────────────────────\n\n/**\n * The five severities, in order, most→least severe.\n *\n * `info` earns its place: five of the eight packs default to `ask`/`warn`, and a\n * warn-only rule labelled `low` overstates itself.\n *\n * Severity says how serious a match is. This package maps it to nothing else,\n * including cost: the guard prints no currency.\n */\nexport const SEVERITIES = [\"critical\", \"high\", \"medium\", \"low\", \"info\"] as const;\n\n/** How dangerous the thing a rule catches is. An authoring signal, not a price. */\nexport const SeveritySchema = z.enum(SEVERITIES);\n\n// ── Actions ─────────────────────────────────────────────────────────────────\n\n/**\n * What the guard does when a rule matches.\n *\n * The guard maps these onto Claude Code's `PreToolUse` verdicts:\n * `block` → deny, `require_approval` → ask, `warn` → allow-but-recorded.\n */\nexport const ACTIONS = [\"block\", \"require_approval\", \"warn\"] as const;\n\n/** The action a rule takes by default, before any user override. */\nexport const ActionSchema = z.enum(ACTIONS);\n\n// ── detail_matches: regex limits, mirrored from the engine ───────────────────\n\n/** Longest accepted `detail_matches` pattern. Long enough for any real rule. */\nexport const DETAIL_MATCHES_MAX_PATTERN_LENGTH = 200;\n\n/** Most `detail_matches` patterns allowed on one condition (they are ORed). */\nexport const DETAIL_MATCHES_MAX_PATTERNS = 10;\n\n/**\n * Reject a regex that can backtrack catastrophically, at PARSE time.\n *\n * Node has no regex timeout, so the only real bounds are a non-backtracking\n * engine (RE2 — a runtime dependency deliberately not taken) or a syntactic\n * restriction. This is the syntactic one: it rejects **star height >= 2**, an\n * unbounded quantifier applied to a group that itself contains an unbounded\n * quantifier — `(a+)+`, `(a*)*`, `(\\s+x?)+` — the shape behind essentially every\n * practical ReDoS.\n *\n * The guard's hook has a 10s Claude Code timeout, no internal watchdog, and it\n * **fails open**: a pattern that backtracks does not merely make enforcement slow,\n * it lets the action through. A slow rule in the guard is a disabled rule.\n *\n * HONEST LIMIT: this is a bound, not a proof. Star height 1 can still be\n * quadratic (`(a|a)*`), which is slow but not exponential, and is acceptable\n * against the length cap above. A hard guarantee needs RE2.\n *\n * @param source - The regex body (no delimiters, no flags).\n * @returns `true` when the pattern nests unbounded quantifiers.\n */\nexport function hasNestedUnboundedQuantifier(source: string): boolean {\n /** Is `source[i]` an unbounded quantifier (`*`, `+`, or `{n,}`)? */\n const unboundedQuantifierAt = (s: string, i: number): boolean => {\n const ch = s[i];\n if (ch === \"*\" || ch === \"+\") return true;\n if (ch !== \"{\") return false;\n const close = s.indexOf(\"}\", i);\n // `{n,}` is unbounded; `{n}` and `{n,m}` are not.\n return close !== -1 && /^\\{\\d*,\\}$/.test(s.slice(i, close + 1));\n };\n\n /** Does `body` contain an unbounded quantifier outside a character class? */\n const containsUnbounded = (body: string): boolean => {\n let inClass = false;\n for (let i = 0; i < body.length; i++) {\n const ch = body[i];\n if (ch === \"\\\\\") {\n i++;\n continue;\n }\n if (inClass) {\n if (ch === \"]\") inClass = false;\n continue;\n }\n if (ch === \"[\") {\n inClass = true;\n continue;\n }\n if (unboundedQuantifierAt(body, i)) return true;\n }\n return false;\n };\n\n const groupStarts: number[] = [];\n let inClass = false;\n for (let i = 0; i < source.length; i++) {\n const ch = source[i];\n if (ch === \"\\\\\") {\n i++; // skip the escaped char — `\\(` is a literal, not a group\n continue;\n }\n if (inClass) {\n if (ch === \"]\") inClass = false;\n continue;\n }\n if (ch === \"[\") {\n inClass = true;\n continue;\n }\n if (ch === \"(\") {\n groupStarts.push(i);\n continue;\n }\n if (ch === \")\") {\n const start = groupStarts.pop();\n if (start === undefined) continue; // unbalanced — the RegExp compile catches it\n // Quantified group? Then its body must not itself repeat unboundedly.\n if (unboundedQuantifierAt(source, i + 1) && containsUnbounded(source.slice(start + 1, i))) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * One `detail_matches` pattern: compilable, length-capped, and free of nested\n * unbounded quantifiers. The engine compiles these with `i` and never `g`/`y` —\n * a global regex carries `lastIndex` between calls and would intermittently miss.\n */\nconst DetailMatchPatternSchema = z\n .string()\n .min(1)\n .max(DETAIL_MATCHES_MAX_PATTERN_LENGTH)\n .superRefine((pattern, ctx) => {\n try {\n new RegExp(pattern, \"i\");\n } catch (error) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `detail_matches: not a valid regular expression (${\n error instanceof Error ? error.message : \"unknown error\"\n })`,\n });\n return;\n }\n if (hasNestedUnboundedQuantifier(pattern)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"detail_matches: nested unbounded quantifier (e.g. `(a+)+`) can backtrack catastrophically; rewrite without repeating a repeating group\",\n });\n }\n });\n\n// ── Match conditions ────────────────────────────────────────────────────────\n\n/**\n * One condition matched against a tool call.\n *\n * - `kind`: required. The span kind, e.g. `execute_tool`. Case-insensitive.\n * - `label`: optional. The tool name — a case-insensitive GLOB, so\n * `mcp__chrome__*` and `{Bash,PowerShell}` are one condition rather than a\n * list that rots. A plain label has no glob character and matches exactly.\n * - `detail_contains`: optional. Substrings that must ALL appear (AND).\n * **CASE-SENSITIVE**, and deliberately: `AKIA`, `ghp_`, `sk_live_` and the\n * upper-case `TRUNCATE` arm are rules where case IS the signal. Author\n * case-insensitive text matches with `detail_matches` instead.\n * - `detail_matches`: optional. Regex patterns against the same text; at least\n * one must match (OR, unlike `detail_contains`). Case-insensitive.\n * - `file_glob`: optional. Case-insensitive glob against the file path.\n *\n * ── Two engine fields are deliberately absent ────────────────────────────────\n *\n * `numeric` is BANNED. At guard decision time `tokens`,\n * `cachedTokens` and `durationMs` are all 0, because the action has not run yet.\n * `gt` is therefore always false — the rule can never fire — and, worse, **`lt`\n * is always true, so the rule fires on every single command.** Omitting the key\n * from a `.strict()` object makes that a parse error rather than a lint finding.\n *\n * `scope` is BANNED for the same class of reason and is absent from the rule\n * envelope below: `agent_in` compares literally against an agent id that is\n * always a UUID, never the string `\"claude-code\"`, so a scoped rule matches\n * nothing, forever, silently.\n *\n * ── The third ban needs an actual check ──────────────────────────────────────\n *\n * A command matcher (`detail_contains` / `detail_matches`) and a file matcher\n * (`file_glob`) in ONE condition can never both be satisfied: the guard's mapper\n * is an if/else chain, so no real tool call ever carries both a command and a\n * file path. This schema refuses the combination.\n */\nexport const MatchConditionSchema = z\n .object({\n kind: z.string().min(1),\n label: z.string().min(1).optional(),\n detail_contains: z.array(z.string().min(1)).min(1).optional(),\n detail_matches: z\n .array(DetailMatchPatternSchema)\n .min(1)\n .max(DETAIL_MATCHES_MAX_PATTERNS)\n .optional(),\n file_glob: z.string().min(1).optional(),\n })\n .strict()\n .superRefine((condition, ctx) => {\n const hasCommandMatcher =\n condition.detail_contains !== undefined || condition.detail_matches !== undefined;\n if (hasCommandMatcher && condition.file_glob !== undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"a condition may not combine a command matcher (detail_contains / detail_matches) with a file matcher (file_glob): no real tool call carries both, so the condition can never match. Split it into two conditions under any_of.\",\n });\n }\n });\n\n/**\n * Match composition — conditions combined with `any_of` (OR), `all_of` (AND)\n * and/or `none_of` (negation).\n *\n * At least one of `any_of` or `all_of` must be present. `none_of` is an optional\n * refinement that CANNOT stand alone: a match must positively select calls\n * before excluding some. A pure-negation match is satisfied by every\n * non-matching call and, at `block`, would deny everything the agent does.\n */\nexport const MatchSchema = z\n .object({\n any_of: z.array(MatchConditionSchema).min(1).optional(),\n all_of: z.array(MatchConditionSchema).min(1).optional(),\n none_of: z.array(MatchConditionSchema).min(1).optional(),\n })\n .strict()\n .refine((data) => data.any_of !== undefined || data.all_of !== undefined, {\n message: \"At least one of 'any_of' or 'all_of' must be provided\",\n });\n\n// ── Fixtures ────────────────────────────────────────────────────────────────\n\n/**\n * The tools a fixture may name. Free-form on purpose — the guard's matcher grows\n * (`mcp__*` alone is unbounded) and a closed enum here would reject a valid rule\n * for a tool this package has not heard of yet.\n */\nconst FixtureToolSchema = z.string().min(1);\n\n/**\n * A fixture on the COMMAND channel: `Bash`, `PowerShell`, `WebSearch` (its\n * query is ordinary text), and `mcp__*` (its serialized input).\n */\nconst CommandFixtureSchema = z\n .object({ tool: FixtureToolSchema, command: z.string().min(1) })\n .strict();\n\n/** A fixture on the FILE channel: `Edit`, `Write`, `Read`, `NotebookEdit`. */\nconst FileFixtureSchema = z\n .object({ tool: FixtureToolSchema, file_path: z.string().min(1) })\n .strict();\n\n/**\n * One fixture: a tool call this rule must match, or must not.\n *\n * A bare string is shorthand for `{ tool: \"Bash\", command: \"<string>\" }`, which\n * covers most rules and keeps them terse. Anything else is an object with a\n * `tool` plus **exactly one** of `command` or `file_path` — the two channels.\n * `.strict()` on each variant is what makes \"exactly one\" true: a\n * `command` object carrying a `file_path` matches neither variant.\n *\n * The parse output is always normalized to the object form, so the harness has\n * exactly one shape to feed the evaluator.\n *\n * ── There is no `url` channel ─────────────────────────────────────────────────\n *\n * **The evaluator cannot see a URL**: the only two attribute keys any matcher\n * reads are `detail` and `file_path`, and there is no `url` field in the condition\n * schema. A value written to `url` would be read by nothing, so a `WebFetch` rule\n * would match nothing, silently — the same failure `scope` is banned for. There\n * are no website rules and `WebFetch` is not intercepted; `WebSearch` is, because\n * its query is text on the command channel.\n *\n * A `url` key therefore fails to parse rather than becoming a fixture that tests\n * nothing. `__tests__/schema.test.ts` pins that.\n */\nexport const FixtureSchema = z.union([\n z\n .string()\n .min(1)\n .transform((command) => ({ tool: \"Bash\" as const, command })),\n CommandFixtureSchema,\n FileFixtureSchema,\n]);\n\n/**\n * The two directions every rule must prove, through the real evaluator.\n *\n * **`block` means \"this rule MUST match\"; `allow` means \"this rule MUST NOT\n * match\".** They are not verdicts. Five of the eight packs default to `ask` or\n * `warn`, and `warn` folds into an allow verdict, so reading `block` as \"the\n * guard denies\" would fail every `warn` rule and make the negative fixture\n * vacuous for them.\n *\n * Both are `.min(1)`: CI only has to fail on a rule missing a\n * negative fixture, but requiring it here moves that failure to authoring time,\n * where it costs a keystroke instead of a pull-request round trip.\n */\nexport const FixturesSchema = z\n .object({\n block: z.array(FixtureSchema).min(1),\n allow: z.array(FixtureSchema).min(1),\n })\n .strict();\n\n// ── The rule ────────────────────────────────────────────────────────────────\n\n/**\n * One published rule.\n *\n * `.strict()` rejects any key not declared here, so an unknown field fails to parse\n * rather than riding along into the published package.\n *\n * `description` carries the rule's COVERAGE LIMITS, verbatim — e.g. that an\n * `rm -rf` rule misses `rm -fr`. It is a convention rather than a schema\n * constraint, because no validator can tell a real limit from a sentence shaped\n * like one.\n */\nexport const RuleSchema = z\n .object({\n /** Stable: a rule id is never renamed. */\n id: z.string().min(1),\n /** The pack this rule belongs to. Validated against `PACKS` by the registry. */\n category: z.string().min(1),\n severity: SeveritySchema,\n defaultAction: ActionSchema,\n title: z.string().min(1),\n description: z.string().min(1),\n match: MatchSchema,\n fixtures: FixturesSchema,\n })\n .strict();\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\n/** How dangerous the caught action is. Never a price. */\nexport type Severity = (typeof SEVERITIES)[number];\n\n/** What the guard does on a match. */\nexport type Action = (typeof ACTIONS)[number];\n\n/** One condition matched against a tool call. */\nexport type MatchCondition = z.infer<typeof MatchConditionSchema>;\n\n/** A composed match — `any_of` / `all_of` / `none_of`. */\nexport type Match = z.infer<typeof MatchSchema>;\n\n/** A fixture as authored: a bare command string, or a tagged single-channel object. */\nexport type FixtureInput = z.input<typeof FixtureSchema>;\n\n/** A fixture after parsing — always the tagged object form. */\nexport type Fixture = z.output<typeof FixtureSchema>;\n\n/** The must-match / must-not-match pair every rule ships. */\nexport type Fixtures = z.infer<typeof FixturesSchema>;\n\n/** One published rule, as authored. */\nexport type RuleInput = z.input<typeof RuleSchema>;\n\n/** One published rule, parsed and normalized. */\nexport type Rule = z.output<typeof RuleSchema>;\n\n// ── Parsing ─────────────────────────────────────────────────────────────────\n\n/**\n * Validate a rule from unknown input.\n *\n * It answers \"is this a well-formed rule?\" — it does NOT answer \"does this rule\n * actually fire on the command you think it does.\" That needs the evaluator, which\n * is not part of this package.\n *\n * @param input - Unknown input, e.g. parsed JSON or a rule module's default export.\n * @returns A Zod SafeParseReturnType — check `.success` before reading `.data`.\n */\nexport function parseRule(input: unknown): z.SafeParseReturnType<unknown, Rule> {\n return RuleSchema.safeParse(input);\n}\n\n/**\n * Parse a rule, or throw with the reason.\n *\n * Used by rule modules themselves via {@link defineRule}, so an authoring\n * mistake fails at import — which is to say, at test time — rather than\n * surviving into a corpus that only validates what it remembers to check.\n */\nexport function parseRuleOrThrow(input: unknown): Rule {\n const result = parseRule(input);\n if (!result.success) {\n const id =\n typeof input === \"object\" && input !== null && \"id\" in input\n ? String((input as { id: unknown }).id)\n : \"<unknown id>\";\n throw new Error(`invalid guardrails rule ${id}: ${result.error.message}`);\n }\n return result.data;\n}\n\n/**\n * Declare a rule. Validates at module load, so a malformed rule cannot reach the\n * registry — the corpus is only as trustworthy as its weakest unvalidated entry.\n */\nexport function defineRule(rule: RuleInput): Rule {\n return parseRuleOrThrow(rule);\n}\n"],"mappings":"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","names":["inClass"]}
|
package/dist/schema.d.cts
CHANGED
|
@@ -3,56 +3,22 @@ import { z } from 'zod';
|
|
|
3
3
|
/**
|
|
4
4
|
* The guardrails rule format — self-contained, and deliberately so.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* This file defines the rule format and imports nothing but zod, so the package
|
|
7
|
+
* builds on its own. The condition and match shapes mirror the evaluator's rather
|
|
8
|
+
* than importing them; a change to either has to be made in both places.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* does not build for anyone outside this repo, and open source whose source does
|
|
11
|
-
* not compile is the first thing this audience checks
|
|
12
|
-
* (AGENTTRAIL-GUARD-SPEC.md 2.1).
|
|
13
|
-
*
|
|
14
|
-
* So this file mirrors the engine's `MatchCondition` / `Match` rather than
|
|
15
|
-
* importing them, and `@agenttrail/guardrails` declares NO dependency on either
|
|
16
|
-
* package — not even a dev one. That is not a convention anyone has to remember:
|
|
17
|
-
* pnpm does not hoist workspace packages, so a stray import here fails to
|
|
18
|
-
* resolve at typecheck and build time. A `MODULE_NOT_FOUND` cannot be argued
|
|
19
|
-
* with, and a lint rule over source text can — this repo has already had comment
|
|
20
|
-
* prose parsed as an import.
|
|
21
|
-
*
|
|
22
|
-
* The price of owning a second copy is drift, so a parity test that lives with
|
|
23
|
-
* the policy engine pins this schema to the engine's own:
|
|
24
|
-
* `guardrails-schema-parity.test.ts`.
|
|
25
|
-
* It is not optional and not redundant. Read it before changing anything here.
|
|
26
|
-
*
|
|
27
|
-
* ── What this is NOT ─────────────────────────────────────────────────────────
|
|
28
|
-
*
|
|
29
|
-
* This is not the engine's `PolicyPredicate`. That schema is `.strict()` with a
|
|
30
|
-
* required `version` and `action`, so it rejects 100% of this rule format — it
|
|
31
|
-
* is the schema of the `match` SUB-OBJECT, not of a rule. A rule carries
|
|
32
|
-
* `defaultAction` (what the guard does when it matches) and no `version`; the
|
|
33
|
-
* package version is the corpus version.
|
|
10
|
+
* A rule carries `defaultAction` (what the guard does when it matches) and no
|
|
11
|
+
* `version`; the package version is the corpus version.
|
|
34
12
|
*/
|
|
35
13
|
|
|
36
14
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* This tuple mirrors `shared/src/types/common.ts` `Severity` and `risk.ts`
|
|
40
|
-
* `RISK_SEVERITIES` exactly, and the parity test pins them together so they
|
|
41
|
-
* cannot drift. Matching the product means the hosted catalog layers its own
|
|
42
|
-
* metadata by rule id with no translation table in between (spec 8.3).
|
|
15
|
+
* The five severities, in order, most→least severe.
|
|
43
16
|
*
|
|
44
17
|
* `info` earns its place: five of the eight packs default to `ask`/`warn`, and a
|
|
45
18
|
* warn-only rule labelled `low` overstates itself.
|
|
46
19
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* These same five values key `DEFAULT_PER_INCIDENT_CENTS` in the private
|
|
50
|
-
* `risk.ts` — `critical` is $160,000. Because the tuples now line up perfectly,
|
|
51
|
-
* joining severity to money is one line. **Do not.** The guard prints no
|
|
52
|
-
* currency anywhere (Appendix 0 #12, spec 9.1), and this package therefore ships
|
|
53
|
-
* NO mapping from severity to money. The reason is on the record: a slightly
|
|
54
|
-
* loose rule once produced the headline "rm -rf seen 17 times, $5,070,000",
|
|
55
|
-
* built entirely out of a test suite deleting its own scratch folders.
|
|
20
|
+
* Severity says how serious a match is. This package maps it to nothing else,
|
|
21
|
+
* including cost: the guard prints no currency.
|
|
56
22
|
*/
|
|
57
23
|
declare const SEVERITIES: readonly ["critical", "high", "medium", "low", "info"];
|
|
58
24
|
/** How dangerous the thing a rule catches is. An authoring signal, not a price. */
|
|
@@ -73,10 +39,6 @@ declare const DETAIL_MATCHES_MAX_PATTERNS = 10;
|
|
|
73
39
|
/**
|
|
74
40
|
* Reject a regex that can backtrack catastrophically, at PARSE time.
|
|
75
41
|
*
|
|
76
|
-
* Mirrored from the engine's `hasNestedUnboundedQuantifier`, and pinned to it by
|
|
77
|
-
* the parity test — including behavior over a shared pattern corpus, not just
|
|
78
|
-
* the fact that a function exists.
|
|
79
|
-
*
|
|
80
42
|
* Node has no regex timeout, so the only real bounds are a non-backtracking
|
|
81
43
|
* engine (RE2 — a runtime dependency deliberately not taken) or a syntactic
|
|
82
44
|
* restriction. This is the syntactic one: it rejects **star height >= 2**, an
|
|
@@ -84,11 +46,9 @@ declare const DETAIL_MATCHES_MAX_PATTERNS = 10;
|
|
|
84
46
|
* quantifier — `(a+)+`, `(a*)*`, `(\s+x?)+` — the shape behind essentially every
|
|
85
47
|
* practical ReDoS.
|
|
86
48
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* through. A slow rule on the hosted PDP is a slow rule; a slow rule in the
|
|
91
|
-
* guard is a disabled rule.
|
|
49
|
+
* The guard's hook has a 10s Claude Code timeout, no internal watchdog, and it
|
|
50
|
+
* **fails open**: a pattern that backtracks does not merely make enforcement slow,
|
|
51
|
+
* it lets the action through. A slow rule in the guard is a disabled rule.
|
|
92
52
|
*
|
|
93
53
|
* HONEST LIMIT: this is a bound, not a proof. Star height 1 can still be
|
|
94
54
|
* quadratic (`(a|a)*`), which is slow but not exponential, and is acceptable
|
|
@@ -115,7 +75,7 @@ declare function hasNestedUnboundedQuantifier(source: string): boolean;
|
|
|
115
75
|
*
|
|
116
76
|
* ── Two engine fields are deliberately absent ────────────────────────────────
|
|
117
77
|
*
|
|
118
|
-
* `numeric` is BANNED
|
|
78
|
+
* `numeric` is BANNED. At guard decision time `tokens`,
|
|
119
79
|
* `cachedTokens` and `durationMs` are all 0, because the action has not run yet.
|
|
120
80
|
* `gt` is therefore always false — the rule can never fire — and, worse, **`lt`
|
|
121
81
|
* is always true, so the rule fires on every single command.** Omitting the key
|
|
@@ -131,8 +91,7 @@ declare function hasNestedUnboundedQuantifier(source: string): boolean;
|
|
|
131
91
|
* A command matcher (`detail_contains` / `detail_matches`) and a file matcher
|
|
132
92
|
* (`file_glob`) in ONE condition can never both be satisfied: the guard's mapper
|
|
133
93
|
* is an if/else chain, so no real tool call ever carries both a command and a
|
|
134
|
-
* file path.
|
|
135
|
-
* which is a deliberate divergence the parity test knows about.
|
|
94
|
+
* file path. This schema refuses the combination.
|
|
136
95
|
*/
|
|
137
96
|
declare const MatchConditionSchema: z.ZodEffects<z.ZodObject<{
|
|
138
97
|
kind: z.ZodString;
|
|
@@ -362,32 +321,24 @@ declare const MatchSchema: z.ZodEffects<z.ZodObject<{
|
|
|
362
321
|
*
|
|
363
322
|
* A bare string is shorthand for `{ tool: "Bash", command: "<string>" }`, which
|
|
364
323
|
* covers most rules and keeps them terse. Anything else is an object with a
|
|
365
|
-
* `tool` plus **exactly one** of `command` or `file_path` — the two channels
|
|
366
|
-
*
|
|
324
|
+
* `tool` plus **exactly one** of `command` or `file_path` — the two channels.
|
|
325
|
+
* `.strict()` on each variant is what makes "exactly one" true: a
|
|
367
326
|
* `command` object carrying a `file_path` matches neither variant.
|
|
368
327
|
*
|
|
369
328
|
* The parse output is always normalized to the object form, so the harness has
|
|
370
329
|
* exactly one shape to feed the evaluator.
|
|
371
330
|
*
|
|
372
|
-
* ── There is no `url` channel
|
|
373
|
-
*
|
|
374
|
-
* Spec 7.2 permitted `{ tool: "WebFetch", url: … }` until 2026-09-07. It was
|
|
375
|
-
* removed (PR #532) because **the engine cannot see a URL**: the only two
|
|
376
|
-
* attribute keys any matcher reads are `detail` and `file_path`, and there is no
|
|
377
|
-
* `url` field in the condition schema. A value written to `url` is read by
|
|
378
|
-
* nothing, so a `WebFetch` rule would have matched nothing, forever, silently —
|
|
379
|
-
* exactly the failure `scope` is banned for.
|
|
331
|
+
* ── There is no `url` channel ─────────────────────────────────────────────────
|
|
380
332
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* channel.
|
|
333
|
+
* **The evaluator cannot see a URL**: the only two attribute keys any matcher
|
|
334
|
+
* reads are `detail` and `file_path`, and there is no `url` field in the condition
|
|
335
|
+
* schema. A value written to `url` would be read by nothing, so a `WebFetch` rule
|
|
336
|
+
* would match nothing, silently — the same failure `scope` is banned for. There
|
|
337
|
+
* are no website rules and `WebFetch` is not intercepted; `WebSearch` is, because
|
|
338
|
+
* its query is text on the command channel.
|
|
387
339
|
*
|
|
388
|
-
* A `url` key therefore fails to parse
|
|
389
|
-
*
|
|
390
|
-
* working from a cached reading of the old spec will write exactly it.
|
|
340
|
+
* A `url` key therefore fails to parse rather than becoming a fixture that tests
|
|
341
|
+
* nothing. `__tests__/schema.test.ts` pins that.
|
|
391
342
|
*/
|
|
392
343
|
declare const FixtureSchema: z.ZodUnion<[z.ZodEffects<z.ZodString, {
|
|
393
344
|
tool: "Bash";
|
|
@@ -420,7 +371,7 @@ declare const FixtureSchema: z.ZodUnion<[z.ZodEffects<z.ZodString, {
|
|
|
420
371
|
* guard denies" would fail every `warn` rule and make the negative fixture
|
|
421
372
|
* vacuous for them.
|
|
422
373
|
*
|
|
423
|
-
* Both are `.min(1)`:
|
|
374
|
+
* Both are `.min(1)`: CI only has to fail on a rule missing a
|
|
424
375
|
* negative fixture, but requiring it here moves that failure to authoring time,
|
|
425
376
|
* where it costs a keystroke instead of a pull-request round trip.
|
|
426
377
|
*/
|
|
@@ -509,21 +460,16 @@ declare const FixturesSchema: z.ZodObject<{
|
|
|
509
460
|
/**
|
|
510
461
|
* One published rule.
|
|
511
462
|
*
|
|
512
|
-
* `
|
|
513
|
-
*
|
|
514
|
-
* fiction by its own docstring — shipping invented adoption numbers in an
|
|
515
|
-
* open-source security tool becomes the story. `.strict()` turns either one into
|
|
516
|
-
* a parse error rather than a field that quietly rides along into the public
|
|
517
|
-
* package.
|
|
463
|
+
* `.strict()` rejects any key not declared here, so an unknown field fails to parse
|
|
464
|
+
* rather than riding along into the published package.
|
|
518
465
|
*
|
|
519
|
-
* `description` carries the rule's
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* validator can tell an honest limit from a sentence shaped like one.
|
|
466
|
+
* `description` carries the rule's COVERAGE LIMITS, verbatim — e.g. that an
|
|
467
|
+
* `rm -rf` rule misses `rm -fr`. It is a convention rather than a schema
|
|
468
|
+
* constraint, because no validator can tell a real limit from a sentence shaped
|
|
469
|
+
* like one.
|
|
524
470
|
*/
|
|
525
471
|
declare const RuleSchema: z.ZodObject<{
|
|
526
|
-
/** Stable
|
|
472
|
+
/** Stable: a rule id is never renamed. */
|
|
527
473
|
id: z.ZodString;
|
|
528
474
|
/** The pack this rule belongs to. Validated against `PACKS` by the registry. */
|
|
529
475
|
category: z.ZodString;
|
|
@@ -916,12 +862,9 @@ type Rule = z.output<typeof RuleSchema>;
|
|
|
916
862
|
/**
|
|
917
863
|
* Validate a rule from unknown input.
|
|
918
864
|
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
* private package, so it runs in our CI on the pull request. A contributor gets
|
|
923
|
-
* the schema here and the real answer there, which is where it has to run to be
|
|
924
|
-
* trusted anyway.
|
|
865
|
+
* It answers "is this a well-formed rule?" — it does NOT answer "does this rule
|
|
866
|
+
* actually fire on the command you think it does." That needs the evaluator, which
|
|
867
|
+
* is not part of this package.
|
|
925
868
|
*
|
|
926
869
|
* @param input - Unknown input, e.g. parsed JSON or a rule module's default export.
|
|
927
870
|
* @returns A Zod SafeParseReturnType — check `.success` before reading `.data`.
|
package/dist/schema.d.ts
CHANGED
|
@@ -3,56 +3,22 @@ import { z } from 'zod';
|
|
|
3
3
|
/**
|
|
4
4
|
* The guardrails rule format — self-contained, and deliberately so.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* This file defines the rule format and imports nothing but zod, so the package
|
|
7
|
+
* builds on its own. The condition and match shapes mirror the evaluator's rather
|
|
8
|
+
* than importing them; a change to either has to be made in both places.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* does not build for anyone outside this repo, and open source whose source does
|
|
11
|
-
* not compile is the first thing this audience checks
|
|
12
|
-
* (AGENTTRAIL-GUARD-SPEC.md 2.1).
|
|
13
|
-
*
|
|
14
|
-
* So this file mirrors the engine's `MatchCondition` / `Match` rather than
|
|
15
|
-
* importing them, and `@agenttrail/guardrails` declares NO dependency on either
|
|
16
|
-
* package — not even a dev one. That is not a convention anyone has to remember:
|
|
17
|
-
* pnpm does not hoist workspace packages, so a stray import here fails to
|
|
18
|
-
* resolve at typecheck and build time. A `MODULE_NOT_FOUND` cannot be argued
|
|
19
|
-
* with, and a lint rule over source text can — this repo has already had comment
|
|
20
|
-
* prose parsed as an import.
|
|
21
|
-
*
|
|
22
|
-
* The price of owning a second copy is drift, so a parity test that lives with
|
|
23
|
-
* the policy engine pins this schema to the engine's own:
|
|
24
|
-
* `guardrails-schema-parity.test.ts`.
|
|
25
|
-
* It is not optional and not redundant. Read it before changing anything here.
|
|
26
|
-
*
|
|
27
|
-
* ── What this is NOT ─────────────────────────────────────────────────────────
|
|
28
|
-
*
|
|
29
|
-
* This is not the engine's `PolicyPredicate`. That schema is `.strict()` with a
|
|
30
|
-
* required `version` and `action`, so it rejects 100% of this rule format — it
|
|
31
|
-
* is the schema of the `match` SUB-OBJECT, not of a rule. A rule carries
|
|
32
|
-
* `defaultAction` (what the guard does when it matches) and no `version`; the
|
|
33
|
-
* package version is the corpus version.
|
|
10
|
+
* A rule carries `defaultAction` (what the guard does when it matches) and no
|
|
11
|
+
* `version`; the package version is the corpus version.
|
|
34
12
|
*/
|
|
35
13
|
|
|
36
14
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* This tuple mirrors `shared/src/types/common.ts` `Severity` and `risk.ts`
|
|
40
|
-
* `RISK_SEVERITIES` exactly, and the parity test pins them together so they
|
|
41
|
-
* cannot drift. Matching the product means the hosted catalog layers its own
|
|
42
|
-
* metadata by rule id with no translation table in between (spec 8.3).
|
|
15
|
+
* The five severities, in order, most→least severe.
|
|
43
16
|
*
|
|
44
17
|
* `info` earns its place: five of the eight packs default to `ask`/`warn`, and a
|
|
45
18
|
* warn-only rule labelled `low` overstates itself.
|
|
46
19
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* These same five values key `DEFAULT_PER_INCIDENT_CENTS` in the private
|
|
50
|
-
* `risk.ts` — `critical` is $160,000. Because the tuples now line up perfectly,
|
|
51
|
-
* joining severity to money is one line. **Do not.** The guard prints no
|
|
52
|
-
* currency anywhere (Appendix 0 #12, spec 9.1), and this package therefore ships
|
|
53
|
-
* NO mapping from severity to money. The reason is on the record: a slightly
|
|
54
|
-
* loose rule once produced the headline "rm -rf seen 17 times, $5,070,000",
|
|
55
|
-
* built entirely out of a test suite deleting its own scratch folders.
|
|
20
|
+
* Severity says how serious a match is. This package maps it to nothing else,
|
|
21
|
+
* including cost: the guard prints no currency.
|
|
56
22
|
*/
|
|
57
23
|
declare const SEVERITIES: readonly ["critical", "high", "medium", "low", "info"];
|
|
58
24
|
/** How dangerous the thing a rule catches is. An authoring signal, not a price. */
|
|
@@ -73,10 +39,6 @@ declare const DETAIL_MATCHES_MAX_PATTERNS = 10;
|
|
|
73
39
|
/**
|
|
74
40
|
* Reject a regex that can backtrack catastrophically, at PARSE time.
|
|
75
41
|
*
|
|
76
|
-
* Mirrored from the engine's `hasNestedUnboundedQuantifier`, and pinned to it by
|
|
77
|
-
* the parity test — including behavior over a shared pattern corpus, not just
|
|
78
|
-
* the fact that a function exists.
|
|
79
|
-
*
|
|
80
42
|
* Node has no regex timeout, so the only real bounds are a non-backtracking
|
|
81
43
|
* engine (RE2 — a runtime dependency deliberately not taken) or a syntactic
|
|
82
44
|
* restriction. This is the syntactic one: it rejects **star height >= 2**, an
|
|
@@ -84,11 +46,9 @@ declare const DETAIL_MATCHES_MAX_PATTERNS = 10;
|
|
|
84
46
|
* quantifier — `(a+)+`, `(a*)*`, `(\s+x?)+` — the shape behind essentially every
|
|
85
47
|
* practical ReDoS.
|
|
86
48
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* through. A slow rule on the hosted PDP is a slow rule; a slow rule in the
|
|
91
|
-
* guard is a disabled rule.
|
|
49
|
+
* The guard's hook has a 10s Claude Code timeout, no internal watchdog, and it
|
|
50
|
+
* **fails open**: a pattern that backtracks does not merely make enforcement slow,
|
|
51
|
+
* it lets the action through. A slow rule in the guard is a disabled rule.
|
|
92
52
|
*
|
|
93
53
|
* HONEST LIMIT: this is a bound, not a proof. Star height 1 can still be
|
|
94
54
|
* quadratic (`(a|a)*`), which is slow but not exponential, and is acceptable
|
|
@@ -115,7 +75,7 @@ declare function hasNestedUnboundedQuantifier(source: string): boolean;
|
|
|
115
75
|
*
|
|
116
76
|
* ── Two engine fields are deliberately absent ────────────────────────────────
|
|
117
77
|
*
|
|
118
|
-
* `numeric` is BANNED
|
|
78
|
+
* `numeric` is BANNED. At guard decision time `tokens`,
|
|
119
79
|
* `cachedTokens` and `durationMs` are all 0, because the action has not run yet.
|
|
120
80
|
* `gt` is therefore always false — the rule can never fire — and, worse, **`lt`
|
|
121
81
|
* is always true, so the rule fires on every single command.** Omitting the key
|
|
@@ -131,8 +91,7 @@ declare function hasNestedUnboundedQuantifier(source: string): boolean;
|
|
|
131
91
|
* A command matcher (`detail_contains` / `detail_matches`) and a file matcher
|
|
132
92
|
* (`file_glob`) in ONE condition can never both be satisfied: the guard's mapper
|
|
133
93
|
* is an if/else chain, so no real tool call ever carries both a command and a
|
|
134
|
-
* file path.
|
|
135
|
-
* which is a deliberate divergence the parity test knows about.
|
|
94
|
+
* file path. This schema refuses the combination.
|
|
136
95
|
*/
|
|
137
96
|
declare const MatchConditionSchema: z.ZodEffects<z.ZodObject<{
|
|
138
97
|
kind: z.ZodString;
|
|
@@ -362,32 +321,24 @@ declare const MatchSchema: z.ZodEffects<z.ZodObject<{
|
|
|
362
321
|
*
|
|
363
322
|
* A bare string is shorthand for `{ tool: "Bash", command: "<string>" }`, which
|
|
364
323
|
* covers most rules and keeps them terse. Anything else is an object with a
|
|
365
|
-
* `tool` plus **exactly one** of `command` or `file_path` — the two channels
|
|
366
|
-
*
|
|
324
|
+
* `tool` plus **exactly one** of `command` or `file_path` — the two channels.
|
|
325
|
+
* `.strict()` on each variant is what makes "exactly one" true: a
|
|
367
326
|
* `command` object carrying a `file_path` matches neither variant.
|
|
368
327
|
*
|
|
369
328
|
* The parse output is always normalized to the object form, so the harness has
|
|
370
329
|
* exactly one shape to feed the evaluator.
|
|
371
330
|
*
|
|
372
|
-
* ── There is no `url` channel
|
|
373
|
-
*
|
|
374
|
-
* Spec 7.2 permitted `{ tool: "WebFetch", url: … }` until 2026-09-07. It was
|
|
375
|
-
* removed (PR #532) because **the engine cannot see a URL**: the only two
|
|
376
|
-
* attribute keys any matcher reads are `detail` and `file_path`, and there is no
|
|
377
|
-
* `url` field in the condition schema. A value written to `url` is read by
|
|
378
|
-
* nothing, so a `WebFetch` rule would have matched nothing, forever, silently —
|
|
379
|
-
* exactly the failure `scope` is banned for.
|
|
331
|
+
* ── There is no `url` channel ─────────────────────────────────────────────────
|
|
380
332
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* channel.
|
|
333
|
+
* **The evaluator cannot see a URL**: the only two attribute keys any matcher
|
|
334
|
+
* reads are `detail` and `file_path`, and there is no `url` field in the condition
|
|
335
|
+
* schema. A value written to `url` would be read by nothing, so a `WebFetch` rule
|
|
336
|
+
* would match nothing, silently — the same failure `scope` is banned for. There
|
|
337
|
+
* are no website rules and `WebFetch` is not intercepted; `WebSearch` is, because
|
|
338
|
+
* its query is text on the command channel.
|
|
387
339
|
*
|
|
388
|
-
* A `url` key therefore fails to parse
|
|
389
|
-
*
|
|
390
|
-
* working from a cached reading of the old spec will write exactly it.
|
|
340
|
+
* A `url` key therefore fails to parse rather than becoming a fixture that tests
|
|
341
|
+
* nothing. `__tests__/schema.test.ts` pins that.
|
|
391
342
|
*/
|
|
392
343
|
declare const FixtureSchema: z.ZodUnion<[z.ZodEffects<z.ZodString, {
|
|
393
344
|
tool: "Bash";
|
|
@@ -420,7 +371,7 @@ declare const FixtureSchema: z.ZodUnion<[z.ZodEffects<z.ZodString, {
|
|
|
420
371
|
* guard denies" would fail every `warn` rule and make the negative fixture
|
|
421
372
|
* vacuous for them.
|
|
422
373
|
*
|
|
423
|
-
* Both are `.min(1)`:
|
|
374
|
+
* Both are `.min(1)`: CI only has to fail on a rule missing a
|
|
424
375
|
* negative fixture, but requiring it here moves that failure to authoring time,
|
|
425
376
|
* where it costs a keystroke instead of a pull-request round trip.
|
|
426
377
|
*/
|
|
@@ -509,21 +460,16 @@ declare const FixturesSchema: z.ZodObject<{
|
|
|
509
460
|
/**
|
|
510
461
|
* One published rule.
|
|
511
462
|
*
|
|
512
|
-
* `
|
|
513
|
-
*
|
|
514
|
-
* fiction by its own docstring — shipping invented adoption numbers in an
|
|
515
|
-
* open-source security tool becomes the story. `.strict()` turns either one into
|
|
516
|
-
* a parse error rather than a field that quietly rides along into the public
|
|
517
|
-
* package.
|
|
463
|
+
* `.strict()` rejects any key not declared here, so an unknown field fails to parse
|
|
464
|
+
* rather than riding along into the published package.
|
|
518
465
|
*
|
|
519
|
-
* `description` carries the rule's
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* validator can tell an honest limit from a sentence shaped like one.
|
|
466
|
+
* `description` carries the rule's COVERAGE LIMITS, verbatim — e.g. that an
|
|
467
|
+
* `rm -rf` rule misses `rm -fr`. It is a convention rather than a schema
|
|
468
|
+
* constraint, because no validator can tell a real limit from a sentence shaped
|
|
469
|
+
* like one.
|
|
524
470
|
*/
|
|
525
471
|
declare const RuleSchema: z.ZodObject<{
|
|
526
|
-
/** Stable
|
|
472
|
+
/** Stable: a rule id is never renamed. */
|
|
527
473
|
id: z.ZodString;
|
|
528
474
|
/** The pack this rule belongs to. Validated against `PACKS` by the registry. */
|
|
529
475
|
category: z.ZodString;
|
|
@@ -916,12 +862,9 @@ type Rule = z.output<typeof RuleSchema>;
|
|
|
916
862
|
/**
|
|
917
863
|
* Validate a rule from unknown input.
|
|
918
864
|
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
* private package, so it runs in our CI on the pull request. A contributor gets
|
|
923
|
-
* the schema here and the real answer there, which is where it has to run to be
|
|
924
|
-
* trusted anyway.
|
|
865
|
+
* It answers "is this a well-formed rule?" — it does NOT answer "does this rule
|
|
866
|
+
* actually fire on the command you think it does." That needs the evaluator, which
|
|
867
|
+
* is not part of this package.
|
|
925
868
|
*
|
|
926
869
|
* @param input - Unknown input, e.g. parsed JSON or a rule module's default export.
|
|
927
870
|
* @returns A Zod SafeParseReturnType — check `.success` before reading `.data`.
|
package/dist/schema.js
CHANGED