@guilz-dev/belay 0.9.4 → 0.10.1
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 +3 -2
- package/dist/adapters/claude/runtime-entry.js +5 -18
- package/dist/adapters/codex/runtime-entry.js +10 -11
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +3 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +11 -0
- package/dist/adapters/cursor/hook-router.d.ts +1 -0
- package/dist/adapters/cursor/hook-router.js +39 -9
- package/dist/adapters/cursor/hook-routing-health.d.ts +1 -0
- package/dist/adapters/cursor/hook-routing-health.js +53 -0
- package/dist/adapters/cursor/hooks.d.ts +1 -1
- package/dist/adapters/cursor/hooks.js +16 -5
- package/dist/adapters/cursor/routing-config-trust.d.ts +1 -0
- package/dist/adapters/cursor/routing-config-trust.js +67 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +2 -2
- package/dist/adapters/cursor/runtime-entry.js +35 -10
- package/dist/adapters/shared/gate-runtime.d.ts +0 -2
- package/dist/adapters/shared/gate-runtime.js +12 -52
- package/dist/bundle/claude-runtime.mjs +1083 -669
- package/dist/bundle/codex-runtime.mjs +1092 -698
- package/dist/bundle/cursor-dispatcher.mjs +119 -28
- package/dist/bundle/cursor-runtime.mjs +1132 -677
- package/dist/cli.js +53 -4
- package/dist/commands/approval-token.d.ts +10 -0
- package/dist/commands/approval-token.js +26 -0
- package/dist/commands/audit.d.ts +2 -1
- package/dist/commands/audit.js +2 -2
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +28 -2
- package/dist/commands/doctor.js +14 -54
- package/dist/commands/dogfood-check.d.ts +3 -0
- package/dist/commands/dogfood-check.js +116 -0
- package/dist/commands/dogfood.d.ts +1 -0
- package/dist/commands/dogfood.js +4 -3
- package/dist/commands/judge.js +2 -2
- package/dist/commands/recovery-checkpoints.js +2 -11
- package/dist/config-io.d.ts +1 -0
- package/dist/config-io.js +8 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.d.ts +19 -0
- package/dist/core/capability/policy-engine.js +31 -0
- package/dist/core/classify-tool.js +372 -148
- package/dist/core/config.js +2 -2
- package/dist/core/dogfood-environment.d.ts +8 -0
- package/dist/core/dogfood-environment.js +55 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +12 -0
- package/dist/core/egress-approval.js +0 -18
- package/dist/core/gate-engine.js +2 -9
- package/dist/core/notify.d.ts +8 -2
- package/dist/core/notify.js +63 -7
- package/dist/core/recovery/operator-guidance.js +4 -4
- package/dist/core/repo-config-trust.d.ts +31 -0
- package/dist/core/repo-config-trust.js +152 -0
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +0 -1
- package/dist/defaults.js +0 -25
- package/dist/installer/scope-config.js +2 -2
- package/dist/installer.js +4 -4
- package/dist/types.d.ts +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
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
|
|
110
|
+
import path3 from "node:path";
|
|
51
111
|
function cursorRoutingConfigPath(repoRoot) {
|
|
52
|
-
return
|
|
112
|
+
return path3.join(repoRoot, ".cursor", "belay.config.json");
|
|
53
113
|
}
|
|
54
114
|
function cursorRoutingHooksDir(repoRoot) {
|
|
55
|
-
return
|
|
115
|
+
return path3.join(repoRoot, ".cursor", "hooks");
|
|
56
116
|
}
|
|
57
117
|
function cursorRoutingHooksSettingsPath(repoRoot) {
|
|
58
|
-
return
|
|
118
|
+
return path3.join(repoRoot, ".cursor", "hooks.json");
|
|
59
119
|
}
|
|
60
120
|
function cursorRoutingRuntimeDir(repoRoot) {
|
|
61
|
-
return
|
|
121
|
+
return path3.join(repoRoot, ".cursor", "belay", "runtime");
|
|
62
122
|
}
|
|
63
123
|
function findCursorRoutingRepoRoot(startPath) {
|
|
64
|
-
const resolvedStart =
|
|
124
|
+
const resolvedStart = path3.resolve(startPath);
|
|
65
125
|
let current = resolvedStart;
|
|
66
126
|
while (true) {
|
|
67
|
-
if (existsSync(
|
|
127
|
+
if (existsSync(path3.join(current, ".git")) || existsSync(path3.join(current, ".cursor")) && existsSync(cursorRoutingConfigPath(current))) {
|
|
68
128
|
return current;
|
|
69
129
|
}
|
|
70
|
-
const parent =
|
|
130
|
+
const parent = path3.dirname(current);
|
|
71
131
|
if (parent === current) {
|
|
72
132
|
return resolvedStart;
|
|
73
133
|
}
|
|
@@ -76,12 +136,13 @@ function findCursorRoutingRepoRoot(startPath) {
|
|
|
76
136
|
}
|
|
77
137
|
|
|
78
138
|
// src/adapters/cursor/hook-router.ts
|
|
139
|
+
var CURSOR_GLOBAL_SENTINEL_BLOCK_MESSAGE = "belay project hook owner is unavailable; the global sentinel blocked this action.";
|
|
79
140
|
function canonicalExistingPath(value) {
|
|
80
141
|
if (!existsSync2(value)) {
|
|
81
142
|
return void 0;
|
|
82
143
|
}
|
|
83
144
|
try {
|
|
84
|
-
return
|
|
145
|
+
return realpathSync2(value);
|
|
85
146
|
} catch {
|
|
86
147
|
return void 0;
|
|
87
148
|
}
|
|
@@ -126,7 +187,7 @@ function eventNameFor(params) {
|
|
|
126
187
|
return "postToolUse";
|
|
127
188
|
}
|
|
128
189
|
function selectedRunnerPath(hooksDir) {
|
|
129
|
-
const runnerPath =
|
|
190
|
+
const runnerPath = path4.join(
|
|
130
191
|
hooksDir,
|
|
131
192
|
process.platform === "win32" ? "belay-runner.ps1" : "belay-runner"
|
|
132
193
|
);
|
|
@@ -179,21 +240,25 @@ function commandInvokesProjectHook(command, runnerPath, kind, eventName) {
|
|
|
179
240
|
].join(" ");
|
|
180
241
|
return Buffer.from(encoded, "base64").toString("utf16le") === expected;
|
|
181
242
|
}
|
|
182
|
-
function
|
|
243
|
+
function expectedMatchers(eventName, payload) {
|
|
183
244
|
if (eventName === "preToolUse") {
|
|
184
|
-
|
|
245
|
+
const values = [void 0];
|
|
246
|
+
if (typeof payload.tool_name === "string") {
|
|
247
|
+
values.push(payload.tool_name);
|
|
248
|
+
}
|
|
249
|
+
return values;
|
|
185
250
|
}
|
|
186
251
|
if (eventName === "subagentStart") {
|
|
187
|
-
return typeof payload.subagent_type === "string" ? payload.subagent_type : void 0;
|
|
252
|
+
return [typeof payload.subagent_type === "string" ? payload.subagent_type : void 0];
|
|
188
253
|
}
|
|
189
|
-
return void 0;
|
|
254
|
+
return [void 0];
|
|
190
255
|
}
|
|
191
256
|
function hasManagedProjectHookEntry(repoRoot, runnerPath, params) {
|
|
192
257
|
const eventName = eventNameFor(params);
|
|
193
|
-
const
|
|
258
|
+
const matchers = new Set(expectedMatchers(eventName, params.payload));
|
|
194
259
|
let parsed;
|
|
195
260
|
try {
|
|
196
|
-
parsed = JSON.parse(
|
|
261
|
+
parsed = JSON.parse(readFileSync2(cursorRoutingHooksSettingsPath(repoRoot), "utf8"));
|
|
197
262
|
} catch {
|
|
198
263
|
return false;
|
|
199
264
|
}
|
|
@@ -213,23 +278,29 @@ function hasManagedProjectHookEntry(repoRoot, runnerPath, params) {
|
|
|
213
278
|
return false;
|
|
214
279
|
}
|
|
215
280
|
const definition = entry;
|
|
216
|
-
return definition.failClosed === true && definition.matcher
|
|
281
|
+
return definition.failClosed === true && matchers.has(definition.matcher) && typeof definition.command === "string" && commandInvokesProjectHook(definition.command, runnerPath, params.kind, eventName);
|
|
217
282
|
});
|
|
218
283
|
}
|
|
219
284
|
function hasMatchingProjectShim(repoRoot, kind) {
|
|
220
285
|
let source;
|
|
221
286
|
try {
|
|
222
|
-
source =
|
|
287
|
+
source = readFileSync2(path4.join(cursorRoutingHooksDir(repoRoot), hookScriptFor(kind)), "utf8");
|
|
223
288
|
} catch {
|
|
224
289
|
return false;
|
|
225
290
|
}
|
|
226
291
|
return source.includes("from '../belay/runtime/dispatcher.mjs'") && source.includes(`origin: ${JSON.stringify({ scope: "project", repoRoot })}`);
|
|
227
292
|
}
|
|
293
|
+
function hasProjectOwnerInstallation(repoRoot) {
|
|
294
|
+
const hooksDir = cursorRoutingHooksDir(repoRoot);
|
|
295
|
+
return Boolean(
|
|
296
|
+
selectedRunnerPath(hooksDir) && isRegularFile(path4.join(cursorRoutingRuntimeDir(repoRoot), "dispatcher.mjs"))
|
|
297
|
+
);
|
|
298
|
+
}
|
|
228
299
|
function hasCallableProjectOwner(repoRoot, params) {
|
|
229
300
|
const hooksDir = cursorRoutingHooksDir(repoRoot);
|
|
230
301
|
const runnerPath = selectedRunnerPath(hooksDir);
|
|
231
302
|
return Boolean(
|
|
232
|
-
runnerPath && isRegularFile(
|
|
303
|
+
runnerPath && isRegularFile(path4.join(cursorRoutingRuntimeDir(repoRoot), "dispatcher.mjs")) && hasMatchingProjectShim(repoRoot, params.kind) && hasManagedProjectHookEntry(repoRoot, runnerPath, params)
|
|
233
304
|
);
|
|
234
305
|
}
|
|
235
306
|
function payloadForKind(payload, kind) {
|
|
@@ -267,18 +338,24 @@ function selectedActionPath(payload) {
|
|
|
267
338
|
}
|
|
268
339
|
return void 0;
|
|
269
340
|
}
|
|
341
|
+
function hasRoutingRootMarker(dir) {
|
|
342
|
+
return existsSync2(path4.join(dir, ".git")) || existsSync2(path4.join(dir, ".cursor")) && existsSync2(cursorRoutingConfigPath(dir));
|
|
343
|
+
}
|
|
270
344
|
function readInstallScope(repoRoot) {
|
|
271
345
|
const configPath = cursorRoutingConfigPath(repoRoot);
|
|
272
346
|
let source;
|
|
273
347
|
try {
|
|
274
|
-
source =
|
|
348
|
+
source = readFileSync2(configPath, "utf8");
|
|
275
349
|
} catch (error) {
|
|
276
350
|
return error.code === "ENOENT" ? "missing" : "project";
|
|
277
351
|
}
|
|
278
352
|
try {
|
|
279
353
|
const parsed = JSON.parse(source);
|
|
280
354
|
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.installScope === "global") {
|
|
281
|
-
|
|
355
|
+
if (isTrustedCursorRoutingConfig(repoRoot, parsed)) {
|
|
356
|
+
return "global";
|
|
357
|
+
}
|
|
358
|
+
return hasProjectOwnerInstallation(repoRoot) ? "project" : "global";
|
|
282
359
|
}
|
|
283
360
|
} catch {
|
|
284
361
|
}
|
|
@@ -287,7 +364,7 @@ function readInstallScope(repoRoot) {
|
|
|
287
364
|
function routeCursorHook(params) {
|
|
288
365
|
const relevantPayload = payloadForKind(params.payload, params.kind);
|
|
289
366
|
const selectedPath = selectedActionPath(relevantPayload);
|
|
290
|
-
if (!selectedPath || !
|
|
367
|
+
if (!selectedPath || !path4.isAbsolute(selectedPath)) {
|
|
291
368
|
return { decision: "fail_closed", message: "belay could not determine the workspace." };
|
|
292
369
|
}
|
|
293
370
|
const resolution = resolveCursorActionCwdDetails(relevantPayload, "/");
|
|
@@ -295,10 +372,14 @@ function routeCursorHook(params) {
|
|
|
295
372
|
if (!canonicalCwd) {
|
|
296
373
|
return { decision: "fail_closed", message: "belay could not determine the workspace." };
|
|
297
374
|
}
|
|
298
|
-
const
|
|
375
|
+
const discoveredRepoRoot = findCursorRoutingRepoRoot(canonicalCwd);
|
|
376
|
+
const repoRoot = canonicalExistingPath(discoveredRepoRoot);
|
|
299
377
|
if (!repoRoot) {
|
|
300
378
|
return { decision: "neutral" };
|
|
301
379
|
}
|
|
380
|
+
if (repoRoot === canonicalCwd && !hasRoutingRootMarker(repoRoot)) {
|
|
381
|
+
return { decision: "neutral" };
|
|
382
|
+
}
|
|
302
383
|
const installScope = readInstallScope(repoRoot);
|
|
303
384
|
if (installScope === "missing") {
|
|
304
385
|
return { decision: "neutral" };
|
|
@@ -313,10 +394,10 @@ function routeCursorHook(params) {
|
|
|
313
394
|
if (params.origin.scope === "global") {
|
|
314
395
|
return callableProjectOwner ? { decision: "neutral" } : {
|
|
315
396
|
decision: "fail_closed",
|
|
316
|
-
message:
|
|
397
|
+
message: `${CURSOR_GLOBAL_SENTINEL_BLOCK_MESSAGE} Run belay doctor from this repository to diagnose hook routing.`
|
|
317
398
|
};
|
|
318
399
|
}
|
|
319
|
-
const complete = callableProjectOwner && isRegularFile(
|
|
400
|
+
const complete = callableProjectOwner && isRegularFile(path4.join(cursorRoutingRuntimeDir(repoRoot), "core.mjs"));
|
|
320
401
|
return complete ? { decision: "execute", repoRoot } : { decision: "fail_closed", message: "belay project hook installation is incomplete." };
|
|
321
402
|
}
|
|
322
403
|
|
|
@@ -408,7 +489,16 @@ async function dispatchCursorHookResponse(params) {
|
|
|
408
489
|
if (!input.ok) {
|
|
409
490
|
return failClosedResponse(params.kind, "belay received malformed Cursor hook input.");
|
|
410
491
|
}
|
|
492
|
+
if (params.kind === "tool-gate" && (params.eventName === "preToolUse" || params.eventName === "PreToolUse") && input.payload.tool_name === "Shell") {
|
|
493
|
+
return neutralResponse(params.kind);
|
|
494
|
+
}
|
|
411
495
|
if (!isRecognizedPayload(params, input.payload)) {
|
|
496
|
+
if (params.kind === "tool-gate" && (params.eventName === "preToolUse" || params.eventName === "PreToolUse") && isNonEmptyString(input.payload.tool_name)) {
|
|
497
|
+
return failClosedResponse(
|
|
498
|
+
params.kind,
|
|
499
|
+
"belay received a malformed preToolUse payload. Run belay doctor, then retry."
|
|
500
|
+
);
|
|
501
|
+
}
|
|
412
502
|
return neutralResponse(params.kind);
|
|
413
503
|
}
|
|
414
504
|
try {
|
|
@@ -439,5 +529,6 @@ async function dispatchCursorHook(params) {
|
|
|
439
529
|
`);
|
|
440
530
|
}
|
|
441
531
|
export {
|
|
442
|
-
dispatchCursorHook
|
|
532
|
+
dispatchCursorHook,
|
|
533
|
+
dispatchCursorHookResponse
|
|
443
534
|
};
|