@scopebond/hook 0.8.1 → 0.10.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 (110) hide show
  1. package/README.md +239 -0
  2. package/dist/budget-load.d.ts +68 -0
  3. package/dist/budget-load.d.ts.map +1 -0
  4. package/dist/budget-load.js +164 -0
  5. package/dist/budget-load.js.map +1 -0
  6. package/dist/capabilities.d.ts +103 -0
  7. package/dist/capabilities.d.ts.map +1 -0
  8. package/dist/capabilities.js +212 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/classify.d.ts +17 -0
  11. package/dist/classify.d.ts.map +1 -0
  12. package/dist/classify.js +78 -0
  13. package/dist/classify.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +585 -13
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cloud.d.ts +8 -0
  18. package/dist/cloud.d.ts.map +1 -1
  19. package/dist/cloud.js.map +1 -1
  20. package/dist/dispatch-cli.d.ts +2 -0
  21. package/dist/dispatch-cli.d.ts.map +1 -0
  22. package/dist/dispatch-cli.js +128 -0
  23. package/dist/dispatch-cli.js.map +1 -0
  24. package/dist/explain.d.ts.map +1 -1
  25. package/dist/explain.js +2 -1
  26. package/dist/explain.js.map +1 -1
  27. package/dist/group.d.ts +12 -0
  28. package/dist/group.d.ts.map +1 -0
  29. package/dist/group.js +55 -0
  30. package/dist/group.js.map +1 -0
  31. package/dist/index.d.ts +29 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +19 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.d.ts +10 -0
  36. package/dist/install.d.ts.map +1 -1
  37. package/dist/install.js +56 -0
  38. package/dist/install.js.map +1 -1
  39. package/dist/managed.d.ts +69 -0
  40. package/dist/managed.d.ts.map +1 -0
  41. package/dist/managed.js +184 -0
  42. package/dist/managed.js.map +1 -0
  43. package/dist/map.d.ts +4 -0
  44. package/dist/map.d.ts.map +1 -1
  45. package/dist/map.js +29 -1
  46. package/dist/map.js.map +1 -1
  47. package/dist/obs-emitter.d.ts +132 -0
  48. package/dist/obs-emitter.d.ts.map +1 -0
  49. package/dist/obs-emitter.js +415 -0
  50. package/dist/obs-emitter.js.map +1 -0
  51. package/dist/obs-store.d.ts +160 -0
  52. package/dist/obs-store.d.ts.map +1 -0
  53. package/dist/obs-store.js +367 -0
  54. package/dist/obs-store.js.map +1 -0
  55. package/dist/obs-upload.d.ts +24 -0
  56. package/dist/obs-upload.d.ts.map +1 -0
  57. package/dist/obs-upload.js +231 -0
  58. package/dist/obs-upload.js.map +1 -0
  59. package/dist/observation.d.ts +232 -0
  60. package/dist/observation.d.ts.map +1 -0
  61. package/dist/observation.js +308 -0
  62. package/dist/observation.js.map +1 -0
  63. package/dist/paths.d.ts +15 -0
  64. package/dist/paths.d.ts.map +1 -0
  65. package/dist/paths.js +79 -0
  66. package/dist/paths.js.map +1 -0
  67. package/dist/policy-load.d.ts +60 -0
  68. package/dist/policy-load.d.ts.map +1 -0
  69. package/dist/policy-load.js +151 -0
  70. package/dist/policy-load.js.map +1 -0
  71. package/dist/policy-sync.d.ts +45 -0
  72. package/dist/policy-sync.d.ts.map +1 -0
  73. package/dist/policy-sync.js +171 -0
  74. package/dist/policy-sync.js.map +1 -0
  75. package/dist/proof.d.ts +30 -0
  76. package/dist/proof.d.ts.map +1 -0
  77. package/dist/proof.js +186 -0
  78. package/dist/proof.js.map +1 -0
  79. package/dist/rules.d.ts +20 -0
  80. package/dist/rules.d.ts.map +1 -1
  81. package/dist/rules.js +50 -0
  82. package/dist/rules.js.map +1 -1
  83. package/dist/runtime.d.ts +21 -1
  84. package/dist/runtime.d.ts.map +1 -1
  85. package/dist/runtime.js +77 -10
  86. package/dist/runtime.js.map +1 -1
  87. package/dist/scan.d.ts +7 -0
  88. package/dist/scan.d.ts.map +1 -0
  89. package/dist/scan.js +38 -0
  90. package/dist/scan.js.map +1 -0
  91. package/dist/shell.d.ts.map +1 -1
  92. package/dist/shell.js +12 -3
  93. package/dist/shell.js.map +1 -1
  94. package/dist/sql-classify.d.ts +11 -0
  95. package/dist/sql-classify.d.ts.map +1 -0
  96. package/dist/sql-classify.js +251 -0
  97. package/dist/sql-classify.js.map +1 -0
  98. package/dist/typed-infra.d.ts +73 -0
  99. package/dist/typed-infra.d.ts.map +1 -0
  100. package/dist/typed-infra.js +758 -0
  101. package/dist/typed-infra.js.map +1 -0
  102. package/dist/typed-ops.d.ts +140 -0
  103. package/dist/typed-ops.d.ts.map +1 -0
  104. package/dist/typed-ops.js +549 -0
  105. package/dist/typed-ops.js.map +1 -0
  106. package/dist/vectors.d.ts +40 -0
  107. package/dist/vectors.d.ts.map +1 -0
  108. package/dist/vectors.js +219 -0
  109. package/dist/vectors.js.map +1 -0
  110. package/package.json +5 -5
package/dist/cli.js CHANGED
@@ -29,17 +29,30 @@ import { hostname, tmpdir } from "node:os";
29
29
  import { execFileSync } from "node:child_process";
30
30
  import { verifyReceipt } from "@scopebond/gateway";
31
31
  import { openReceiptStore, loadOrCreateAttester } from "@scopebond/gateway/node";
