@scopebond/hook 0.5.0 → 0.7.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 (54) hide show
  1. package/README.md +139 -12
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +667 -69
  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 +9 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/init.d.ts +9 -3
  19. package/dist/init.d.ts.map +1 -1
  20. package/dist/init.js +36 -28
  21. package/dist/init.js.map +1 -1
  22. package/dist/install.d.ts +63 -3
  23. package/dist/install.d.ts.map +1 -1
  24. package/dist/install.js +242 -28
  25. package/dist/install.js.map +1 -1
  26. package/dist/map.d.ts +12 -3
  27. package/dist/map.d.ts.map +1 -1
  28. package/dist/map.js +765 -60
  29. package/dist/map.js.map +1 -1
  30. package/dist/minimize.d.ts +6 -3
  31. package/dist/minimize.d.ts.map +1 -1
  32. package/dist/minimize.js +24 -4
  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 +17 -0
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +122 -11
  45. package/dist/runtime.js.map +1 -1
  46. package/dist/shell.d.ts +53 -4
  47. package/dist/shell.d.ts.map +1 -1
  48. package/dist/shell.js +737 -81
  49. package/dist/shell.js.map +1 -1
  50. package/dist/version.d.ts +5 -1
  51. package/dist/version.d.ts.map +1 -1
  52. package/dist/version.js +6 -2
  53. package/dist/version.js.map +1 -1
  54. package/package.json +3 -3
package/dist/runtime.js CHANGED
@@ -2,18 +2,88 @@
2
2
  // machine key, policy and a durable local receipt log. Each mapped action is
3
3
  // signed by the machine key, decided against policy and countersigned — locally,
4
4
  // with no HTTP server and no Cloud dependency in 0.1.
5
- import { readFileSync } from "node:fs";
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
6
7
  import { createGateway, StaticPrincipalKeyRegistry } from "@scopebond/gateway";
7
8
  import { loadOrCreateAttester, openReceiptStore } from "@scopebond/gateway/node";
8
9
  import { createSigner } from "@scopebond/sdk";
9
10
  import { attachExporter, flushBounded } from "./cloud.js";
11
+ import { explainDeny } from "./explain.js";
12
+ import { RULES_FILE } from "./rules.js";
13
+ import { cliCommand } from "./version.js";
10
14
  // The hook's own config and keys must be off-limits to the agent it governs:
11
15
  // otherwise the agent could rewrite its policy or read the signing keys and the
12
16
  // receipts stop meaning anything. These patterns are "allow if the path does NOT
13
17
  // match a protected location"; they run against the cwd-relative path the mapper
14
18
  // produces (and equally against an absolute one).
