@scopebond/hook 0.15.2 → 0.16.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 CHANGED
@@ -266,6 +266,15 @@ patterns: the hook compiles its choices with the same compiler as `rules apply`.
266
266
  - **What the workspace cannot change.** Protection of Scopebond's own settings and of the
267
267
  agents' hook settings, the machine key policy, fail-closed handling of anything unreadable,
268
268
  and this computer's own opt-ins (`allowed_roots`, `protect_remote_database`).
269
+ - **Overrides.** The workspace can set a rule to *Block, user may override* and say who may
270
+ override. A matching action is then blocked until the person at the computer allows it once,
271
+ with a reason, in the Scopebond Agent's window (`@scopebond/agent`): the hook asks the running
272
+ agent and waits up to 45 seconds (20 seconds for Codex and Cursor), then blocks. The receipt
273
+ records the override, the reason's digest and the rule; the workspace adds who and why. Where the
274
+ workspace allows it and the window is not available, Claude Code's own prompt is offered instead,
275
+ but only in a permission mode where Claude Code really asks; that prompt takes no reason and is
276
+ recorded as *offered*. Scopebond's own protection, rules set to Block and the kill switch are
277
+ never overridable, and the workspace's daily limit holds.
269
278
  - **Going back.** While the workspace sets the rules, `rules` edits and `policy load` are
270
279
  refused here. If the connection is revoked, or the workspace stops setting rules for this
271
280
  computer, the hook recompiles `policy.json` from `rules.json`: a computer is never left
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAqqBA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA+rBA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}
package/dist/cli.js CHANGED
@@ -45,6 +45,7 @@ import { connectCloud, ingestUrl, loadConnection } from "./cloud.js";
45
45
  import { recoverEarlierReceipts } from "./recover.js";
46
46
  import { loadPolicyExport, policyBuilds } from "./policy-load.js";
47
47
  import { isManaged, readMeta, MANAGED_DOC_FILE } from "./managed.js";
48
+ import { createOverrideHandler, overrideHint } from "./override.js";
48
49
  import { syncIfDue, syncPolicy } from "./policy-sync.js";
49
50
  import { loadBudgetExport } from "./budget-load.js";
50
51
  import { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor } from "./rules.js";
@@ -129,6 +130,17 @@ function readStdin() {
129
130
  return "";
130
131
  }
131
132
  }
133
+ /** How long a hook waits for a person in the Scopebond window: well inside each agent's hook timeout (Claude Code 60 s by
134
+ * default; the Codex entry Scopebond writes says 30 s), so the wait ends as a block, never as a timed-out hook. */
135
+ const OVERRIDE_WAIT_MS = { claude: 45_000, codex: 20_000, cursor: 20_000 };
136
+ /** Warn mode: Claude Code shows the person its own prompt for this action (only where the workspace allows it and Claude Code
137
+ * really asks; see override.ts). */
138
+ function askClaude(reason) {
139
+ process.stdout.write(JSON.stringify({
140
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: reason },
141
+ }) + "\n");
142
+ process.exit(0);
143
+ }
132
144
  function denyClaude(reason) {
133
145
  process.stdout.write(JSON.stringify({
134
146
  hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
@@ -218,7 +230,11 @@ async function runPreToolUse(mapper, deny = denyClaude, raw, harness = "claude")
218
230
  try {
219
231
  const cwd = input?.cwd ? String(input.cwd) : process.cwd();
220
232
  const dir = resolveConfigDir(cwd);
221
- runtime = createHookRuntime(runtimePaths(dir, cwd));
233
+ const permissionMode = typeof input.permission_mode === "string" ? input.permission_mode : null;
234
+ runtime = createHookRuntime({ ...runtimePaths(dir, cwd), override: (agentKid) => {
235
+ const made = createOverrideHandler({ dir, home: userHome(), agentKid, harness, permissionMode, waitMs: OVERRIDE_WAIT_MS[harness] });
236
+ return made ? { handler: made.handler, hint: () => overrideHint(made.note()) } : null;
237
+ } });
222
238
  useDigestKey(loadOrCreateDigestKey(dir));
223
239
  const decision = await runtime.evaluate([...fillPushBranch(mapper(input), currentBranch(cwd)), ...databaseGuard(dir, cwd, input)], { groupKey: callId(input) });
224
240
  const observer = recordObservations(dir, cwd, input, decision, harness);
@@ -231,6 +247,12 @@ async function runPreToolUse(mapper, deny = denyClaude, raw, harness = "claude")
231
247
  runtime = undefined;
232
248
  if (decision.decision === "deny")
233
249
  deny(decision.reason);
250
+ // Only Claude Code is ever offered "ask" (override.ts); any other harness treats it as a denial.
251
+ if (decision.decision === "ask") {
252
+ if (harness === "claude")
253
+ askClaude(decision.reason);
254
+ deny(decision.reason);
255
+ }
234
256
  // Stay silent on allow/not_evaluated so the coding agent's normal permission
235
257
  // flow remains in charge. Scopebond blocks; it never silently approves.
236
258
  process.exit(0);
@@ -329,9 +351,15 @@ async function runCursor() {
329
351
  try {
330
352
  const cwd = input?.cwd ? String(input.cwd) : process.cwd();
331
353
  const dir = resolveConfigDir(cwd);
332
- runtime = createHookRuntime(runtimePaths(dir, cwd));
333
- useDigestKey(loadOrCreateDigestKey(dir));
354
+ const permissionMode = typeof input.permission_mode === "string" ? input.permission_mode : null;
334
355
  const mapped = fillPushBranch(mapCursorEvent(event, input), currentBranch(cwd));
356
+ // An edit Cursor reports after saving it cannot be overridden: it already happened.
357
+ const overridable = !mapped.some((m) => m.postHoc);
358
+ runtime = createHookRuntime({ ...runtimePaths(dir, cwd), override: (agentKid) => {
359
+ const made = overridable ? createOverrideHandler({ dir, home: userHome(), agentKid, harness: "cursor", permissionMode, waitMs: OVERRIDE_WAIT_MS.cursor }) : null;
360
+ return made ? { handler: made.handler, hint: () => overrideHint(made.note()) } : null;
361
+ } });
362
+ useDigestKey(loadOrCreateDigestKey(dir));
335
363
  const decision = await runtime.evaluate(mapped, { groupKey: callId(input) });
336
364
  const observer = recordObservations(dir, cwd, input, decision, "cursor");
337
365
  await Promise.all([runtime.flush(), observer?.flush() ?? Promise.resolve(), syncIfDue(dir, () => syncOptionsFor(dir))]);