@scopebond/hook 0.6.0 → 0.8.0

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.
Files changed (50) hide show
  1. package/README.md +123 -13
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +825 -92
  5. package/dist/cli.js.map +1 -1
  6. package/dist/cloud.d.ts +2 -0
  7. package/dist/cloud.d.ts.map +1 -1
  8. package/dist/cloud.js +1 -1
  9. package/dist/cloud.js.map +1 -1
  10. package/dist/explain.d.ts +44 -0
  11. package/dist/explain.d.ts.map +1 -0
  12. package/dist/explain.js +75 -0
  13. package/dist/explain.js.map +1 -0
  14. package/dist/index.d.ts +11 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/init.d.ts +41 -4
  19. package/dist/init.d.ts.map +1 -1
  20. package/dist/init.js +104 -21
  21. package/dist/init.js.map +1 -1
  22. package/dist/install.d.ts +70 -2
  23. package/dist/install.d.ts.map +1 -1
  24. package/dist/install.js +262 -10
  25. package/dist/install.js.map +1 -1
  26. package/dist/map.d.ts +6 -0
  27. package/dist/map.d.ts.map +1 -1
  28. package/dist/map.js +4 -1
  29. package/dist/map.js.map +1 -1
  30. package/dist/minimize.d.ts +11 -3
  31. package/dist/minimize.d.ts.map +1 -1
  32. package/dist/minimize.js +35 -6
  33. package/dist/minimize.js.map +1 -1
  34. package/dist/rules.d.ts +51 -0
  35. package/dist/rules.d.ts.map +1 -0
  36. package/dist/rules.js +216 -0
  37. package/dist/rules.js.map +1 -0
  38. package/dist/runtime-install.d.ts +25 -0
  39. package/dist/runtime-install.d.ts.map +1 -0
  40. package/dist/runtime-install.js +106 -0
  41. package/dist/runtime-install.js.map +1 -0
  42. package/dist/runtime.d.ts +13 -0
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +52 -7
  45. package/dist/runtime.js.map +1 -1
  46. package/dist/version.d.ts +5 -1
  47. package/dist/version.d.ts.map +1 -1
  48. package/dist/version.js +6 -2
  49. package/dist/version.js.map +1 -1
  50. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"minimize.js","sourceRoot":"","sources":["../src/minimize.ts"],"names":[],"mappings":"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","sourcesContent":["// Data minimization for the hook: file contents are never stored, shell commands\n// are reduced to a scrubbed head plus a digest, and common secret shapes are\n// removed before anything is written or signed (SB09).\n\nimport { createHash } from \"node:crypto\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\n\nexport const sha256 = (value: string): string => \"sha256:\" + createHash(\"sha256\").update(value).digest(\"hex\");\nexport const digest = (value: unknown): string => sha256(canonical(value as never));\n\nconst MASK = \"***\";\n// A flag or header value: a quoted string or a run of non-whitespace.\nconst VALUE = String.raw`(\"[^\"]*\"|'[^']*'|\\S+)`;\n\n// Each rule states its own replacement. `$1` keeps a non-secret label (a flag or\n// header name); the secret itself is always dropped whole. Over-scrubbing is safe.\n// Every pattern is a single pass over simple character classes (no nested\n// quantifiers), so matching stays linear in the command length.\nconst SECRET_RULES: ReadonlyArray<readonly [RegExp, string]> = [\n [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, MASK],\n [new RegExp(String.raw`(--?(?:password|passwd|pwd|token|secret|api[-_]?key|access[-_]?key|auth|credentials?)[=\\s]+)${VALUE}`, \"gi\"), `$1${MASK}`],\n [new RegExp(String.raw`((?:^|\\s)(?:-u|--user)[=\\s]+)${VALUE}`, \"g\"), `$1${MASK}`],\n [/(Authorization:\\s*(?:Bearer|Basic|Token)\\s+)[^\\s\"']+/gi, `$1${MASK}`],\n [/((?:x-api-key|api-key|x-auth-token|x-access-token|private-token)\\s*:\\s*)[^\\s\"']+/gi, `$1${MASK}`],\n [/(:\\/\\/)[^\\s/@:]+:[^\\s/@]+@/g, `$1${MASK}@`], // URL userinfo\n [/\\bgh[pousr]_[A-Za-z0-9]{20,}/g, MASK], // GitHub tokens\n [/\\bgithub_pat_[A-Za-z0-9_]{20,}/g, MASK], // GitHub fine-grained tokens\n [/\\bglpat-[A-Za-z0-9_-]{20,}/g, MASK], // GitLab tokens\n [/\\bnpm_[A-Za-z0-9]{20,}/g, MASK], // npm tokens\n [/\\bxox[abeoprs]-[A-Za-z0-9-]{10,}/g, MASK], // Slack tokens\n [/\\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g, MASK], // Stripe keys\n [/\\bsk-[A-Za-z0-9_-]{20,}/g, MASK], // OpenAI/Anthropic-style keys\n [/\\bAIza[0-9A-Za-z_-]{30,}/g, MASK], // Google API keys\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, MASK], // AWS access key ids\n [/\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}/g, MASK], // JWTs\n];\n\n// Free-text-only rules (the command head), never applied to structured parameters a\n// policy matches on. These cover common shapes that carry a secret on argv without a\n// recognizable token format:\n// - an attached `-p`/`-u` value (`mysql -phunter2`, `psql -uadmin`) — the single most\n// common way a password reaches a shell. A space-separated `-p value` (the mkdir\n// flag, or `-u origin`) is not attached and is left alone; the `--password`/`--user`\n// forms are handled by the labelled rules above.\n// - a header whose name looks like a credential (`X-Custom-Secret: …`, `My-Token: …`).\n// Bare `key:` is deliberately excluded so ordinary `key: value` text is untouched.\nconst HEAD_RULES: ReadonlyArray<readonly [RegExp, string]> = [\n [/(^|\\s)(-[pu])[^\\s=-]\\S*/g, `$1$2${MASK}`],\n // `auth` is omitted: `Authorization:` is handled above (with its Bearer/Basic label),\n // and other auth headers (`X-Auth-Token`) still match on `token`.\n [/(\\b[\\w-]*(?:secret|token|api[-_]?key|apikey|password|passwd|credential)[\\w-]*\\s*:\\s*)[^\\s\"']+/gi, `$1${MASK}`],\n];\n\n// Generic high-entropy shapes. Applied to free text only (the command head), never to\n// structured parameters a policy matches on: a long path or a commit id is not a secret.\nconst BLOB_RULES: ReadonlyArray<readonly [RegExp, string]> = [\n [/\\b[A-Fa-f0-9]{32,}\\b/g, MASK], // long hex blobs\n [/[A-Za-z0-9+/_-]{40,}={0,2}/g, MASK], // long base64/base64url blobs\n];\n\nconst SECRET_NAME = /token|secret|passw|pwd|api_?key|access_?key|private_?key|auth|credential|session/i;\n\n// `NAME=value` where the name looks like a credential (`GH_TOKEN=…`, `export\n// AWS_SECRET_ACCESS_KEY=…`). Done per whitespace-delimited word rather than with\n// one regex so a long identifier cannot cause backtracking.\nfunction scrubAssignments(text: string): string {\n return text.replace(/\\S+/g, (word) => {\n const eq = word.indexOf(\"=\");\n if (eq <= 0 || eq === word.length - 1) return word;\n const name = word.slice(0, eq);\n if (name.startsWith(\"-\") || !SECRET_NAME.test(name)) return word; // flags are handled by SECRET_RULES\n return `${name}=${MASK}`;\n });\n}\n\n/** Scrub a structured parameter (a program name, a remote, a URL path): credential\n * assignments and recognizable token shapes only, so policy matching on ordinary\n * values is unaffected. */\nexport function scrubParam(text: string): string {\n let out = scrubAssignments(text);\n for (const [re, replacement] of SECRET_RULES) out = out.replace(re, replacement);\n return out;\n}\n\n/** Scrub free text: everything `scrubParam` removes, plus the free-text-only argv and\n * header shapes and generic high-entropy blobs. */\nexport function scrubSecrets(text: string): string {\n let out = scrubParam(text);\n for (const [re, replacement] of HEAD_RULES) out = out.replace(re, replacement);\n for (const [re, replacement] of BLOB_RULES) out = out.replace(re, replacement);\n return out;\n}\n\n/** A privacy-preserving representation of a shell command: a scrubbed, truncated head\n * plus a digest of the scrubbed command. The full original is never retained, and the\n * digest is taken over the scrubbed text so a secret the head removed cannot be\n * brute-forced back from the digest. */\nexport function redactCommand(command: string, headLen = 64): string {\n const scrubbed = scrubSecrets(command);\n const head = scrubbed.length > headLen ? `${scrubbed.slice(0, headLen)}…` : scrubbed;\n return `${head} (${sha256(scrubbed)})`;\n}\n"]}
