@guilz-dev/belay 0.10.0 → 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/dist/adapters/claude/runtime-entry.js +5 -18
- package/dist/adapters/codex/runtime-entry.js +10 -11
- package/dist/adapters/cursor/hook-router.d.ts +1 -0
- package/dist/adapters/cursor/hook-router.js +26 -3
- package/dist/adapters/cursor/hook-routing-health.d.ts +1 -0
- package/dist/adapters/cursor/hook-routing-health.js +53 -0
- package/dist/adapters/shared/gate-runtime.d.ts +0 -2
- package/dist/adapters/shared/gate-runtime.js +2 -53
- package/dist/bundle/claude-runtime.mjs +446 -196
- package/dist/bundle/codex-runtime.mjs +449 -219
- package/dist/bundle/cursor-dispatcher.mjs +20 -3
- package/dist/bundle/cursor-runtime.mjs +441 -178
- package/dist/commands/doctor.js +4 -0
- 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/gate-engine.js +2 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -209,25 +209,12 @@ export async function runToolGateHook(_eventName) {
|
|
|
209
209
|
return;
|
|
210
210
|
}
|
|
211
211
|
if (!mappedKind) {
|
|
212
|
-
const verdict =
|
|
213
|
-
toolName,
|
|
214
|
-
payload,
|
|
215
|
-
repoRoot: ctx.repoRoot,
|
|
216
|
-
reason: 'unmapped_tool',
|
|
217
|
-
mode: ctx.config.mode,
|
|
218
|
-
userMessage: 'belay does not recognize this tool action. Run belay doctor, then retry.',
|
|
219
|
-
agentMessage: 'Belay denied this action because the tool could not be normalized.',
|
|
220
|
-
});
|
|
221
|
-
await deps.appendAudit(ctx, {
|
|
222
|
-
event: 'preToolUse',
|
|
212
|
+
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
223
213
|
kind: 'tool',
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
permission: 'deny',
|
|
229
|
-
summary: toolName,
|
|
230
|
-
...effectPlanAuditFields(verdict.effectPlan),
|
|
214
|
+
cwd,
|
|
215
|
+
payload,
|
|
216
|
+
toolName,
|
|
217
|
+
sourceEvent: 'PreToolUse',
|
|
231
218
|
});
|
|
232
219
|
jsonResponse(gateVerdictToClaudePreToolUseResponse(verdict));
|
|
233
220
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { codexLayout } from '../layouts/codex.js';
|
|
4
|
-
import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction,
|
|
4
|
+
import { appendObservedAudit, createDefaultGateRuntimeDeps, evaluateGatedAction, gateVerdictToCodexPreToolUseResponse, gateVerdictToCodexUserPromptResponse, processApprovalPrompt, resolveGateConfig, } from '../shared/gate-runtime.js';
|
|
5
5
|
import { findRepoRoot } from '../shared/repo-root.js';
|
|
6
6
|
async function readStdinJson() {
|
|
7
7
|
const chunks = [];
|
|
@@ -169,7 +169,7 @@ export async function runBeforeSubmitPromptHook() {
|
|
|
169
169
|
}
|
|
170
170
|
// Codex routes all PreToolUse through this unified handler (matcher ".*"), since the exact
|
|
171
171
|
// shell tool name is not yet confirmed. SubagentStart is also routed to this handler.
|
|
172
|
-
// Unmapped tools
|
|
172
|
+
// Unmapped tools use the shared effect-based classifier (no tool-name denylist).
|
|
173
173
|
export async function runToolGateHook(eventName) {
|
|
174
174
|
try {
|
|
175
175
|
const payload = await readStdinJson();
|
|
@@ -180,15 +180,14 @@ export async function runToolGateHook(eventName) {
|
|
|
180
180
|
const ctx = await loadRuntimeContext(cwd);
|
|
181
181
|
const deps = createDefaultGateRuntimeDeps();
|
|
182
182
|
if (!kind) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const verdict = await gateUnmappedToolVerdict(ctx, deps, toolName, payload);
|
|
183
|
+
const normalizedPayload = normalizeCodexToolPayload('tool', payload);
|
|
184
|
+
const verdict = await evaluateGatedAction(ctx, deps, {
|
|
185
|
+
kind: 'tool',
|
|
186
|
+
cwd,
|
|
187
|
+
payload: normalizedPayload,
|
|
188
|
+
toolName,
|
|
189
|
+
sourceEvent: eventName,
|
|
190
|
+
});
|
|
192
191
|
jsonResponse(gateVerdictToCodexPreToolUseResponse(verdict));
|
|
193
192
|
return;
|
|
194
193
|
}
|
|
@@ -14,6 +14,7 @@ export type CursorHookRoute = {
|
|
|
14
14
|
decision: 'fail_closed';
|
|
15
15
|
message: string;
|
|
16
16
|
};
|
|
17
|
+
export declare const CURSOR_GLOBAL_SENTINEL_BLOCK_MESSAGE = "belay project hook owner is unavailable; the global sentinel blocked this action.";
|
|
17
18
|
export interface RouteCursorHookParams {
|
|
18
19
|
origin: CursorHookOrigin;
|
|
19
20
|
kind: CursorHookKind;
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { resolveCursorActionCwdDetails } from './cwd-resolution.js';
|
|
4
4
|
import { isTrustedCursorRoutingConfig } from './routing-config-trust.js';
|
|
5
5
|
import { cursorRoutingConfigPath, cursorRoutingHooksDir, cursorRoutingHooksSettingsPath, cursorRoutingRuntimeDir, findCursorRoutingRepoRoot, } from './routing-layout.js';
|
|
6
|
+
export const CURSOR_GLOBAL_SENTINEL_BLOCK_MESSAGE = 'belay project hook owner is unavailable; the global sentinel blocked this action.';
|
|
6
7
|
function canonicalExistingPath(value) {
|
|
7
8
|
if (!existsSync(value)) {
|
|
8
9
|
return undefined;
|
|
@@ -162,6 +163,11 @@ function hasMatchingProjectShim(repoRoot, kind) {
|
|
|
162
163
|
return (source.includes("from '../belay/runtime/dispatcher.mjs'") &&
|
|
163
164
|
source.includes(`origin: ${JSON.stringify({ scope: 'project', repoRoot })}`));
|
|
164
165
|
}
|
|
166
|
+
function hasProjectOwnerInstallation(repoRoot) {
|
|
167
|
+
const hooksDir = cursorRoutingHooksDir(repoRoot);
|
|
168
|
+
return Boolean(selectedRunnerPath(hooksDir) &&
|
|
169
|
+
isRegularFile(path.join(cursorRoutingRuntimeDir(repoRoot), 'dispatcher.mjs')));
|
|
170
|
+
}
|
|
165
171
|
function hasCallableProjectOwner(repoRoot, params) {
|
|
166
172
|
const hooksDir = cursorRoutingHooksDir(repoRoot);
|
|
167
173
|
const runnerPath = selectedRunnerPath(hooksDir);
|
|
@@ -205,6 +211,10 @@ function selectedActionPath(payload) {
|
|
|
205
211
|
}
|
|
206
212
|
return undefined;
|
|
207
213
|
}
|
|
214
|
+
function hasRoutingRootMarker(dir) {
|
|
215
|
+
return (existsSync(path.join(dir, '.git')) ||
|
|
216
|
+
(existsSync(path.join(dir, '.cursor')) && existsSync(cursorRoutingConfigPath(dir))));
|
|
217
|
+
}
|
|
208
218
|
function readInstallScope(repoRoot) {
|
|
209
219
|
const configPath = cursorRoutingConfigPath(repoRoot);
|
|
210
220
|
let source;
|
|
@@ -220,7 +230,13 @@ function readInstallScope(repoRoot) {
|
|
|
220
230
|
typeof parsed === 'object' &&
|
|
221
231
|
!Array.isArray(parsed) &&
|
|
222
232
|
parsed.installScope === 'global') {
|
|
223
|
-
|
|
233
|
+
if (isTrustedCursorRoutingConfig(repoRoot, parsed)) {
|
|
234
|
+
return 'global';
|
|
235
|
+
}
|
|
236
|
+
// Untrusted global scope still routes through the global owner when no project
|
|
237
|
+
// installation exists. Otherwise a global-only workspace is blocked by the sentinel
|
|
238
|
+
// even though ADR-008 assigns ownership to the User/global installation.
|
|
239
|
+
return hasProjectOwnerInstallation(repoRoot) ? 'project' : 'global';
|
|
224
240
|
}
|
|
225
241
|
}
|
|
226
242
|
catch {
|
|
@@ -240,10 +256,17 @@ export function routeCursorHook(params) {
|
|
|
240
256
|
if (!canonicalCwd) {
|
|
241
257
|
return { decision: 'fail_closed', message: 'belay could not determine the workspace.' };
|
|
242
258
|
}
|
|
243
|
-
const
|
|
259
|
+
const discoveredRepoRoot = findCursorRoutingRepoRoot(canonicalCwd);
|
|
260
|
+
const repoRoot = canonicalExistingPath(discoveredRepoRoot);
|
|
244
261
|
if (!repoRoot) {
|
|
245
262
|
return { decision: 'neutral' };
|
|
246
263
|
}
|
|
264
|
+
// If repository discovery fell back to the action directory without finding a marker,
|
|
265
|
+
// keep the global source neutral instead of treating an unreadable/non-repo directory
|
|
266
|
+
// as a broken project owner.
|
|
267
|
+
if (repoRoot === canonicalCwd && !hasRoutingRootMarker(repoRoot)) {
|
|
268
|
+
return { decision: 'neutral' };
|
|
269
|
+
}
|
|
247
270
|
const installScope = readInstallScope(repoRoot);
|
|
248
271
|
if (installScope === 'missing') {
|
|
249
272
|
return { decision: 'neutral' };
|
|
@@ -263,7 +286,7 @@ export function routeCursorHook(params) {
|
|
|
263
286
|
? { decision: 'neutral' }
|
|
264
287
|
: {
|
|
265
288
|
decision: 'fail_closed',
|
|
266
|
-
message:
|
|
289
|
+
message: `${CURSOR_GLOBAL_SENTINEL_BLOCK_MESSAGE} Run belay doctor from this repository to diagnose hook routing.`,
|
|
267
290
|
};
|
|
268
291
|
}
|
|
269
292
|
const complete = callableProjectOwner && isRegularFile(path.join(cursorRoutingRuntimeDir(repoRoot), 'core.mjs'));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cursorHookRoutingIssues(repoRoot: string): string[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { CURSOR_GLOBAL_SENTINEL_BLOCK_MESSAGE, routeCursorHook, } from './hook-router.js';
|
|
3
|
+
import { cursorRoutingConfigPath } from './routing-layout.js';
|
|
4
|
+
const SENTINEL_KINDS = ['before-submit', 'shell-gate'];
|
|
5
|
+
function canonicalRepoRoot(repoRoot) {
|
|
6
|
+
try {
|
|
7
|
+
return realpathSync(repoRoot);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return repoRoot;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function configuredInstallScope(repoRoot) {
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(readFileSync(cursorRoutingConfigPath(repoRoot), 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return parsed.installScope === 'global' ? 'global' : 'project';
|
|
25
|
+
}
|
|
26
|
+
function upgradeCommand(repoRoot, installScope) {
|
|
27
|
+
return `belay upgrade --scope ${installScope} --target ${JSON.stringify(repoRoot)}`;
|
|
28
|
+
}
|
|
29
|
+
export function cursorHookRoutingIssues(repoRoot) {
|
|
30
|
+
if (!existsSync(cursorRoutingConfigPath(repoRoot))) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const canonicalRoot = canonicalRepoRoot(repoRoot);
|
|
34
|
+
const payload = { cwd: canonicalRoot, workspace_roots: [canonicalRoot] };
|
|
35
|
+
const installScope = configuredInstallScope(repoRoot) ?? 'project';
|
|
36
|
+
const issues = [];
|
|
37
|
+
for (const kind of SENTINEL_KINDS) {
|
|
38
|
+
const route = routeCursorHook({
|
|
39
|
+
origin: { scope: 'global' },
|
|
40
|
+
kind,
|
|
41
|
+
payload,
|
|
42
|
+
});
|
|
43
|
+
if (route.decision !== 'fail_closed') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (route.message.includes(CURSOR_GLOBAL_SENTINEL_BLOCK_MESSAGE)) {
|
|
47
|
+
issues.push(`Global Cursor sentinel would block ${kind} in this workspace. Run ${upgradeCommand(repoRoot, installScope)}; if the config was edited manually, also run belay config trust --target ${JSON.stringify(repoRoot)}.`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
issues.push(`Global Cursor sentinel would block ${kind}: ${route.message}`);
|
|
51
|
+
}
|
|
52
|
+
return issues;
|
|
53
|
+
}
|
|
@@ -48,8 +48,6 @@ export declare function evaluateGatedAction(ctx: GateRuntimeContext, deps: GateR
|
|
|
48
48
|
toolName?: string;
|
|
49
49
|
sourceEvent?: string;
|
|
50
50
|
}): Promise<GateVerdict>;
|
|
51
|
-
/** R39: unmapped Codex tools ask via pending approval — not hard deny without approval path. */
|
|
52
|
-
export declare function gateUnmappedToolVerdict(ctx: GateRuntimeContext, deps: GateRuntimeDeps, toolName: string, payload: Record<string, unknown>): Promise<GateVerdict>;
|
|
53
51
|
export declare function processApprovalPrompt(ctx: GateRuntimeContext, deps: GateRuntimeDeps, prompt: string): Promise<ApprovalPromptResult>;
|
|
54
52
|
export declare function gateVerdictToCursorResponse(verdict: GateVerdict): GatePermissionResponse;
|
|
55
53
|
export declare function gateVerdictToClaudePreToolUseResponse(verdict: GateVerdict): Record<string, unknown>;
|
|
@@ -30,7 +30,7 @@ import { effectPlanAuditFields, hashEffectPlan } from '../../core/effect-ir/audi
|
|
|
30
30
|
import { buildCapabilityEffectPlan } from '../../core/effect-ir/build.js';
|
|
31
31
|
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
32
32
|
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
33
|
-
import { approvalCommandMatch, approvedApprovalsFile, belayStateDir, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, hashValue, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubString, scrubValue,
|
|
33
|
+
import { approvalCommandMatch, approvedApprovalsFile, belayStateDir, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, hashValue, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubString, scrubValue, } from '../../core/index.js';
|
|
34
34
|
import { extractJudgeFallbackReason, formatJudgeInfrastructureDenyMessage, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../../core/judge-fallback-hints.js';
|
|
35
35
|
import { notifyDeny } from '../../core/notify.js';
|
|
36
36
|
import { canonicalPath } from '../../core/path-utils.js';
|
|
@@ -684,23 +684,7 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
684
684
|
...authorization,
|
|
685
685
|
egressProxyActive,
|
|
686
686
|
};
|
|
687
|
-
|
|
688
|
-
if (action.kind === 'tool' &&
|
|
689
|
-
predicted.reason === 'unclassified_tool' &&
|
|
690
|
-
(ctx.config.policy.codexUnmappedTool ?? 'deny') === 'deny') {
|
|
691
|
-
predicted = {
|
|
692
|
-
...predicted,
|
|
693
|
-
verdict: 'deny_pending_approval',
|
|
694
|
-
reason: 'unmapped_tool',
|
|
695
|
-
assessment: {
|
|
696
|
-
reversibility: 'irreversible',
|
|
697
|
-
external: false,
|
|
698
|
-
blastRadius: 'unknown tool action',
|
|
699
|
-
confidence: 0.5,
|
|
700
|
-
signals: ['unmapped_tool'],
|
|
701
|
-
},
|
|
702
|
-
};
|
|
703
|
-
}
|
|
687
|
+
const predicted = await classifyGatedActionAsync(action, ctx.config, enrichedClassifierOptions);
|
|
704
688
|
if (action.kind === 'shell' &&
|
|
705
689
|
action.command &&
|
|
706
690
|
isContainedUnknownExecutionEligible(ctx.config, action, predicted)) {
|
|
@@ -842,41 +826,6 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
842
826
|
scopeHintPayload: params.payload,
|
|
843
827
|
});
|
|
844
828
|
}
|
|
845
|
-
/** R39: unmapped Codex tools ask via pending approval — not hard deny without approval path. */
|
|
846
|
-
export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
847
|
-
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
848
|
-
const replayPayload = fingerprintReplayPayload('tool', payload, scrubOpts) ?? {};
|
|
849
|
-
const result = {
|
|
850
|
-
verdict: 'deny_pending_approval',
|
|
851
|
-
reason: 'unmapped_tool',
|
|
852
|
-
summary: toolName,
|
|
853
|
-
fingerprint: toolFingerprint(toolName, replayPayload, ctx.repoRoot),
|
|
854
|
-
assessment: {
|
|
855
|
-
reversibility: 'irreversible',
|
|
856
|
-
external: false,
|
|
857
|
-
blastRadius: 'unknown Codex tool action',
|
|
858
|
-
confidence: 0.5,
|
|
859
|
-
signals: ['unmapped_tool'],
|
|
860
|
-
},
|
|
861
|
-
};
|
|
862
|
-
return gateDecisionToVerdict(ctx, deps, 'tool', result, {
|
|
863
|
-
approvalInput: {
|
|
864
|
-
input: toolName,
|
|
865
|
-
inputKind: 'tool',
|
|
866
|
-
cwd: ctx.repoRoot,
|
|
867
|
-
toolName,
|
|
868
|
-
payload: replayPayload,
|
|
869
|
-
},
|
|
870
|
-
replayAction: {
|
|
871
|
-
kind: 'tool',
|
|
872
|
-
cwd: ctx.repoRoot,
|
|
873
|
-
toolName,
|
|
874
|
-
payload: replayPayload,
|
|
875
|
-
fingerprint: result.fingerprint,
|
|
876
|
-
repoRoot: ctx.repoRoot,
|
|
877
|
-
},
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
829
|
async function consumeCapabilityGrantIfUsed(ctx, deps, result) {
|
|
881
830
|
if (result.reason !== 'capability_grant' &&
|
|
882
831
|
result.authorizationDecision?.matchedRule !== 'grant.exact') {
|