15
- const PROTECTED_WRITE = "^(?!(?:.*/)?\\.scopebond/)(?!(?:.*/)?\\.claude/settings)(?!(?:.*/)?\\.cursor/hooks)(?!(?:.*/)?\\.codex/(?:hooks\\.json|config\\.toml)$)(?!(?:.*/)?\\.git/hooks/)(?!(?:.*/)?\\.github/workflows/)(?!(?:.*/)?\\.github/actions/)(?!(?:.*/)?\\.gitlab-ci\\.yml$)(?!(?:.*/)?\\.circleci/)(?!(?:.*/)?azure-pipelines\\.yml$)(?!(?:.*/)?Jenkinsfile$).+";
16
- const PROTECTED_READ = "^(?!(?:.*/)?\\.scopebond/)(?!.*\\.key$)(?!(?:.*/)?\\.env(?:\\.(?!example$|sample$|template$)[^/]*)?$).+";
19
+ //
20
+ // Matching is case-insensitive — Windows and macOS open `.ENV` and `.env` as the
21
+ // same file — by expanding each letter to a two-case class (policy patterns are
22
+ // plain regular expressions with no flags). `ci()` is applied to literal text only.
23
+ export const ci = (s) => s.replace(/[A-Za-z]/g, (c) => `[${c.toLowerCase()}${c.toUpperCase()}]`);
24
+ export const under = (dir) => `(?!(?:.*/)?${ci(dir)}(?:/|$))`; // dir itself or anything inside it
25
+ export const named = (file) => `(?!(?:.*/)?${ci(file)}$)`; // exactly this file name
26
+ export const dir = (d) => `(?!(?:.*/)?${ci(d)}/?$)`; // the directory itself (a recursive read or copy)
27
+ const PROTECTED_WRITE = "^" + [
28
+ under("\\.scopebond"), `(?!(?:.*/)?${ci("\\.claude/settings")})`, under("\\.claude/hooks"), under("\\.claude/agents"),
29
+ `(?!(?:.*/)?${ci("\\.cursor/hooks")})`, named("\\.codex/hooks\\.json"), named("\\.codex/config\\.toml"), named("\\.mcp\\.json"),
30
+ under("\\.git/hooks"), named("\\.git/config"), under("\\.husky"),
31
+ under("\\.github/workflows"), under("\\.github/actions"), named("\\.gitlab-ci\\.yml"), named("\\.gitlab-ci\\.yaml"), under("\\.circleci"),
32
+ named("azure-pipelines\\.yml"), named("Jenkinsfile"),
33
+ ].join("") + ".+";
34
+ const PROTECTED_READ = "^" + [
35
+ under("\\.scopebond"),
36
+ `(?!.*${ci("\\.(?:key|pem|p12|pfx|jks|keystore)")}$)`,
37
+ // .env, .env.* — except templates whose name ends in .example/.sample/.template/.dist
38
+ `(?!(?:.*/)?${ci("\\.env")}(?!(?:\\.[^/]*)?\\.(?:${ci("example")}|${ci("sample")}|${ci("template")}|${ci("dist")})$)(?:\\.[^/]*)?$)`,
39
+ named("\\.envrc"),
40
+ // Credentials outside the workspace that an agent can reach by absolute or ~ path,
41
+ // and their directories as a whole (`cp -r ~/.ssh`, `grep -r x ~/.aws`).
42
+ `(?!(?:.*/)?${ci("\\.ssh")}(?:$|/(?!.*${ci("\\.pub")}$)(?!${ci("known_hosts")}$)(?!${ci("config")}$)))`,
43
+ `(?!(?:.*/)?${ci("\\.aws")}(?:$|/(?!${ci("config")}$)))`,
44
+ named("\\.npmrc"), named("\\.pypirc"), named("\\.netrc"), named("_netrc"), named("\\.git-credentials"),
45
+ dir("\\.kube"), named("\\.kube/config"), dir("\\.docker"), named("\\.docker/config\\.json"),
46
+ under("\\.config/gcloud"), under("\\.azure"), under("\\.gnupg"),
47
+ dir("\\.config/gh"), named("\\.config/gh/hosts\\.yml"), named("\\.claude/\\.credentials\\.json"),
48
+ ].join("") + ".+";
49
+ // Destructive programs, POSIX and Windows. The mapper records the program as typed,
50
+ // so the pattern accepts any case and an executable suffix (`RM.exe`, `Remove-Item`).
51
+ export const DESTRUCTIVE = [
52
+ "rm", "sudo", "doas", "shutdown", "reboot", "halt", "poweroff", "mkfs", "dd", "shred", "truncate", "unlink", "wipe", "srm",
53
+ "del", "rd", "rmdir", "erase", "deltree", "format", "diskpart", "remove-item", "ri", "clear-content", "clc", "stop-computer", "restart-computer",
54
+ ];
55
+ const SAFE_SHELL = `^(?!(?:${DESTRUCTIVE.map(ci).join("|")})(?:${ci("\\.(?:exe|cmd|bat|com|ps1)")})?$).+`;
56
+ // A push destination the starter policy refuses: main, master, release/*, the
57
+ // "every branch" flags (`--all`, `--mirror`, `--branches`) and a push whose
58
+ // destination the mapper could not read (`--unknown`: a git alias, a configured push
59
+ // refspec, `send-pack`), in any case. `--tags` alone pushes only tags and is allowed.
60
+ const SAFE_REF = `^(?!(?:${ci("main")}|${ci("master")})$)(?!${ci("release")}/)(?!-(?!-${ci("tags")}$)).+`;
61
+ // Patterns written by earlier starter policies (0.3–0.5), upgraded in memory when a
62
+ // policy on disk still carries them verbatim. An operator's own edits never match
63
+ // these strings and are left untouched.
64
+ const LEGACY_STARTER_PATTERNS = {
65
+ "^(?!(?:main|master)$)(?!release/).+": SAFE_REF,
66
+ "^(?!(?:rm|sudo|shutdown|reboot|mkfs|dd|del|rd|rmdir|erase|deltree|format|Remove-Item|ri)$).+": SAFE_SHELL,
67
+ "^(?!(?:rm|sudo|shutdown|reboot|mkfs|dd)$).+": SAFE_SHELL,
68
+ "^(?!(?:.*/)?\\.scopebond/)(?!(?:.*/)?\\.claude/settings)(?!(?:.*/)?\\.cursor/hooks)(?!(?:.*/)?\\.codex/(?:hooks\\.json|config\\.toml)$)(?!(?:.*/)?\\.git/hooks/)(?!(?:.*/)?\\.github/workflows/)(?!(?:.*/)?\\.github/actions/)(?!(?:.*/)?\\.gitlab-ci\\.yml$)(?!(?:.*/)?\\.circleci/)(?!(?:.*/)?azure-pipelines\\.yml$)(?!(?:.*/)?Jenkinsfile$).+": PROTECTED_WRITE,
69
+ "^(?!(?:.*/)?\\.scopebond/)(?!(?:.*/)?\\.claude/settings)(?!(?:.*/)?\\.cursor/hooks)(?!(?:.*/)?\\.git/hooks/)(?!(?:.*/)?\\.github/workflows/)(?!(?:.*/)?\\.github/actions/)(?!(?:.*/)?\\.gitlab-ci\\.yml$)(?!(?:.*/)?\\.circleci/)(?!(?:.*/)?azure-pipelines\\.yml$)(?!(?:.*/)?Jenkinsfile$).+": PROTECTED_WRITE,
70
+ "^(?!(?:.*/)?\\.scopebond/)(?!(?:.*/)?\\.claude/settings)(?!(?:.*/)?\\.cursor/hooks)(?!(?:.*/)?\\.git/hooks/).+": PROTECTED_WRITE,
71
+ "^(?!(?:.*/)?\\.scopebond/)(?!.*\\.key$)(?!(?:.*/)?\\.env(?:\\.(?!example$|sample$|template$)[^/]*)?$).+": PROTECTED_READ,
72
+ "^(?!(?:.*/)?\\.scopebond/)(?!.*\\.key$).+": PROTECTED_READ,
73
+ };
74
+ /** Upgrade the starter-policy patterns an older hook wrote, in memory, so a user who
75
+ * installed an earlier version gets the current protections without re-running init.
76
+ * Only exact legacy strings are replaced; any other pattern is the operator's own. */
77
+ export function upgradeStarterPolicy(policy) {
78
+ const p = policy;
79
+ for (const clause of p?.clauses ?? []) {
80
+ for (const bound of Object.values(clause?.param_bounds ?? {})) {
81
+ if (bound && typeof bound.pattern === "string" && LEGACY_STARTER_PATTERNS[bound.pattern])
82
+ bound.pattern = LEGACY_STARTER_PATTERNS[bound.pattern];
83
+ }
84
+ }
85
+ return policy;
86
+ }
17
87
  /** The default starter policy for a coding agent: protect release branches, deny
18
88
  * destructive programs, allow workspace file access except the hook's own config
19
89
  * and keys, and trust only the enrolled machine key. Every threshold is the
@@ -24,23 +94,23 @@ export function starterPolicy(agentKid) {
24
94
  clauses: [
25
95
  {
26
96
  id: "protect-branches", type: "action_allowlist", mode: "enforce", action_types: ["git.push"],
27
- param_bounds: { ref: { pattern: "^(?!(?:main|master)$)(?!release/).+" } },
28
- description: "Deny pushes to main, master and release/*.",
97
+ param_bounds: { ref: { pattern: SAFE_REF } },
98
+ description: "Deny pushes to main, master and release/* (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.",
29
99
  },
30
100
  {
31
101
  id: "safe-shell", type: "action_allowlist", mode: "enforce", action_types: ["shell.exec"],
32
- param_bounds: { program: { pattern: "^(?!(?:rm|sudo|shutdown|reboot|mkfs|dd|del|rd|rmdir|erase|deltree|format|Remove-Item|ri)$).+" } },
33
- description: "Deny destructive programs — POSIX (rm, sudo, shutdown, reboot, mkfs, dd) and Windows/PowerShell (del, rd, rmdir, erase, deltree, format, Remove-Item). An empty program (an unparseable command) is denied.",
102
+ param_bounds: { program: { pattern: SAFE_SHELL } },
103
+ description: "Deny destructive programs — POSIX (rm, sudo, doas, shutdown, reboot, mkfs, dd, shred, truncate, unlink, wipe) and Windows/PowerShell (del, rd, rmdir, erase, deltree, format, diskpart, Remove-Item, Clear-Content) — 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.",
34
104
  },
35
105
  {
36
106
  id: "protect-write", type: "action_allowlist", mode: "enforce", action_types: ["file.write"],
37
107
  param_bounds: { path: { pattern: PROTECTED_WRITE } },
38
- description: "Allow workspace writes, but never to the hook's policy/keys, Claude Code, Cursor or Codex hook settings, git hooks, or CI config (.github/workflows, .github/actions, .gitlab-ci.yml, .circleci, azure-pipelines.yml, Jenkinsfile).",
108
+ description: "Allow workspace writes, but never to the hook's policy/keys, Claude Code settings, hooks and agents, Cursor or Codex hook settings, .mcp.json, git hooks and git config, Husky hooks, or CI config (.github/workflows, .github/actions, .gitlab-ci.yml/.yaml, .circleci, azure-pipelines.yml, Jenkinsfile). Case-insensitive.",
39
109
  },
40
110
  {
41
111
  id: "protect-read", type: "action_allowlist", mode: "enforce", action_types: ["file.read"],
42
112
  param_bounds: { path: { pattern: PROTECTED_READ } },
43
- description: "Allow workspace reads, but never the signing keys (*.key), environment secret files (.env, .env.*, except .env.example/.sample/.template) or the hook's own .scopebond directory.",
113
+ description: "Allow workspace reads, but never signing keys and key containers (*.key, *.pem, *.p12, *.pfx, *.jks), environment secret files (.env, .env.*, .envrc — except names ending in .example/.sample/.template/.dist), SSH private keys and the .ssh directory, cloud/registry/git credentials (.aws except .aws/config, .npmrc, .pypirc, .netrc, .git-credentials, .kube/config, .docker/config.json, gcloud, Azure, GnuPG, the GitHub CLI's hosts.yml, Claude Code's .credentials.json) or the hook's own .scopebond directory. Case-insensitive.",
44
114
  },
45
115
  {
46
116
  id: "observe-net-mcp", type: "action_allowlist", mode: "monitor",
@@ -54,21 +124,28 @@ export function starterPolicy(agentKid) {
54
124
  /** Build the runtime. Throws on any setup failure (unparseable policy, missing
55
125
  * key, unavailable store) — the CLI turns that into a fail-closed deny. */