1
+ {"version":3,"file":"minimize.js","sourceRoot":"","sources":["../src/minimize.ts"],"names":[],"mappings":"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","sourcesContent":["// Data minimization for the hook: file contents are never stored, shell commands\n// are reduced to a scrubbed head plus a digest, and common secret shapes are\n// removed before anything is written or signed (SB09).\n\nimport { createHash, createHmac, randomBytes } from \"node:crypto\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\n\n/** A plain SHA-256 content hash, for values that are not secret. */\nexport const sha256 = (value: string): string => \"sha256:\" + createHash(\"sha256\").update(value).digest(\"hex\");\n\n// Digests of what an agent ran — a scrubbed shell command, an MCP tool's arguments — are\n// keyed. The scrubber cannot recognise every secret, and a plain hash of a command whose\n// head is shown beside it leaves only the unseen remainder to guess: a short password the\n// scrubber missed could be recovered offline by anyone holding the receipt. Keyed with a\n// per-machine secret that never leaves `.scopebond/`, the digest still tells two identical\n// actions on this machine apart from different ones, but cannot be tested against guesses.\nlet digestKey: Buffer | null = null;\n\n/** Set the key for keyed digests (32 random bytes, hex). Without one, a random key for\n * this process is used — safe, but digests then compare only within the process. */\nexport function useDigestKey(key: string | null): void {\n digestKey = key ? Buffer.from(key, \"hex\") : null;\n}\n\n/** The per-machine digest key in a config dir, created on first use. */\nexport function loadOrCreateDigestKey(dir: string): string {\n const file = join(dir, \"digest.key\");\n if (existsSync(file)) {\n const key = readFileSync(file, \"utf8\").trim();\n if (/^[0-9a-f]{64}$/.test(key)) return key;\n }\n const key = randomBytes(32).toString(\"hex\");\n writeFileSync(file, key + \"\\n\", { mode: 0o600 });\n return key;\n}\n\n/** HMAC-SHA-256 under the digest key, labelled so it is never mistaken for a plain hash. */\nexport const keyedDigest = (value: string): string =>\n \"hmac-sha256:\" + createHmac(\"sha256\", (digestKey ??= randomBytes(32))).update(value).digest(\"hex\");\nexport const digest = (value: unknown): string => keyedDigest(canonical(value as never));\n\nconst MASK = \"***\";\n// A flag or header value: a quoted string or a run of non-whitespace.\nconst VALUE = String.raw`(\"[^\"]*\"|'[^']*'|\\S+)`;\n\n// Each rule states its own replacement. `$1` keeps a non-secret label (a flag or\n// header name); the secret itself is always dropped whole. Over-scrubbing is safe.\n// Every pattern is a single pass over simple character classes (no nested\n// quantifiers), so matching stays linear in the command length.\nconst SECRET_RULES: ReadonlyArray<readonly [RegExp, string]> = [\n [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, MASK],\n [new RegExp(String.raw`(--?(?:password|passwd|pwd|token|secret|api[-_]?key|access[-_]?key|auth|credentials?)[=\\s]+)${VALUE}`, \"gi\"), `$1${MASK}`],\n [new RegExp(String.raw`((?:^|\\s)(?:-u|--user)[=\\s]+)${VALUE}`, \"g\"), `$1${MASK}`],\n [/(Authorization:\\s*(?:Bearer|Basic|Token)\\s+)[^\\s\"']+/gi, `$1${MASK}`],\n [/((?:x-api-key|api-key|x-auth-token|x-access-token|private-token)\\s*:\\s*)[^\\s\"']+/gi, `$1${MASK}`],\n [/(:\\/\\/)[^\\s/@:]+:[^\\s/@]+@/g, `$1${MASK}@`], // URL userinfo\n [/\\bgh[pousr]_[A-Za-z0-9]{20,}/g, MASK], // GitHub tokens\n [/\\bgithub_pat_[A-Za-z0-9_]{20,}/g, MASK], // GitHub fine-grained tokens\n [/\\bglpat-[A-Za-z0-9_-]{20,}/g, MASK], // GitLab tokens\n [/\\bnpm_[A-Za-z0-9]{20,}/g, MASK], // npm tokens\n [/\\bxox[abeoprs]-[A-Za-z0-9-]{10,}/g, MASK], // Slack tokens\n [/\\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g, MASK], // Stripe keys\n [/\\bsk-[A-Za-z0-9_-]{20,}/g, MASK], // OpenAI/Anthropic-style keys\n [/\\bAIza[0-9A-Za-z_-]{30,}/g, MASK], // Google API keys\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, MASK], // AWS access key ids\n [/\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}/g, MASK], // JWTs\n];\n\n// Free-text-only rules (the command head), never applied to structured parameters a\n// policy matches on. These cover common shapes that carry a secret on argv without a\n// recognizable token format:\n// - an attached `-p`/`-u` value (`mysql -phunter2`, `psql -uadmin`) — the single most\n// common way a password reaches a shell. A space-separated `-p value` (the mkdir\n// flag, or `-u origin`) is not attached and is left alone; the `--password`/`--user`\n// forms are handled by the labelled rules above.\n// - a header whose name looks like a credential (`X-Custom-Secret: …`, `My-Token: …`).\n// Bare `key:` is deliberately excluded so ordinary `key: value` text is untouched.\nconst HEAD_RULES: ReadonlyArray<readonly [RegExp, string]> = [\n [/(^|\\s)(-[pu])[^\\s=-]\\S*/g, `$1$2${MASK}`],\n // `auth` is omitted: `Authorization:` is handled above (with its Bearer/Basic label),\n // and other auth headers (`X-Auth-Token`) still match on `token`.\n [/(\\b[\\w-]*(?:secret|token|api[-_]?key|apikey|password|passwd|credential)[\\w-]*\\s*:\\s*)[^\\s\"']+/gi, `$1${MASK}`],\n];\n\n// Generic high-entropy shapes. Applied to free text only (the command head), never to\n// structured parameters a policy matches on: a long path or a commit id is not a secret.\nconst BLOB_RULES: ReadonlyArray<readonly [RegExp, string]> = [\n [/\\b[A-Fa-f0-9]{32,}\\b/g, MASK], // long hex blobs\n [/[A-Za-z0-9+/_-]{40,}={0,2}/g, MASK], // long base64/base64url blobs\n];\n\nconst SECRET_NAME = /token|secret|passw|pwd|api_?key|access_?key|private_?key|auth|credential|session/i;\n\n// `NAME=value` where the name looks like a credential (`GH_TOKEN=…`, `export\n// AWS_SECRET_ACCESS_KEY=…`). Done per whitespace-delimited word rather than with\n// one regex so a long identifier cannot cause backtracking.\nfunction scrubAssignments(text: string): string {\n return text.replace(/\\S+/g, (word) => {\n const eq = word.indexOf(\"=\");\n if (eq <= 0 || eq === word.length - 1) return word;\n const name = word.slice(0, eq);\n if (name.startsWith(\"-\") || !SECRET_NAME.test(name)) return word; // flags are handled by SECRET_RULES\n return `${name}=${MASK}`;\n });\n}\n\n/** Scrub a structured parameter (a program name, a remote, a URL path): credential\n * assignments and recognizable token shapes only, so policy matching on ordinary\n * values is unaffected. */\nexport function scrubParam(text: string): string {\n let out = scrubAssignments(text);\n for (const [re, replacement] of SECRET_RULES) out = out.replace(re, replacement);\n return out;\n}\n\n/** Scrub free text: everything `scrubParam` removes, plus the free-text-only argv and\n * header shapes and generic high-entropy blobs. */\nexport function scrubSecrets(text: string): string {\n let out = scrubParam(text);\n for (const [re, replacement] of HEAD_RULES) out = out.replace(re, replacement);\n for (const [re, replacement] of BLOB_RULES) out = out.replace(re, replacement);\n return out;\n}\n\n/** A privacy-preserving representation of a shell command: a scrubbed, truncated head\n * plus a keyed digest of the scrubbed command. The full original is never retained; the\n * digest is taken over the scrubbed text so a secret the head removed is not in it, and\n * it is keyed so a secret the scrubber missed cannot be guessed back from it. */\nexport function redactCommand(command: string, headLen = 64): string {\n const scrubbed = scrubSecrets(command);\n const head = scrubbed.length > headLen ? `${scrubbed.slice(0, headLen)}…` : scrubbed;\n return `${head} (${keyedDigest(scrubbed)})`;\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /** One protected location. `under`/`named`/`dir` are the readable, editable shapes;
2
+ * `raw` carries the few patterns with bespoke exceptions (`.env` templates are allowed,
3
+ * `.ssh/config` and `*.pub` are allowed) that no simple shape expresses. */
4
+ export type PathRule = {
5
+ kind: "under";
6
+ value: string;
7
+ label: string;
8
+ } | {
9
+ kind: "named";
10
+ value: string;
11
+ label: string;
12
+ } | {
13
+ kind: "dir";
14
+ value: string;
15
+ label: string;
16
+ } | {
17
+ kind: "raw";
18
+ pattern: string;
19
+ label: string;
20
+ };
21
+ export interface RuleSet {
22
+ version: 1;
23
+ /** Refs an agent may not push to. `release/*` means the prefix `release/`. */
24
+ protected_branches: string[];
25
+ /** Programs an agent may not run, in any case and with or without .exe. */
26
+ destructive_programs: string[];
27
+ protected_write: PathRule[];
28
+ protected_read: PathRule[];
29
+ /** Action types recorded but not blocked. */
30
+ observe: string[];
31
+ }
32
+ export declare const RULES_FILE = "rules.json";
33
+ /** A protected-path rule from a plain path the user typed. A trailing `/` (or a path with
34
+ * no dot in its last segment) reads as a directory; anything else as a file name.
35
+ *
36
+ * Normalisation is one obvious step at a time. The previous version folded separator
37
+ * conversion and leading-`./` stripping into a single `^[./\\]+` replace with a callback,
38
+ * which was both hard to read and quadratic on a path of many leading dots
39
+ * (CodeQL js/polynomial-redos). */
40
+ export declare function pathRuleFor(input: string): PathRule;
41
+ /** Compile a rule set into the policy the gateway evaluates. */
42
+ export declare function compile(rules: RuleSet, agentKid: string): Record<string, unknown>;
43
+ /** The shipped defaults. Every entry here reproduces one piece of the starter policy's
44
+ * patterns; `rules.test.mjs` pins that equivalence. */
45
+ export declare function defaultRules(): RuleSet;
46
+ export declare const rulesPath: (dir: string) => string;
47
+ export declare function loadRules(configDir: string): RuleSet | null;
48
+ export declare function saveRules(configDir: string, rules: RuleSet): string;
49
+ /** Plain English, for `scopebond-hook rules`. */
50
+ export declare function describeRules(rules: RuleSet): string;
51
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAsBA;;6EAE6E;AAC7E,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,CAAC,CAAC;IACX,8EAA8E;IAC9E,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,2EAA2E;IAC3E,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,QAAQ,EAAE,CAAC;IAC3B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,UAAU,eAAe,CAAC;AAiBvC;;;;;;oCAMoC;AACpC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAenD;AA6BD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCjF;AAED;wDACwD;AACxD,wBAAgB,YAAY,IAAI,OAAO,CA0DtC;AAED,eAAO,MAAM,SAAS,GAAI,KAAK,MAAM,KAAG,MAA+B,CAAC;AAExE,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAQ3D;AAED,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAInE;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAiBpD"}
package/dist/rules.js ADDED
@@ -0,0 +1,216 @@
1
+ // Rules you can actually read and change.
2
+ //
3
+ // The site says "set rules in plain terms" and "it is a plain JSON file — edit the
4
+ // limits". What `init` wrote was 6.7 KB of generated regular expression: the `safe-shell`
5
+ // clause alone is a ~700-character case-folded negative lookahead. Nobody edits that, so
6
+ // in practice the starter policy was the *only* policy and "set your own rules" was not
7
+ // true.
8
+ //
9
+ // The fix is not to weaken the patterns — they are careful, and the canonicalization
10
+ // tests exist because bypasses are subtle. It is to stop making the pattern the
11
+ // interface. The lists the patterns are built from live in `.scopebond/rules.json`, a
12
+ // short readable file, and `policy.json` is compiled from it. The regex stays as the
13
+ // compiled form.
14
+ //
15
+ // `compile()` reproduces the shipped starter policy's enforcement patterns exactly —
16
+ // `rules.test.mjs` asserts that against `starterPolicy()`, so a change here cannot
17
+ // quietly alter what is enforced.
18
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { ci, under, named, dir, DESTRUCTIVE } from "./runtime.js";
21
+ export const RULES_FILE = "rules.json";
22
+ const pathPattern = (rule) => rule.kind === "under" ? under(rule.value)
23
+ : rule.kind === "named" ? named(rule.value)
24
+ : rule.kind === "dir" ? dir(rule.value)
25
+ : rule.pattern;
26
+ /** Escape every regular-expression metacharacter, backslash included.
27
+ *
28
+ * A path the user typed is literal text, and this value goes straight into a rule that
29
+ * decides what the agent may touch: if one metacharacter escapes escaping, the rule
30
+ * silently protects something other than what they asked for. The class is therefore
31
+ * complete on its own rather than relying on an earlier step having removed backslashes
32
+ * (CodeQL js/incomplete-sanitization). */
33
+ const escapeRegex = (text) => text.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
34
+ /** A protected-path rule from a plain path the user typed. A trailing `/` (or a path with
35
+ * no dot in its last segment) reads as a directory; anything else as a file name.
36
+ *
37
+ * Normalisation is one obvious step at a time. The previous version folded separator
38
+ * conversion and leading-`./` stripping into a single `^[./\\]+` replace with a callback,
39
+ * which was both hard to read and quadratic on a path of many leading dots
40
+ * (CodeQL js/polynomial-redos). */
41
+ export function pathRuleFor(input) {
42
+ const trimmed = input.trim();
43
+ const withForwardSlashes = trimmed.split("\\").join("/");
44
+ const withoutDotSlash = withForwardSlashes.startsWith("./") ? withForwardSlashes.slice(2) : withForwardSlashes;
45
+ // Drop trailing separators without a quantifier, so the cost is plainly linear.
46
+ let clean = withoutDotSlash;
47
+ while (clean.endsWith("/"))
48
+ clean = clean.slice(0, -1);
49
+ // A trailing slash is an explicit directory; so is a last segment with no extension.
50
+ // Checked by string, not by `/\.[^/]*$/`: that pattern is unanchored at the front, so it
51
+ // retries at every position and goes quadratic on a long path.
52
+ const lastSegment = clean.slice(clean.lastIndexOf("/") + 1);
53
+ const isDirectory = trimmed.endsWith("/") || !lastSegment.includes(".");
54
+ return isDirectory
55
+ ? { kind: "under", value: escapeRegex(clean), label: `${clean}/ and everything in it` }
56
+ : { kind: "named", value: escapeRegex(clean), label: clean };
57
+ }
58
+ /** The branch pattern: deny the listed refs, plus the "every branch at once" flags and a
59
+ * push whose destination could not be read. `--tags` alone is allowed. */
60
+ function branchPattern(branches) {
61
+ const exact = branches.filter((b) => !b.endsWith("/*"));
62
+ const prefixes = branches.filter((b) => b.endsWith("/*")).map((b) => b.slice(0, -1));
63
+ const parts = [
64
+ exact.length ? `(?!(?:${exact.map(ci).join("|")})$)` : "",
65
+ ...prefixes.map((p) => `(?!${ci(p)})`),
66
+ `(?!-(?!-${ci("tags")}$))`,
67
+ ];
68
+ return `^${parts.join("")}.+`;
69
+ }
70
+ const programPattern = (programs) => `^(?!(?:${programs.map(ci).join("|")})(?:${ci("\\.(?:exe|cmd|bat|com|ps1)")})?$).+`;
71
+ const pathsPattern = (rules) => `^${rules.map(pathPattern).join("")}.+`;
72
+ /** A sentence listing what a clause covers, generated from the list rather than written by
73
+ * hand — so it stays true after an edit. The block message quotes this, so a stale
74
+ * description would be a lie told at the worst moment. */
75
+ function sentence(items, limit = 12) {
76
+ const shown = items.slice(0, limit);
77
+ const rest = items.length - shown.length;
78
+ return shown.join(", ") + (rest > 0 ? `, and ${rest} more` : "");
79
+ }
80
+ /** Compile a rule set into the policy the gateway evaluates. */
81
+ export function compile(rules, agentKid) {
82
+ return {
83
+ vocabulary_version: "1.0", policy_id: "coding-agent", version: 1,
84
+ clauses: [
85
+ {
86
+ id: "protect-branches", type: "action_allowlist", mode: "enforce", action_types: ["git.push"],
87
+ param_bounds: { ref: { pattern: branchPattern(rules.protected_branches) } },
88
+ description: `Deny pushes to ${sentence(rules.protected_branches)} (any case, any refspec spelling), pushes of every branch at once (--all, --mirror) and pushes whose destination cannot be read from the command (a git alias, a configured push refspec, send-pack). A tags-only push (--tags) is allowed. Change these in .scopebond/${RULES_FILE}.`,
89
+ },
90
+ {
91
+ id: "safe-shell", type: "action_allowlist", mode: "enforce", action_types: ["shell.exec"],
92
+ param_bounds: { program: { pattern: programPattern(rules.destructive_programs) } },
93
+ description: `Deny destructive programs (${sentence(rules.destructive_programs, 10)}) in any case and with or without .exe. An empty program — a command that could not be parsed, or whose program is only known at run time ($VAR, $(…), eval of a variable) — is denied. Argument-shaped deletion (find -delete, git clean) is not a program name and is not covered here. Change this list in .scopebond/${RULES_FILE}.`,
94
+ },
95
+ {
96
+ id: "protect-write", type: "action_allowlist", mode: "enforce", action_types: ["file.write"],
97
+ param_bounds: { path: { pattern: pathsPattern(rules.protected_write) } },
98
+ description: `Allow workspace writes, but never to ${sentence(rules.protected_write.map((r) => r.label))}. Case-insensitive. Change this list in .scopebond/${RULES_FILE}.`,
99
+ },
100
+ {
101
+ id: "protect-read", type: "action_allowlist", mode: "enforce", action_types: ["file.read"],
102
+ param_bounds: { path: { pattern: pathsPattern(rules.protected_read) } },
103
+ description: `Allow workspace reads, but never ${sentence(rules.protected_read.map((r) => r.label))}. Case-insensitive. Change this list in .scopebond/${RULES_FILE}.`,
104
+ },
105
+ {
106
+ id: "observe-net-mcp", type: "action_allowlist", mode: "monitor",
107
+ action_types: rules.observe,
108
+ description: `Observe ${sentence(rules.observe)} — recorded, not blocked. Add bounds to enforce. Change this list in .scopebond/${RULES_FILE}.`,
109
+ },
110
+ { id: "keys", type: "key_policy", active_keys: [agentKid], description: "Only the enrolled machine key may sign." },
111
+ ],
112
+ };
113
+ }
114
+ /** The shipped defaults. Every entry here reproduces one piece of the starter policy's
115
+ * patterns; `rules.test.mjs` pins that equivalence. */
116
+ export function defaultRules() {
117
+ return {
118
+ version: 1,
119
+ protected_branches: ["main", "master", "release/*"],
120
+ destructive_programs: [...DESTRUCTIVE],
121
+ protected_write: [
122
+ { kind: "under", value: "\\.scopebond", label: "the hook's own policy and keys (.scopebond)" },
123
+ { kind: "raw", pattern: `(?!(?:.*/)?${ci("\\.claude/settings")})`, label: "Claude Code settings" },
124
+ { kind: "under", value: "\\.claude/hooks", label: "Claude Code hooks" },
125
+ { kind: "under", value: "\\.claude/agents", label: "Claude Code agents" },
126
+ { kind: "raw", pattern: `(?!(?:.*/)?${ci("\\.cursor/hooks")})`, label: "Cursor hook settings" },
127
+ { kind: "named", value: "\\.codex/hooks\\.json", label: "Codex hook settings" },
128
+ { kind: "named", value: "\\.codex/config\\.toml", label: "Codex config" },
129
+ { kind: "named", value: "\\.mcp\\.json", label: ".mcp.json" },
130
+ { kind: "under", value: "\\.git/hooks", label: "git hooks" },
131
+ { kind: "named", value: "\\.git/config", label: "git config" },
132
+ { kind: "under", value: "\\.husky", label: "Husky hooks" },
133
+ { kind: "under", value: "\\.github/workflows", label: "GitHub workflows" },
134
+ { kind: "under", value: "\\.github/actions", label: "GitHub actions" },
135
+ { kind: "named", value: "\\.gitlab-ci\\.yml", label: ".gitlab-ci.yml" },
136
+ { kind: "named", value: "\\.gitlab-ci\\.yaml", label: ".gitlab-ci.yaml" },
137
+ { kind: "under", value: "\\.circleci", label: "CircleCI config" },
138
+ { kind: "named", value: "azure-pipelines\\.yml", label: "azure-pipelines.yml" },
139
+ { kind: "named", value: "Jenkinsfile", label: "Jenkinsfile" },
140
+ ],
141
+ protected_read: [
142
+ { kind: "under", value: "\\.scopebond", label: "the hook's own policy and keys (.scopebond)" },
143
+ { kind: "raw", pattern: `(?!.*${ci("\\.(?:key|pem|p12|pfx|jks|keystore)")}$)`, label: "signing keys and key containers (*.key, *.pem, *.p12, *.pfx, *.jks)" },
144
+ {
145
+ kind: "raw",
146
+ pattern: `(?!(?:.*/)?${ci("\\.env")}(?!(?:\\.[^/]*)?\\.(?:${ci("example")}|${ci("sample")}|${ci("template")}|${ci("dist")})$)(?:\\.[^/]*)?$)`,
147
+ label: "environment secret files (.env, .env.* — except .example/.sample/.template/.dist)",
148
+ },
149
+ { kind: "named", value: "\\.envrc", label: ".envrc" },
150
+ {
151
+ kind: "raw",
152
+ pattern: `(?!(?:.*/)?${ci("\\.ssh")}(?:$|/(?!.*${ci("\\.pub")}$)(?!${ci("known_hosts")}$)(?!${ci("config")}$)))`,
153
+ label: "SSH private keys and the .ssh directory (public keys, known_hosts and config are allowed)",
154
+ },
155
+ { kind: "raw", pattern: `(?!(?:.*/)?${ci("\\.aws")}(?:$|/(?!${ci("config")}$)))`, label: "AWS credentials (.aws, except .aws/config)" },
156
+ { kind: "named", value: "\\.npmrc", label: ".npmrc" },
157
+ { kind: "named", value: "\\.pypirc", label: ".pypirc" },
158
+ { kind: "named", value: "\\.netrc", label: ".netrc" },
159
+ { kind: "named", value: "_netrc", label: "_netrc" },
160
+ { kind: "named", value: "\\.git-credentials", label: ".git-credentials" },
161
+ { kind: "dir", value: "\\.kube", label: "the .kube directory" },
162
+ { kind: "named", value: "\\.kube/config", label: ".kube/config" },
163
+ { kind: "dir", value: "\\.docker", label: "the .docker directory" },
164
+ { kind: "named", value: "\\.docker/config\\.json", label: ".docker/config.json" },
165
+ { kind: "under", value: "\\.config/gcloud", label: "gcloud credentials" },
166
+ { kind: "under", value: "\\.azure", label: "Azure credentials" },
167
+ { kind: "under", value: "\\.gnupg", label: "GnuPG keys" },
168
+ { kind: "dir", value: "\\.config/gh", label: "the GitHub CLI config directory" },
169
+ { kind: "named", value: "\\.config/gh/hosts\\.yml", label: "the GitHub CLI's hosts.yml" },
170
+ { kind: "named", value: "\\.claude/\\.credentials\\.json", label: "Claude Code's .credentials.json" },
171
+ ],
172
+ observe: ["net.fetch", "mcp.tool.call"],
173
+ };
174
+ }
175
+ export const rulesPath = (dir) => join(dir, RULES_FILE);
176
+ export function loadRules(configDir) {
177
+ const file = rulesPath(configDir);
178
+ if (!existsSync(file))
179
+ return null;
180
+ try {
181
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
182
+ if (parsed?.version !== 1 || !Array.isArray(parsed.protected_branches))
183
+ return null;
184
+ return parsed;
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ export function saveRules(configDir, rules) {
191
+ const file = rulesPath(configDir);
192
+ writeFileSync(file, `${JSON.stringify(rules, null, 2)}\n`);
193
+ return file;
194
+ }
195
+ /** Plain English, for `scopebond-hook rules`. */
196
+ export function describeRules(rules) {
197
+ const lines = [];
198
+ lines.push("Blocked before it runs:");
199
+ lines.push("");
200
+ lines.push(` pushes to ${rules.protected_branches.join(", ")}`);
201
+ lines.push(` and any push whose destination cannot be read`);
202
+ lines.push(` programs ${rules.destructive_programs.join(", ")}`);
203
+ lines.push("");
204
+ lines.push(` writes to ${rules.protected_write.length} protected location(s):`);
205
+ for (const rule of rules.protected_write)
206
+ lines.push(` ${rule.label}`);
207
+ lines.push("");
208
+ lines.push(` reads of ${rules.protected_read.length} protected location(s):`);
209
+ for (const rule of rules.protected_read)
210
+ lines.push(` ${rule.label}`);
211
+ lines.push("");
212
+ lines.push("Recorded, not blocked:");
213
+ lines.push(` ${rules.observe.join(", ")}`);
214
+ return lines.join("\n");
215
+ }
216
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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","sourcesContent":["// Rules you can actually read and change.\n//\n// The site says \"set rules in plain terms\" and \"it is a plain JSON file — edit the\n// limits\". What `init` wrote was 6.7 KB of generated regular expression: the `safe-shell`\n// clause alone is a ~700-character case-folded negative lookahead. Nobody edits that, so\n// in practice the starter policy was the *only* policy and \"set your own rules\" was not\n// true.\n//\n// The fix is not to weaken the patterns — they are careful, and the canonicalization\n// tests exist because bypasses are subtle. It is to stop making the pattern the\n// interface. The lists the patterns are built from live in `.scopebond/rules.json`, a\n// short readable file, and `policy.json` is compiled from it. The regex stays as the\n// compiled form.\n//\n// `compile()` reproduces the shipped starter policy's enforcement patterns exactly —\n// `rules.test.mjs` asserts that against `starterPolicy()`, so a change here cannot\n// quietly alter what is enforced.\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { ci, under, named, dir, DESTRUCTIVE } from \"./runtime.js\";\n\n/** One protected location. `under`/`named`/`dir` are the readable, editable shapes;\n * `raw` carries the few patterns with bespoke exceptions (`.env` templates are allowed,\n * `.ssh/config` and `*.pub` are allowed) that no simple shape expresses. */\nexport type PathRule =\n | { kind: \"under\"; value: string; label: string }\n | { kind: \"named\"; value: string; label: string }\n | { kind: \"dir\"; value: string; label: string }\n | { kind: \"raw\"; pattern: string; label: string };\n\nexport interface RuleSet {\n version: 1;\n /** Refs an agent may not push to. `release/*` means the prefix `release/`. */\n protected_branches: string[];\n /** Programs an agent may not run, in any case and with or without .exe. */\n destructive_programs: string[];\n protected_write: PathRule[];\n protected_read: PathRule[];\n /** Action types recorded but not blocked. */\n observe: string[];\n}\n\nexport const RULES_FILE = \"rules.json\";\n\nconst pathPattern = (rule: PathRule): string =>\n rule.kind === \"under\" ? under(rule.value)\n : rule.kind === \"named\" ? named(rule.value)\n : rule.kind === \"dir\" ? dir(rule.value)\n : rule.pattern;\n\n/** Escape every regular-expression metacharacter, backslash included.\n *\n * A path the user typed is literal text, and this value goes straight into a rule that\n * decides what the agent may touch: if one metacharacter escapes escaping, the rule\n * silently protects something other than what they asked for. The class is therefore\n * complete on its own rather than relying on an earlier step having removed backslashes\n * (CodeQL js/incomplete-sanitization). */\nconst escapeRegex = (text: string): string => text.replace(/[\\\\^$.*+?()[\\]{}|/]/g, \"\\\\$&\");\n\n/** A protected-path rule from a plain path the user typed. A trailing `/` (or a path with\n * no dot in its last segment) reads as a directory; anything else as a file name.\n *\n * Normalisation is one obvious step at a time. The previous version folded separator\n * conversion and leading-`./` stripping into a single `^[./\\\\]+` replace with a callback,\n * which was both hard to read and quadratic on a path of many leading dots\n * (CodeQL js/polynomial-redos). */\nexport function pathRuleFor(input: string): PathRule {\n const trimmed = input.trim();\n const withForwardSlashes = trimmed.split(\"\\\\\").join(\"/\");\n const withoutDotSlash = withForwardSlashes.startsWith(\"./\") ? withForwardSlashes.slice(2) : withForwardSlashes;\n // Drop trailing separators without a quantifier, so the cost is plainly linear.\n let clean = withoutDotSlash;\n while (clean.endsWith(\"/\")) clean = clean.slice(0, -1);\n // A trailing slash is an explicit directory; so is a last segment with no extension.\n // Checked by string, not by `/\\.[^/]*$/`: that pattern is unanchored at the front, so it\n // retries at every position and goes quadratic on a long path.\n const lastSegment = clean.slice(clean.lastIndexOf(\"/\") + 1);\n const isDirectory = trimmed.endsWith(\"/\") || !lastSegment.includes(\".\");\n return isDirectory\n ? { kind: \"under\", value: escapeRegex(clean), label: `${clean}/ and everything in it` }\n : { kind: \"named\", value: escapeRegex(clean), label: clean };\n}\n\n/** The branch pattern: deny the listed refs, plus the \"every branch at once\" flags and a\n * push whose destination could not be read. `--tags` alone is allowed. */\nfunction branchPattern(branches: string[]): string {\n const exact = branches.filter((b) => !b.endsWith(\"/*\"));\n const prefixes = branches.filter((b) => b.endsWith(\"/*\")).map((b) => b.slice(0, -1));\n const parts = [\n exact.length ? `(?!(?:${exact.map(ci).join(\"|\")})$)` : \"\",\n ...prefixes.map((p) => `(?!${ci(p)})`),\n `(?!-(?!-${ci(\"tags\")}$))`,\n ];\n return `^${parts.join(\"\")}.+`;\n}\n\nconst programPattern = (programs: string[]): string =>\n `^(?!(?:${programs.map(ci).join(\"|\")})(?:${ci(\"\\\\.(?:exe|cmd|bat|com|ps1)\")})?$).+`;\n\nconst pathsPattern = (rules: PathRule[]): string => `^${rules.map(pathPattern).join(\"\")}.+`;\n\n/** A sentence listing what a clause covers, generated from the list rather than written by\n * hand — so it stays true after an edit. The block message quotes this, so a stale\n * description would be a lie told at the worst moment. */\nfunction sentence(items: string[], limit = 12): string {\n const shown = items.slice(0, limit);\n const rest = items.length - shown.length;\n return shown.join(\", \") + (rest > 0 ? `, and ${rest} more` : \"\");\n}\n\n/** Compile a rule set into the policy the gateway evaluates. */\nexport function compile(rules: RuleSet, agentKid: string): Record<string, unknown> {\n return {\n vocabulary_version: \"1.0\", policy_id: \"coding-agent\", version: 1,\n clauses: [\n {\n id: \"protect-branches\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"git.push\"],\n param_bounds: { ref: { pattern: branchPattern(rules.protected_branches) } },\n description: `Deny pushes to ${sentence(rules.protected_branches)} (any case, any refspec spelling), pushes of every branch at once (--all, --mirror) and pushes whose destination cannot be read from the command (a git alias, a configured push refspec, send-pack). A tags-only push (--tags) is allowed. Change these in .scopebond/${RULES_FILE}.`,\n },\n {\n id: \"safe-shell\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"shell.exec\"],\n param_bounds: { program: { pattern: programPattern(rules.destructive_programs) } },\n description: `Deny destructive programs (${sentence(rules.destructive_programs, 10)}) in any case and with or without .exe. An empty program — a command that could not be parsed, or whose program is only known at run time ($VAR, $(…), eval of a variable) — is denied. Argument-shaped deletion (find -delete, git clean) is not a program name and is not covered here. Change this list in .scopebond/${RULES_FILE}.`,\n },\n {\n id: \"protect-write\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"file.write\"],\n param_bounds: { path: { pattern: pathsPattern(rules.protected_write) } },\n description: `Allow workspace writes, but never to ${sentence(rules.protected_write.map((r) => r.label))}. Case-insensitive. Change this list in .scopebond/${RULES_FILE}.`,\n },\n {\n id: \"protect-read\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"file.read\"],\n param_bounds: { path: { pattern: pathsPattern(rules.protected_read) } },\n description: `Allow workspace reads, but never ${sentence(rules.protected_read.map((r) => r.label))}. Case-insensitive. Change this list in .scopebond/${RULES_FILE}.`,\n },\n {\n id: \"observe-net-mcp\", type: \"action_allowlist\", mode: \"monitor\",\n action_types: rules.observe,\n description: `Observe ${sentence(rules.observe)} — recorded, not blocked. Add bounds to enforce. Change this list in .scopebond/${RULES_FILE}.`,\n },\n { id: \"keys\", type: \"key_policy\", active_keys: [agentKid], description: \"Only the enrolled machine key may sign.\" },\n ],\n };\n}\n\n/** The shipped defaults. Every entry here reproduces one piece of the starter policy's\n * patterns; `rules.test.mjs` pins that equivalence. */\nexport function defaultRules(): RuleSet {\n return {\n version: 1,\n protected_branches: [\"main\", \"master\", \"release/*\"],\n destructive_programs: [...DESTRUCTIVE],\n protected_write: [\n { kind: \"under\", value: \"\\\\.scopebond\", label: \"the hook's own policy and keys (.scopebond)\" },\n { kind: \"raw\", pattern: `(?!(?:.*/)?${ci(\"\\\\.claude/settings\")})`, label: \"Claude Code settings\" },\n { kind: \"under\", value: \"\\\\.claude/hooks\", label: \"Claude Code hooks\" },\n { kind: \"under\", value: \"\\\\.claude/agents\", label: \"Claude Code agents\" },\n { kind: \"raw\", pattern: `(?!(?:.*/)?${ci(\"\\\\.cursor/hooks\")})`, label: \"Cursor hook settings\" },\n { kind: \"named\", value: \"\\\\.codex/hooks\\\\.json\", label: \"Codex hook settings\" },\n { kind: \"named\", value: \"\\\\.codex/config\\\\.toml\", label: \"Codex config\" },\n { kind: \"named\", value: \"\\\\.mcp\\\\.json\", label: \".mcp.json\" },\n { kind: \"under\", value: \"\\\\.git/hooks\", label: \"git hooks\" },\n { kind: \"named\", value: \"\\\\.git/config\", label: \"git config\" },\n { kind: \"under\", value: \"\\\\.husky\", label: \"Husky hooks\" },\n { kind: \"under\", value: \"\\\\.github/workflows\", label: \"GitHub workflows\" },\n { kind: \"under\", value: \"\\\\.github/actions\", label: \"GitHub actions\" },\n { kind: \"named\", value: \"\\\\.gitlab-ci\\\\.yml\", label: \".gitlab-ci.yml\" },\n { kind: \"named\", value: \"\\\\.gitlab-ci\\\\.yaml\", label: \".gitlab-ci.yaml\" },\n { kind: \"under\", value: \"\\\\.circleci\", label: \"CircleCI config\" },\n { kind: \"named\", value: \"azure-pipelines\\\\.yml\", label: \"azure-pipelines.yml\" },\n { kind: \"named\", value: \"Jenkinsfile\", label: \"Jenkinsfile\" },\n ],\n protected_read: [\n { kind: \"under\", value: \"\\\\.scopebond\", label: \"the hook's own policy and keys (.scopebond)\" },\n { kind: \"raw\", pattern: `(?!.*${ci(\"\\\\.(?:key|pem|p12|pfx|jks|keystore)\")}$)`, label: \"signing keys and key containers (*.key, *.pem, *.p12, *.pfx, *.jks)\" },\n {\n kind: \"raw\",\n pattern: `(?!(?:.*/)?${ci(\"\\\\.env\")}(?!(?:\\\\.[^/]*)?\\\\.(?:${ci(\"example\")}|${ci(\"sample\")}|${ci(\"template\")}|${ci(\"dist\")})$)(?:\\\\.[^/]*)?$)`,\n label: \"environment secret files (.env, .env.* — except .example/.sample/.template/.dist)\",\n },\n { kind: \"named\", value: \"\\\\.envrc\", label: \".envrc\" },\n {\n kind: \"raw\",\n pattern: `(?!(?:.*/)?${ci(\"\\\\.ssh\")}(?:$|/(?!.*${ci(\"\\\\.pub\")}$)(?!${ci(\"known_hosts\")}$)(?!${ci(\"config\")}$)))`,\n label: \"SSH private keys and the .ssh directory (public keys, known_hosts and config are allowed)\",\n },\n { kind: \"raw\", pattern: `(?!(?:.*/)?${ci(\"\\\\.aws\")}(?:$|/(?!${ci(\"config\")}$)))`, label: \"AWS credentials (.aws, except .aws/config)\" },\n { kind: \"named\", value: \"\\\\.npmrc\", label: \".npmrc\" },\n { kind: \"named\", value: \"\\\\.pypirc\", label: \".pypirc\" },\n { kind: \"named\", value: \"\\\\.netrc\", label: \".netrc\" },\n { kind: \"named\", value: \"_netrc\", label: \"_netrc\" },\n { kind: \"named\", value: \"\\\\.git-credentials\", label: \".git-credentials\" },\n { kind: \"dir\", value: \"\\\\.kube\", label: \"the .kube directory\" },\n { kind: \"named\", value: \"\\\\.kube/config\", label: \".kube/config\" },\n { kind: \"dir\", value: \"\\\\.docker\", label: \"the .docker directory\" },\n { kind: \"named\", value: \"\\\\.docker/config\\\\.json\", label: \".docker/config.json\" },\n { kind: \"under\", value: \"\\\\.config/gcloud\", label: \"gcloud credentials\" },\n { kind: \"under\", value: \"\\\\.azure\", label: \"Azure credentials\" },\n { kind: \"under\", value: \"\\\\.gnupg\", label: \"GnuPG keys\" },\n { kind: \"dir\", value: \"\\\\.config/gh\", label: \"the GitHub CLI config directory\" },\n { kind: \"named\", value: \"\\\\.config/gh/hosts\\\\.yml\", label: \"the GitHub CLI's hosts.yml\" },\n { kind: \"named\", value: \"\\\\.claude/\\\\.credentials\\\\.json\", label: \"Claude Code's .credentials.json\" },\n ],\n observe: [\"net.fetch\", \"mcp.tool.call\"],\n };\n}\n\nexport const rulesPath = (dir: string): string => join(dir, RULES_FILE);\n\nexport function loadRules(configDir: string): RuleSet | null {\n const file = rulesPath(configDir);\n if (!existsSync(file)) return null;\n try {\n const parsed = JSON.parse(readFileSync(file, \"utf8\")) as RuleSet;\n if (parsed?.version !== 1 || !Array.isArray(parsed.protected_branches)) return null;\n return parsed;\n } catch { return null; }\n}\n\nexport function saveRules(configDir: string, rules: RuleSet): string {\n const file = rulesPath(configDir);\n writeFileSync(file, `${JSON.stringify(rules, null, 2)}\\n`);\n return file;\n}\n\n/** Plain English, for `scopebond-hook rules`. */\nexport function describeRules(rules: RuleSet): string {\n const lines: string[] = [];\n lines.push(\"Blocked before it runs:\");\n lines.push(\"\");\n lines.push(` pushes to ${rules.protected_branches.join(\", \")}`);\n lines.push(` and any push whose destination cannot be read`);\n lines.push(` programs ${rules.destructive_programs.join(\", \")}`);\n lines.push(\"\");\n lines.push(` writes to ${rules.protected_write.length} protected location(s):`);\n for (const rule of rules.protected_write) lines.push(` ${rule.label}`);\n lines.push(\"\");\n lines.push(` reads of ${rules.protected_read.length} protected location(s):`);\n for (const rule of rules.protected_read) lines.push(` ${rule.label}`);\n lines.push(\"\");\n lines.push(\"Recorded, not blocked:\");\n lines.push(` ${rules.observe.join(\", \")}`);\n return lines.join(\"\\n\");\n}\n"]}
@@ -0,0 +1,25 @@
1
+ /** Where pinned copies live: one directory per hook version. */
2
+ export declare function runtimeRoot(): string;
3
+ export declare function runtimeDirFor(version: string): string;
4
+ /** The CLI inside a pinned copy. */
5
+ export declare function pinnedCliPath(version: string): string;
6
+ /** True when a path sits in npm's throwaway `npx` cache, which npm may clear at any
7
+ * time. Anything else — a project `node_modules`, a global install, a pinned copy —
8
+ * is durable enough to reference from a config file. */
9
+ export declare function isEphemeralPath(file: string): boolean;
10
+ /** Walk up from `dist/cli.js` to the `node_modules` directory that contains the whole
11
+ * materialised dependency tree (`node_modules/@scopebond/hook/dist/cli.js` → four up).
12
+ * Returns null if the layout is not what we expect, rather than copying the wrong thing. */
13
+ export declare function nodeModulesRootOf(cliFile: string): string | null;
14
+ export interface PinResult {
15
+ /** The CLI path to pin, or null to keep the `npx` form. */
16
+ cli: string | null;
17
+ /** Why, in one phrase, for the line `init` prints. */
18
+ how: "already-durable" | "pinned" | "reused" | "unavailable";
19
+ }
20
+ /** Make sure a durable copy of this CLI exists and say which path to pin.
21
+ *
22
+ * Never throws: every failure degrades to `{ cli: null }` so the caller falls back
23
+ * to `npx`, which is slower but always starts. */
24
+ export declare function ensureDurableRuntime(cliFile: string, version: string): PinResult;
25
+ //# sourceMappingURL=runtime-install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-install.d.ts","sourceRoot":"","sources":["../src/runtime-install.ts"],"names":[],"mappings":"AAuBA,gEAAgE;AAChE,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;yDAEyD;AACzD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;6FAE6F;AAC7F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAShE;AAeD,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,sDAAsD;IACtD,GAAG,EAAE,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;CAC9D;AAED;;;mDAGmD;AACnD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CA0BhF"}
@@ -0,0 +1,106 @@
1
+ // Pinning the hook to a stable path, so the harness does not pay `npx` on every
2
+ // tool call.
3
+ //
4
+ // `init` installs a hook command into the agent's config, and that command runs
5
+ // once per tool call — hundreds of times an hour. The obvious command,
6
+ // `npx -y @scopebond/hook@<version> claude`, costs about 830 ms a call on a warm
7
+ // cache; running the same CLI directly costs about 110 ms. The ~720 ms difference
8
+ // is npm resolving a package that is already on disk.
9
+ //
10
+ // The reason `init` used `npx` anyway is sound: a hook entry that cannot start is
11
+ // worse than no hook, because a harness can read the failure as "no hook", and an
12
+ // absolute path into npm's `_npx` cache is not durable — npm may clear it.
13
+ //
14
+ // So: copy the tree npx already materialised into the Scopebond home once per
15
+ // machine and per version, and pin *that*. It is durable, self-contained, needs no
16
+ // network, lives under the home the governed agent's own policy protects, and
17
+ // `doctor` re-checks that the path still resolves. If anything here fails, the
18
+ // caller keeps the `npx` form — slow beats broken.
19
+ import { existsSync, mkdirSync, cpSync, rmSync, renameSync, readdirSync } from "node:fs";
20
+ import { dirname, join, sep } from "node:path";
21
+ import { userHome } from "./install.js";
22
+ /** Where pinned copies live: one directory per hook version. */
23
+ export function runtimeRoot() {
24
+ return join(userHome(), "runtime");
25
+ }
26
+ export function runtimeDirFor(version) {
27
+ return join(runtimeRoot(), version);
28
+ }
29
+ /** The CLI inside a pinned copy. */
30
+ export function pinnedCliPath(version) {
31
+ return join(runtimeDirFor(version), "node_modules", "@scopebond", "hook", "dist", "cli.js");
32
+ }
33
+ /** True when a path sits in npm's throwaway `npx` cache, which npm may clear at any
34
+ * time. Anything else — a project `node_modules`, a global install, a pinned copy —
35
+ * is durable enough to reference from a config file. */
36
+ export function isEphemeralPath(file) {
37
+ return `${sep}${file.replace(/[\\/]/g, sep)}${sep}`.includes(`${sep}_npx${sep}`);
38
+ }
39
+ /** Walk up from `dist/cli.js` to the `node_modules` directory that contains the whole
40
+ * materialised dependency tree (`node_modules/@scopebond/hook/dist/cli.js` → four up).
41
+ * Returns null if the layout is not what we expect, rather than copying the wrong thing. */
42
+ export function nodeModulesRootOf(cliFile) {
43
+ let dir = dirname(cliFile); // .../dist
44
+ for (let i = 0; i < 6; i += 1) {
45
+ const parent = dirname(dir);
46
+ if (parent === dir)
47
+ return null;
48
+ if (parent.endsWith(`${sep}node_modules`) || parent.endsWith("/node_modules"))
49
+ return parent;
50
+ dir = parent;
51
+ }
52
+ return null;
53
+ }
54
+ /** A pinned copy is usable only if the CLI and its sibling packages are all there —
55
+ * a half-copied tree would fail closed on every tool call. */
56
+ function pinnedCopyIsComplete(version) {
57
+ const cli = pinnedCliPath(version);
58
+ if (!existsSync(cli))
59
+ return false;
60
+ const scope = join(runtimeDirFor(version), "node_modules", "@scopebond");
61
+ try {
62
+ // The hook cannot run without the gateway and sdk it imports at startup.
63
+ const names = new Set(readdirSync(scope));
64
+ return names.has("hook") && names.has("gateway") && names.has("sdk");
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ /** Make sure a durable copy of this CLI exists and say which path to pin.
71
+ *
72
+ * Never throws: every failure degrades to `{ cli: null }` so the caller falls back
73
+ * to `npx`, which is slower but always starts. */
74
+ export function ensureDurableRuntime(cliFile, version) {
75
+ try {
76
+ if (!existsSync(cliFile))
77
+ return { cli: null, how: "unavailable" };
78
+ // Already installed somewhere npm will not delete — pin it as it stands.
79
+ if (!isEphemeralPath(cliFile))
80
+ return { cli: cliFile, how: "already-durable" };
81
+ if (pinnedCopyIsComplete(version))
82
+ return { cli: pinnedCliPath(version), how: "reused" };
83
+ const source = nodeModulesRootOf(cliFile);
84
+ if (!source || !existsSync(source))
85
+ return { cli: null, how: "unavailable" };
86
+ const target = runtimeDirFor(version);
87
+ // Copy to a staging directory and move it into place, so an interrupted copy
88
+ // never leaves a partial tree that looks complete.
89
+ const staging = `${target}.incoming-${process.pid}`;
90
+ rmSync(staging, { recursive: true, force: true });
91
+ mkdirSync(staging, { recursive: true });
92
+ cpSync(source, join(staging, "node_modules"), { recursive: true, dereference: true });
93
+ rmSync(target, { recursive: true, force: true });
94
+ mkdirSync(dirname(target), { recursive: true });
95
+ renameSync(staging, target);
96
+ if (!pinnedCopyIsComplete(version)) {
97
+ rmSync(target, { recursive: true, force: true });
98
+ return { cli: null, how: "unavailable" };
99
+ }
100
+ return { cli: pinnedCliPath(version), how: "pinned" };
101
+ }
102
+ catch {
103
+ return { cli: null, how: "unavailable" };
104
+ }
105
+ }
106
+ //# sourceMappingURL=runtime-install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-install.js","sourceRoot":"","sources":["../src/runtime-install.ts"],"names":[],"mappings":"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","sourcesContent":["// Pinning the hook to a stable path, so the harness does not pay `npx` on every\n// tool call.\n//\n// `init` installs a hook command into the agent's config, and that command runs\n// once per tool call — hundreds of times an hour. The obvious command,\n// `npx -y @scopebond/hook@<version> claude`, costs about 830 ms a call on a warm\n// cache; running the same CLI directly costs about 110 ms. The ~720 ms difference\n// is npm resolving a package that is already on disk.\n//\n// The reason `init` used `npx` anyway is sound: a hook entry that cannot start is\n// worse than no hook, because a harness can read the failure as \"no hook\", and an\n// absolute path into npm's `_npx` cache is not durable — npm may clear it.\n//\n// So: copy the tree npx already materialised into the Scopebond home once per\n// machine and per version, and pin *that*. It is durable, self-contained, needs no\n// network, lives under the home the governed agent's own policy protects, and\n// `doctor` re-checks that the path still resolves. If anything here fails, the\n// caller keeps the `npx` form — slow beats broken.\n\nimport { existsSync, mkdirSync, cpSync, rmSync, renameSync, readdirSync } from \"node:fs\";\nimport { dirname, join, sep } from \"node:path\";\nimport { userHome } from \"./install.js\";\n\n/** Where pinned copies live: one directory per hook version. */\nexport function runtimeRoot(): string {\n return join(userHome(), \"runtime\");\n}\n\nexport function runtimeDirFor(version: string): string {\n return join(runtimeRoot(), version);\n}\n\n/** The CLI inside a pinned copy. */\nexport function pinnedCliPath(version: string): string {\n return join(runtimeDirFor(version), \"node_modules\", \"@scopebond\", \"hook\", \"dist\", \"cli.js\");\n}\n\n/** True when a path sits in npm's throwaway `npx` cache, which npm may clear at any\n * time. Anything else — a project `node_modules`, a global install, a pinned copy —\n * is durable enough to reference from a config file. */\nexport function isEphemeralPath(file: string): boolean {\n return `${sep}${file.replace(/[\\\\/]/g, sep)}${sep}`.includes(`${sep}_npx${sep}`);\n}\n\n/** Walk up from `dist/cli.js` to the `node_modules` directory that contains the whole\n * materialised dependency tree (`node_modules/@scopebond/hook/dist/cli.js` → four up).\n * Returns null if the layout is not what we expect, rather than copying the wrong thing. */\nexport function nodeModulesRootOf(cliFile: string): string | null {\n let dir = dirname(cliFile); // .../dist\n for (let i = 0; i < 6; i += 1) {\n const parent = dirname(dir);\n if (parent === dir) return null;\n if (parent.endsWith(`${sep}node_modules`) || parent.endsWith(\"/node_modules\")) return parent;\n dir = parent;\n }\n return null;\n}\n\n/** A pinned copy is usable only if the CLI and its sibling packages are all there —\n * a half-copied tree would fail closed on every tool call. */\nfunction pinnedCopyIsComplete(version: string): boolean {\n const cli = pinnedCliPath(version);\n if (!existsSync(cli)) return false;\n const scope = join(runtimeDirFor(version), \"node_modules\", \"@scopebond\");\n try {\n // The hook cannot run without the gateway and sdk it imports at startup.\n const names = new Set(readdirSync(scope));\n return names.has(\"hook\") && names.has(\"gateway\") && names.has(\"sdk\");\n } catch { return false; }\n}\n\nexport interface PinResult {\n /** The CLI path to pin, or null to keep the `npx` form. */\n cli: string | null;\n /** Why, in one phrase, for the line `init` prints. */\n how: \"already-durable\" | \"pinned\" | \"reused\" | \"unavailable\";\n}\n\n/** Make sure a durable copy of this CLI exists and say which path to pin.\n *\n * Never throws: every failure degrades to `{ cli: null }` so the caller falls back\n * to `npx`, which is slower but always starts. */\nexport function ensureDurableRuntime(cliFile: string, version: string): PinResult {\n try {\n if (!existsSync(cliFile)) return { cli: null, how: \"unavailable\" };\n // Already installed somewhere npm will not delete — pin it as it stands.\n if (!isEphemeralPath(cliFile)) return { cli: cliFile, how: \"already-durable\" };\n if (pinnedCopyIsComplete(version)) return { cli: pinnedCliPath(version), how: \"reused\" };\n const source = nodeModulesRootOf(cliFile);\n if (!source || !existsSync(source)) return { cli: null, how: \"unavailable\" };\n const target = runtimeDirFor(version);\n // Copy to a staging directory and move it into place, so an interrupted copy\n // never leaves a partial tree that looks complete.\n const staging = `${target}.incoming-${process.pid}`;\n rmSync(staging, { recursive: true, force: true });\n mkdirSync(staging, { recursive: true });\n cpSync(source, join(staging, \"node_modules\"), { recursive: true, dereference: true });\n rmSync(target, { recursive: true, force: true });\n mkdirSync(dirname(target), { recursive: true });\n renameSync(staging, target);\n if (!pinnedCopyIsComplete(version)) {\n rmSync(target, { recursive: true, force: true });\n return { cli: null, how: \"unavailable\" };\n }\n return { cli: pinnedCliPath(version), how: \"pinned\" };\n } catch {\n return { cli: null, how: \"unavailable\" };\n }\n}\n"]}
package/dist/runtime.d.ts CHANGED
@@ -21,11 +21,18 @@ export interface RuntimeConfig {
21
21
  export interface Decision {
22
22
  decision: "allow" | "deny" | "not_evaluated";
23
23
  reason: string;
24
+ /** The clause that decided a deny, when the verdict named one. */
25
+ clauseId?: string | null;
24
26
  /** The deciding receipt (the denied one, or the first allow). */
25
27
  receipt?: unknown;
26
28
  /** Every receipt produced — one per simple command in a decomposed shell call. */
27
29
  receipts?: unknown[];
28
30
  }
31
+ export declare const ci: (s: string) => string;
32
+ export declare const under: (dir: string) => string;
33
+ export declare const named: (file: string) => string;
34
+ export declare const dir: (d: string) => string;
35
+ export declare const DESTRUCTIVE: string[];
29
36
  /** Upgrade the starter-policy patterns an older hook wrote, in memory, so a user who
30
37
  * installed an earlier version gets the current protections without re-running init.
31
38
  * Only exact legacy strings are replaced; any other pattern is the operator's own. */
@@ -44,6 +51,12 @@ export declare function createHookRuntime(config: RuntimeConfig): {
44
51
  /** Deliver queued receipts to Cloud with a bounded timeout, then it is safe to
45
52
  * exit. Undelivered receipts persist in the durable outbox for the next run. */
46
53
  flush(): Promise<void>;
54
+ /** Release the SQLite handles. The hook is a per-tool-call process, and a writer that
55
+ * exits without closing leaves its write-ahead log on disk for the next process to
56
+ * extend — measured at ~11 KiB of WAL per receipt against ~1.7 KiB when closed, so a
57
+ * busy session was writing tens of megabytes of pure overhead into the user's project.
58
+ * Always safe to call, and every exit path should. */
59
+ close(): void;
47
60
  /** Decide one mapped action, recording a receipt either way. */
48
61
  evaluateOne(mapped: Mapped): Promise<Decision>;
49
62
  /** Decide a whole tool call. A shell call decomposes into several simple
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAMA,OAAO,EAA6C,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE/E,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf;;uFAEmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;sEACkE;IAClE,KAAK,CAAC,EAAE;QAAE,UAAU,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvF;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,eAAe,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAoED;;uFAEuF;AACvF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAQpD;AAED;;;0BAG0B;AAC1B,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCvE;AAED;4EAC4E;AAC5E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa;;;;IAuBnD;qFACiF;aAClE,OAAO,CAAC,IAAI,CAAC;IAG5B,gEAAgE;wBACtC,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAapD;8EAC0E;qBACnD,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;EAkB/D"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAOA,OAAO,EAA6C,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAK/E,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf;;uFAEmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;sEACkE;IAClE,KAAK,CAAC,EAAE;QAAE,UAAU,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvF;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,eAAe,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAWD,eAAO,MAAM,EAAE,GAAI,GAAG,MAAM,KAAG,MAAiF,CAAC;AACjH,eAAO,MAAM,KAAK,GAAI,KAAK,MAAM,KAAG,MAAyC,CAAC;AAC9E,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,KAAG,MAAoC,CAAC;AAC1E,eAAO,MAAM,GAAG,GAAI,GAAG,MAAM,KAAG,MAAmC,CAAC;AA4BpE,eAAO,MAAM,WAAW,UAGvB,CAAC;AAuBF;;uFAEuF;AACvF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAQpD;AAED;;;0BAG0B;AAC1B,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCvE;AAED;4EAC4E;AAC5E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa;;;;IA8BnD;qFACiF;aAClE,OAAO,CAAC,IAAI,CAAC;IAG5B;;;;2DAIuD;aAC9C,IAAI;IAIb,gEAAgE;wBACtC,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IA+BpD;8EAC0E;qBACnD,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;EAkB/D"}