@guilz-dev/belay 0.1.0 → 0.2.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 +59 -12
- package/dist/adapters/shared/gate-runtime.js +12 -4
- package/dist/bundle/claude-runtime.mjs +155 -36
- package/dist/bundle/codex-runtime.mjs +155 -36
- package/dist/bundle/cursor-runtime.mjs +155 -36
- package/dist/cli.js +15 -4
- package/dist/commands/classify-for-report.js +3 -3
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/explain.js +14 -14
- package/dist/commands/init-wizard.d.ts +5 -0
- package/dist/commands/init-wizard.js +24 -11
- package/dist/commands/recover.js +2 -2
- package/dist/core/approval.d.ts +3 -0
- package/dist/core/approval.js +18 -3
- package/dist/core/audit-query.js +5 -1
- package/dist/core/classify-tool.js +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +2 -2
- package/dist/core/gate-contract.d.ts +1 -1
- package/dist/core/gate-contract.js +1 -1
- package/dist/core/gate-engine.js +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/core/judge-config.d.ts +5 -1
- package/dist/core/judge-config.js +17 -1
- package/dist/core/judge-doctor.js +2 -2
- package/dist/core/types.d.ts +5 -3
- package/dist/core/{v2 → verdict}/adapter.js +9 -3
- package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
- package/dist/core/{v2 → verdict}/judge.js +10 -12
- package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
- package/dist/core/{v2 → verdict}/verdict.js +16 -0
- package/dist/corpus/evaluate.js +2 -2
- package/dist/installer.js +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +19 -5
- /package/dist/core/{v2 → verdict}/adapter.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/containment.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/containment.js +0 -0
- /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
- /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/index.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-factory.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-factory.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
- /package/dist/core/{v2 → verdict}/judge.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.js +0 -0
- /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/parser.js +0 -0
- /package/dist/core/{v2 → verdict}/types.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/types.js +0 -0
- /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
package/dist/core/approval.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
/** Cursor may invoke the same shell gate more than once per retry; lease covers that window. */
|
|
2
|
+
export const APPROVAL_EXECUTION_LEASE_MS = 60_000;
|
|
1
3
|
export function nowIso() {
|
|
2
4
|
return new Date().toISOString();
|
|
3
5
|
}
|
|
4
6
|
export function isExpired(approval) {
|
|
5
7
|
return Date.parse(approval.expiresAt) <= Date.now();
|
|
6
8
|
}
|
|
9
|
+
export function isExecutionLeaseExpired(approval) {
|
|
10
|
+
if (!approval.executionLeaseExpiresAt) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
|
|
14
|
+
}
|
|
7
15
|
export function compactApprovals(state) {
|
|
8
16
|
return {
|
|
9
17
|
version: state.version,
|
|
10
|
-
approvals: state.approvals.filter((approval) => !isExpired(approval)),
|
|
18
|
+
approvals: state.approvals.filter((approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)),
|
|
11
19
|
};
|
|
12
20
|
}
|
|
13
21
|
export function mergeApprovalStates(target, source) {
|
|
@@ -31,8 +39,15 @@ export function escapeRegex(value) {
|
|
|
31
39
|
}
|
|
32
40
|
export function approvalCommandMatch(prompt, tokenPrefix) {
|
|
33
41
|
const escapedPrefix = escapeRegex(tokenPrefix);
|
|
34
|
-
const
|
|
35
|
-
|
|
42
|
+
const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, 'i');
|
|
43
|
+
for (const line of prompt.split(/\r?\n/)) {
|
|
44
|
+
if (!line.trim()) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const match = line.match(linePattern);
|
|
48
|
+
return match?.[1] ?? null;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
36
51
|
}
|
|
37
52
|
export function buildRetryInstruction(tokenPrefix, approvalId) {
|
|
38
53
|
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
package/dist/core/audit-query.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { GATE_EVENTS } from './audit-types.js';
|
|
2
2
|
export function toAuditRecord(value) {
|
|
3
|
-
|
|
3
|
+
const record = { ...value };
|
|
4
|
+
if (record.by === 'v2') {
|
|
5
|
+
record.by = 'verdict';
|
|
6
|
+
}
|
|
7
|
+
return record;
|
|
4
8
|
}
|
|
5
9
|
export function parseTimestamp(value) {
|
|
6
10
|
if (!value) {
|
|
@@ -3,7 +3,7 @@ import { canonicalStringify, toolFingerprint } from './fingerprint.js';
|
|
|
3
3
|
import { matchesSensitivePath } from './glob.js';
|
|
4
4
|
import { pathWithinRoot, relativeWithinRepo } from './path-utils.js';
|
|
5
5
|
import { scrubValue } from './scrub.js';
|
|
6
|
-
import { classifyShell } from './
|
|
6
|
+
import { classifyShell } from './verdict/adapter.js';
|
|
7
7
|
const DEFAULT_SENSITIVE_PATHS = ['.env', '.env.*', '**/credentials/**'];
|
|
8
8
|
const FILE_WRITE_TOOL_NAMES = new Set(['write']);
|
|
9
9
|
const FILE_EDIT_TOOL_NAMES = new Set([
|
package/dist/core/config.d.ts
CHANGED
|
@@ -155,7 +155,7 @@ export declare const DEFAULT_CONFIDENCE_THRESHOLDS: BelayConfidenceThresholds;
|
|
|
155
155
|
export declare const DEFAULT_MODEL_ASSIST: BelayModelAssistConfig;
|
|
156
156
|
export declare const DEFAULT_TRANSACTIONAL_V3: BelayTransactionalConfig;
|
|
157
157
|
export declare const LEGACY_POLICY_V3: BelayPolicyConfig;
|
|
158
|
-
/** Fresh
|
|
158
|
+
/** Fresh install defaults: recoverable-first with opaque/unparseable fail-closed. */
|
|
159
159
|
export declare const DEFAULT_POLICY_V3: BelayPolicyConfig;
|
|
160
160
|
export declare const DEFAULT_OVERRIDES_V3: BelayOverridesConfig;
|
|
161
161
|
export declare const DEFAULT_REDACTION_V3: BelayRedactionConfig;
|
package/dist/core/config.js
CHANGED
|
@@ -45,9 +45,9 @@ export const LEGACY_POLICY_V3 = {
|
|
|
45
45
|
transactional: { ...DEFAULT_TRANSACTIONAL_V3 },
|
|
46
46
|
fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD,
|
|
47
47
|
};
|
|
48
|
-
/** Fresh
|
|
48
|
+
/** Fresh install defaults: recoverable-first with opaque/unparseable fail-closed. */
|
|
49
49
|
export const DEFAULT_POLICY_V3 = {
|
|
50
|
-
unknownLocalEffect: '
|
|
50
|
+
unknownLocalEffect: 'allow_flagged',
|
|
51
51
|
unparseableShell: 'deny',
|
|
52
52
|
codexUnmappedTool: 'deny',
|
|
53
53
|
confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
|
|
@@ -28,7 +28,7 @@ export interface GateVerdict extends GatePermissionResponse {
|
|
|
28
28
|
approvalId?: string;
|
|
29
29
|
wouldBlock: boolean;
|
|
30
30
|
mode: 'enforce' | 'audit';
|
|
31
|
-
|
|
31
|
+
axes?: ClassifyResult['axes'];
|
|
32
32
|
}
|
|
33
33
|
export declare function isGatedAction(value: unknown): value is GatedAction;
|
|
34
34
|
export declare function classifyResultToGateVerdict(params: {
|
package/dist/core/gate-engine.js
CHANGED
|
@@ -5,7 +5,7 @@ import { classifyToolUse } from './classify-tool.js';
|
|
|
5
5
|
import { classifierOptionsFromConfig } from './config.js';
|
|
6
6
|
import { GATE_CONTRACT_VERSION } from './gate-contract.js';
|
|
7
7
|
import { mergeAgentAssessment } from './judgment.js';
|
|
8
|
-
import { classifyShell } from './
|
|
8
|
+
import { classifyShell } from './verdict/adapter.js';
|
|
9
9
|
export class GateNormalizationError extends Error {
|
|
10
10
|
reason = 'normalization_failed';
|
|
11
11
|
constructor(message) {
|
|
@@ -98,7 +98,7 @@ function applyShellPeripheralPolicy(command, action, result, options) {
|
|
|
98
98
|
(result.reason === 'outside_repo_mutation' ||
|
|
99
99
|
result.reason === 'outside_repo_redirect' ||
|
|
100
100
|
result.reason === 'repo_outside_mutation' ||
|
|
101
|
-
result.
|
|
101
|
+
result.axes?.location === 'repo_outside')) {
|
|
102
102
|
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
|
|
103
103
|
if (outsideRepoPaths.length > 0 &&
|
|
104
104
|
options.fsScopeAllowlist &&
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
|
|
1
|
+
export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
|
|
2
2
|
export type { AuditMetricsReport } from './audit-metrics.js';
|
|
3
3
|
export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
|
|
4
4
|
export { classifySubagent } from './classify-subagent.js';
|
|
@@ -16,4 +16,4 @@ export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substi
|
|
|
16
16
|
export type { TransactionalDiffEvaluation, TransactionalExecutionResult, } from './transactional/index.js';
|
|
17
17
|
export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
|
|
18
18
|
export type { ApprovalRecord, ApprovalStateFile, Assessment, ClassifierOptions, ClassifyResult, HookVerdict, Reversibility, ScrubOptions, UnknownLocalEffectPolicy, } from './types.js';
|
|
19
|
-
export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './
|
|
19
|
+
export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './verdict/index.js';
|
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
|
|
1
|
+
export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
|
|
2
2
|
export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
|
|
3
3
|
export { classifySubagent } from './classify-subagent.js';
|
|
4
4
|
export { classifyToolUse } from './classify-tool.js';
|
|
@@ -12,4 +12,4 @@ export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, rela
|
|
|
12
12
|
export { scrubString, scrubValue } from './scrub.js';
|
|
13
13
|
export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
|
|
14
14
|
export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
|
|
15
|
-
export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './
|
|
15
|
+
export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './verdict/index.js';
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { BelayJudgeConfig } from './config.js';
|
|
2
|
-
export type JudgeProfileName = 'local-ollama';
|
|
2
|
+
export type JudgeProfileName = 'local-ollama' | 'cursor' | 'claude' | 'codex';
|
|
3
3
|
export declare const JUDGE_PROFILE_LOCAL_OLLAMA: BelayJudgeConfig;
|
|
4
|
+
export declare const JUDGE_PROFILE_CURSOR: BelayJudgeConfig;
|
|
5
|
+
export declare const JUDGE_PROFILE_CLAUDE: BelayJudgeConfig;
|
|
6
|
+
export declare const JUDGE_PROFILE_CODEX: BelayJudgeConfig;
|
|
4
7
|
export declare const JUDGE_PROFILES: Record<JudgeProfileName, BelayJudgeConfig>;
|
|
5
8
|
export interface ResolveJudgeConfigInput {
|
|
6
9
|
judgeProfile?: JudgeProfileName;
|
|
@@ -26,4 +29,5 @@ export declare function resolveInitJudgeConfig(input: {
|
|
|
26
29
|
judgeEndpoint?: string;
|
|
27
30
|
acceptCloudJudge?: boolean;
|
|
28
31
|
existingJudge?: BelayJudgeConfig;
|
|
32
|
+
defaultJudgeProfile?: JudgeProfileName;
|
|
29
33
|
}): BelayJudgeConfig;
|
|
@@ -6,8 +6,24 @@ export const JUDGE_PROFILE_LOCAL_OLLAMA = {
|
|
|
6
6
|
timeoutMs: 25000,
|
|
7
7
|
keepAlive: '30m',
|
|
8
8
|
};
|
|
9
|
+
export const JUDGE_PROFILE_CURSOR = {
|
|
10
|
+
provider: 'openai-compatible',
|
|
11
|
+
model: 'auto',
|
|
12
|
+
endpoint: 'https://api.openai.com/v1',
|
|
13
|
+
timeoutMs: 8000,
|
|
14
|
+
keepAlive: null,
|
|
15
|
+
};
|
|
16
|
+
export const JUDGE_PROFILE_CLAUDE = {
|
|
17
|
+
...JUDGE_PROFILE_CURSOR,
|
|
18
|
+
};
|
|
19
|
+
export const JUDGE_PROFILE_CODEX = {
|
|
20
|
+
...JUDGE_PROFILE_CURSOR,
|
|
21
|
+
};
|
|
9
22
|
export const JUDGE_PROFILES = {
|
|
10
23
|
'local-ollama': JUDGE_PROFILE_LOCAL_OLLAMA,
|
|
24
|
+
cursor: JUDGE_PROFILE_CURSOR,
|
|
25
|
+
claude: JUDGE_PROFILE_CLAUDE,
|
|
26
|
+
codex: JUDGE_PROFILE_CODEX,
|
|
11
27
|
};
|
|
12
28
|
export function resolveJudgeConfig(input = {}) {
|
|
13
29
|
if (input.judgeProvider) {
|
|
@@ -81,5 +97,5 @@ export function resolveInitJudgeConfig(input) {
|
|
|
81
97
|
assertJudgeEndpoint(judge);
|
|
82
98
|
return judge;
|
|
83
99
|
}
|
|
84
|
-
return resolveJudgeConfig({ judgeProfile: '
|
|
100
|
+
return resolveJudgeConfig({ judgeProfile: input.defaultJudgeProfile ?? 'cursor' });
|
|
85
101
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { normalizeJudgeProvider, scrubOptionsFromConfig } from './config.js';
|
|
2
2
|
import { resolveJudgeApiKey } from './judge-api-key.js';
|
|
3
3
|
import { assertJudgeEndpoint } from './judge-config.js';
|
|
4
|
-
import { createOllamaJudge, createOpenAiCompatibleJudge } from './
|
|
5
|
-
import { loadPinnedJudgeModels, resolveCloudModel } from './
|
|
4
|
+
import { createOllamaJudge, createOpenAiCompatibleJudge } from './verdict/judge.js';
|
|
5
|
+
import { loadPinnedJudgeModels, resolveCloudModel } from './verdict/judge-factory.js';
|
|
6
6
|
export async function diagnoseJudge(config) {
|
|
7
7
|
const issues = [];
|
|
8
8
|
const warnings = [];
|
package/dist/core/types.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface Assessment {
|
|
|
9
9
|
confidence: number;
|
|
10
10
|
signals: string[];
|
|
11
11
|
}
|
|
12
|
-
export interface
|
|
12
|
+
export interface VerdictAxes {
|
|
13
13
|
location: string;
|
|
14
14
|
opacity: string;
|
|
15
15
|
effect: string;
|
|
@@ -33,7 +33,7 @@ export interface ClassifyResult {
|
|
|
33
33
|
assessment: Assessment;
|
|
34
34
|
normalizedCommand?: string;
|
|
35
35
|
summary?: string;
|
|
36
|
-
|
|
36
|
+
axes?: VerdictAxes;
|
|
37
37
|
}
|
|
38
38
|
export type UnknownLocalEffectPolicy = 'allow_flagged' | 'deny';
|
|
39
39
|
export type UnparseableShellPolicy = 'allow_flagged' | 'deny';
|
|
@@ -68,7 +68,7 @@ export interface ClassifierOptions {
|
|
|
68
68
|
brokerFsScope?: boolean;
|
|
69
69
|
fsScopeAllowlist?: FsScopeAllowlistFile;
|
|
70
70
|
/** Test override: inject Tier1 judge without changing config.judge. */
|
|
71
|
-
tier1Judge?: import('./
|
|
71
|
+
tier1Judge?: import('./verdict/types.js').Tier1Judge;
|
|
72
72
|
}
|
|
73
73
|
export interface ApprovalRecord {
|
|
74
74
|
approvalId: string;
|
|
@@ -80,6 +80,8 @@ export interface ApprovalRecord {
|
|
|
80
80
|
createdAt: string;
|
|
81
81
|
expiresAt: string;
|
|
82
82
|
approvedAt?: string;
|
|
83
|
+
/** Short-lived lease so duplicate hook invocations for one retry can share approval. */
|
|
84
|
+
executionLeaseExpiresAt?: string;
|
|
83
85
|
/** Original gated input for explain-last-ask (ApprovalState v2). */
|
|
84
86
|
input?: string;
|
|
85
87
|
inputKind?: 'shell' | 'tool' | 'subagent';
|
|
@@ -48,6 +48,12 @@ function mapLegacyReason(result) {
|
|
|
48
48
|
if (result.reason === 'repo_local_mutation') {
|
|
49
49
|
return 'local_mutation';
|
|
50
50
|
}
|
|
51
|
+
if (result.reason === 'tier1_not_restorable') {
|
|
52
|
+
return 'tier1_catastrophic';
|
|
53
|
+
}
|
|
54
|
+
if (result.reason === 'tier0_restorable' || result.reason === 'tier1_restorable') {
|
|
55
|
+
return result.effect === 'local_mutation' ? 'local_mutation' : result.reason;
|
|
56
|
+
}
|
|
51
57
|
return result.reason;
|
|
52
58
|
}
|
|
53
59
|
export function verdictToClassifyResult(result) {
|
|
@@ -87,13 +93,13 @@ export function verdictToClassifyResult(result) {
|
|
|
87
93
|
assessment,
|
|
88
94
|
normalizedCommand: result.commandRedacted,
|
|
89
95
|
summary: result.commandRedacted,
|
|
90
|
-
|
|
96
|
+
axes: {
|
|
91
97
|
location: result.location,
|
|
92
98
|
opacity: result.opacity,
|
|
93
99
|
effect: result.effect,
|
|
94
100
|
confidence: result.confidence,
|
|
95
101
|
would: result.permission,
|
|
96
|
-
by: '
|
|
102
|
+
by: 'verdict',
|
|
97
103
|
commandRedacted: result.commandRedacted,
|
|
98
104
|
commandFingerprint: result.fingerprint,
|
|
99
105
|
signals: result.signals,
|
|
@@ -111,7 +117,7 @@ export function verdictAuditFields(result) {
|
|
|
111
117
|
effect: result.effect,
|
|
112
118
|
confidence: result.confidence,
|
|
113
119
|
would: result.permission,
|
|
114
|
-
by: '
|
|
120
|
+
by: 'verdict',
|
|
115
121
|
signals: result.signals,
|
|
116
122
|
...judgeTraceAuditFields(result.judgeTrace),
|
|
117
123
|
};
|
|
@@ -2,12 +2,12 @@ import { scrubOutboundForJudge } from './judge-outbound.js';
|
|
|
2
2
|
const SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
3
3
|
const DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
4
4
|
const GIT_PATTERNS = /\.git\b/i;
|
|
5
|
-
const TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"
|
|
6
|
-
|
|
5
|
+
const TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"reason":string}
|
|
6
|
+
reason should be a short snake_case label.
|
|
7
7
|
|
|
8
|
-
external_change:
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
external_change: true if this operation CREATEs, MODIFYs, DELETEs, or SENDs data to any system outside the local machine and its git repository working tree.
|
|
9
|
+
READ/QUERY/GET/list/describe/SELECT/HTTP GET is NOT a change. Local build, test, and package install inside the repo are NOT external changes.
|
|
10
|
+
If unsure, answer true.
|
|
11
11
|
|
|
12
12
|
Command/code:
|
|
13
13
|
`;
|
|
@@ -22,15 +22,13 @@ function failClosedVerdict(reason) {
|
|
|
22
22
|
function parseTier1Json(raw) {
|
|
23
23
|
try {
|
|
24
24
|
const parsed = JSON.parse(raw);
|
|
25
|
-
if (typeof parsed.external_change !== 'boolean'
|
|
26
|
-
typeof parsed.destroys_outside_repo !== 'boolean' ||
|
|
27
|
-
typeof parsed.destroys_history_or_secrets !== 'boolean') {
|
|
25
|
+
if (typeof parsed.external_change !== 'boolean') {
|
|
28
26
|
return null;
|
|
29
27
|
}
|
|
30
28
|
return {
|
|
31
|
-
external_change: parsed.external_change
|
|
32
|
-
destroys_outside_repo:
|
|
33
|
-
destroys_history_or_secrets:
|
|
29
|
+
external_change: parsed.external_change,
|
|
30
|
+
destroys_outside_repo: false,
|
|
31
|
+
destroys_history_or_secrets: false,
|
|
34
32
|
reason: typeof parsed.reason === 'string' ? parsed.reason : 'tier1_llm',
|
|
35
33
|
};
|
|
36
34
|
}
|
|
@@ -260,5 +258,5 @@ export function createOpenAiCompatibleJudge(options) {
|
|
|
260
258
|
/** @deprecated Use createOpenAiCompatibleJudge */
|
|
261
259
|
export const createCursorJudge = createOpenAiCompatibleJudge;
|
|
262
260
|
export function tier1RequiresAsk(verdict) {
|
|
263
|
-
return
|
|
261
|
+
return verdict.external_change || verdict.destroys_history_or_secrets;
|
|
264
262
|
}
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const MAX_RESOLVE_DEPTH = 8;
|
|
4
|
+
const PNPM_BUILTIN_COMMANDS = new Set([
|
|
5
|
+
'add',
|
|
6
|
+
'audit',
|
|
7
|
+
'cache',
|
|
8
|
+
'config',
|
|
9
|
+
'deploy',
|
|
10
|
+
'dlx',
|
|
11
|
+
'exec',
|
|
12
|
+
'fetch',
|
|
13
|
+
'help',
|
|
14
|
+
'import',
|
|
15
|
+
'init',
|
|
16
|
+
'install',
|
|
17
|
+
'i',
|
|
18
|
+
'licenses',
|
|
19
|
+
'link',
|
|
20
|
+
'list',
|
|
21
|
+
'outdated',
|
|
22
|
+
'pack',
|
|
23
|
+
'patch',
|
|
24
|
+
'patch-commit',
|
|
25
|
+
'patch-remove',
|
|
26
|
+
'publish',
|
|
27
|
+
'prune',
|
|
28
|
+
'rebuild',
|
|
29
|
+
'remove',
|
|
30
|
+
'rm',
|
|
31
|
+
'store',
|
|
32
|
+
'unlink',
|
|
33
|
+
'update',
|
|
34
|
+
'up',
|
|
35
|
+
'why',
|
|
36
|
+
]);
|
|
37
|
+
const PNPM_EXEC_LIKE_HEADS = new Set([
|
|
38
|
+
'vitest',
|
|
39
|
+
'vite',
|
|
40
|
+
'biome',
|
|
41
|
+
'eslint',
|
|
42
|
+
'jest',
|
|
43
|
+
'mocha',
|
|
44
|
+
'tsc',
|
|
45
|
+
'tsx',
|
|
46
|
+
'node',
|
|
47
|
+
]);
|
|
4
48
|
function readPackageJson(dir) {
|
|
5
49
|
const packagePath = path.join(dir, 'package.json');
|
|
6
50
|
if (!existsSync(packagePath)) {
|
|
@@ -57,6 +101,15 @@ function npmScriptName(tokens) {
|
|
|
57
101
|
if (launcher[0] === 'pnpm' && launcher[1] === 'run' && launcher[2]) {
|
|
58
102
|
return launcher[2];
|
|
59
103
|
}
|
|
104
|
+
if (launcher[0] === 'pnpm' && launcher[1] === 'test') {
|
|
105
|
+
return 'test';
|
|
106
|
+
}
|
|
107
|
+
if (launcher[0] === 'pnpm' &&
|
|
108
|
+
launcher[1] &&
|
|
109
|
+
!launcher[1].startsWith('-') &&
|
|
110
|
+
!PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
|
|
111
|
+
return launcher[1];
|
|
112
|
+
}
|
|
60
113
|
if (launcher[0] === 'npm' && launcher[1] && launcher[1] !== 'run' && launcher[1] !== 'install') {
|
|
61
114
|
return null;
|
|
62
115
|
}
|
|
@@ -176,11 +229,29 @@ export function resolveLauncherRecipe(params) {
|
|
|
176
229
|
const tokens = params.tokens;
|
|
177
230
|
const scriptName = npmScriptName(tokens);
|
|
178
231
|
if (scriptName) {
|
|
179
|
-
|
|
232
|
+
const resolution = resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
|
|
233
|
+
if (tokens[0] === 'pnpm' &&
|
|
234
|
+
tokens[1] &&
|
|
235
|
+
PNPM_EXEC_LIKE_HEADS.has(tokens[1]) &&
|
|
236
|
+
resolution.reason === 'npm_script_undefined') {
|
|
237
|
+
return {
|
|
238
|
+
recipes: [tokens.slice(1).join(' ')],
|
|
239
|
+
opaque: false,
|
|
240
|
+
reason: 'pnpm_exec_like',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return resolution;
|
|
180
244
|
}
|
|
181
245
|
if (tokens[0] === 'make' && tokens[1] && !tokens[1].startsWith('-')) {
|
|
182
246
|
return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
|
|
183
247
|
}
|
|
248
|
+
if (tokens[0] === 'pnpm' && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
|
|
249
|
+
return {
|
|
250
|
+
recipes: [tokens.slice(1).join(' ')],
|
|
251
|
+
opaque: false,
|
|
252
|
+
reason: 'pnpm_exec_like',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
184
255
|
return null;
|
|
185
256
|
}
|
|
186
257
|
export function isRoutineLauncher(tokens) {
|
|
@@ -91,6 +91,7 @@ const LOCAL_ROUTINE_HEADS = new Set([
|
|
|
91
91
|
'make',
|
|
92
92
|
'cmake',
|
|
93
93
|
]);
|
|
94
|
+
const BELAY_SELF_COMMANDS = new Set(['approve', 'revoke']);
|
|
94
95
|
const FIND_DANGEROUS_FLAGS = new Set(['-delete', '-exec', '-execdir', '-ok', '-okdir']);
|
|
95
96
|
function isFindDangerous(tokens) {
|
|
96
97
|
return tokens.some((token) => FIND_DANGEROUS_FLAGS.has(token) || token.startsWith('-exec') || token.startsWith('-ok'));
|
|
@@ -256,6 +257,11 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
256
257
|
}
|
|
257
258
|
return false;
|
|
258
259
|
}
|
|
260
|
+
function isBelaySelfCommand(tokens) {
|
|
261
|
+
const head = tokens[0] ?? '';
|
|
262
|
+
const subcommand = tokens[1] ?? '';
|
|
263
|
+
return head === 'belay' && BELAY_SELF_COMMANDS.has(subcommand);
|
|
264
|
+
}
|
|
259
265
|
function tier0HighStakesRm(tokens, context) {
|
|
260
266
|
const head = tokens[0] ?? '';
|
|
261
267
|
if (head !== 'rm') {
|
|
@@ -488,6 +494,16 @@ async function evaluateSegment(command, context, depth) {
|
|
|
488
494
|
if (rmVerdict) {
|
|
489
495
|
return rmVerdict;
|
|
490
496
|
}
|
|
497
|
+
if (isBelaySelfCommand(peeled)) {
|
|
498
|
+
return allowVerdict({
|
|
499
|
+
location: 'unknown',
|
|
500
|
+
opacity: 'transparent',
|
|
501
|
+
effect: 'local_mutation',
|
|
502
|
+
confidence: 'deterministic',
|
|
503
|
+
reason: 'belay_control_plane_command',
|
|
504
|
+
signals: ['belay_control_plane_command', segment.head],
|
|
505
|
+
});
|
|
506
|
+
}
|
|
491
507
|
let effect = 'unknown';
|
|
492
508
|
if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
|
|
493
509
|
effect = 'read_only';
|
package/dist/corpus/evaluate.js
CHANGED
|
@@ -2,8 +2,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { classifierOptionsFromConfig, DEFAULT_CONFIG_V3 } from '../core/config.js';
|
|
5
|
-
import { classifyShell } from '../core/
|
|
6
|
-
import { createDeterministicJudgeStub } from '../core/
|
|
5
|
+
import { classifyShell } from '../core/verdict/adapter.js';
|
|
6
|
+
import { createDeterministicJudgeStub } from '../core/verdict/judge.js';
|
|
7
7
|
export function assessmentsDiverge(predicted, observed) {
|
|
8
8
|
return (predicted.reversibility !== observed.reversibility ||
|
|
9
9
|
predicted.external !== observed.external ||
|
package/dist/installer.js
CHANGED
|
@@ -126,6 +126,7 @@ async function applyInitJudgeConfig(repoRoot, adapterName, options) {
|
|
|
126
126
|
judgeEndpoint: options.judgeEndpoint,
|
|
127
127
|
acceptCloudJudge: options.acceptCloudJudge,
|
|
128
128
|
existingJudge: mergedConfig.judge,
|
|
129
|
+
defaultJudgeProfile: adapterName,
|
|
129
130
|
});
|
|
130
131
|
const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge });
|
|
131
132
|
await writeConfigFile(repoRoot, configWithJudge, adapterName);
|
package/dist/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface InitOptions {
|
|
|
20
20
|
adapter?: AdapterName;
|
|
21
21
|
scope?: InstallScope;
|
|
22
22
|
preset?: import('./presets.js').ConfigPresetName;
|
|
23
|
-
judgeProfile?: 'local-ollama';
|
|
23
|
+
judgeProfile?: 'local-ollama' | 'cursor' | 'claude' | 'codex';
|
|
24
24
|
judgeProvider?: 'ollama' | 'openai-compatible' | 'cursor';
|
|
25
25
|
judgeModel?: string;
|
|
26
26
|
judgeEndpoint?: string;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "0.0
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.2.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
// Generated by scripts/sync-version.mjs — do not edit.
|
|
2
|
+
export const PACKAGE_VERSION = '0.2.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guilz-dev/belay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Belay-style approval and audit gating for agent runtimes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,10 +48,13 @@
|
|
|
48
48
|
"agent-belay-logo.png"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/build-runtime.mjs",
|
|
51
|
+
"build": "rm -rf dist && node scripts/sync-version.mjs && tsc -p tsconfig.build.json && node scripts/build-runtime.mjs",
|
|
52
|
+
"check:version": "node scripts/check-cli-version.mjs",
|
|
53
|
+
"prepublishOnly": "pnpm build && node scripts/check-cli-version.mjs",
|
|
52
54
|
"lint": "biome check src package.json README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md tsconfig.json tsconfig.build.json vitest.config.ts scripts docs",
|
|
53
55
|
"typecheck": "tsc --noEmit",
|
|
54
56
|
"test": "pnpm build && vitest run",
|
|
57
|
+
"test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
|
|
55
58
|
"test:stable": "pnpm build && vitest run && vitest run && vitest run",
|
|
56
59
|
"corpus": "pnpm build && node scripts/corpus.mjs"
|
|
57
60
|
},
|
package/skills/belay/SKILL.md
CHANGED
|
@@ -2,16 +2,30 @@
|
|
|
2
2
|
name: belay
|
|
3
3
|
description: >-
|
|
4
4
|
Guides approval when belay blocks a high-risk shell command, subagent launch,
|
|
5
|
-
or tool action. Use when an action is
|
|
6
|
-
|
|
5
|
+
or tool action across Cursor, Claude Code, and Codex. Use when an action is
|
|
6
|
+
denied, blocked, or needs belay-approve, or when installing or checking belay
|
|
7
|
+
hook health in a repository.
|
|
7
8
|
disable-model-invocation: true
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# Belay
|
|
11
12
|
|
|
12
|
-
Belay
|
|
13
|
-
subagent
|
|
14
|
-
|
|
13
|
+
Belay is a safety gate for coding agents: it inspects each shell command,
|
|
14
|
+
subagent launch, and file mutation *before* it runs, lets safe-and-local actions
|
|
15
|
+
through, and holds back only the irreversible-and-catastrophic ones for one-shot
|
|
16
|
+
human approval. Every decision is written to an audit log.
|
|
17
|
+
|
|
18
|
+
It runs on **Cursor**, **Claude Code**, and **Codex (experimental)**, wiring the
|
|
19
|
+
same classifier into each agent through its native hooks:
|
|
20
|
+
|
|
21
|
+
| Agent | Hook config |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Cursor | `.cursor/hooks.json` |
|
|
24
|
+
| Claude Code | `.claude/settings.json` |
|
|
25
|
+
| Codex | `.codex/config.toml` |
|
|
26
|
+
|
|
27
|
+
Enforcement lives in those hooks; this skill only explains the flow and routes
|
|
28
|
+
you to the CLI. It does not classify commands itself.
|
|
15
29
|
|
|
16
30
|
## Prerequisites
|
|
17
31
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|