@guilz-dev/belay 0.9.4 → 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 (49) hide show
  1. package/README.md +3 -2
  2. package/dist/adapters/cursor/hook-dispatch-entry.d.ts +3 -0
  3. package/dist/adapters/cursor/hook-dispatch-entry.js +11 -0
  4. package/dist/adapters/cursor/hook-router.js +14 -7
  5. package/dist/adapters/cursor/hooks.d.ts +1 -1
  6. package/dist/adapters/cursor/hooks.js +16 -5
  7. package/dist/adapters/cursor/routing-config-trust.d.ts +1 -0
  8. package/dist/adapters/cursor/routing-config-trust.js +67 -0
  9. package/dist/adapters/cursor/runtime-entry.d.ts +2 -2
  10. package/dist/adapters/cursor/runtime-entry.js +35 -10
  11. package/dist/adapters/shared/gate-runtime.js +28 -17
  12. package/dist/bundle/claude-runtime.mjs +602 -438
  13. package/dist/bundle/codex-runtime.mjs +612 -448
  14. package/dist/bundle/cursor-dispatcher.mjs +100 -26
  15. package/dist/bundle/cursor-runtime.mjs +662 -470
  16. package/dist/cli.js +53 -4
  17. package/dist/commands/approval-token.d.ts +10 -0
  18. package/dist/commands/approval-token.js +26 -0
  19. package/dist/commands/audit.d.ts +2 -1
  20. package/dist/commands/audit.js +2 -2
  21. package/dist/commands/config.d.ts +1 -1
  22. package/dist/commands/config.js +28 -2
  23. package/dist/commands/doctor.js +10 -54
  24. package/dist/commands/dogfood-check.d.ts +3 -0
  25. package/dist/commands/dogfood-check.js +116 -0
  26. package/dist/commands/dogfood.d.ts +1 -0
  27. package/dist/commands/dogfood.js +4 -3
  28. package/dist/commands/judge.js +2 -2
  29. package/dist/commands/recovery-checkpoints.js +2 -11
  30. package/dist/config-io.d.ts +1 -0
  31. package/dist/config-io.js +8 -1
  32. package/dist/core/dogfood-environment.d.ts +8 -0
  33. package/dist/core/dogfood-environment.js +55 -0
  34. package/dist/core/effect-ir/shell-lower/decoders/belay.js +12 -0
  35. package/dist/core/egress-approval.js +0 -18
  36. package/dist/core/notify.d.ts +8 -2
  37. package/dist/core/notify.js +63 -7
  38. package/dist/core/recovery/operator-guidance.js +4 -4
  39. package/dist/core/repo-config-trust.d.ts +31 -0
  40. package/dist/core/repo-config-trust.js +152 -0
  41. package/dist/corpus/benign-probe-cores.d.ts +1 -1
  42. package/dist/corpus/benign-probe-cores.js +0 -1
  43. package/dist/defaults.js +0 -25
  44. package/dist/installer/scope-config.js +2 -2
  45. package/dist/installer.js +4 -4
  46. package/dist/types.d.ts +19 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +5 -1
@@ -4,8 +4,8 @@
4
4
  import process2 from "node:process";
5
5
 
6
6
  // src/adapters/cursor/hook-router.ts
7
- import { accessSync, constants, existsSync as existsSync2, readFileSync, realpathSync, statSync } from "node:fs";
8
- import path3 from "node:path";
7
+ import { accessSync, constants, existsSync as existsSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync } from "node:fs";
8
+ import path4 from "node:path";
9
9
 
10
10
  // src/adapters/cursor/cwd-resolution.ts
11
11
  import path from "node:path";
@@ -45,29 +45,89 @@ function resolveCursorActionCwdDetails(payload, fallback = process.cwd()) {
45
45
  };
46
46
  }
47
47
 
