@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.
- package/README.md +3 -2
- 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.js +14 -7
- 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.js +28 -17
- package/dist/bundle/claude-runtime.mjs +602 -438
- package/dist/bundle/codex-runtime.mjs +612 -448
- package/dist/bundle/cursor-dispatcher.mjs +100 -26
- package/dist/bundle/cursor-runtime.mjs +662 -470
- 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 +10 -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/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/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
|
}
|
|
@@ -81,7 +141,7 @@ function canonicalExistingPath(value) {
|
|
|
81
141
|
return void 0;
|
|
82
142
|
}
|
|
83
143
|
try {
|
|
84
|
-
return
|
|
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 =
|
|
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
|
|
242
|
+
function expectedMatchers(eventName, payload) {
|
|
183
243
|
if (eventName === "preToolUse") {
|
|
184
|
-
|
|
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
|
|
257
|
+
const matchers = new Set(expectedMatchers(eventName, params.payload));
|
|
194
258
|
let parsed;
|
|
195
259
|
try {
|
|
196
|
-
parsed = JSON.parse(
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 || !
|
|
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(
|
|
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
|
};
|