56
126
  export function createHookRuntime(config) {
57
- const policy = JSON.parse(readFileSync(config.policyPath, "utf8"));
127
+ const policy = upgradeStarterPolicy(JSON.parse(readFileSync(config.policyPath, "utf8")));
58
128
  const agent = createSigner({ privateKeyPem: readFileSync(config.keyPath, "utf8") });
59
129
  const keys = new StaticPrincipalKeyRegistry([
60
130
  { kid: agent.kid, publicKeyPem: agent.publicKeyPem, purposes: ["agent"], status: "active" },
61
131
  ]);
62
132
  const { attester } = loadOrCreateAttester({ file: config.attesterPath });
133
+ // When the project has a readable rule set, a denial names the command that edits it
134
+ // rather than the generated file.
135
+ const rulesRemedy = existsSync(join(dirname(config.policyPath), RULES_FILE))
136
+ ? `run \`${cliCommand("rules")}\` to see the limits in plain terms, or edit ${join(dirname(config.policyPath), RULES_FILE)}`
137
+ : undefined;
63
138
  const { store: baseStore } = openReceiptStore({ db: config.dbPath });
64
139
  // When connected, mirror every stored receipt to the hosted portal through a
65
140
  // durable outbox. The wrapped store's decision is unchanged; export is best-effort.
66
141
  let store = baseStore;
67
142
  let exporter;
143
+ let outbox;
68
144
  if (config.cloud) {
69
145
  const attached = attachExporter(config.dbPath + ".cloud-outbox.db", config.cloud.connection, baseStore, config.cloud.fetch);
70
146
  store = attached.store;
71
147
  exporter = attached.exporter;
148
+ outbox = attached.outbox;
72
149
  }
73
150
  const gateway = createGateway({ policy, authentication: { keys }, attester, store, mode: "check_only" });
74
151
  return {
@@ -81,6 +158,21 @@ export function createHookRuntime(config) {
81
158
  if (exporter)
82
159
  await flushBounded(exporter, config.cloud?.flushTimeoutMs);
83
160
  },
161
+ /** Release the SQLite handles. The hook is a per-tool-call process, and a writer that
162
+ * exits without closing leaves its write-ahead log on disk for the next process to
163
+ * extend — measured at ~11 KiB of WAL per receipt against ~1.7 KiB when closed, so a
164
+ * busy session was writing tens of megabytes of pure overhead into the user's project.
165
+ * Always safe to call, and every exit path should. */
166
+ close() {
167
+ try {
168
+ baseStore.close?.();
169
+ }
170
+ catch { /* the decision is already recorded */ }
171
+ try {
172
+ outbox?.close();
173
+ }
174
+ catch { /* best effort */ }
175
+ },
84
176
  /** Decide one mapped action, recording a receipt either way. */
85
177
  async evaluateOne(mapped) {
86
178
  const signed = agent.sign(mapped.intent);
@@ -93,7 +185,26 @@ export function createHookRuntime(config) {
93
185
  // Evaluated actions, and (in strict mode) unmapped tool.<name>/opaque commands,
94
186
  // go through policy — a closed allowlist denies an unlisted action.
95
187
  const result = await gateway.handleAction({ intent: signed.intent, authorization: signed.authorization });
96
- return { decision: result.allowed ? "allow" : "deny", reason: result.reason, receipt: result.receipt };
188
+ if (result.allowed)
189
+ return { decision: "allow", reason: result.reason, receipt: result.receipt };
190
+ // A deny is the one message the user and their agent actually read, so it is
191
+ // composed from the deciding clause's own words rather than the engine's
192
+ // internal reason ("param ref fails pattern").
193
+ const clauseId = result.verdict?.clause_id ?? null;
194
+ return {
195
+ decision: "deny",
196
+ reason: explainDeny({
197
+ policy, clauseId, detail: result.reason,
198
+ intent: signed.intent, policyPath: config.policyPath,
199
+ postHoc: mapped.postHoc,
200
+ // Point at the editable surface when there is one. `policy.json` is compiled
201
+ // from `rules.json`, so telling someone to hand-edit it invites a change the
202
+ // next `rules` run would overwrite.
203
+ remedy: rulesRemedy,
204
+ }),
205
+ clauseId,
206
+ receipt: result.receipt,
207
+ };
97
208
  },
98
209
  /** Decide a whole tool call. A shell call decomposes into several simple
99
210
  * commands; every one is recorded, and a single deny denies the call. */
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["// The hook runtime: a check-only (M0) gateway built once from the enrolled\n// machine key, policy and a durable local receipt log. Each mapped action is\n// signed by the machine key, decided against policy and countersigned — locally,\n// with no HTTP server and no Cloud dependency in 0.1.\n\nimport { readFileSync } from \"node:fs\";\nimport { createGateway, StaticPrincipalKeyRegistry, type CloudExporter } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester, openReceiptStore } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport type { Mapped } from \"./map.js\";\nimport { attachExporter, flushBounded, type HookConnection } from \"./cloud.js\";\n\nexport interface RuntimeConfig {\n policyPath: string;\n keyPath: string;\n attesterPath: string;\n dbPath: string;\n /** Strict mode: an unmapped tool is policy-checked (and denied by a closed\n * allowlist) instead of observed. Fail-closed for tools with no taxonomy\n * mapping; default false (observe, matching the connector conformance vector). */\n strict?: boolean;\n /** When connected to a Cloud workspace, receipts are auto-exported to the portal.\n * Export is best-effort and never changes the local decision. */\n cloud?: { connection: HookConnection; fetch?: typeof fetch; flushTimeoutMs?: number };\n}\n\nexport interface Decision {\n decision: \"allow\" | \"deny\" | \"not_evaluated\";\n reason: string;\n /** The deciding receipt (the denied one, or the first allow). */\n receipt?: unknown;\n /** Every receipt produced — one per simple command in a decomposed shell call. */\n receipts?: unknown[];\n}\n\n// The hook's own config and keys must be off-limits to the agent it governs:\n// otherwise the agent could rewrite its policy or read the signing keys and the\n// receipts stop meaning anything. These patterns are \"allow if the path does NOT\n// match a protected location\"; they run against the cwd-relative path the mapper\n// produces (and equally against an absolute one).\nconst PROTECTED_WRITE = \"^(?!(?:.*/)?\\\\.scopebond/)(?!(?:.*/)?\\\\.claude/settings)(?!(?:.*/)?\\\\.cursor/hooks)(?!(?:.*/)?\\\\.codex/(?:hooks\\\\.json|config\\\\.toml)$)(?!(?:.*/)?\\\\.git/hooks/)(?!(?:.*/)?\\\\.github/workflows/)(?!(?:.*/)?\\\\.github/actions/)(?!(?:.*/)?\\\\.gitlab-ci\\\\.yml$)(?!(?:.*/)?\\\\.circleci/)(?!(?:.*/)?azure-pipelines\\\\.yml$)(?!(?:.*/)?Jenkinsfile$).+\";\nconst PROTECTED_READ = \"^(?!(?:.*/)?\\\\.scopebond/)(?!.*\\\\.key$)(?!(?:.*/)?\\\\.env(?:\\\\.(?!example$|sample$|template$)[^/]*)?$).+\";\n\n/** The default starter policy for a coding agent: protect release branches, deny\n * destructive programs, allow workspace file access except the hook's own config\n * and keys, and trust only the enrolled machine key. Every threshold is the\n * operator's to edit. */\nexport function starterPolicy(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: \"^(?!(?:main|master)$)(?!release/).+\" } },\n description: \"Deny pushes to main, master and release/*.\",\n },\n {\n id: \"safe-shell\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"shell.exec\"],\n param_bounds: { program: { pattern: \"^(?!(?:rm|sudo|shutdown|reboot|mkfs|dd|del|rd|rmdir|erase|deltree|format|Remove-Item|ri)$).+\" } },\n description: \"Deny destructive programs — POSIX (rm, sudo, shutdown, reboot, mkfs, dd) and Windows/PowerShell (del, rd, rmdir, erase, deltree, format, Remove-Item). An empty program (an unparseable command) is denied.\",\n },\n {\n id: \"protect-write\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"file.write\"],\n param_bounds: { path: { pattern: PROTECTED_WRITE } },\n description: \"Allow workspace writes, but never to the hook's policy/keys, Claude Code, Cursor or Codex hook settings, git hooks, or CI config (.github/workflows, .github/actions, .gitlab-ci.yml, .circleci, azure-pipelines.yml, Jenkinsfile).\",\n },\n {\n id: \"protect-read\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"file.read\"],\n param_bounds: { path: { pattern: PROTECTED_READ } },\n description: \"Allow workspace reads, but never the signing keys (*.key), environment secret files (.env, .env.*, except .env.example/.sample/.template) or the hook's own .scopebond directory.\",\n },\n {\n id: \"observe-net-mcp\", type: \"action_allowlist\", mode: \"monitor\",\n action_types: [\"net.fetch\", \"mcp.tool.call\"],\n description: \"Observe network fetches and MCP tool calls (recorded, not blocked) — add bounds to enforce.\",\n },\n { id: \"keys\", type: \"key_policy\", active_keys: [agentKid], description: \"Only the enrolled machine key may sign.\" },\n ],\n };\n}\n\n/** Build the runtime. Throws on any setup failure (unparseable policy, missing\n * key, unavailable store) — the CLI turns that into a fail-closed deny. */\nexport function createHookRuntime(config: RuntimeConfig) {\n const policy = JSON.parse(readFileSync(config.policyPath, \"utf8\"));\n const agent = createSigner({ privateKeyPem: readFileSync(config.keyPath, \"utf8\") });\n const keys = new StaticPrincipalKeyRegistry([\n { kid: agent.kid, publicKeyPem: agent.publicKeyPem, purposes: [\"agent\"], status: \"active\" },\n ]);\n const { attester } = loadOrCreateAttester({ file: config.attesterPath });\n const { store: baseStore } = openReceiptStore({ db: config.dbPath });\n // When connected, mirror every stored receipt to the hosted portal through a\n // durable outbox. The wrapped store's decision is unchanged; export is best-effort.\n let store = baseStore;\n let exporter: CloudExporter | undefined;\n if (config.cloud) {\n const attached = attachExporter(config.dbPath + \".cloud-outbox.db\", config.cloud.connection, baseStore, config.cloud.fetch);\n store = attached.store;\n exporter = attached.exporter;\n }\n const gateway = createGateway({ policy, authentication: { keys }, attester, store, mode: \"check_only\" });\n\n return {\n gateway,\n agentKid: agent.kid,\n exporter,\n /** Deliver queued receipts to Cloud with a bounded timeout, then it is safe to\n * exit. Undelivered receipts persist in the durable outbox for the next run. */\n async flush(): Promise<void> {\n if (exporter) await flushBounded(exporter, config.cloud?.flushTimeoutMs);\n },\n /** Decide one mapped action, recording a receipt either way. */\n async evaluateOne(mapped: Mapped): Promise<Decision> {\n const signed = agent.sign(mapped.intent);\n if (!mapped.evaluated && !config.strict) {\n // Unknown tool or unparseable command, non-strict: observe without\n // evaluating — grants nothing (D30).\n const { receipt } = await gateway.observeAction({ intent: signed.intent, authorization: signed.authorization });\n return { decision: \"not_evaluated\", reason: `no policy applies to ${mapped.intent.action_type}`, receipt };\n }\n // Evaluated actions, and (in strict mode) unmapped tool.<name>/opaque commands,\n // go through policy — a closed allowlist denies an unlisted action.\n const result = await gateway.handleAction({ intent: signed.intent, authorization: signed.authorization });\n return { decision: result.allowed ? \"allow\" : \"deny\", reason: result.reason, receipt: result.receipt };\n },\n /** Decide a whole tool call. A shell call decomposes into several simple\n * commands; every one is recorded, and a single deny denies the call. */\n async evaluate(mapped: Mapped | Mapped[]): Promise<Decision> {\n const list = Array.isArray(mapped) ? mapped : [mapped];\n if (list.length === 0) return { decision: \"not_evaluated\", reason: \"no action\", receipts: [] };\n const receipts: unknown[] = [];\n let allow: Decision | null = null;\n let notEvaluated: Decision | null = null;\n for (const m of list) {\n const d = await this.evaluateOne(m);\n if (d.receipt !== undefined) receipts.push(d.receipt);\n if (d.decision === \"deny\") return { ...d, receipts }; // any deny denies the call\n if (d.decision === \"allow\" && !allow) allow = d;\n if (d.decision === \"not_evaluated\" && !notEvaluated) notEvaluated = d;\n }\n // No deny: allow if any command was evaluated-and-allowed, else not_evaluated.\n const chosen = allow ?? notEvaluated!;\n return { ...chosen, receipts };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["// The hook runtime: a check-only (M0) gateway built once from the enrolled\n// machine key, policy and a durable local receipt log. Each mapped action is\n// signed by the machine key, decided against policy and countersigned — locally,\n// with no HTTP server and no Cloud dependency in 0.1.\n\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { createGateway, StaticPrincipalKeyRegistry, type CloudExporter } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester, openReceiptStore } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport type { Mapped } from \"./map.js\";\nimport { attachExporter, flushBounded, type HookConnection } from \"./cloud.js\";\nimport { explainDeny, type ExplainIntent } from \"./explain.js\";\nimport { RULES_FILE } from \"./rules.js\";\nimport { cliCommand } from \"./version.js\";\n\nexport interface RuntimeConfig {\n policyPath: string;\n keyPath: string;\n attesterPath: string;\n dbPath: string;\n /** Strict mode: an unmapped tool is policy-checked (and denied by a closed\n * allowlist) instead of observed. Fail-closed for tools with no taxonomy\n * mapping; default false (observe, matching the connector conformance vector). */\n strict?: boolean;\n /** When connected to a Cloud workspace, receipts are auto-exported to the portal.\n * Export is best-effort and never changes the local decision. */\n cloud?: { connection: HookConnection; fetch?: typeof fetch; flushTimeoutMs?: number };\n}\n\nexport interface Decision {\n decision: \"allow\" | \"deny\" | \"not_evaluated\";\n reason: string;\n /** The clause that decided a deny, when the verdict named one. */\n clauseId?: string | null;\n /** The deciding receipt (the denied one, or the first allow). */\n receipt?: unknown;\n /** Every receipt produced — one per simple command in a decomposed shell call. */\n receipts?: unknown[];\n}\n\n// The hook's own config and keys must be off-limits to the agent it governs:\n// otherwise the agent could rewrite its policy or read the signing keys and the\n// receipts stop meaning anything. These patterns are \"allow if the path does NOT\n// match a protected location\"; they run against the cwd-relative path the mapper\n// produces (and equally against an absolute one).\n//\n// Matching is case-insensitive — Windows and macOS open `.ENV` and `.env` as the\n// same file — by expanding each letter to a two-case class (policy patterns are\n// plain regular expressions with no flags). `ci()` is applied to literal text only.\nexport const ci = (s: string): string => s.replace(/[A-Za-z]/g, (c) => `[${c.toLowerCase()}${c.toUpperCase()}]`);\nexport const under = (dir: string): string => `(?!(?:.*/)?${ci(dir)}(?:/|$))`; // dir itself or anything inside it\nexport const named = (file: string): string => `(?!(?:.*/)?${ci(file)}$)`; // exactly this file name\nexport const dir = (d: string): string => `(?!(?:.*/)?${ci(d)}/?$)`; // the directory itself (a recursive read or copy)\n\nconst PROTECTED_WRITE = \"^\" + [\n under(\"\\\\.scopebond\"), `(?!(?:.*/)?${ci(\"\\\\.claude/settings\")})`, under(\"\\\\.claude/hooks\"), under(\"\\\\.claude/agents\"),\n `(?!(?:.*/)?${ci(\"\\\\.cursor/hooks\")})`, named(\"\\\\.codex/hooks\\\\.json\"), named(\"\\\\.codex/config\\\\.toml\"), named(\"\\\\.mcp\\\\.json\"),\n under(\"\\\\.git/hooks\"), named(\"\\\\.git/config\"), under(\"\\\\.husky\"),\n under(\"\\\\.github/workflows\"), under(\"\\\\.github/actions\"), named(\"\\\\.gitlab-ci\\\\.yml\"), named(\"\\\\.gitlab-ci\\\\.yaml\"), under(\"\\\\.circleci\"),\n named(\"azure-pipelines\\\\.yml\"), named(\"Jenkinsfile\"),\n].join(\"\") + \".+\";\n\nconst PROTECTED_READ = \"^\" + [\n under(\"\\\\.scopebond\"),\n `(?!.*${ci(\"\\\\.(?:key|pem|p12|pfx|jks|keystore)\")}$)`,\n // .env, .env.* — except templates whose name ends in .example/.sample/.template/.dist\n `(?!(?:.*/)?${ci(\"\\\\.env\")}(?!(?:\\\\.[^/]*)?\\\\.(?:${ci(\"example\")}|${ci(\"sample\")}|${ci(\"template\")}|${ci(\"dist\")})$)(?:\\\\.[^/]*)?$)`,\n named(\"\\\\.envrc\"),\n // Credentials outside the workspace that an agent can reach by absolute or ~ path,\n // and their directories as a whole (`cp -r ~/.ssh`, `grep -r x ~/.aws`).\n `(?!(?:.*/)?${ci(\"\\\\.ssh\")}(?:$|/(?!.*${ci(\"\\\\.pub\")}$)(?!${ci(\"known_hosts\")}$)(?!${ci(\"config\")}$)))`,\n `(?!(?:.*/)?${ci(\"\\\\.aws\")}(?:$|/(?!${ci(\"config\")}$)))`,\n named(\"\\\\.npmrc\"), named(\"\\\\.pypirc\"), named(\"\\\\.netrc\"), named(\"_netrc\"), named(\"\\\\.git-credentials\"),\n dir(\"\\\\.kube\"), named(\"\\\\.kube/config\"), dir(\"\\\\.docker\"), named(\"\\\\.docker/config\\\\.json\"),\n under(\"\\\\.config/gcloud\"), under(\"\\\\.azure\"), under(\"\\\\.gnupg\"),\n dir(\"\\\\.config/gh\"), named(\"\\\\.config/gh/hosts\\\\.yml\"), named(\"\\\\.claude/\\\\.credentials\\\\.json\"),\n].join(\"\") + \".+\";\n\n// Destructive programs, POSIX and Windows. The mapper records the program as typed,\n// so the pattern accepts any case and an executable suffix (`RM.exe`, `Remove-Item`).\nexport const DESTRUCTIVE = [\n \"rm\", \"sudo\", \"doas\", \"shutdown\", \"reboot\", \"halt\", \"poweroff\", \"mkfs\", \"dd\", \"shred\", \"truncate\", \"unlink\", \"wipe\", \"srm\",\n \"del\", \"rd\", \"rmdir\", \"erase\", \"deltree\", \"format\", \"diskpart\", \"remove-item\", \"ri\", \"clear-content\", \"clc\", \"stop-computer\", \"restart-computer\",\n];\nconst SAFE_SHELL = `^(?!(?:${DESTRUCTIVE.map(ci).join(\"|\")})(?:${ci(\"\\\\.(?:exe|cmd|bat|com|ps1)\")})?$).+`;\n\n// A push destination the starter policy refuses: main, master, release/*, the\n// \"every branch\" flags (`--all`, `--mirror`, `--branches`) and a push whose\n// destination the mapper could not read (`--unknown`: a git alias, a configured push\n// refspec, `send-pack`), in any case. `--tags` alone pushes only tags and is allowed.\nconst SAFE_REF = `^(?!(?:${ci(\"main\")}|${ci(\"master\")})$)(?!${ci(\"release\")}/)(?!-(?!-${ci(\"tags\")}$)).+`;\n\n// Patterns written by earlier starter policies (0.3–0.5), upgraded in memory when a\n// policy on disk still carries them verbatim. An operator's own edits never match\n// these strings and are left untouched.\nconst LEGACY_STARTER_PATTERNS: Record<string, string> = {\n \"^(?!(?:main|master)$)(?!release/).+\": SAFE_REF,\n \"^(?!(?:rm|sudo|shutdown|reboot|mkfs|dd|del|rd|rmdir|erase|deltree|format|Remove-Item|ri)$).+\": SAFE_SHELL,\n \"^(?!(?:rm|sudo|shutdown|reboot|mkfs|dd)$).+\": SAFE_SHELL,\n \"^(?!(?:.*/)?\\\\.scopebond/)(?!(?:.*/)?\\\\.claude/settings)(?!(?:.*/)?\\\\.cursor/hooks)(?!(?:.*/)?\\\\.codex/(?:hooks\\\\.json|config\\\\.toml)$)(?!(?:.*/)?\\\\.git/hooks/)(?!(?:.*/)?\\\\.github/workflows/)(?!(?:.*/)?\\\\.github/actions/)(?!(?:.*/)?\\\\.gitlab-ci\\\\.yml$)(?!(?:.*/)?\\\\.circleci/)(?!(?:.*/)?azure-pipelines\\\\.yml$)(?!(?:.*/)?Jenkinsfile$).+\": PROTECTED_WRITE,\n \"^(?!(?:.*/)?\\\\.scopebond/)(?!(?:.*/)?\\\\.claude/settings)(?!(?:.*/)?\\\\.cursor/hooks)(?!(?:.*/)?\\\\.git/hooks/)(?!(?:.*/)?\\\\.github/workflows/)(?!(?:.*/)?\\\\.github/actions/)(?!(?:.*/)?\\\\.gitlab-ci\\\\.yml$)(?!(?:.*/)?\\\\.circleci/)(?!(?:.*/)?azure-pipelines\\\\.yml$)(?!(?:.*/)?Jenkinsfile$).+\": PROTECTED_WRITE,\n \"^(?!(?:.*/)?\\\\.scopebond/)(?!(?:.*/)?\\\\.claude/settings)(?!(?:.*/)?\\\\.cursor/hooks)(?!(?:.*/)?\\\\.git/hooks/).+\": PROTECTED_WRITE,\n \"^(?!(?:.*/)?\\\\.scopebond/)(?!.*\\\\.key$)(?!(?:.*/)?\\\\.env(?:\\\\.(?!example$|sample$|template$)[^/]*)?$).+\": PROTECTED_READ,\n \"^(?!(?:.*/)?\\\\.scopebond/)(?!.*\\\\.key$).+\": PROTECTED_READ,\n};\n\n/** Upgrade the starter-policy patterns an older hook wrote, in memory, so a user who\n * installed an earlier version gets the current protections without re-running init.\n * Only exact legacy strings are replaced; any other pattern is the operator's own. */\nexport function upgradeStarterPolicy<T>(policy: T): T {\n const p = policy as { clauses?: Array<{ param_bounds?: Record<string, { pattern?: string }> }> };\n for (const clause of p?.clauses ?? []) {\n for (const bound of Object.values(clause?.param_bounds ?? {})) {\n if (bound && typeof bound.pattern === \"string\" && LEGACY_STARTER_PATTERNS[bound.pattern]) bound.pattern = LEGACY_STARTER_PATTERNS[bound.pattern];\n }\n }\n return policy;\n}\n\n/** The default starter policy for a coding agent: protect release branches, deny\n * destructive programs, allow workspace file access except the hook's own config\n * and keys, and trust only the enrolled machine key. Every threshold is the\n * operator's to edit. */\nexport function starterPolicy(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: SAFE_REF } },\n description: \"Deny pushes to main, master and release/* (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.\",\n },\n {\n id: \"safe-shell\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"shell.exec\"],\n param_bounds: { program: { pattern: SAFE_SHELL } },\n description: \"Deny destructive programs — POSIX (rm, sudo, doas, shutdown, reboot, mkfs, dd, shred, truncate, unlink, wipe) and Windows/PowerShell (del, rd, rmdir, erase, deltree, format, diskpart, Remove-Item, Clear-Content) — 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.\",\n },\n {\n id: \"protect-write\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"file.write\"],\n param_bounds: { path: { pattern: PROTECTED_WRITE } },\n description: \"Allow workspace writes, but never to the hook's policy/keys, Claude Code settings, hooks and agents, Cursor or Codex hook settings, .mcp.json, git hooks and git config, Husky hooks, or CI config (.github/workflows, .github/actions, .gitlab-ci.yml/.yaml, .circleci, azure-pipelines.yml, Jenkinsfile). Case-insensitive.\",\n },\n {\n id: \"protect-read\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"file.read\"],\n param_bounds: { path: { pattern: PROTECTED_READ } },\n description: \"Allow workspace reads, but never signing keys and key containers (*.key, *.pem, *.p12, *.pfx, *.jks), environment secret files (.env, .env.*, .envrc — except names ending in .example/.sample/.template/.dist), SSH private keys and the .ssh directory, cloud/registry/git credentials (.aws except .aws/config, .npmrc, .pypirc, .netrc, .git-credentials, .kube/config, .docker/config.json, gcloud, Azure, GnuPG, the GitHub CLI's hosts.yml, Claude Code's .credentials.json) or the hook's own .scopebond directory. Case-insensitive.\",\n },\n {\n id: \"observe-net-mcp\", type: \"action_allowlist\", mode: \"monitor\",\n action_types: [\"net.fetch\", \"mcp.tool.call\"],\n description: \"Observe network fetches and MCP tool calls (recorded, not blocked) — add bounds to enforce.\",\n },\n { id: \"keys\", type: \"key_policy\", active_keys: [agentKid], description: \"Only the enrolled machine key may sign.\" },\n ],\n };\n}\n\n/** Build the runtime. Throws on any setup failure (unparseable policy, missing\n * key, unavailable store) — the CLI turns that into a fail-closed deny. */\nexport function createHookRuntime(config: RuntimeConfig) {\n const policy = upgradeStarterPolicy(JSON.parse(readFileSync(config.policyPath, \"utf8\")));\n const agent = createSigner({ privateKeyPem: readFileSync(config.keyPath, \"utf8\") });\n const keys = new StaticPrincipalKeyRegistry([\n { kid: agent.kid, publicKeyPem: agent.publicKeyPem, purposes: [\"agent\"], status: \"active\" },\n ]);\n const { attester } = loadOrCreateAttester({ file: config.attesterPath });\n // When the project has a readable rule set, a denial names the command that edits it\n // rather than the generated file.\n const rulesRemedy = existsSync(join(dirname(config.policyPath), RULES_FILE))\n ? `run \\`${cliCommand(\"rules\")}\\` to see the limits in plain terms, or edit ${join(dirname(config.policyPath), RULES_FILE)}`\n : undefined;\n const { store: baseStore } = openReceiptStore({ db: config.dbPath });\n // When connected, mirror every stored receipt to the hosted portal through a\n // durable outbox. The wrapped store's decision is unchanged; export is best-effort.\n let store = baseStore;\n let exporter: CloudExporter | undefined;\n let outbox: { close(): void } | undefined;\n if (config.cloud) {\n const attached = attachExporter(config.dbPath + \".cloud-outbox.db\", config.cloud.connection, baseStore, config.cloud.fetch);\n store = attached.store;\n exporter = attached.exporter;\n outbox = attached.outbox;\n }\n const gateway = createGateway({ policy, authentication: { keys }, attester, store, mode: \"check_only\" });\n\n return {\n gateway,\n agentKid: agent.kid,\n exporter,\n /** Deliver queued receipts to Cloud with a bounded timeout, then it is safe to\n * exit. Undelivered receipts persist in the durable outbox for the next run. */\n async flush(): Promise<void> {\n if (exporter) await flushBounded(exporter, config.cloud?.flushTimeoutMs);\n },\n /** Release the SQLite handles. The hook is a per-tool-call process, and a writer that\n * exits without closing leaves its write-ahead log on disk for the next process to\n * extend — measured at ~11 KiB of WAL per receipt against ~1.7 KiB when closed, so a\n * busy session was writing tens of megabytes of pure overhead into the user's project.\n * Always safe to call, and every exit path should. */\n close(): void {\n try { baseStore.close?.(); } catch { /* the decision is already recorded */ }\n try { outbox?.close(); } catch { /* best effort */ }\n },\n /** Decide one mapped action, recording a receipt either way. */\n async evaluateOne(mapped: Mapped): Promise<Decision> {\n const signed = agent.sign(mapped.intent);\n if (!mapped.evaluated && !config.strict) {\n // Unknown tool or unparseable command, non-strict: observe without\n // evaluating — grants nothing (D30).\n const { receipt } = await gateway.observeAction({ intent: signed.intent, authorization: signed.authorization });\n return { decision: \"not_evaluated\", reason: `no policy applies to ${mapped.intent.action_type}`, receipt };\n }\n // Evaluated actions, and (in strict mode) unmapped tool.<name>/opaque commands,\n // go through policy — a closed allowlist denies an unlisted action.\n const result = await gateway.handleAction({ intent: signed.intent, authorization: signed.authorization });\n if (result.allowed) return { decision: \"allow\", reason: result.reason, receipt: result.receipt };\n // A deny is the one message the user and their agent actually read, so it is\n // composed from the deciding clause's own words rather than the engine's\n // internal reason (\"param ref fails pattern\").\n const clauseId = result.verdict?.clause_id ?? null;\n return {\n decision: \"deny\",\n reason: explainDeny({\n policy, clauseId, detail: result.reason,\n intent: signed.intent as ExplainIntent, policyPath: config.policyPath,\n postHoc: mapped.postHoc,\n // Point at the editable surface when there is one. `policy.json` is compiled\n // from `rules.json`, so telling someone to hand-edit it invites a change the\n // next `rules` run would overwrite.\n remedy: rulesRemedy,\n }),\n clauseId,\n receipt: result.receipt,\n };\n },\n /** Decide a whole tool call. A shell call decomposes into several simple\n * commands; every one is recorded, and a single deny denies the call. */\n async evaluate(mapped: Mapped | Mapped[]): Promise<Decision> {\n const list = Array.isArray(mapped) ? mapped : [mapped];\n if (list.length === 0) return { decision: \"not_evaluated\", reason: \"no action\", receipts: [] };\n const receipts: unknown[] = [];\n let allow: Decision | null = null;\n let notEvaluated: Decision | null = null;\n for (const m of list) {\n const d = await this.evaluateOne(m);\n if (d.receipt !== undefined) receipts.push(d.receipt);\n if (d.decision === \"deny\") return { ...d, receipts }; // any deny denies the call\n if (d.decision === \"allow\" && !allow) allow = d;\n if (d.decision === \"not_evaluated\" && !notEvaluated) notEvaluated = d;\n }\n // No deny: allow if any command was evaluated-and-allowed, else not_evaluated.\n const chosen = allow ?? notEvaluated!;\n return { ...chosen, receipts };\n },\n };\n}\n"]}
package/dist/shell.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ export interface Redirect {
2
+ /** The operator without its file descriptor: `>`, `>>`, `>|`, `<`, `<>`. */
3
+ op: string;
4
+ target: string;
5
+ }
1
6
  export interface SimpleCommand {
2
7
  /** The invoked program's basename, after stripping sudo/env/assignment prefixes.
3
8
  * Empty when it cannot be determined. */
@@ -5,22 +10,66 @@ export interface SimpleCommand {
5
10
  /** The first token whole (pre-basename), so a caller can scrub it before splitting
6
11
  * on "/": a bare-secret command must not leak a path-fragment as the program. */
7
12
  programRaw: string;
8
- /** Remaining argv (quotes removed), best-effort — used to parse git push. */
13
+ /** Remaining argv (quotes and redirections removed), best-effort. */
9
14
  argv: string[];
15
+ /** File redirections (`> f`, `>>f`, `x>f`, `< f`, `&> f`), in order. */
16
+ redirects: Redirect[];
10
17
  /** The raw simple-command text, for the redacted command digest. */
11
18
  raw: string;
12
19
  /** True when parsing was not confident; the caller must fail closed. */
13
20
  opaque: boolean;
14
21
  }
22
+ /** The canonical program name used for every decision: basename, lower-cased, with
23
+ * a Windows executable suffix removed. `RM.EXE`, `/bin/rm` and `rm` are one program
24
+ * — Windows and macOS resolve them case-insensitively, so matching the literal
25
+ * spelling would let a case or suffix variant through. */
26
+ export declare const canonProgram: (t: string) => string;
15
27
  /** Decompose a command line into the simple commands it will run. Recurses into
16
28
  * `-c` scripts and command substitutions up to a bounded depth. */
17
29
  export declare function decomposeShell(command: string, depth?: number): SimpleCommand[];
18
- /** Parse a `git … push …` simple command into git.push params, or null if it is
19
- * not a push. Handles global options that take an argument (`-C dir`, `-c k=v`),
20
- * `--force`/`-f`/`--force-with-lease`, and a leading `+` force refspec. */
30
+ /** One pushed destination: the branch it updates and whether that update is forced. */
31
+ export interface PushTarget {
32
+ ref?: string;
33
+ force: boolean;
34
+ /** True when the destination ref is being deleted (`:dst`, `--delete`), which
35
+ * rewrites the branch's existence and so is destructive even without `--force`. */
36
+ del?: boolean;
37
+ /** True when the push targets every branch (`--all`, `--mirror`, `--branches`), so
38
+ * it necessarily reaches any protected ref; `--mirror` also prunes and forces. */
39
+ all?: boolean;
40
+ }
41
+ /** Canonicalize a push destination to the short branch name a policy names:
42
+ * `refs/heads/main` → `main`, `heads/main` → `main`. `HEAD` and `@` mean the
43
+ * current branch and resolve to undefined (the runtime fills the real branch in). */
44
+ export declare function canonRef(dst: string): string | undefined;
45
+ /** A git invocation split into its global `-c` config values, its subcommand and
46
+ * the subcommand's arguments, or null if the program is not git. Global options
47
+ * that take a value (`-C dir`, `-c k=v`, `--git-dir d` …) are skipped with it. */
48
+ export declare function gitArgs(cmd: SimpleCommand): {
49
+ sub?: string;
50
+ args: string[];
51
+ configs: string[];
52
+ } | null;
53
+ /** The ref recorded for a push whose destination cannot be read from the command
54
+ * line: an alias (`git -c alias.ship=push ship`), a configured push refspec
55
+ * (`-c remote.origin.push=…`, `-c push.default=matching`) or a lower-level push
56
+ * (`git send-pack`, `git http-push`, `git subtree push`). It starts with `-`, which
57
+ * the starter branch guard refuses, so an unreadable push fails closed. */
58
+ export declare const UNKNOWN_REF = "--unknown";
59
+ /** Parse a `git … push …` simple command, or null if it is not a push. Handles
60
+ * global options that take an argument (`-C dir`, `-c k=v`), push options that take
61
+ * one (`-o`, `--repo`, `--receive-pack`), every force spelling (`--force`, `-f`,
62
+ * `-uf`, `--force-with-lease`, `--force-if-includes`, a `+` refspec), `src:dst` and
63
+ * fully qualified refspecs, and multiple refspecs — each destination is returned,
64
+ * so `git push origin feature refs/heads/main` cannot hide `main` in second place.
65
+ * `--all`, `--mirror` and `--branches` push every branch: they return the literal
66
+ * flag as the ref, which the starter policy denies; `--tags` alone pushes only tags
67
+ * and returns `--tags`, which it allows. `ref`/`force` mirror the first target for
68
+ * callers that expect a single push. */
21
69
  export declare function parseGitPush(cmd: SimpleCommand): {
22
70
  force: boolean;
23
71
  remote?: string;
24
72
  ref?: string;
73
+ targets: PushTarget[];
25
74
  } | null;
26
75
  //# sourceMappingURL=shell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B;8CAC0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB;sFACkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,MAAM,EAAE,OAAO,CAAC;CACjB;AAuJD;oEACoE;AACpE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,aAAa,EAAE,CA0B1E;AAED;;4EAE4E;AAC5E,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA2BzG"}
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,QAAQ;IACvB,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B;8CAC0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB;sFACkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,wEAAwE;IACxE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;2DAG2D;AAC3D,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,KAAG,MAC0C,CAAC;AA6dpF;oEACoE;AACpE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,aAAa,EAAE,CAwE1E;AAED,uFAAuF;AACvF,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf;wFACoF;IACpF,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;uFACmF;IACnF,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED;;sFAEsF;AACtF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIxD;AAGD;;mFAEmF;AACnF,wBAAgB,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CActG;AAED;;;;4EAI4E;AAC5E,eAAO,MAAM,WAAW,cAAc,CAAC;AAEvC;;;;;;;;;yCASyC;AACzC,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,GAAG,IAAI,CAyDhI"}