48
+ // src/adapters/cursor/routing-config-trust.ts
49
+ import { createHash } from "node:crypto";
50
+ import { readFileSync, realpathSync } from "node:fs";
51
+ import path2 from "node:path";
52
+ function canonicalStringify(value) {
53
+ if (value === null || typeof value !== "object") {
54
+ return JSON.stringify(value);
55
+ }
56
+ if (Array.isArray(value)) {
57
+ return `[${value.map((item) => canonicalStringify(item)).join(",")}]`;
58
+ }
59
+ const entries = Object.entries(value).sort(
60
+ ([left], [right]) => left.localeCompare(right)
61
+ );
62
+ return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(",")}}`;
63
+ }
64
+ function hashValue(value) {
65
+ return createHash("sha256").update(value).digest("hex");
66
+ }
67
+ function defaultControlPlaneDir() {
68
+ if (process.platform === "win32" && process.env.APPDATA?.trim()) {
69
+ return path2.join(process.env.APPDATA.trim(), "agent-belay");
70
+ }
71
+ const base = process.env.XDG_CONFIG_HOME?.trim() || path2.join(process.env.HOME ?? "", ".config");
72
+ return path2.join(base, "agent-belay");
73
+ }
74
+ function canonicalRepoRoot(repoRoot) {
75
+ try {
76
+ return realpathSync(repoRoot);
77
+ } catch {
78
+ return path2.resolve(repoRoot);
79
+ }
80
+ }
81
+ function isTrustedCursorRoutingConfig(repoRoot, rawConfig) {
82
+ const canonicalRoot = canonicalRepoRoot(repoRoot);
83
+ const identity = hashValue(`${canonicalRoot}\0cursor`);
84
+ const recordPath = path2.join(defaultControlPlaneDir(), "config-trust", `${identity}.json`);
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(readFileSync(recordPath, "utf8"));
88
+ } catch {
89
+ return false;
90
+ }
91
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
92
+ return false;
93
+ }
94
+ const record = parsed;
95
+ const expectedKeys = /* @__PURE__ */ new Set([
96
+ "schemaVersion",
97
+ "repoRoot",
98
+ "adapter",
99
+ "repoConfigFingerprint",
100
+ "trustedAt"
101
+ ]);
102
+ if (Object.keys(record).length !== expectedKeys.size || !Object.keys(record).every((key) => expectedKeys.has(key)) || record.schemaVersion !== 1 || record.adapter !== "cursor" || typeof record.repoRoot !== "string" || canonicalRepoRoot(record.repoRoot) !== canonicalRoot || typeof record.repoConfigFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(record.repoConfigFingerprint) || typeof record.trustedAt !== "string" || !Number.isFinite(Date.parse(record.trustedAt))) {
103
+ return false;
104
+ }
105
+ return record.repoConfigFingerprint === hashValue(canonicalStringify(rawConfig));
106
+ }
107
+
48
108
  // src/adapters/cursor/routing-layout.ts
49
109
  import { existsSync } from "node:fs";
50
- import path2 from "node:path";
110
+ import path3 from "node:path";
51
111
  function cursorRoutingConfigPath(repoRoot) {
52
- return path2.join(repoRoot, ".cursor", "belay.config.json");
112
+ return path3.join(repoRoot, ".cursor", "belay.config.json");
53
113
  }
54
114
  function cursorRoutingHooksDir(repoRoot) {
55
- return path2.join(repoRoot, ".cursor", "hooks");
115
+ return path3.join(repoRoot, ".cursor", "hooks");
56
116
  }
57
117
  function cursorRoutingHooksSettingsPath(repoRoot) {
58
- return path2.join(repoRoot, ".cursor", "hooks.json");
118
+ return path3.join(repoRoot, ".cursor", "hooks.json");
59
119
  }
60
120
  function cursorRoutingRuntimeDir(repoRoot) {
61
- return path2.join(repoRoot, ".cursor", "belay", "runtime");
121
+ return path3.join(repoRoot, ".cursor", "belay", "runtime");
62
122
  }
63
123
  function findCursorRoutingRepoRoot(startPath) {
64
- const resolvedStart = path2.resolve(startPath);
124
+ const resolvedStart = path3.resolve(startPath);
65
125
  let current = resolvedStart;
66
126
  while (true) {
67
- if (existsSync(path2.join(current, ".git")) || existsSync(path2.join(current, ".cursor")) && existsSync(cursorRoutingConfigPath(current))) {
127
+ if (existsSync(path3.join(current, ".git")) || existsSync(path3.join(current, ".cursor")) && existsSync(cursorRoutingConfigPath(current))) {
68
128
  return current;
69
129
  }
70
- const parent = path2.dirname(current);
130
+ const parent = path3.dirname(current);
71
131
  if (parent === current) {
72
132
  return resolvedStart;
73
133
  }
@@ -81,7 +141,7 @@ function canonicalExistingPath(value) {
81
141
  return void 0;
82
142
  }
83
143
  try {
84
- return realpathSync(value);
144
+ return realpathSync2(value);
85
145
  } catch {
86
146
  return void 0;
87
147
  }
@@ -126,7 +186,7 @@ function eventNameFor(params) {
126
186
  return "postToolUse";
127
187
  }
128
188
  function selectedRunnerPath(hooksDir) {
129
- const runnerPath = path3.join(
189
+ const runnerPath = path4.join(
130
190
  hooksDir,
131
191
  process.platform === "win32" ? "belay-runner.ps1" : "belay-runner"
132
192
  );
@@ -179,21 +239,25 @@ function commandInvokesProjectHook(command, runnerPath, kind, eventName) {
179
239
  ].join(" ");
180
240
  return Buffer.from(encoded, "base64").toString("utf16le") === expected;
181
241
  }
182
- function expectedMatcher(eventName, payload) {
242
+ function expectedMatchers(eventName, payload) {
183
243
  if (eventName === "preToolUse") {
184
- return typeof payload.tool_name === "string" ? payload.tool_name : void 0;
244
+ const values = [void 0];
245
+ if (typeof payload.tool_name === "string") {
246
+ values.push(payload.tool_name);
247
+ }
248
+ return values;
185
249
  }
186
250
  if (eventName === "subagentStart") {
187
- return typeof payload.subagent_type === "string" ? payload.subagent_type : void 0;
251
+ return [typeof payload.subagent_type === "string" ? payload.subagent_type : void 0];
188
252
  }
189
- return void 0;
253
+ return [void 0];
190
254
  }
191
255
  function hasManagedProjectHookEntry(repoRoot, runnerPath, params) {
192
256
  const eventName = eventNameFor(params);
193
- const matcher = expectedMatcher(eventName, params.payload);
257
+ const matchers = new Set(expectedMatchers(eventName, params.payload));
194
258
  let parsed;
195
259
  try {
196
- parsed = JSON.parse(readFileSync(cursorRoutingHooksSettingsPath(repoRoot), "utf8"));
260
+ parsed = JSON.parse(readFileSync2(cursorRoutingHooksSettingsPath(repoRoot), "utf8"));
197
261
  } catch {
198
262
  return false;
199
263
  }
@@ -213,13 +277,13 @@ function hasManagedProjectHookEntry(repoRoot, runnerPath, params) {
213
277
  return false;
214
278
  }
215
279
  const definition = entry;
216
- return definition.failClosed === true && definition.matcher === matcher && typeof definition.command === "string" && commandInvokesProjectHook(definition.command, runnerPath, params.kind, eventName);
280
+ return definition.failClosed === true && matchers.has(definition.matcher) && typeof definition.command === "string" && commandInvokesProjectHook(definition.command, runnerPath, params.kind, eventName);
217
281
  });
218
282
  }
219
283
  function hasMatchingProjectShim(repoRoot, kind) {
220
284
  let source;
221
285
  try {
222
- source = readFileSync(path3.join(cursorRoutingHooksDir(repoRoot), hookScriptFor(kind)), "utf8");
286
+ source = readFileSync2(path4.join(cursorRoutingHooksDir(repoRoot), hookScriptFor(kind)), "utf8");
223
287
  } catch {
224
288
  return false;
225
289
  }
@@ -229,7 +293,7 @@ function hasCallableProjectOwner(repoRoot, params) {
229
293
  const hooksDir = cursorRoutingHooksDir(repoRoot);
230
294
  const runnerPath = selectedRunnerPath(hooksDir);
231
295
  return Boolean(
232
- runnerPath && isRegularFile(path3.join(cursorRoutingRuntimeDir(repoRoot), "dispatcher.mjs")) && hasMatchingProjectShim(repoRoot, params.kind) && hasManagedProjectHookEntry(repoRoot, runnerPath, params)
296
+ runnerPath && isRegularFile(path4.join(cursorRoutingRuntimeDir(repoRoot), "dispatcher.mjs")) && hasMatchingProjectShim(repoRoot, params.kind) && hasManagedProjectHookEntry(repoRoot, runnerPath, params)
233
297
  );
234
298
  }
235
299
  function payloadForKind(payload, kind) {
@@ -271,14 +335,14 @@ function readInstallScope(repoRoot) {
271
335
  const configPath = cursorRoutingConfigPath(repoRoot);
272
336
  let source;
273
337
  try {
274
- source = readFileSync(configPath, "utf8");
338
+ source = readFileSync2(configPath, "utf8");
275
339
  } catch (error) {
276
340
  return error.code === "ENOENT" ? "missing" : "project";
277
341
  }
278
342
  try {
279
343
  const parsed = JSON.parse(source);
280
344
  if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.installScope === "global") {
281
- return "global";
345
+ return isTrustedCursorRoutingConfig(repoRoot, parsed) ? "global" : "project";
282
346
  }
283
347
  } catch {
284
348
  }
@@ -287,7 +351,7 @@ function readInstallScope(repoRoot) {
287
351
  function routeCursorHook(params) {
288
352
  const relevantPayload = payloadForKind(params.payload, params.kind);
289
353
  const selectedPath = selectedActionPath(relevantPayload);
290
- if (!selectedPath || !path3.isAbsolute(selectedPath)) {
354
+ if (!selectedPath || !path4.isAbsolute(selectedPath)) {
291
355
  return { decision: "fail_closed", message: "belay could not determine the workspace." };
292
356
  }
293
357
  const resolution = resolveCursorActionCwdDetails(relevantPayload, "/");
@@ -316,7 +380,7 @@ function routeCursorHook(params) {
316
380
  message: "belay project hook owner is unavailable; the global sentinel blocked this action."
317
381
  };
318
382
  }
319
- const complete = callableProjectOwner && isRegularFile(path3.join(cursorRoutingRuntimeDir(repoRoot), "core.mjs"));
383
+ const complete = callableProjectOwner && isRegularFile(path4.join(cursorRoutingRuntimeDir(repoRoot), "core.mjs"));
320
384
  return complete ? { decision: "execute", repoRoot } : { decision: "fail_closed", message: "belay project hook installation is incomplete." };
321
385
  }
322
386
 
@@ -408,7 +472,16 @@ async function dispatchCursorHookResponse(params) {
408
472
  if (!input.ok) {
409
473
  return failClosedResponse(params.kind, "belay received malformed Cursor hook input.");
410
474
  }
475
+ if (params.kind === "tool-gate" && (params.eventName === "preToolUse" || params.eventName === "PreToolUse") && input.payload.tool_name === "Shell") {
476
+ return neutralResponse(params.kind);
477
+ }
411
478
  if (!isRecognizedPayload(params, input.payload)) {
479
+ if (params.kind === "tool-gate" && (params.eventName === "preToolUse" || params.eventName === "PreToolUse") && isNonEmptyString(input.payload.tool_name)) {
480
+ return failClosedResponse(
481
+ params.kind,
482
+ "belay received a malformed preToolUse payload. Run belay doctor, then retry."
483
+ );
484
+ }
412
485
  return neutralResponse(params.kind);
413
486
  }
414
487
  try {
@@ -439,5 +512,6 @@ async function dispatchCursorHook(params) {
439
512
  `);
440
513
  }
441
514
  export {
442
- dispatchCursorHook
515
+ dispatchCursorHook,
516
+ dispatchCursorHookResponse
443
517
  };