32
+ import { callRequestOf, keyedIdFor, TYPED_ACTION_TYPES } from "./typed-ops.js";
33
+ import { databaseGuardActions } from "./typed-infra.js";
32
34
  import { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
33
35
  import { createHookRuntime } from "./runtime.js";
34
36
  import { useDigestKey, loadOrCreateDigestKey } from "./minimize.js";
35
37
  import { scaffold, harnessSnippet, placeHook } from "./init.js";
36
- import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, projectHarnessFile, localHarnessFile, gitShareState, isMachineSpecificCommand, trustProjectPolicy, untrustedProjectPolicy, } from "./install.js";
38
+ import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, projectHarnessFile, localHarnessFile, gitShareState, isMachineSpecificCommand, trustProjectPolicy, untrustedProjectPolicy, isTrustedProject, wireLifecycleHooks, unwireLifecycleHooks, } from "./install.js";
39
+ import { openObservations, describeObservations, observationStatus, stopReasonFromClaude, exitFromClaudeFailure, HEARTBEAT_INTERVAL_MS, OBSERVATIONS_SCOPE, } from "./obs-emitter.js";
40
+ import { OBSERVATION_DB, ObservationStore } from "./obs-store.js";
41
+ import { loadOrCreateBindingKey } from "./observation.js";
42
+ import { uploadPending } from "./obs-upload.js";
37
43
  import { connectCloud, loadConnection } from "./cloud.js";
44
+ import { loadPolicyExport, policyBuilds } from "./policy-load.js";
45
+ import { isManaged, readMeta, MANAGED_DOC_FILE } from "./managed.js";
46
+ import { syncIfDue, syncPolicy } from "./policy-sync.js";
47
+ import { loadBudgetExport } from "./budget-load.js";
38
48
  import { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor } from "./rules.js";
39
49
  import { createSigner } from "@scopebond/sdk";
50
+ import { runDispatchCommand } from "./dispatch-cli.js";
40
51
  import { describeAction } from "./explain.js";
41
52
  import { ensureDurableRuntime, pinnedCliPath, isEphemeralPath } from "./runtime-install.js";
42
53
  import { cliCommand, hookCommand, hookVersion } from "./version.js";
54
+ import { computeManifest, renderManifest } from "./capabilities.js";
55
+ import { runProofFixtures, loadProofs, saveProofs, proofPassed, deliverProofReceipts } from "./proof.js";
43
56
  import { fileURLToPath } from "node:url";
