@scopebond/hook 0.2.0 → 0.3.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.
- package/README.md +13 -8
- package/dist/cli.js +203 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +6 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +56 -7
- package/dist/init.js.map +1 -1
- package/dist/map.d.ts +12 -6
- package/dist/map.d.ts.map +1 -1
- package/dist/map.js +69 -45
- package/dist/map.js.map +1 -1
- package/dist/minimize.d.ts +5 -0
- package/dist/minimize.d.ts.map +1 -1
- package/dist/minimize.js +58 -12
- package/dist/minimize.js.map +1 -1
- package/dist/runtime.d.ts +11 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +56 -8
- package/dist/runtime.js.map +1 -1
- package/dist/shell.d.ts +26 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +331 -0
- package/dist/shell.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -2
package/dist/runtime.js.map
CHANGED
|
@@ -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 receipt?: unknown;\n}\n\n/** The default starter policy for a coding agent: protect release branches, deny\n * destructive programs, allow workspace file access, and trust only the enrolled\n * machine key. Every threshold is the 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)$).+\" } },\n description: \"Deny destructive programs (rm, sudo, …).\",\n },\n { id: \"workspace-files\", type: \"action_allowlist\", mode: \"enforce\", action_types: [\"file.write\", \"file.read\"] },\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 async evaluate(mapped: Mapped): Promise<Decision> {\n const signed = agent.sign(mapped.intent);\n if (!mapped.evaluated && !config.strict) {\n // Unknown tool, non-strict: observe without 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 tools, and (in strict mode) unmapped tool.<name> actions, go\n // 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 };\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 } 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)(?!(?:.*/)?\\\\.git/hooks/).+\";\nconst PROTECTED_READ = \"^(?!(?:.*/)?\\\\.scopebond/)(?!.*\\\\.key$).+\";\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)$).+\" } },\n description: \"Deny destructive programs (rm, sudo, …). 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/settings, .cursor/hooks or git hooks.\",\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) 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"]}
|
package/dist/shell.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SimpleCommand {
|
|
2
|
+
/** The invoked program's basename, after stripping sudo/env/assignment prefixes.
|
|
3
|
+
* Empty when it cannot be determined. */
|
|
4
|
+
program: string;
|
|
5
|
+
/** The first token whole (pre-basename), so a caller can scrub it before splitting
|
|
6
|
+
* on "/": a bare-secret command must not leak a path-fragment as the program. */
|
|
7
|
+
programRaw: string;
|
|
8
|
+
/** Remaining argv (quotes removed), best-effort — used to parse git push. */
|
|
9
|
+
argv: string[];
|
|
10
|
+
/** The raw simple-command text, for the redacted command digest. */
|
|
11
|
+
raw: string;
|
|
12
|
+
/** True when parsing was not confident; the caller must fail closed. */
|
|
13
|
+
opaque: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Decompose a command line into the simple commands it will run. Recurses into
|
|
16
|
+
* `-c` scripts and command substitutions up to a bounded depth. */
|
|
17
|
+
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. */
|
|
21
|
+
export declare function parseGitPush(cmd: SimpleCommand): {
|
|
22
|
+
force: boolean;
|
|
23
|
+
remote?: string;
|
|
24
|
+
ref?: string;
|
|
25
|
+
} | null;
|
|
26
|
+
//# sourceMappingURL=shell.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// Shell decomposition for the hook mapper. A single Bash tool call can carry many
|
|
2
|
+
// commands — `a && b`, `a; b`, `a | b`, `$(c)`, `` `c` ``, `bash -c '…'`, `( c )` —
|
|
3
|
+
// so mapping only the first token lets a denied program ride in behind an allowed
|
|
4
|
+
// one. This module splits a command line into the simple commands it will actually
|
|
5
|
+
// run, so the mapper can emit an intent for each and the runtime can deny if any
|
|
6
|
+
// one of them is out of policy.
|
|
7
|
+
//
|
|
8
|
+
// It is best-effort and fail-safe: anything it cannot parse with confidence is
|
|
9
|
+
// returned as a single `opaque` command, which the caller denies (strict) or
|
|
10
|
+
// observes (non-strict) rather than trusting. Every scan is linear in the input
|
|
11
|
+
// length with no backtracking, so a hostile command cannot stall the hot path.
|
|
12
|
+
const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "busybox"]);
|
|
13
|
+
const MAX_DEPTH = 6;
|
|
14
|
+
/** Split a command line into top-level simple-command strings at unquoted
|
|
15
|
+
* separators (`;`, newline, `&&`, `||`, `|`, `&`), leaving quotes, `$( )`,
|
|
16
|
+
* backticks and `( )` groups intact inside each piece. */
|
|
17
|
+
function splitTopLevel(src) {
|
|
18
|
+
const segments = [];
|
|
19
|
+
let cur = "";
|
|
20
|
+
let quote = null;
|
|
21
|
+
let paren = 0; // depth of $( ) and ( )
|
|
22
|
+
let backtick = false;
|
|
23
|
+
const flush = () => { const t = cur.trim(); if (t)
|
|
24
|
+
segments.push(t); cur = ""; };
|
|
25
|
+
for (let i = 0; i < src.length; i++) {
|
|
26
|
+
const c = src[i];
|
|
27
|
+
const n = src[i + 1];
|
|
28
|
+
if (quote) {
|
|
29
|
+
cur += c;
|
|
30
|
+
if (quote === "'") {
|
|
31
|
+
if (c === "'")
|
|
32
|
+
quote = null;
|
|
33
|
+
}
|
|
34
|
+
else if (c === "\\" && n !== undefined)
|
|
35
|
+
cur += src[++i];
|
|
36
|
+
else if (c === '"')
|
|
37
|
+
quote = null;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (c === "'" || c === '"') {
|
|
41
|
+
quote = c;
|
|
42
|
+
cur += c;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (c === "`") {
|
|
46
|
+
backtick = !backtick;
|
|
47
|
+
cur += c;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (backtick) {
|
|
51
|
+
cur += c;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (c === "\\" && n !== undefined) {
|
|
55
|
+
cur += c + src[++i];
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c === "$" && n === "(") {
|
|
59
|
+
paren++;
|
|
60
|
+
cur += "$(";
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === "(") {
|
|
65
|
+
paren++;
|
|
66
|
+
cur += c;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === ")") {
|
|
70
|
+
if (paren > 0)
|
|
71
|
+
paren--;
|
|
72
|
+
cur += c;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (paren > 0) {
|
|
76
|
+
cur += c;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (c === ";" || c === "\n") {
|
|
80
|
+
flush();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === "&" && n === "&") {
|
|
84
|
+
flush();
|
|
85
|
+
i++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (c === "|" && n === "|") {
|
|
89
|
+
flush();
|
|
90
|
+
i++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (c === "&" || c === "|") {
|
|
94
|
+
flush();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
cur += c;
|
|
98
|
+
}
|
|
99
|
+
flush();
|
|
100
|
+
return { segments, unbalanced: quote !== null || paren !== 0 || backtick };
|
|
101
|
+
}
|
|
102
|
+
/** Pull the contents of command substitutions and subshell groups out of one
|
|
103
|
+
* segment — `$( … )`, `` ` … ` ``, and a leading `( … )` — for recursion, and
|
|
104
|
+
* return the segment with those regions blanked so the outer command tokenizes
|
|
105
|
+
* cleanly. */
|
|
106
|
+
function extractSubstitutions(seg) {
|
|
107
|
+
const inner = [];
|
|
108
|
+
let outer = "";
|
|
109
|
+
let quote = null;
|
|
110
|
+
for (let i = 0; i < seg.length; i++) {
|
|
111
|
+
const c = seg[i];
|
|
112
|
+
const n = seg[i + 1];
|
|
113
|
+
if (quote) {
|
|
114
|
+
outer += c;
|
|
115
|
+
if (quote === "'") {
|
|
116
|
+
if (c === "'")
|
|
117
|
+
quote = null;
|
|
118
|
+
}
|
|
119
|
+
else if (c === "\\" && n !== undefined)
|
|
120
|
+
outer += seg[++i];
|
|
121
|
+
else if (c === '"')
|
|
122
|
+
quote = null;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (c === "'" || c === '"') {
|
|
126
|
+
quote = c;
|
|
127
|
+
outer += c;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (c === "`") {
|
|
131
|
+
const end = seg.indexOf("`", i + 1);
|
|
132
|
+
if (end === -1) {
|
|
133
|
+
outer += c;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
inner.push(seg.slice(i + 1, end));
|
|
137
|
+
i = end;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if ((c === "$" && n === "(") || c === "(") {
|
|
141
|
+
const start = c === "(" ? i + 1 : i + 2;
|
|
142
|
+
let depth = 1;
|
|
143
|
+
let j = start;
|
|
144
|
+
for (; j < seg.length && depth > 0; j++) {
|
|
145
|
+
if (seg[j] === "(")
|
|
146
|
+
depth++;
|
|
147
|
+
else if (seg[j] === ")")
|
|
148
|
+
depth--;
|
|
149
|
+
}
|
|
150
|
+
inner.push(seg.slice(start, depth === 0 ? j - 1 : j));
|
|
151
|
+
i = j - 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
outer += c;
|
|
155
|
+
}
|
|
156
|
+
return { outer, inner };
|
|
157
|
+
}
|
|
158
|
+
/** Tokenize a simple command into words, honoring single and double quotes. */
|
|
159
|
+
function tokenize(s) {
|
|
160
|
+
const out = [];
|
|
161
|
+
let cur = "";
|
|
162
|
+
let has = false;
|
|
163
|
+
let quote = null;
|
|
164
|
+
for (let i = 0; i < s.length; i++) {
|
|
165
|
+
const c = s[i];
|
|
166
|
+
const n = s[i + 1];
|
|
167
|
+
if (quote === "'") {
|
|
168
|
+
if (c === "'")
|
|
169
|
+
quote = null;
|
|
170
|
+
else
|
|
171
|
+
cur += c;
|
|
172
|
+
has = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (quote === '"') {
|
|
176
|
+
if (c === '"')
|
|
177
|
+
quote = null;
|
|
178
|
+
else if (c === "\\" && n !== undefined)
|
|
179
|
+
cur += s[++i];
|
|
180
|
+
else
|
|
181
|
+
cur += c;
|
|
182
|
+
has = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (c === "'" || c === '"') {
|
|
186
|
+
quote = c;
|
|
187
|
+
has = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (c === "\\" && n !== undefined) {
|
|
191
|
+
cur += s[++i];
|
|
192
|
+
has = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (/\s/.test(c)) {
|
|
196
|
+
if (has) {
|
|
197
|
+
out.push(cur);
|
|
198
|
+
cur = "";
|
|
199
|
+
has = false;
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
cur += c;
|
|
204
|
+
has = true;
|
|
205
|
+
}
|
|
206
|
+
if (has)
|
|
207
|
+
out.push(cur);
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
const basename = (t) => t.replace(/^.*[\\/]/, "");
|
|
211
|
+
const isAssignment = (t) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(t);
|
|
212
|
+
/** Strip a leading run of `sudo`, `env`, `command`, `nohup`, `nice`, `time`,
|
|
213
|
+
* `xargs`, `exec` wrappers and `NAME=value` assignments, so the program is the
|
|
214
|
+
* real one about to run. `env`/`xargs` consume their own following words as the
|
|
215
|
+
* command, which is handled by simply dropping the wrapper token. */
|
|
216
|
+
function stripPrefixes(tokens) {
|
|
217
|
+
const wrappers = new Set(["sudo", "env", "command", "nohup", "nice", "time", "exec", "stdbuf", "setsid", "xargs"]);
|
|
218
|
+
let i = 0;
|
|
219
|
+
while (i < tokens.length) {
|
|
220
|
+
const t = tokens[i];
|
|
221
|
+
if (isAssignment(t)) {
|
|
222
|
+
i++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (wrappers.has(basename(t))) {
|
|
226
|
+
i++;
|
|
227
|
+
// `sudo -u user`, `nice -n 10`, `env -i` … skip option words and their args.
|
|
228
|
+
while (i < tokens.length && tokens[i].startsWith("-")) {
|
|
229
|
+
const opt = tokens[i];
|
|
230
|
+
i++;
|
|
231
|
+
if (/^-[unC]$/.test(opt) && i < tokens.length && !tokens[i].startsWith("-"))
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
return tokens.slice(i);
|
|
239
|
+
}
|
|
240
|
+
/** Find the script argument of a shell invoked with `-c`, including combined short
|
|
241
|
+
* flags where `c` is last (`-lc`, `-xec`). The script is the next non-option word. */
|
|
242
|
+
function shellScriptArg(argv) {
|
|
243
|
+
for (let i = 0; i < argv.length; i++) {
|
|
244
|
+
if (argv[i] === "-c" || /^-[A-Za-z]*c$/.test(argv[i])) {
|
|
245
|
+
for (let j = i + 1; j < argv.length; j++)
|
|
246
|
+
if (!argv[j].startsWith("-"))
|
|
247
|
+
return argv[j];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
/** Decompose a command line into the simple commands it will run. Recurses into
|
|
253
|
+
* `-c` scripts and command substitutions up to a bounded depth. */
|
|
254
|
+
export function decomposeShell(command, depth = 0) {
|
|
255
|
+
const src = command.trim();
|
|
256
|
+
if (!src)
|
|
257
|
+
return [];
|
|
258
|
+
const opaque = () => [{ program: "", programRaw: "", argv: [], raw: src, opaque: true }];
|
|
259
|
+
if (depth > MAX_DEPTH)
|
|
260
|
+
return opaque();
|
|
261
|
+
const { segments, unbalanced } = splitTopLevel(src);
|
|
262
|
+
if (unbalanced)
|
|
263
|
+
return opaque();
|
|
264
|
+
const out = [];
|
|
265
|
+
for (const seg of segments) {
|
|
266
|
+
const { outer, inner } = extractSubstitutions(seg);
|
|
267
|
+
for (const sub of inner)
|
|
268
|
+
out.push(...decomposeShell(sub, depth + 1));
|
|
269
|
+
const tokens = stripPrefixes(tokenize(outer));
|
|
270
|
+
if (tokens.length === 0)
|
|
271
|
+
continue; // pure substitution/subshell — inner already handled
|
|
272
|
+
const program = basename(tokens[0]);
|
|
273
|
+
const argv = tokens.slice(1);
|
|
274
|
+
out.push({ program, programRaw: tokens[0], argv, raw: seg, opaque: false });
|
|
275
|
+
if (SHELLS.has(program)) {
|
|
276
|
+
const script = shellScriptArg(argv);
|
|
277
|
+
if (script)
|
|
278
|
+
out.push(...decomposeShell(script, depth + 1));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return out.length ? out : opaque();
|
|
282
|
+
}
|
|
283
|
+
/** Parse a `git … push …` simple command into git.push params, or null if it is
|
|
284
|
+
* not a push. Handles global options that take an argument (`-C dir`, `-c k=v`),
|
|
285
|
+
* `--force`/`-f`/`--force-with-lease`, and a leading `+` force refspec. */
|
|
286
|
+
export function parseGitPush(cmd) {
|
|
287
|
+
if (cmd.program !== "git")
|
|
288
|
+
return null;
|
|
289
|
+
const a = cmd.argv;
|
|
290
|
+
let i = 0;
|
|
291
|
+
while (i < a.length) {
|
|
292
|
+
const t = a[i];
|
|
293
|
+
if (t === "-C" || t === "-c" || t === "--git-dir" || t === "--work-tree" || t === "--namespace") {
|
|
294
|
+
i += 2;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (t.startsWith("-")) {
|
|
298
|
+
i++;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
if (a[i] !== "push")
|
|
304
|
+
return null;
|
|
305
|
+
const rest = a.slice(i + 1);
|
|
306
|
+
let force = false;
|
|
307
|
+
const positional = [];
|
|
308
|
+
for (const t of rest) {
|
|
309
|
+
if (t === "--force" || t === "-f" || t === "--force-with-lease" || t.startsWith("--force-with-lease=")) {
|
|
310
|
+
force = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (t.startsWith("-"))
|
|
314
|
+
continue;
|
|
315
|
+
positional.push(t);
|
|
316
|
+
}
|
|
317
|
+
const params = { force };
|
|
318
|
+
if (positional[0])
|
|
319
|
+
params.remote = positional[0];
|
|
320
|
+
if (positional[1]) {
|
|
321
|
+
let refspec = positional[1];
|
|
322
|
+
if (refspec.startsWith("+")) {
|
|
323
|
+
force = true;
|
|
324
|
+
params.force = true;
|
|
325
|
+
refspec = refspec.slice(1);
|
|
326
|
+
}
|
|
327
|
+
params.ref = refspec.replace(/^[^:]*:\+?/, ""); // src:dst → dst
|
|
328
|
+
}
|
|
329
|
+
return params;
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"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","sourcesContent":["// Shell decomposition for the hook mapper. A single Bash tool call can carry many\n// commands — `a && b`, `a; b`, `a | b`, `$(c)`, `` `c` ``, `bash -c '…'`, `( c )` —\n// so mapping only the first token lets a denied program ride in behind an allowed\n// one. This module splits a command line into the simple commands it will actually\n// run, so the mapper can emit an intent for each and the runtime can deny if any\n// one of them is out of policy.\n//\n// It is best-effort and fail-safe: anything it cannot parse with confidence is\n// returned as a single `opaque` command, which the caller denies (strict) or\n// observes (non-strict) rather than trusting. Every scan is linear in the input\n// length with no backtracking, so a hostile command cannot stall the hot path.\n\nexport interface SimpleCommand {\n /** The invoked program's basename, after stripping sudo/env/assignment prefixes.\n * Empty when it cannot be determined. */\n program: string;\n /** The first token whole (pre-basename), so a caller can scrub it before splitting\n * on \"/\": a bare-secret command must not leak a path-fragment as the program. */\n programRaw: string;\n /** Remaining argv (quotes removed), best-effort — used to parse git push. */\n argv: string[];\n /** The raw simple-command text, for the redacted command digest. */\n raw: string;\n /** True when parsing was not confident; the caller must fail closed. */\n opaque: boolean;\n}\n\nconst SHELLS = new Set([\"sh\", \"bash\", \"zsh\", \"dash\", \"ksh\", \"ash\", \"busybox\"]);\nconst MAX_DEPTH = 6;\n\n/** Split a command line into top-level simple-command strings at unquoted\n * separators (`;`, newline, `&&`, `||`, `|`, `&`), leaving quotes, `$( )`,\n * backticks and `( )` groups intact inside each piece. */\nfunction splitTopLevel(src: string): { segments: string[]; unbalanced: boolean } {\n const segments: string[] = [];\n let cur = \"\";\n let quote: '\"' | \"'\" | null = null;\n let paren = 0; // depth of $( ) and ( )\n let backtick = false;\n const flush = () => { const t = cur.trim(); if (t) segments.push(t); cur = \"\"; };\n\n for (let i = 0; i < src.length; i++) {\n const c = src[i];\n const n = src[i + 1];\n if (quote) {\n cur += c;\n if (quote === \"'\") { if (c === \"'\") quote = null; }\n else if (c === \"\\\\\" && n !== undefined) cur += src[++i];\n else if (c === '\"') quote = null;\n continue;\n }\n if (c === \"'\" || c === '\"') { quote = c; cur += c; continue; }\n if (c === \"`\") { backtick = !backtick; cur += c; continue; }\n if (backtick) { cur += c; continue; }\n if (c === \"\\\\\" && n !== undefined) { cur += c + src[++i]; continue; }\n if (c === \"$\" && n === \"(\") { paren++; cur += \"$(\"; i++; continue; }\n if (c === \"(\") { paren++; cur += c; continue; }\n if (c === \")\") { if (paren > 0) paren--; cur += c; continue; }\n if (paren > 0) { cur += c; continue; }\n if (c === \";\" || c === \"\\n\") { flush(); continue; }\n if (c === \"&\" && n === \"&\") { flush(); i++; continue; }\n if (c === \"|\" && n === \"|\") { flush(); i++; continue; }\n if (c === \"&\" || c === \"|\") { flush(); continue; }\n cur += c;\n }\n flush();\n return { segments, unbalanced: quote !== null || paren !== 0 || backtick };\n}\n\n/** Pull the contents of command substitutions and subshell groups out of one\n * segment — `$( … )`, `` ` … ` ``, and a leading `( … )` — for recursion, and\n * return the segment with those regions blanked so the outer command tokenizes\n * cleanly. */\nfunction extractSubstitutions(seg: string): { outer: string; inner: string[] } {\n const inner: string[] = [];\n let outer = \"\";\n let quote: '\"' | \"'\" | null = null;\n for (let i = 0; i < seg.length; i++) {\n const c = seg[i];\n const n = seg[i + 1];\n if (quote) {\n outer += c;\n if (quote === \"'\") { if (c === \"'\") quote = null; }\n else if (c === \"\\\\\" && n !== undefined) outer += seg[++i];\n else if (c === '\"') quote = null;\n continue;\n }\n if (c === \"'\" || c === '\"') { quote = c; outer += c; continue; }\n if (c === \"`\") {\n const end = seg.indexOf(\"`\", i + 1);\n if (end === -1) { outer += c; continue; }\n inner.push(seg.slice(i + 1, end));\n i = end;\n continue;\n }\n if ((c === \"$\" && n === \"(\") || c === \"(\") {\n const start = c === \"(\" ? i + 1 : i + 2;\n let depth = 1;\n let j = start;\n for (; j < seg.length && depth > 0; j++) {\n if (seg[j] === \"(\") depth++;\n else if (seg[j] === \")\") depth--;\n }\n inner.push(seg.slice(start, depth === 0 ? j - 1 : j));\n i = j - 1;\n continue;\n }\n outer += c;\n }\n return { outer, inner };\n}\n\n/** Tokenize a simple command into words, honoring single and double quotes. */\nfunction tokenize(s: string): string[] {\n const out: string[] = [];\n let cur = \"\";\n let has = false;\n let quote: '\"' | \"'\" | null = null;\n for (let i = 0; i < s.length; i++) {\n const c = s[i];\n const n = s[i + 1];\n if (quote === \"'\") { if (c === \"'\") quote = null; else cur += c; has = true; continue; }\n if (quote === '\"') {\n if (c === '\"') quote = null;\n else if (c === \"\\\\\" && n !== undefined) cur += s[++i];\n else cur += c;\n has = true; continue;\n }\n if (c === \"'\" || c === '\"') { quote = c; has = true; continue; }\n if (c === \"\\\\\" && n !== undefined) { cur += s[++i]; has = true; continue; }\n if (/\\s/.test(c)) { if (has) { out.push(cur); cur = \"\"; has = false; } continue; }\n cur += c; has = true;\n }\n if (has) out.push(cur);\n return out;\n}\n\nconst basename = (t: string): string => t.replace(/^.*[\\\\/]/, \"\");\nconst isAssignment = (t: string): boolean => /^[A-Za-z_][A-Za-z0-9_]*=/.test(t);\n\n/** Strip a leading run of `sudo`, `env`, `command`, `nohup`, `nice`, `time`,\n * `xargs`, `exec` wrappers and `NAME=value` assignments, so the program is the\n * real one about to run. `env`/`xargs` consume their own following words as the\n * command, which is handled by simply dropping the wrapper token. */\nfunction stripPrefixes(tokens: string[]): string[] {\n const wrappers = new Set([\"sudo\", \"env\", \"command\", \"nohup\", \"nice\", \"time\", \"exec\", \"stdbuf\", \"setsid\", \"xargs\"]);\n let i = 0;\n while (i < tokens.length) {\n const t = tokens[i];\n if (isAssignment(t)) { i++; continue; }\n if (wrappers.has(basename(t))) {\n i++;\n // `sudo -u user`, `nice -n 10`, `env -i` … skip option words and their args.\n while (i < tokens.length && tokens[i].startsWith(\"-\")) {\n const opt = tokens[i];\n i++;\n if (/^-[unC]$/.test(opt) && i < tokens.length && !tokens[i].startsWith(\"-\")) i++;\n }\n continue;\n }\n break;\n }\n return tokens.slice(i);\n}\n\n/** Find the script argument of a shell invoked with `-c`, including combined short\n * flags where `c` is last (`-lc`, `-xec`). The script is the next non-option word. */\nfunction shellScriptArg(argv: string[]): string | null {\n for (let i = 0; i < argv.length; i++) {\n if (argv[i] === \"-c\" || /^-[A-Za-z]*c$/.test(argv[i])) {\n for (let j = i + 1; j < argv.length; j++) if (!argv[j].startsWith(\"-\")) return argv[j];\n }\n }\n return null;\n}\n\n/** Decompose a command line into the simple commands it will run. Recurses into\n * `-c` scripts and command substitutions up to a bounded depth. */\nexport function decomposeShell(command: string, depth = 0): SimpleCommand[] {\n const src = command.trim();\n if (!src) return [];\n const opaque = (): SimpleCommand[] => [{ program: \"\", programRaw: \"\", argv: [], raw: src, opaque: true }];\n if (depth > MAX_DEPTH) return opaque();\n\n const { segments, unbalanced } = splitTopLevel(src);\n if (unbalanced) return opaque();\n\n const out: SimpleCommand[] = [];\n for (const seg of segments) {\n const { outer, inner } = extractSubstitutions(seg);\n for (const sub of inner) out.push(...decomposeShell(sub, depth + 1));\n\n const tokens = stripPrefixes(tokenize(outer));\n if (tokens.length === 0) continue; // pure substitution/subshell — inner already handled\n const program = basename(tokens[0]);\n const argv = tokens.slice(1);\n out.push({ program, programRaw: tokens[0], argv, raw: seg, opaque: false });\n\n if (SHELLS.has(program)) {\n const script = shellScriptArg(argv);\n if (script) out.push(...decomposeShell(script, depth + 1));\n }\n }\n return out.length ? out : opaque();\n}\n\n/** Parse a `git … push …` simple command into git.push params, or null if it is\n * not a push. Handles global options that take an argument (`-C dir`, `-c k=v`),\n * `--force`/`-f`/`--force-with-lease`, and a leading `+` force refspec. */\nexport function parseGitPush(cmd: SimpleCommand): { force: boolean; remote?: string; ref?: string } | null {\n if (cmd.program !== \"git\") return null;\n const a = cmd.argv;\n let i = 0;\n while (i < a.length) {\n const t = a[i];\n if (t === \"-C\" || t === \"-c\" || t === \"--git-dir\" || t === \"--work-tree\" || t === \"--namespace\") { i += 2; continue; }\n if (t.startsWith(\"-\")) { i++; continue; }\n break;\n }\n if (a[i] !== \"push\") return null;\n const rest = a.slice(i + 1);\n let force = false;\n const positional: string[] = [];\n for (const t of rest) {\n if (t === \"--force\" || t === \"-f\" || t === \"--force-with-lease\" || t.startsWith(\"--force-with-lease=\")) { force = true; continue; }\n if (t.startsWith(\"-\")) continue;\n positional.push(t);\n }\n const params: { force: boolean; remote?: string; ref?: string } = { force };\n if (positional[0]) params.remote = positional[0];\n if (positional[1]) {\n let refspec = positional[1];\n if (refspec.startsWith(\"+\")) { force = true; params.force = true; refspec = refspec.slice(1); }\n params.ref = refspec.replace(/^[^:]*:\\+?/, \"\"); // src:dst → dst\n }\n return params;\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function hookVersion(): string;
|
|
2
|
+
/** The command the agent harness runs for each tool call: a version-pinned npx
|
|
3
|
+
* invocation so a missing global binary is fetched rather than silently skipped
|
|
4
|
+
* (which a harness can treat as "no hook" and fail open). */
|
|
5
|
+
export declare function hookCommand(harness: "claude" | "cursor"): string;
|
|
6
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAOA,wBAAgB,WAAW,IAAI,MAAM,CASpC;AAED;;8DAE8D;AAC9D,wBAAgB,WAAW,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAEhE"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The installed package version, read from the package's own package.json at
|
|
2
|
+
// runtime. Used to pin the `npx` command the hook installs into the agent config,
|
|
3
|
+
// so the harness always runs the exact version the user set up rather than a
|
|
4
|
+
// floating latest.
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
export function hookVersion() {
|
|
7
|
+
try {
|
|
8
|
+
// dist/version.js → ../package.json is the package root in both the source
|
|
9
|
+
// tree and the published tarball.
|
|
10
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
|
+
return typeof pkg.version === "string" ? pkg.version : "latest";
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return "latest";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The command the agent harness runs for each tool call: a version-pinned npx
|
|
18
|
+
* invocation so a missing global binary is fetched rather than silently skipped
|
|
19
|
+
* (which a harness can treat as "no hook" and fail open). */
|
|
20
|
+
export function hookCommand(harness) {
|
|
21
|
+
return `npx -y @scopebond/hook@${hookVersion()} ${harness}`;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,kFAAkF;AAClF,6EAA6E;AAC7E,mBAAmB;AAEnB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC;QACH,2EAA2E;QAC3E,kCAAkC;QAClC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1F,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;8DAE8D;AAC9D,MAAM,UAAU,WAAW,CAAC,OAA4B;IACtD,OAAO,0BAA0B,WAAW,EAAE,IAAI,OAAO,EAAE,CAAC;AAC9D,CAAC","sourcesContent":["// The installed package version, read from the package's own package.json at\n// runtime. Used to pin the `npx` command the hook installs into the agent config,\n// so the harness always runs the exact version the user set up rather than a\n// floating latest.\n\nimport { readFileSync } from \"node:fs\";\n\nexport function hookVersion(): string {\n try {\n // dist/version.js → ../package.json is the package root in both the source\n // tree and the published tarball.\n const pkg = JSON.parse(readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"));\n return typeof pkg.version === \"string\" ? pkg.version : \"latest\";\n } catch {\n return \"latest\";\n }\n}\n\n/** The command the agent harness runs for each tool call: a version-pinned npx\n * invocation so a missing global binary is fetched rather than silently skipped\n * (which a harness can treat as \"no hook\" and fail open). */\nexport function hookCommand(harness: \"claude\" | \"cursor\"): string {\n return `npx -y @scopebond/hook@${hookVersion()} ${harness}`;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scopebond/hook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Scopebond hook for Claude Code and Cursor — checks each coding-agent tool call against policy before it runs (in-path, cooperative M0) and records a signed receipt, locally.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Avouro LLC",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@scopebond/policy-schema": "^0.3.0",
|
|
44
44
|
"@scopebond/sdk": "^0.1.1",
|
|
45
|
-
"@scopebond/gateway": "^0.
|
|
45
|
+
"@scopebond/gateway": "^0.6.0"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|