44
57
  /** The current git branch in `cwd` (best-effort). A bare `git push` pushes it, so
45
58
  * the runtime fills it in before evaluating; on failure the ref stays absent and
@@ -77,13 +90,14 @@ function readBundleArg(bundleArg, stdin) {
77
90
  function configDir() {
78
91
  return process.env.SCOPEBOND_HOOK_DIR ?? join(process.cwd(), ".scopebond");
79
92
  }
80
- function runtimePaths(dir) {
93
+ function runtimePaths(dir, cwd) {
81
94
  const connection = loadConnection(dir);
82
95
  return {
83
96
  policyPath: join(dir, "policy.json"),
84
97
  keyPath: join(dir, "agent.key"),
85
98
  attesterPath: join(dir, "attester.key"),
86
99
  dbPath: join(dir, "receipts.db"),
100
+ ...(cwd ? { cwd } : {}),
87
101
  // Strict: deny (not just observe) tools with no taxonomy mapping.
88
102
  strict: process.argv.includes("--strict") || process.env.SCOPEBOND_HOOK_STRICT === "1",
89
103
  // When connected, auto-export receipts. A short bounded flush keeps the hot path
@@ -92,6 +106,16 @@ function runtimePaths(dir) {
92
106
  ...(connection ? { cloud: { connection, flushTimeoutMs: Number(process.env.SCOPEBOND_HOOK_FLUSH_MS ?? 800) } } : {}),
93
107
  };
94
108
  }
109
+ /** The harness's own id for this tool call, when it gives one (Claude Code and Codex send
110
+ * `tool_use_id`), so the receipts of one call share a stable action group. */
111
+ function callId(input) {
112
+ for (const key of ["tool_use_id", "tool_call_id", "call_id"]) {
113
+ const value = input?.[key];
114
+ if (typeof value === "string" && value !== "")
115
+ return value;
116
+ }
117
+ return undefined;
118
+ }
95
119
  function readStdin() {
96
120
  try {
97
121
  return readFileSync(0, "utf8");
@@ -134,10 +158,53 @@ const cursorCoverageNote = [
134
158
  "For edits that must be blocked before they land, use the GitHub Action as a required",
135
159
  "check on pull requests.",
136
160
  ].join("\n");
137
- async function runPreToolUse(mapper, deny = denyClaude) {
161
+ /** Record what was dispatched as observations, when this workspace enrolled for them. Purely
162
+ * additive: it runs after the decision is made and can neither change it nor fail it. */
163
+ function recordObservations(dir, cwd, input, decision, harness) {
164
+ try {
165
+ const { emitter } = openObservations(dir, { adapterVersion: hookVersion() });
166
+ if (!emitter)
167
+ return undefined;
168
+ const sessionId = typeof input.session_id === "string" && input.session_id !== "" ? input.session_id : undefined;
169
+ // Session lifecycle is Claude Code only: its SessionStart/SessionEnd hooks are the ones
170
+ // the tested mapping covers. Other hosts still get tool intents from their PreToolUse.
171
+ if (harness === "claude" && sessionId)
172
+ emitter.activity(sessionId, cwd);
173
+ emitter.toolIntents({ harnessSessionId: sessionId, callId: callId(input), cwd, dispatched: decision.dispatched ?? [], request: callRequestOf(input) });
174
+ return emitter;
175
+ }
176
+ catch {
177
+ return undefined;
178
+ }
179
+ }
180
+ /** The remote-database actions of a shell call, only when the rule set opts in (`protect_remote_database`).
181
+ * They are extra evaluated intents read from the raw command, so the deny happens before it runs. */
182
+ function databaseGuard(dir, cwd, input) {
183
+ let enabled = false;
184
+ try {
185
+ enabled = loadRules(dir)?.protect_remote_database === true;
186
+ }
187
+ catch {
188
+ return [];
189
+ }
190
+ if (!enabled)
191
+ return [];
192
+ const request = callRequestOf(input);
193
+ if (request?.command === undefined)
194
+ return [];
195
+ const intent = (params) => ({ intent: { action_type: "db.exec", params }, evaluated: true, source: "shell" });
196
+ try {
197
+ return databaseGuardActions(request.command, request.dialect ?? "posix", { cwd }).map((action) => intent({ ...action }));
198
+ }
199
+ catch {
200
+ // The rule is on and the reader failed: a command that names a database tool is not waved through.
201
+ return /(?:wrangler|psql)/i.test(request.command) ? [intent({ provider: "unknown", verb: "unknown", scope: "unknown", risk: "unknown" })] : [];
202
+ }
203
+ }
204
+ async function runPreToolUse(mapper, deny = denyClaude, raw, harness = "claude") {
138
205
  let input;
139
206
  try {
140
- input = JSON.parse(readStdin());
207
+ input = JSON.parse(raw ?? readStdin());
141
208
  }
142
209
  catch {
143
210
  deny("hook received invalid JSON on stdin");
@@ -146,10 +213,13 @@ async function runPreToolUse(mapper, deny = denyClaude) {
146
213
  try {
147
214
  const cwd = input?.cwd ? String(input.cwd) : process.cwd();
148
215
  const dir = resolveConfigDir(cwd);
149
- runtime = createHookRuntime(runtimePaths(dir));
216
+ runtime = createHookRuntime(runtimePaths(dir, cwd));
150
217
  useDigestKey(loadOrCreateDigestKey(dir));
151
- const decision = await runtime.evaluate(fillPushBranch(mapper(input), currentBranch(cwd)));
152
- await runtime.flush();
218
+ const decision = await runtime.evaluate([...fillPushBranch(mapper(input), currentBranch(cwd)), ...databaseGuard(dir, cwd, input)], { groupKey: callId(input) });
219
+ const observer = recordObservations(dir, cwd, input, decision, harness);
220
+ // Workspace rules: at most every five minutes, a capped check alongside the record delivery. It never fails the call.
221
+ await Promise.all([runtime.flush(), observer?.flush() ?? Promise.resolve(), syncIfDue(dir, () => syncOptionsFor(dir))]);
222
+ observer?.close();
153
223
  // Close before deciding: the receipt is already committed, and leaving the handle
154
224
  // open is what made the write-ahead log grow without bound.
155
225
  runtime.close();
@@ -168,11 +238,63 @@ async function runPreToolUse(mapper, deny = denyClaude) {
168
238
  deny(`Scopebond hook failed closed: ${error.message}. Repair: run \`${cliCommand("init")}\`.`);
169
239
  }
170
240
  }
241
+ /** Claude Code events other than PreToolUse: session start and end, and the after-action
242
+ * events. They only feed observations; they never print a decision and always exit 0. */
243
+ async function runClaudeLifecycle(input) {
244
+ try {
245
+ const cwd = input.cwd ? String(input.cwd) : process.cwd();
246
+ const { emitter } = openObservations(resolveConfigDir(cwd), { adapterVersion: hookVersion() });
247
+ const sessionId = typeof input.session_id === "string" && input.session_id !== "" ? input.session_id : undefined;
248
+ if (emitter) {
249
+ try {
250
+ switch (input.hook_event_name) {
251
+ case "SessionStart":
252
+ if (sessionId)
253
+ emitter.sessionStart(sessionId, cwd);
254
+ break;
255
+ case "SessionEnd":
256
+ if (sessionId)
257
+ emitter.sessionStop(sessionId, stopReasonFromClaude(input.reason));
258
+ break;
259
+ case "PostToolUse": {
260
+ const id = callId(input);
261
+ if (id)
262
+ emitter.toolOutcomes(sessionId, id, "ok");
263
+ break;
264
+ }
265
+ case "PostToolUseFailure": {
266
+ const id = callId(input);
267
+ if (id)
268
+ emitter.toolOutcomes(sessionId, id, exitFromClaudeFailure(input.is_interrupt));
269
+ break;
270
+ }
271
+ }
272
+ await emitter.flush(input.hook_event_name === "SessionEnd" ? 1500 : 800);
273
+ }
274
+ finally {
275
+ emitter.close();
276
+ }
277
+ }
278
+ }
279
+ catch { /* observations are best effort; nothing here may affect the agent */ }
280
+ process.exit(0);
281
+ }
282
+ const CLAUDE_OBSERVATION_EVENTS = new Set(["SessionStart", "SessionEnd", "PostToolUse", "PostToolUseFailure"]);
171
283
  async function runClaude() {
172
- await runPreToolUse(mapClaudeToolUse);
284
+ const raw = readStdin();
285
+ let event;
286
+ let input;
287
+ try {
288
+ input = JSON.parse(raw);
289
+ event = input?.hook_event_name;
290
+ }
291
+ catch { /* PreToolUse reports invalid input */ }
292
+ if (input && typeof event === "string" && CLAUDE_OBSERVATION_EVENTS.has(event))
293
+ await runClaudeLifecycle(input);
294
+ await runPreToolUse(mapClaudeToolUse, denyClaude, raw, "claude");
173
295
  }
174
296
  async function runCodex() {
175
- await runPreToolUse(mapCodexToolUse, denyCodex);
297
+ await runPreToolUse(mapCodexToolUse, denyCodex, undefined, "codex");
176
298
  }
177
299
  function denyCursor(reason) {
178
300
  process.stdout.write(JSON.stringify({ permission: "deny", agentMessage: reason }) + "\n");
@@ -202,11 +324,13 @@ async function runCursor() {
202
324
  try {
203
325
  const cwd = input?.cwd ? String(input.cwd) : process.cwd();
204
326
  const dir = resolveConfigDir(cwd);
205
- runtime = createHookRuntime(runtimePaths(dir));
327
+ runtime = createHookRuntime(runtimePaths(dir, cwd));
206
328
  useDigestKey(loadOrCreateDigestKey(dir));
207
329
  const mapped = fillPushBranch(mapCursorEvent(event, input), currentBranch(cwd));
208
- const decision = await runtime.evaluate(mapped);
209
- await runtime.flush();
330
+ const decision = await runtime.evaluate(mapped, { groupKey: callId(input) });
331
+ const observer = recordObservations(dir, cwd, input, decision, "cursor");
332
+ await Promise.all([runtime.flush(), observer?.flush() ?? Promise.resolve(), syncIfDue(dir, () => syncOptionsFor(dir))]);
333
+ observer?.close();
210
334
  // An `afterFileEdit` violation is real and recorded, but the edit has already
211
335
  // landed. Say so rather than letting "blocked" imply it was stopped.
212
336
  postHoc = mapped.some((m) => m.postHoc);
@@ -458,6 +582,7 @@ function runRules(args) {
458
582
  console.log(` ${cliCommand("rules unprotect <path>")} allow writing there again`);
459
583
  console.log(` ${cliCommand("rules protect-branch <name>")} never push there`);
460
584
  console.log(` ${cliCommand("rules unprotect-branch <name>")}`);
585
+ console.log(` ${cliCommand("rules protect-remote-database")} block destructive or unreadable SQL on a remote database (off by default)`);
461
586
  console.log(`Or edit ${rulesPath(dir)} directly, then \`${cliCommand("rules apply")}\`.`);
462
587
  process.exit(0);
463
588
  }
@@ -531,6 +656,22 @@ function runRules(args) {
531
656
  changed = `pushes to ${value} are allowed again`;
532
657
  break;
533
658
  }
659
+ case "protect-remote-database":
660
+ if (rules.protect_remote_database === true) {
661
+ console.log("Remote databases are already protected.");
662
+ process.exit(0);
663
+ }
664
+ rules.protect_remote_database = true;
665
+ changed = "remote SQL that drops, deletes or updates every row, or cannot be read, is now blocked";
666
+ break;
667
+ case "unprotect-remote-database":
668
+ if (rules.protect_remote_database !== true) {
669
+ console.log("Remote databases were not protected; nothing to change.");
670
+ process.exit(0);
671
+ }
672
+ delete rules.protect_remote_database;
673
+ changed = "remote SQL is no longer checked";
674
+ break;
534
675
  case "apply":
535
676
  changed = `recompiled from ${rulesPath(dir)}`;
536
677
  break;
@@ -539,6 +680,11 @@ function runRules(args) {
539
680
  printHelp("rules", true);
540
681
  process.exit(1);
541
682
  }
683
+ if (isManaged(dir)) {
684
+ console.error("The rules on this computer are set by your Scopebond workspace, so they cannot be changed here.");
685
+ console.error("Change them in the workspace (Rules), or disconnect this computer to manage its rules locally again.");
686
+ process.exit(1);
687
+ }
542
688
  // Changing what governs the agent is the same class of action as `init`.
543
689
  requireInteractive("rules", args);
544
690
  const policyPath = join(dir, "policy.json");
@@ -778,6 +924,21 @@ async function finishConnect(dir, url, bundle, harness, args) {
778
924
  console.log(`Add this to your ${harnessFileName(harness)}:`);
779
925
  console.log(harnessSnippet(harness));
780
926
  }
927
+ // Session and after-action observations are opt-in through the enrollment: wired only
928
+ // when this workspace granted observations:write and Claude Code is the host. Off is
929
+ // silent here (`status` says why); a granted scope the hook cannot use is worth a line.
930
+ const observing = observationStatus(loadConnection(dir));
931
+ if (observing.state === "on" && harness === "claude" && !args.includes("--no-install")) {
932
+ const placed = harnessScopes("claude", process.cwd());
933
+ const target = placed.local ?? placed.project ?? placed.user;
934
+ const command = target ? configuredHookCommands(target)[0] : undefined;
935
+ if (target && command) {
936
+ wireLifecycleHooks(target, command);
937
+ console.log(`✓ Session and after-action observations enabled in ${target}`);
938
+ }
939
+ }
940
+ else if (observing.state === "unsupported")
941
+ console.log(`Observations: ${observing.reason}`);
781
942
  console.log("");
782
943
  console.log("Run your agent — the first action appears in your workspace within seconds.");
783
944
  }
@@ -812,6 +973,263 @@ async function runFlush() {
812
973
  // Let pending HTTP handles close normally (forced exit can abort on Windows).
813
974
  process.exitCode = status && status.pending > 0 ? 1 : 0;
814
975
  }
976
+ /** `observations`: what the observation emitters are doing, and the local queue. */
977
+ /** `policy load <export.json> [--yes]`: check a policy exported from the workspace and, with
978
+ * `--yes`, make it the active policy here; then acknowledge it (or its refusal) to the workspace. */
979
+ async function runPolicy(args) {
980
+ const [sub, file] = args;
981
+ if (sub === "sync") {
982
+ await runPolicySync(args.includes("--background"));
983
+ return;
984
+ }
985
+ if (sub !== "load" || !file) {
986
+ console.error(`usage: ${cliCommand("policy sync")} | ${cliCommand("policy load <export.json> [--yes]")}`);
987
+ process.exitCode = 2;
988
+ return;
989
+ }
990
+ const dir = resolveConfigDir(process.cwd());
991
+ if (isManaged(dir)) {
992
+ console.error("The rules on this computer are set by your Scopebond workspace; a policy file cannot replace them here.");
993
+ process.exitCode = 1;
994
+ return;
995
+ }
996
+ const apply = args.includes("--yes");
997
+ const connection = loadConnection(dir);
998
+ const outcome = loadPolicyExport(dir, file, { apply, environmentId: connection?.environment_id });
999
+ let ack;
1000
+ if (outcome.state === "rejected") {
1001
+ console.error(`refused: ${outcome.message} (${outcome.error})`);
1002
+ if (outcome.ack)
1003
+ ack = { ...outcome.ack, error: outcome.error };
1004
+ process.exitCode = 1;
1005
+ }
1006
+ else if (outcome.state === "would_load") {
1007
+ console.log(`This export checks out (policy ${outcome.facts.policyId} v${outcome.facts.policyVersion}, digest ${outcome.facts.policyDigest.slice(0, 12)}...).`);
1008
+ console.log(`Loading it REPLACES the active policy at ${join(dir, "policy.json")} (the old one is kept as policy.previous.json), so the starter protections apply only if the export includes them.`);
1009
+ console.log("Run again with --yes to load it.");
1010
+ return;
1011
+ }
1012
+ else {
1013
+ console.log(`loaded policy ${outcome.facts.policyId} v${outcome.facts.policyVersion} (digest ${outcome.facts.policyDigest.slice(0, 12)}...) from export ${outcome.facts.exportId}`);
1014
+ console.log(` active policy ${outcome.policyPath}${outcome.previous ? ` (previous kept at ${outcome.previous})` : ""}`);
1015
+ console.log(` note \`${cliCommand("rules apply")}\` recompiles policy.json from rules.json and would replace it`);
1016
+ ack = { exportId: outcome.facts.exportId, policyId: outcome.facts.policyId, policyVersion: outcome.facts.policyVersion, policyDigest: outcome.facts.policyDigest, scopeDigest: outcome.facts.scopeDigest };
1017
+ }
1018
+ if (!ack)
1019
+ return;
1020
+ await sendPolicyAck(dir, ack);
1021
+ }
1022
+ /** What a rules check needs for this config directory. A project policy governs only once trusted, so it is re-pinned after a
1023
+ * write, exactly when `rules apply` would. */
1024
+ function syncOptionsFor(dir) {
1025
+ const home = userHome();
1026
+ const repin = dir !== home && existsSync(join(home, "policy.json")) && isTrustedProject(dir);
1027
+ const agentKid = createSigner({ privateKeyPem: readFileSync(join(dir, "agent.key"), "utf8") }).kid;
1028
+ return { agentKid, hookVersion: hookVersion(), policyBuilds, afterPolicyWrite: repin ? (d) => { trustProjectPolicy(d); } : undefined };
1029
+ }
1030
+ /** `policy sync`: bring this computer's rules in line with its workspace now (the hook also checks every five minutes). */
1031
+ async function runPolicySync(background) {
1032
+ const dir = resolveConfigDir(process.cwd());
1033
+ let outcome;
1034
+ try {
1035
+ outcome = await syncPolicy(dir, syncOptionsFor(dir));
1036
+ }
1037
+ catch (error) {
1038
+ outcome = { state: "unavailable", message: error.message };
1039
+ }
1040
+ if (background)
1041
+ return;
1042
+ const lines = {
1043
+ not_connected: "This computer is not connected to a Scopebond workspace; it uses its own rules.",
1044
+ own_rules: "Your workspace does not set rules for this computer; it uses its own rules.",
1045
+ unchanged: "Up to date with your workspace.",
1046
+ applied: "Updated to your workspace's latest rules.",
1047
+ refused: "Could not apply your workspace's rules; the rules already in force stay.",
1048
+ disconnected: "The workspace connection is no longer valid; this computer now uses its own rules.",
1049
+ unavailable: "Could not reach your workspace; the rules already in force stay.",
1050
+ };
1051
+ console.log(lines[outcome.state]);
1052
+ if (outcome.state === "refused" || outcome.state === "unavailable") {
1053
+ console.log(` ${outcome.message}`);
1054
+ process.exitCode = 1;
1055
+ }
1056
+ }
1057
+ /** Queue a `policy_ack` (loaded or rejected) through the observation outbox and try to deliver it now. */
1058
+ async function sendPolicyAck(dir, ack) {
1059
+ const observed = openObservations(dir, { adapterVersion: hookVersion(), spawnHeartbeat: false });
1060
+ if (!observed.emitter) {
1061
+ console.error(`not acknowledged to the workspace: observations are ${observed.status.state}${"reason" in observed.status ? ` (${observed.status.reason})` : ""}`);
1062
+ return;
1063
+ }
1064
+ try {
1065
+ const queued = observed.emitter.policyAck(ack);
1066
+ await observed.emitter.flush(3000);
1067
+ console.error(queued?.queued ? `queued the ${ack.error ? "rejection" : "load"} acknowledgement for the workspace` : "could not queue the acknowledgement");
1068
+ }
1069
+ finally {
1070
+ observed.emitter.close();
1071
+ }
1072
+ }
1073
+ /** `budget load <export.json> [--yes]`: check an action budget exported from the workspace and, with `--yes`,
1074
+ * make it the budget this machine enforces; then acknowledge it (or its refusal) to the workspace. */
1075
+ async function runBudgetLoad(args) {
1076
+ const file = args.find((a) => !a.startsWith("--"));
1077
+ if (!file) {
1078
+ console.error(`usage: ${cliCommand("budget load <export.json> [--yes]")}`);
1079
+ process.exitCode = 2;
1080
+ return;
1081
+ }
1082
+ const dir = resolveConfigDir(process.cwd());
1083
+ const agentKid = (() => { try {
1084
+ return createSigner({ privateKeyPem: readFileSync(join(dir, "agent.key"), "utf8") }).kid;
1085
+ }
1086
+ catch {
1087
+ return "";
1088
+ } })();
1089
+ const outcome = loadBudgetExport(dir, file, { apply: args.includes("--yes"), agentKid, environmentId: loadConnection(dir)?.environment_id });
1090
+ let ack;
1091
+ if (outcome.state === "rejected") {
1092
+ console.error(`refused: ${outcome.message} (${outcome.error})`);
1093
+ if (outcome.ack && outcome.error !== "expired")
1094
+ ack = { ...outcome.ack, error: outcome.error };
1095
+ process.exitCode = 1;
1096
+ }
1097
+ else if (outcome.state === "would_load") {
1098
+ const p = outcome.facts.policy;
1099
+ for (const w of outcome.warnings)
1100
+ console.error(`warning: ${w}`);
1101
+ console.log(`This export checks out: ${p.mode} budget ${outcome.facts.budgetId} v${outcome.facts.budgetVersion}, ${p.max_dispatch} dispatches in ${p.window_seconds}s for this agent on this installation.`);
1102
+ console.log(`Loading it writes the budget to ${join(dir, "dispatch.json")} as acknowledged, and replaces an older workspace budget for this agent. Run again with --yes to load it.`);
1103
+ return;
1104
+ }
1105
+ else {
1106
+ const p = outcome.facts.policy;
1107
+ for (const w of outcome.warnings)
1108
+ console.error(`warning: ${w}`);
1109
+ console.log(`loaded ${p.mode} budget ${outcome.facts.budgetId} v${outcome.facts.budgetVersion} from export ${outcome.facts.exportId}: ${p.max_dispatch} dispatches in ${p.window_seconds}s`);
1110
+ if (outcome.replaced.length > 0)
1111
+ console.log(` replaced ${outcome.replaced.join(", ")}`);
1112
+ console.log(` valid until ${new Date(outcome.facts.validUntil).toISOString()} (after that an enforced budget denies new dispatch until you load a new export)`);
1113
+ ack = { exportId: outcome.facts.exportId, policyId: outcome.facts.budgetId, policyVersion: outcome.facts.budgetVersion, policyDigest: outcome.facts.policyDigest, scopeDigest: outcome.facts.scopeDigest };
1114
+ }
1115
+ if (!ack)
1116
+ return;
1117
+ await sendPolicyAck(dir, ack);
1118
+ }
1119
+ async function runObservations(args) {
1120
+ const sub = args[0] ?? "status";
1121
+ const dir = resolveConfigDir(process.cwd());
1122
+ if (sub === "heartbeat") {
1123
+ await runHeartbeatLoop(args[1] ?? "");
1124
+ return;
1125
+ }
1126
+ if (sub === "status") {
1127
+ const lines = describeObservations(dir);
1128
+ console.log(`observations: ${lines[0]}`);
1129
+ for (const line of lines.slice(1))
1130
+ console.log(` ${line}`);
1131
+ const file = join(dir, OBSERVATION_DB);
1132
+ if (args.includes("--refused") && existsSync(file)) {
1133
+ const store = new ObservationStore(file);
1134
+ try {
1135
+ for (const row of store.terminal())
1136
+ console.log(` refused #${row.sequence} gen ${row.generation} ${row.kind} ${row.observation_id} ${row.code} ${new Date(row.at).toISOString()}`);
1137
+ }
1138
+ finally {
1139
+ store.close();
1140
+ }
1141
+ }
1142
+ return;
1143
+ }
1144
+ if (sub === "id") {
1145
+ // The opaque id this installation gives a ref, remote or repository, for writing reference
1146
+ // sets. Local only: it reads the binding key and prints; nothing is sent anywhere.
1147
+ const id = keyedIdFor(loadOrCreateBindingKey(dir), args[1] ?? "", args.slice(2));
1148
+ if (!id) {
1149
+ console.error("usage: observations id <ref <name> | remote <url> | ghrepo <owner/name> | repo <workspace-path> | mcp <server> <tool> | mcp-resource <kind> <value> | net-dest <host> <port> | cf <kind> <name> | database <pg|sqlite> <key>>");
1150
+ process.exit(1);
1151
+ }
1152
+ console.log(id);
1153
+ return;
1154
+ }
1155
+ if (sub === "wire" || sub === "unwire") {
1156
+ const scopes = harnessScopes("claude", process.cwd());
1157
+ if (sub === "unwire") {
1158
+ let removed = 0;
1159
+ for (const file of [scopes.project, scopes.local, scopes.user])
1160
+ if (file)
1161
+ removed += unwireLifecycleHooks(file);
1162
+ console.log(`removed ${removed} lifecycle hook entr${removed === 1 ? "y" : "ies"}`);
1163
+ return;
1164
+ }
1165
+ const target = scopes.local ?? scopes.project ?? scopes.user;
1166
+ const command = target ? configuredHookCommands(target)[0] : undefined;
1167
+ if (!target || !command) {
1168
+ console.error("Claude Code is not configured with this hook; run install or init first");
1169
+ process.exit(1);
1170
+ }
1171
+ wireLifecycleHooks(target, command);
1172
+ console.log(`session and after-action hooks wired in ${target}`);
1173
+ return;
1174
+ }
1175
+ const opened = openObservations(dir, { adapterVersion: hookVersion(), spawnHeartbeat: false });
1176
+ if (!opened.emitter) {
1177
+ console.error(`observations are ${opened.status.state}${"reason" in opened.status ? `: ${opened.status.reason}` : ""}`);
1178
+ process.exit(1);
1179
+ }
1180
+ const emitter = opened.emitter;
1181
+ try {
1182
+ if (sub === "flush") {
1183
+ const outcome = await uploadPending(emitter.store, { url: emitter.connection.url, credential: emitter.connection.credential, timeoutMs: 10_000 });
1184
+ const left = emitter.store.pendingSummary().count;
1185
+ console.log(`${outcome.result}: ${outcome.acknowledged} acknowledged, ${outcome.deferred} deferred, ${outcome.rejected} refused; ${left} still pending${outcome.detail ? ` (${outcome.detail})` : ""}`);
1186
+ process.exitCode = left > 0 ? 1 : 0;
1187
+ }
1188
+ else if (sub === "retry") {
1189
+ const state = emitter.store.state();
1190
+ if (state?.capability === "unsupported") {
1191
+ emitter.store.setCapability("active", null);
1192
+ console.log("will try the workspace again");
1193
+ }
1194
+ else
1195
+ console.log("nothing to retry (a stale generation clears when you reconnect)");
1196
+ }
1197
+ else {
1198
+ console.error("usage: observations [status [--refused]|flush|retry|wire|unwire]");
1199
+ process.exit(1);
1200
+ }
1201
+ }
1202
+ finally {
1203
+ emitter.close();
1204
+ }
1205
+ }
1206
+ /** The single heartbeat helper for one active session (started by the hook, never by hand). */
1207
+ async function runHeartbeatLoop(sessionId) {
1208
+ if (!sessionId)
1209
+ return;
1210
+ const dir = resolveConfigDir(process.cwd());
1211
+ const { emitter } = openObservations(dir, { adapterVersion: hookVersion(), spawnHeartbeat: false, flushTimeoutMs: 3000 });
1212
+ if (!emitter)
1213
+ return;
1214
+ const override = Number(process.env.SCOPEBOND_HEARTBEAT_INTERVAL_MS);
1215
+ const interval = Number.isFinite(override) && override >= 100 ? override : HEARTBEAT_INTERVAL_MS;
1216
+ const endsAt = Date.now() + 24 * 60 * 60 * 1000;
1217
+ let last = Date.now();
1218
+ try {
1219
+ for (;;) {
1220
+ const verdict = emitter.heartbeatTick(sessionId, last);
1221
+ await emitter.flush();
1222
+ if (verdict === "stop" || Date.now() > endsAt)
1223
+ break;
1224
+ last = Date.now();
1225
+ await new Promise((resolve) => setTimeout(resolve, interval));
1226
+ }
1227
+ }
1228
+ finally {
1229
+ emitter.store.releaseHeartbeat(sessionId);
1230
+ emitter.close();
1231
+ }
1232
+ }
815
1233
  /** The absolute path to this CLI file, for registering the hook by absolute path. */
816
1234
  function cliPath() {
817
1235
  return fileURLToPath(import.meta.url);
@@ -933,6 +1351,20 @@ function runStatus() {
933
1351
  console.log(` Cursor ${harnessScopeLabel(cursor) || (cursorDetected() ? "detected, not configured" : "not detected")}`);
934
1352
  console.log(` Codex ${codex.project || codex.user ? `${harnessScopeLabel(codex)} — approve once with /hooks` : codexDetected() ? "detected, not configured" : "not detected"}`);
935
1353
  console.log(` cloud workspace ${connected ? "connected" : "not connected (local only)"}`);
1354
+ {
1355
+ const activeDir = resolveConfigDir(process.cwd());
1356
+ const meta = readMeta(activeDir);
1357
+ const managed = existsSync(join(activeDir, MANAGED_DOC_FILE));
1358
+ const checked = meta.checked_at ? `, last checked ${meta.checked_at}` : "";
1359
+ console.log(` rules ${managed ? `set by your workspace (version ${meta.revision})${checked}` : `this computer's own (${rulesPath(activeDir)})${connected ? checked : ""}`}`);
1360
+ console.log(` always on: protection of Scopebond's own settings and the agents' hook settings`);
1361
+ if (meta.last_error)
1362
+ console.log(` last problem: ${meta.last_error}`);
1363
+ }
1364
+ const observationLines = describeObservations(resolveConfigDir(process.cwd()));
1365
+ console.log(` observations ${observationLines[0]}`);
1366
+ for (const line of observationLines.slice(1))
1367
+ console.log(` ${line}`);
936
1368
  console.log(` local receipts ${existsSync(dbPath) ? `${dbPath} (${describeStore(dbPath)})` : "none yet"}`);
937
1369
  for (const [name, scopes] of [["Claude Code", claude], ["Cursor", cursor], ["Codex", codex]]) {
938
1370
  for (const file of [scopes.project, scopes.local, scopes.user])
@@ -940,6 +1372,75 @@ function runStatus() {
940
1372
  console.log(` ${name}: ${file}`);
941
1373
  }
942
1374
  }
1375
+ /** `capabilities`: the manifest of what this hook can honestly claim, cell by cell.
1376
+ * `--prove` runs the safe fixtures in temp directories (never touching agent settings);
1377
+ * `--save` records the result beside the policy; `--json` prints the manifest. */
1378
+ async function runCapabilities(args) {
1379
+ const cwd = process.cwd();
1380
+ const dir = resolveConfigDir(cwd);
1381
+ const configured = {
1382
+ claude: !!harnessScopeLabel(harnessScopes("claude", cwd)),
1383
+ cursor: !!harnessScopeLabel(harnessScopes("cursor", cwd)),
1384
+ codex: !!harnessScopeLabel(harnessScopes("codex", cwd)),
1385
+ };
1386
+ const version = hookVersion();
1387
+ let proofs = loadProofs(dir);
1388
+ let failed = false;
1389
+ if (args.includes("--prove")) {
1390
+ // When the workspace accepts observations, the fixtures are signed with this machine's
1391
+ // own keys (copied into the temp home) and their receipts are delivered first, so the
1392
+ // proof can name them and the workspace can resolve them. Otherwise a fixture stays local.
1393
+ const observed = openObservations(dir, { adapterVersion: version, spawnHeartbeat: false });
1394
+ const collected = [];
1395
+ const fresh = await runProofFixtures(version, observed.emitter ? { identityDir: dir, collect: collected } : {});
1396
+ const proven = computeManifest({ adapterVersion: version, configured: { claude: true, codex: true, cursor: true }, proofs: fresh });
1397
+ for (const cell of proven.cells) {
1398
+ const proof = fresh[cell.key];
1399
+ if (proof && !proofPassed(proof, cell)) {
1400
+ failed = true;
1401
+ console.error(`fixture failed: ${cell.key} (allow ${proof.safe_allow}, deny ${proof.safe_deny}, signature ${proof.signature}, grouping ${proof.grouping})`);
1402
+ }
1403
+ }
1404
+ if (observed.emitter) {
1405
+ try {
1406
+ const delivered = await deliverProofReceipts(observed.emitter.connection, collected);
1407
+ if (!delivered)
1408
+ console.error("could not deliver the fixture receipts to the workspace; the capability proofs were not sent (run it again when it is reachable)");
1409
+ else {
1410
+ const queued = observed.emitter.capabilityProofs(proven.cells.flatMap((cell) => {
1411
+ // A typed-operation cell has no receipt of its own action type for a proof to name, so no proof is sent for it.
1412
+ if (TYPED_ACTION_TYPES.has(cell.action_type))
1413
+ return [];
1414
+ const proof = cell.state === "unsupported" ? undefined : fresh[cell.key];
1415
+ return proof ? [{ adapterVersion: cell.adapter_version, hostVariant: cell.host_variant, actionType: cell.action_type, phase: cell.event_phase, requiredFields: cell.emitted_required_fields, fixtureVersion: `fixture/${proof.test_vector_digest}`, passed: proofPassed(proof, cell), proofDigests: proof.proof_digests }] : [];
1416
+ }));
1417
+ await observed.emitter.flush(3000);
1418
+ console.error(`queued ${queued} capability proof observation(s) (fixture origin, ${collected.length} fixture receipt(s) delivered)`);
1419
+ }
1420
+ }
1421
+ finally {
1422
+ observed.emitter.close();
1423
+ }
1424
+ }
1425
+ if (args.includes("--save")) {
1426
+ if (!existsSync(dir)) {
1427
+ console.error(`no ${dir} to record into - run \`${cliCommand("init")}\` first.`);
1428
+ process.exit(1);
1429
+ }
1430
+ console.error(`recorded fixture proofs in ${saveProofs(dir, fresh)}`);
1431
+ }
1432
+ proofs = fresh;
1433
+ }
1434
+ const manifest = computeManifest({ adapterVersion: version, configured, proofs });
1435
+ if (args.includes("--json"))
1436
+ console.log(JSON.stringify(manifest, null, 2));
1437
+ else {
1438
+ console.log(renderManifest(manifest));
1439
+ if (args.includes("--prove"))
1440
+ console.log(`\nFixture run ${failed ? "FAILED - see degraded cells" : "passed"}. It used temporary directories only; no agent setting or policy was changed and no key was modified.`);
1441
+ }
1442
+ process.exitCode = failed ? 1 : 0;
1443
+ }
943
1444
  async function runDoctor() {
944
1445
  const problems = [];
945
1446
  const nodeOk = nodeSupported();
@@ -1165,7 +1666,7 @@ const COMMANDS = [
1165
1666
  "--dry-run prints exactly which files it would touch and changes nothing. Each config",
1166
1667
  "is copied to <file>.scopebond-backup before its first modification.",
1167
1668
  ] },
1168
- { name: "rules", args: "[show|allow|block|protect|unprotect|protect-branch|unprotect-branch|apply] [value]",
1669
+ { name: "rules", args: "[show|allow|block|protect|unprotect|protect-branch|unprotect-branch|protect-remote-database|unprotect-remote-database|apply] [value]",
1169
1670
  summary: "read and change the limits in plain terms",
1170
1671
  detail: [
1171
1672
  "With no arguments, prints what is blocked in plain English — no regular expressions.",
@@ -1177,6 +1678,52 @@ const COMMANDS = [
1177
1678
  " rules apply recompile after editing rules.json by hand",
1178
1679
  ] },
1179
1680
  { name: "status", summary: "what is configured, where, and how big the local log is" },
1681
+ { name: "capabilities", args: "[--prove [--save]] [--json]",
1682
+ summary: "what this hook can honestly claim, per agent host, action and phase",
1683
+ detail: [
1684
+ "Prints the capability manifest: for Claude Code, Codex and Cursor, each action type and",
1685
+ "phase is unsupported, inactive, configured_unverified, degraded or verified_reporting.",
1686
+ "Unsupported stays unsupported. A cell is never verified from a local run alone.",
1687
+ "--prove runs safe fixtures (allow, deny, signature, action group) in temporary",
1688
+ "directories; it does not read or change any agent settings. --save records the result.",
1689
+ ] },
1690
+ { name: "policy", args: "load <export.json> [--yes]",
1691
+ summary: "check a policy exported from your workspace and make it the active policy here",
1692
+ detail: [
1693
+ "Without --yes it only checks the export (its policy hash and scope digest, and that the",
1694
+ "gateway can load it) and says what loading would replace. With --yes the policy is",
1695
+ "written atomically as policy.json, the old one kept as policy.previous.json. If this",
1696
+ "machine is enrolled for observations, the load (or the refusal) is then acknowledged to",
1697
+ "the workspace, echoing the export's digests exactly. An export carries no signature;",
1698
+ "get the file from your workspace.",
1699
+ ] },
1700
+ { name: "observations", args: "[status [--refused]|flush|retry|wire|unwire|id <kind> <value>]",
1701
+ summary: "session, health and action observations sent to your workspace (opt-in)",
1702
+ detail: [
1703
+ `On only when your workspace enrollment grants ${OBSERVATIONS_SCOPE}; otherwise off, and status says why.`,
1704
+ "Local enforcement never depends on it: uploads are best effort and bounded. status shows",
1705
+ "what is pending and what the workspace refused (kept locally, never retried). flush",
1706
+ "sends now; retry re-checks a workspace that lacked the route; wire and unwire add or",
1707
+ "remove the Claude Code session and after-action hook entries.",
1708
+ ] },
1709
+ { name: "budget", args: "init|ack <id>|status|load <export.json>",
1710
+ summary: "per-agent action budgets: how many actions this agent may dispatch in a window",
1711
+ detail: [
1712
+ "load checks a budget exported from your workspace (digests, environment, validity, fail-closed contract) and, with --yes,",
1713
+ "makes it the acknowledged budget here, then acknowledges it to the workspace so its export stops showing as pending.",
1714
+ "init writes the suggested 100 actions per 60 seconds, monitor-only (nothing is limited).",
1715
+ "Enforcing needs the mode set to enforce and an acknowledgement of the exact policy (ack).",
1716
+ "Counters persist across hook processes and restarts. They count dispatched parent actions on",
1717
+ "THIS installation only; a limit shared across installations needs a shared in-path gateway,",
1718
+ "which an independent hook is not, so it refuses to enforce one.",
1719
+ ] },
1720
+ { name: "delegation", args: "add <file>|list|revoke <id>|import <file>",
1721
+ summary: "delegated child scopes: a child may do no more than its parent and never outlives it",
1722
+ detail: [
1723
+ "A session runs under one with SCOPEBOND_DELEGATION=<id>. Every action is checked against it,",
1724
+ "and against every ancestor, for scope, expiry and revocation. revoke takes effect on the next action.",
1725
+ "import adds revoked ids from a file exported from your workspace (add-only).",
1726
+ ] },
1180
1727
  { name: "doctor", summary: "check the setup and whether each configured hook command can start",
1181
1728
  detail: ["Exits non-zero when something is wrong, so it works in a script."] },
1182
1729
  { name: "log", args: "[-n N] [--deny] [--since 7d]",
@@ -1299,6 +1846,31 @@ else if (cmd === "status") {
1299
1846
  else if (cmd === "doctor") {
1300
1847
  await runDoctor();
1301
1848
  }
1849
+ else if (cmd === "capabilities") {
1850
+ await runCapabilities(rest);
1851
+ }
1852
+ else if (cmd === "observations") {
1853
+ await runObservations(rest);
1854
+ }
1855
+ else if (cmd === "policy") {
1856
+ await runPolicy(rest);
1857
+ }
1858
+ else if (cmd === "budget" && rest[0] === "load") {
1859
+ await runBudgetLoad(rest.slice(1));
1860
+ }
1861
+ else if (cmd === "budget" || cmd === "delegation") {
1862
+ // These change what the agent may do, so they are for a person at a terminal.
1863
+ if (rest[0] !== "status" && rest[0] !== "list")
1864
+ requireInteractive(cmd, rest);
1865
+ const dir = resolveConfigDir(process.cwd());
1866
+ const kid = (() => { try {
1867
+ return createSigner({ privateKeyPem: readFileSync(join(dir, "agent.key"), "utf8") }).kid;
1868
+ }
1869
+ catch {
1870
+ return "";
1871
+ } })();
1872
+ process.exit(runDispatchCommand(cmd, rest, dir, kid));
1873
+ }
1302
1874
  else if (cmd === "uninstall") {
1303
1875
  runUninstall(rest);
1304
1876
  }