@guilz-dev/belay 0.1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/agent-belay-logo.png +0 -0
- package/dist/adapters/claude/adapter.d.ts +7 -0
- package/dist/adapters/claude/adapter.js +114 -0
- package/dist/adapters/claude/hooks.d.ts +13 -0
- package/dist/adapters/claude/hooks.js +49 -0
- package/dist/adapters/claude/runtime-entry.d.ts +4 -0
- package/dist/adapters/claude/runtime-entry.js +260 -0
- package/dist/adapters/codex/adapter.d.ts +7 -0
- package/dist/adapters/codex/adapter.js +73 -0
- package/dist/adapters/codex/hooks.d.ts +21 -0
- package/dist/adapters/codex/hooks.js +78 -0
- package/dist/adapters/codex/runtime-entry.d.ts +4 -0
- package/dist/adapters/codex/runtime-entry.js +237 -0
- package/dist/adapters/cursor/adapter.d.ts +7 -0
- package/dist/adapters/cursor/adapter.js +29 -0
- package/dist/adapters/cursor/hooks.d.ts +2 -0
- package/dist/adapters/cursor/hooks.js +26 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
- package/dist/adapters/cursor/runtime-entry.js +143 -0
- package/dist/adapters/layouts/claude.d.ts +2 -0
- package/dist/adapters/layouts/claude.js +40 -0
- package/dist/adapters/layouts/codex.d.ts +2 -0
- package/dist/adapters/layouts/codex.js +43 -0
- package/dist/adapters/layouts/cursor.d.ts +2 -0
- package/dist/adapters/layouts/cursor.js +40 -0
- package/dist/adapters/layouts/index.d.ts +7 -0
- package/dist/adapters/layouts/index.js +23 -0
- package/dist/adapters/layouts/protected-paths.d.ts +3 -0
- package/dist/adapters/layouts/protected-paths.js +15 -0
- package/dist/adapters/layouts/scope.d.ts +19 -0
- package/dist/adapters/layouts/scope.js +86 -0
- package/dist/adapters/layouts/types.d.ts +14 -0
- package/dist/adapters/layouts/types.js +1 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +14 -0
- package/dist/adapters/shared/gate-runtime.d.ts +51 -0
- package/dist/adapters/shared/gate-runtime.js +518 -0
- package/dist/adapters/shared/repo-root.d.ts +2 -0
- package/dist/adapters/shared/repo-root.js +17 -0
- package/dist/adapters/types.d.ts +19 -0
- package/dist/adapters/types.js +1 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.js +3 -0
- package/dist/bundle/claude-runtime.mjs +5323 -0
- package/dist/bundle/codex-runtime.mjs +5310 -0
- package/dist/bundle/cursor-runtime.mjs +5208 -0
- package/dist/cleanup-orphans.d.ts +7 -0
- package/dist/cleanup-orphans.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +631 -0
- package/dist/commands/approve.d.ts +14 -0
- package/dist/commands/approve.js +65 -0
- package/dist/commands/audit.d.ts +59 -0
- package/dist/commands/audit.js +132 -0
- package/dist/commands/classify-for-report.d.ts +9 -0
- package/dist/commands/classify-for-report.js +85 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +366 -0
- package/dist/commands/dogfood.d.ts +5 -0
- package/dist/commands/dogfood.js +71 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.js +133 -0
- package/dist/commands/health-snapshot.d.ts +2 -0
- package/dist/commands/health-snapshot.js +166 -0
- package/dist/commands/init-wizard.d.ts +16 -0
- package/dist/commands/init-wizard.js +50 -0
- package/dist/commands/metrics.d.ts +7 -0
- package/dist/commands/metrics.js +89 -0
- package/dist/commands/recover.d.ts +3 -0
- package/dist/commands/recover.js +105 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +65 -0
- package/dist/commands/revoke.d.ts +5 -0
- package/dist/commands/revoke.js +22 -0
- package/dist/commands/simulate.d.ts +14 -0
- package/dist/commands/simulate.js +55 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +107 -0
- package/dist/config-io.d.ts +23 -0
- package/dist/config-io.js +180 -0
- package/dist/conformance/guarantee-table.d.ts +14 -0
- package/dist/conformance/guarantee-table.js +95 -0
- package/dist/conformance/types.d.ts +6 -0
- package/dist/conformance/types.js +1 -0
- package/dist/core/approval-service.d.ts +26 -0
- package/dist/core/approval-service.js +41 -0
- package/dist/core/approval-token.d.ts +11 -0
- package/dist/core/approval-token.js +61 -0
- package/dist/core/approval.d.ts +19 -0
- package/dist/core/approval.js +58 -0
- package/dist/core/audit-analysis.d.ts +10 -0
- package/dist/core/audit-analysis.js +147 -0
- package/dist/core/audit-metrics.d.ts +51 -0
- package/dist/core/audit-metrics.js +155 -0
- package/dist/core/audit-query.d.ts +11 -0
- package/dist/core/audit-query.js +142 -0
- package/dist/core/audit-summary.d.ts +33 -0
- package/dist/core/audit-summary.js +111 -0
- package/dist/core/audit-types.d.ts +65 -0
- package/dist/core/audit-types.js +2 -0
- package/dist/core/capability/allowlist.d.ts +10 -0
- package/dist/core/capability/allowlist.js +53 -0
- package/dist/core/capability/broker.d.ts +17 -0
- package/dist/core/capability/broker.js +29 -0
- package/dist/core/capability/index.d.ts +5 -0
- package/dist/core/capability/index.js +4 -0
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +20 -0
- package/dist/core/capability/reasons.d.ts +2 -0
- package/dist/core/capability/reasons.js +4 -0
- package/dist/core/capability/types.d.ts +10 -0
- package/dist/core/capability/types.js +1 -0
- package/dist/core/capability-approval.d.ts +28 -0
- package/dist/core/capability-approval.js +43 -0
- package/dist/core/classify-subagent.d.ts +2 -0
- package/dist/core/classify-subagent.js +69 -0
- package/dist/core/classify-tool.d.ts +3 -0
- package/dist/core/classify-tool.js +311 -0
- package/dist/core/config-layers.d.ts +23 -0
- package/dist/core/config-layers.js +59 -0
- package/dist/core/config.d.ts +219 -0
- package/dist/core/config.js +720 -0
- package/dist/core/control-plane-isolation.d.ts +10 -0
- package/dist/core/control-plane-isolation.js +83 -0
- package/dist/core/custom-command-match.d.ts +2 -0
- package/dist/core/custom-command-match.js +8 -0
- package/dist/core/egress/allowlist.d.ts +7 -0
- package/dist/core/egress/allowlist.js +33 -0
- package/dist/core/egress/env.d.ts +3 -0
- package/dist/core/egress/env.js +17 -0
- package/dist/core/egress/fingerprint.d.ts +3 -0
- package/dist/core/egress/fingerprint.js +35 -0
- package/dist/core/egress/policy.d.ts +8 -0
- package/dist/core/egress/policy.js +47 -0
- package/dist/core/egress/proxy-server.d.ts +21 -0
- package/dist/core/egress/proxy-server.js +263 -0
- package/dist/core/egress/types.d.ts +25 -0
- package/dist/core/egress/types.js +1 -0
- package/dist/core/egress-approval.d.ts +48 -0
- package/dist/core/egress-approval.js +129 -0
- package/dist/core/fingerprint.d.ts +6 -0
- package/dist/core/fingerprint.js +24 -0
- package/dist/core/gate-contract.d.ts +48 -0
- package/dist/core/gate-contract.js +50 -0
- package/dist/core/gate-engine.d.ts +20 -0
- package/dist/core/gate-engine.js +172 -0
- package/dist/core/glob.d.ts +1 -0
- package/dist/core/glob.js +39 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +15 -0
- package/dist/core/integrity.d.ts +15 -0
- package/dist/core/integrity.js +68 -0
- package/dist/core/judge-api-key.d.ts +4 -0
- package/dist/core/judge-api-key.js +11 -0
- package/dist/core/judge-config.d.ts +29 -0
- package/dist/core/judge-config.js +85 -0
- package/dist/core/judge-doctor.d.ts +7 -0
- package/dist/core/judge-doctor.js +124 -0
- package/dist/core/judgment.d.ts +6 -0
- package/dist/core/judgment.js +38 -0
- package/dist/core/notify.d.ts +13 -0
- package/dist/core/notify.js +44 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +107 -0
- package/dist/core/reclassify.d.ts +15 -0
- package/dist/core/reclassify.js +82 -0
- package/dist/core/recover-advice.d.ts +30 -0
- package/dist/core/recover-advice.js +177 -0
- package/dist/core/recover-git-probe.d.ts +8 -0
- package/dist/core/recover-git-probe.js +50 -0
- package/dist/core/recover-select.d.ts +10 -0
- package/dist/core/recover-select.js +60 -0
- package/dist/core/scrub.d.ts +3 -0
- package/dist/core/scrub.js +87 -0
- package/dist/core/shell-substitution.d.ts +6 -0
- package/dist/core/shell-substitution.js +130 -0
- package/dist/core/shell-tokenizer.d.ts +5 -0
- package/dist/core/shell-tokenizer.js +129 -0
- package/dist/core/shell-unparseable.d.ts +4 -0
- package/dist/core/shell-unparseable.js +96 -0
- package/dist/core/transactional/diff-evaluator.d.ts +2 -0
- package/dist/core/transactional/diff-evaluator.js +84 -0
- package/dist/core/transactional/eligibility.d.ts +4 -0
- package/dist/core/transactional/eligibility.js +44 -0
- package/dist/core/transactional/git-worktree.d.ts +13 -0
- package/dist/core/transactional/git-worktree.js +189 -0
- package/dist/core/transactional/index.d.ts +5 -0
- package/dist/core/transactional/index.js +4 -0
- package/dist/core/transactional/reasons.d.ts +4 -0
- package/dist/core/transactional/reasons.js +8 -0
- package/dist/core/transactional/runner.d.ts +2 -0
- package/dist/core/transactional/runner.js +113 -0
- package/dist/core/transactional/types.d.ts +46 -0
- package/dist/core/transactional/types.js +1 -0
- package/dist/core/types.d.ts +90 -0
- package/dist/core/types.js +1 -0
- package/dist/core/v2/adapter.d.ts +14 -0
- package/dist/core/v2/adapter.js +118 -0
- package/dist/core/v2/containment.d.ts +19 -0
- package/dist/core/v2/containment.js +91 -0
- package/dist/core/v2/egress-classify.d.ts +7 -0
- package/dist/core/v2/egress-classify.js +216 -0
- package/dist/core/v2/fingerprint.d.ts +1 -0
- package/dist/core/v2/fingerprint.js +4 -0
- package/dist/core/v2/index.d.ts +12 -0
- package/dist/core/v2/index.js +10 -0
- package/dist/core/v2/judge-audit.d.ts +2 -0
- package/dist/core/v2/judge-audit.js +15 -0
- package/dist/core/v2/judge-factory.d.ts +25 -0
- package/dist/core/v2/judge-factory.js +75 -0
- package/dist/core/v2/judge-outbound.d.ts +12 -0
- package/dist/core/v2/judge-outbound.js +73 -0
- package/dist/core/v2/judge.d.ts +47 -0
- package/dist/core/v2/judge.js +264 -0
- package/dist/core/v2/launcher-resolve.d.ts +12 -0
- package/dist/core/v2/launcher-resolve.js +190 -0
- package/dist/core/v2/overrides.d.ts +7 -0
- package/dist/core/v2/overrides.js +37 -0
- package/dist/core/v2/parser.d.ts +21 -0
- package/dist/core/v2/parser.js +213 -0
- package/dist/core/v2/types.d.ts +67 -0
- package/dist/core/v2/types.js +1 -0
- package/dist/core/v2/verdict.d.ts +2 -0
- package/dist/core/v2/verdict.js +699 -0
- package/dist/corpus/evaluate.d.ts +24 -0
- package/dist/corpus/evaluate.js +69 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.js +155 -0
- package/dist/egress-daemon.d.ts +1 -0
- package/dist/egress-daemon.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/installer/bootstrap.d.ts +5 -0
- package/dist/installer/bootstrap.js +61 -0
- package/dist/installer/runtime-artifacts.d.ts +3 -0
- package/dist/installer/runtime-artifacts.js +23 -0
- package/dist/installer/scope-config.d.ts +8 -0
- package/dist/installer/scope-config.js +25 -0
- package/dist/installer.d.ts +22 -0
- package/dist/installer.js +169 -0
- package/dist/node-resolution.d.ts +8 -0
- package/dist/node-resolution.js +237 -0
- package/dist/operational-insights.d.ts +19 -0
- package/dist/operational-insights.js +24 -0
- package/dist/presets.d.ts +4 -0
- package/dist/presets.js +95 -0
- package/dist/services/egress-service.d.ts +57 -0
- package/dist/services/egress-service.js +334 -0
- package/dist/services/sandbox-service.d.ts +38 -0
- package/dist/services/sandbox-service.js +95 -0
- package/dist/templates.d.ts +7 -0
- package/dist/templates.js +56 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +65 -0
- package/skills/belay/SKILL.md +52 -0
- package/skills/belay/belay-approve.md +7 -0
- package/skills/belay/belay-explain.md +11 -0
- package/skills/belay/belay-recover.md +13 -0
- package/skills/belay/belay-report.md +7 -0
- package/skills/belay/belay-status.md +9 -0
- package/skills/belay/belay-why.md +11 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { recordApproval } from '../../core/approval-service.js';
|
|
6
|
+
import { issueApprovalToken } from '../../core/approval-token.js';
|
|
7
|
+
import { fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, shouldSkipBrokerApprovedOnce, } from '../../core/capability/index.js';
|
|
8
|
+
import { resolveLayeredConfig, teamConfigPath } from '../../core/config-layers.js';
|
|
9
|
+
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
10
|
+
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
11
|
+
import { approvalCommandMatch, approvedApprovalsFile, buildRetryInstruction, canonicalStringify, classifierOptionsFromConfig, compactApprovals, configuredControlPlaneDir, createApprovalRecord, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
12
|
+
import { notifyDeny } from '../../core/notify.js';
|
|
13
|
+
import { isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
14
|
+
import { protectedArtifactRoots } from '../layouts/protected-paths.js';
|
|
15
|
+
const EMPTY_APPROVALS = {
|
|
16
|
+
version: 1,
|
|
17
|
+
approvals: [],
|
|
18
|
+
};
|
|
19
|
+
async function loadJsonFile(filePath, fallback) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(filePath, 'utf8');
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function createDefaultGateRuntimeDeps() {
|
|
29
|
+
return {
|
|
30
|
+
async readConfig(configPath) {
|
|
31
|
+
return loadJsonFile(configPath, {});
|
|
32
|
+
},
|
|
33
|
+
async appendAudit(ctx, event) {
|
|
34
|
+
const auditPath = path.join(ctx.repoRoot, ctx.config.audit.logPath);
|
|
35
|
+
await mkdir(path.dirname(auditPath), { recursive: true });
|
|
36
|
+
const record = { timestamp: new Date().toISOString(), ...event };
|
|
37
|
+
if (!ctx.config.audit.includeAssessment) {
|
|
38
|
+
delete record.assessment;
|
|
39
|
+
}
|
|
40
|
+
const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
|
|
41
|
+
await writeFile(auditPath, `${JSON.stringify(scrubbed)}\n`, {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
flag: 'a',
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
async loadApprovals(ctx, fileName) {
|
|
47
|
+
const repoLocalStateDir = ctx.layout.repoLocalStateDir(ctx.repoRoot);
|
|
48
|
+
const filePath = fileName === 'pending-approvals.json'
|
|
49
|
+
? pendingApprovalsFile(ctx.config, repoLocalStateDir)
|
|
50
|
+
: approvedApprovalsFile(ctx.config, repoLocalStateDir);
|
|
51
|
+
const loaded = await loadJsonFile(filePath, EMPTY_APPROVALS);
|
|
52
|
+
return {
|
|
53
|
+
filePath,
|
|
54
|
+
state: {
|
|
55
|
+
version: loaded.version === 2 ? 2 : 1,
|
|
56
|
+
approvals: Array.isArray(loaded.approvals) ? loaded.approvals : [],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
async writeApprovals(filePath, state) {
|
|
61
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
62
|
+
await writeFile(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}\n`, 'utf8');
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function resolveGateConfig(ctx, deps) {
|
|
67
|
+
const loaded = await deps.readConfig(ctx.configPath);
|
|
68
|
+
let teamConfig = null;
|
|
69
|
+
const teamPath = teamConfigPath();
|
|
70
|
+
if (existsSync(teamPath)) {
|
|
71
|
+
teamConfig = JSON.parse(await readFile(teamPath, 'utf8'));
|
|
72
|
+
}
|
|
73
|
+
return resolveLayeredConfig({
|
|
74
|
+
repoConfig: loaded,
|
|
75
|
+
adapterDefaults: ctx.layout.defaultConfig(ctx.repoRoot),
|
|
76
|
+
teamConfig,
|
|
77
|
+
teamConfigPath: teamPath,
|
|
78
|
+
repoConfigPath: ctx.configPath,
|
|
79
|
+
}).config;
|
|
80
|
+
}
|
|
81
|
+
export function repoShellClassifierOptions(config, repoRoot, layout, extras = {}) {
|
|
82
|
+
const controlPlaneDir = config.controlPlane.enabled ? resolveControlPlaneDir(config) : null;
|
|
83
|
+
return {
|
|
84
|
+
...classifierOptionsFromConfig(config),
|
|
85
|
+
controlPlaneDir,
|
|
86
|
+
protectedArtifactRoots: protectedArtifactRoots(layout, repoRoot, controlPlaneDir),
|
|
87
|
+
...extras,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function runtimeClassifierOptions(ctx, config) {
|
|
91
|
+
const repoLocalStateDir = ctx.layout.repoLocalStateDir(ctx.repoRoot);
|
|
92
|
+
const brokerFsScope = isCapabilityBrokerDemotionActive(config);
|
|
93
|
+
return repoShellClassifierOptions(config, ctx.repoRoot, ctx.layout, {
|
|
94
|
+
brokerFsScope,
|
|
95
|
+
fsScopeAllowlist: brokerFsScope
|
|
96
|
+
? loadFsScopeAllowlistSync(fsScopeAllowlistPath(config, repoLocalStateDir))
|
|
97
|
+
: undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function gateAuditEventName(kind) {
|
|
101
|
+
if (kind === 'shell') {
|
|
102
|
+
return 'beforeShellExecution';
|
|
103
|
+
}
|
|
104
|
+
if (kind === 'tool') {
|
|
105
|
+
return 'preToolUse';
|
|
106
|
+
}
|
|
107
|
+
return 'subagentGate';
|
|
108
|
+
}
|
|
109
|
+
async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
|
|
110
|
+
const pending = await deps.loadApprovals(ctx, 'pending-approvals.json');
|
|
111
|
+
pending.state = compactApprovals(pending.state);
|
|
112
|
+
const existing = pending.state.approvals.find((approval) => approval.kind === kind &&
|
|
113
|
+
approval.fingerprint === result.fingerprint &&
|
|
114
|
+
approval.repoRoot === ctx.repoRoot);
|
|
115
|
+
if (existing) {
|
|
116
|
+
await deps.writeApprovals(pending.filePath, pending.state);
|
|
117
|
+
return existing;
|
|
118
|
+
}
|
|
119
|
+
const approval = createApprovalRecord({
|
|
120
|
+
kind,
|
|
121
|
+
fingerprint: result.fingerprint,
|
|
122
|
+
repoRoot: ctx.repoRoot,
|
|
123
|
+
reason: result.reason,
|
|
124
|
+
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
125
|
+
approvalTtlMinutes: ctx.config.approvalTtlMinutes,
|
|
126
|
+
approvalId: `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`,
|
|
127
|
+
input: approvalInput?.input,
|
|
128
|
+
inputKind: approvalInput?.inputKind,
|
|
129
|
+
});
|
|
130
|
+
pending.state.version = 2;
|
|
131
|
+
pending.state.approvals.push(approval);
|
|
132
|
+
await deps.writeApprovals(pending.filePath, pending.state);
|
|
133
|
+
return approval;
|
|
134
|
+
}
|
|
135
|
+
async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
136
|
+
const approved = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
137
|
+
approved.state = compactApprovals(approved.state);
|
|
138
|
+
const index = approved.state.approvals.findIndex((approval) => approval.kind === kind &&
|
|
139
|
+
approval.fingerprint === fingerprint &&
|
|
140
|
+
approval.repoRoot === ctx.repoRoot);
|
|
141
|
+
if (index === -1) {
|
|
142
|
+
await deps.writeApprovals(approved.filePath, approved.state);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const [approval] = approved.state.approvals.splice(index, 1);
|
|
146
|
+
await deps.writeApprovals(approved.filePath, approved.state);
|
|
147
|
+
return approval;
|
|
148
|
+
}
|
|
149
|
+
export async function evaluateGatedAction(ctx, deps, params) {
|
|
150
|
+
let action;
|
|
151
|
+
try {
|
|
152
|
+
action = normalizeGatedAction({
|
|
153
|
+
kind: params.kind,
|
|
154
|
+
repoRoot: ctx.repoRoot,
|
|
155
|
+
cwd: params.cwd,
|
|
156
|
+
command: params.command,
|
|
157
|
+
payload: params.payload,
|
|
158
|
+
toolName: params.toolName,
|
|
159
|
+
agentAssessment: extractAgentAssessment(params.payload),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
const verdict = unnormalizedGateVerdict({
|
|
164
|
+
reason: 'normalization_failed',
|
|
165
|
+
mode: ctx.config.mode,
|
|
166
|
+
user_message: 'belay could not normalize this gated action. Run belay doctor, then retry.',
|
|
167
|
+
agent_message: 'Belay denied this action because the hook payload could not be normalized.',
|
|
168
|
+
});
|
|
169
|
+
await deps.appendAudit(ctx, {
|
|
170
|
+
event: gateAuditEventName(params.kind),
|
|
171
|
+
kind: params.kind,
|
|
172
|
+
verdict: verdict.verdict,
|
|
173
|
+
reason: verdict.reason,
|
|
174
|
+
mode: ctx.config.mode,
|
|
175
|
+
wouldBlock: true,
|
|
176
|
+
permission: 'deny',
|
|
177
|
+
});
|
|
178
|
+
return verdict;
|
|
179
|
+
}
|
|
180
|
+
if (!gateEnabledForAction(ctx.config, action)) {
|
|
181
|
+
return classifyResultToGateVerdict({
|
|
182
|
+
result: {
|
|
183
|
+
verdict: 'allow',
|
|
184
|
+
reason: 'gate_disabled',
|
|
185
|
+
fingerprint: 'gate_disabled',
|
|
186
|
+
assessment: {
|
|
187
|
+
reversibility: 'reversible',
|
|
188
|
+
external: false,
|
|
189
|
+
blastRadius: 'none',
|
|
190
|
+
confidence: 1,
|
|
191
|
+
signals: ['gate_disabled'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
mode: ctx.config.mode,
|
|
195
|
+
permission: 'allow',
|
|
196
|
+
wouldBlock: false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const classifierOptions = runtimeClassifierOptions(ctx, ctx.config);
|
|
200
|
+
const predicted = await classifyGatedActionAsync(action, ctx.config, classifierOptions);
|
|
201
|
+
let result = predicted;
|
|
202
|
+
let predictedAssessment;
|
|
203
|
+
let observedAssessment;
|
|
204
|
+
let transactionalLayer;
|
|
205
|
+
if (isTransactionalEligible(ctx.config, params.kind, predicted) &&
|
|
206
|
+
params.kind === 'shell' &&
|
|
207
|
+
params.command) {
|
|
208
|
+
const transactional = ctx.config.policy.transactional;
|
|
209
|
+
const txResult = await runTransactionalExecution({
|
|
210
|
+
command: params.command,
|
|
211
|
+
cwd: params.cwd,
|
|
212
|
+
repoRoot: ctx.repoRoot,
|
|
213
|
+
stateDir: path.join(ctx.layout.repoLocalStateDir(ctx.repoRoot), 'transactional'),
|
|
214
|
+
timeoutMs: transactional.timeoutMs,
|
|
215
|
+
predicted,
|
|
216
|
+
diffContext: {
|
|
217
|
+
repoRoot: ctx.repoRoot,
|
|
218
|
+
sensitivePaths: ctx.config.classifier.sensitivePaths,
|
|
219
|
+
protectedRoots: classifierOptions.protectedArtifactRoots ?? [],
|
|
220
|
+
maxDeletionCount: transactional.maxDeletionCount,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
if (!txResult.skipped && txResult.observed) {
|
|
224
|
+
result = txResult.result;
|
|
225
|
+
predictedAssessment = txResult.predicted.assessment;
|
|
226
|
+
observedAssessment = txResult.observed.assessment;
|
|
227
|
+
transactionalLayer = {
|
|
228
|
+
transactional: true,
|
|
229
|
+
transactionalReason: txResult.observed.reason,
|
|
230
|
+
transactionalCategories: txResult.observed.categories,
|
|
231
|
+
transactionalChangeCount: txResult.observed.changes.length,
|
|
232
|
+
transactionalTimedOut: txResult.timedOut === true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
else if (txResult.skipReason) {
|
|
236
|
+
transactionalLayer = {
|
|
237
|
+
transactional: false,
|
|
238
|
+
transactionalSkipReason: txResult.skipReason,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return gateDecisionToVerdict(ctx, deps, params.kind, result, {
|
|
243
|
+
predictedAssessment,
|
|
244
|
+
observedAssessment,
|
|
245
|
+
transactionalLayer,
|
|
246
|
+
approvalInput: params.kind === 'shell'
|
|
247
|
+
? {
|
|
248
|
+
input: params.command ?? result.normalizedCommand ?? result.summary ?? '',
|
|
249
|
+
inputKind: 'shell',
|
|
250
|
+
}
|
|
251
|
+
: {
|
|
252
|
+
input: params.command ??
|
|
253
|
+
result.normalizedCommand ??
|
|
254
|
+
result.summary ??
|
|
255
|
+
canonicalStringify(params.payload ?? {}),
|
|
256
|
+
inputKind: params.kind,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** R39: unmapped Codex tools ask via pending approval — not hard deny without approval path. */
|
|
261
|
+
export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
262
|
+
const scrubbed = scrubValue(payload, scrubOptionsFromConfig(ctx.config));
|
|
263
|
+
const result = {
|
|
264
|
+
verdict: 'deny_pending_approval',
|
|
265
|
+
reason: 'unmapped_tool',
|
|
266
|
+
summary: toolName,
|
|
267
|
+
fingerprint: toolFingerprint(toolName, scrubbed, ctx.repoRoot),
|
|
268
|
+
assessment: {
|
|
269
|
+
reversibility: 'irreversible',
|
|
270
|
+
external: false,
|
|
271
|
+
blastRadius: 'unknown Codex tool action',
|
|
272
|
+
confidence: 0.5,
|
|
273
|
+
signals: ['unmapped_tool'],
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
return gateDecisionToVerdict(ctx, deps, 'tool', result, {
|
|
277
|
+
approvalInput: {
|
|
278
|
+
input: toolName,
|
|
279
|
+
inputKind: 'tool',
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
|
|
284
|
+
const gateBase = {
|
|
285
|
+
event: gateAuditEventName(kind),
|
|
286
|
+
kind,
|
|
287
|
+
fingerprint: result.fingerprint,
|
|
288
|
+
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
289
|
+
assessment: result.assessment,
|
|
290
|
+
predictedAssessment: auditExtras.predictedAssessment,
|
|
291
|
+
observedAssessment: auditExtras.observedAssessment,
|
|
292
|
+
mode: ctx.config.mode,
|
|
293
|
+
schemaVersion: result.v2 ? 2 : 1,
|
|
294
|
+
...(result.v2 ?? {}),
|
|
295
|
+
...auditExtras.transactionalLayer,
|
|
296
|
+
};
|
|
297
|
+
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
298
|
+
const userMessage = 'Belay executed this command safely in an isolated git worktree. Observed-safe file changes are already applied; do not retry the same command.';
|
|
299
|
+
const agentMessage = 'Belay already applied the observed-safe effects of this shell command in isolation. Do not run it again.';
|
|
300
|
+
await deps.appendAudit(ctx, {
|
|
301
|
+
...gateBase,
|
|
302
|
+
verdict: 'allow',
|
|
303
|
+
reason: result.reason,
|
|
304
|
+
wouldBlock: false,
|
|
305
|
+
permission: 'deny',
|
|
306
|
+
});
|
|
307
|
+
return classifyResultToGateVerdict({
|
|
308
|
+
result,
|
|
309
|
+
mode: ctx.config.mode,
|
|
310
|
+
permission: 'deny',
|
|
311
|
+
wouldBlock: false,
|
|
312
|
+
user_message: userMessage,
|
|
313
|
+
agent_message: agentMessage,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
317
|
+
const approved = TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) ||
|
|
318
|
+
shouldSkipBrokerApprovedOnce(brokerActive, result.reason)
|
|
319
|
+
? null
|
|
320
|
+
: await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
321
|
+
if (approved) {
|
|
322
|
+
await deps.appendAudit(ctx, {
|
|
323
|
+
...gateBase,
|
|
324
|
+
verdict: 'allow',
|
|
325
|
+
reason: 'approved_once',
|
|
326
|
+
approvalId: approved.approvalId,
|
|
327
|
+
wouldBlock: false,
|
|
328
|
+
permission: 'allow',
|
|
329
|
+
});
|
|
330
|
+
return classifyResultToGateVerdict({
|
|
331
|
+
result: { ...result, verdict: 'allow', reason: 'approved_once' },
|
|
332
|
+
mode: ctx.config.mode,
|
|
333
|
+
permission: 'allow',
|
|
334
|
+
wouldBlock: false,
|
|
335
|
+
approvalId: approved.approvalId,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (result.verdict === 'allow' || result.verdict === 'allow_flagged') {
|
|
339
|
+
await deps.appendAudit(ctx, {
|
|
340
|
+
...gateBase,
|
|
341
|
+
verdict: result.verdict,
|
|
342
|
+
reason: result.reason,
|
|
343
|
+
wouldBlock: false,
|
|
344
|
+
permission: 'allow',
|
|
345
|
+
});
|
|
346
|
+
return classifyResultToGateVerdict({
|
|
347
|
+
result,
|
|
348
|
+
mode: ctx.config.mode,
|
|
349
|
+
permission: 'allow',
|
|
350
|
+
wouldBlock: false,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (ctx.config.mode === 'audit') {
|
|
354
|
+
await deps.appendAudit(ctx, {
|
|
355
|
+
...gateBase,
|
|
356
|
+
verdict: result.verdict,
|
|
357
|
+
reason: result.reason,
|
|
358
|
+
wouldBlock: true,
|
|
359
|
+
permission: 'allow',
|
|
360
|
+
});
|
|
361
|
+
return classifyResultToGateVerdict({
|
|
362
|
+
result,
|
|
363
|
+
mode: ctx.config.mode,
|
|
364
|
+
permission: 'allow',
|
|
365
|
+
wouldBlock: true,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const approval = await ensurePendingApproval(ctx, deps, kind, result, auditExtras.approvalInput);
|
|
369
|
+
let approvalToken;
|
|
370
|
+
try {
|
|
371
|
+
approvalToken = await issueApprovalToken({
|
|
372
|
+
approvalId: approval.approvalId,
|
|
373
|
+
fingerprint: approval.fingerprint,
|
|
374
|
+
repoRoot: approval.repoRoot,
|
|
375
|
+
issuedAt: approval.createdAt,
|
|
376
|
+
expiresAt: approval.expiresAt,
|
|
377
|
+
}, configuredControlPlaneDir(ctx.config));
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
approvalToken = undefined;
|
|
381
|
+
}
|
|
382
|
+
if (ctx.config.notifications.webhookUrl || ctx.config.notifications.commandHook) {
|
|
383
|
+
await notifyDeny(ctx.config.notifications, {
|
|
384
|
+
approvalId: approval.approvalId,
|
|
385
|
+
reason: result.reason,
|
|
386
|
+
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
387
|
+
repoRoot: ctx.repoRoot,
|
|
388
|
+
fingerprint: result.fingerprint,
|
|
389
|
+
approvalToken,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
await deps.appendAudit(ctx, {
|
|
393
|
+
...gateBase,
|
|
394
|
+
verdict: result.verdict,
|
|
395
|
+
reason: result.reason,
|
|
396
|
+
approvalId: approval.approvalId,
|
|
397
|
+
wouldBlock: true,
|
|
398
|
+
permission: 'deny',
|
|
399
|
+
});
|
|
400
|
+
return classifyResultToGateVerdict({
|
|
401
|
+
result,
|
|
402
|
+
mode: ctx.config.mode,
|
|
403
|
+
permission: 'deny',
|
|
404
|
+
wouldBlock: true,
|
|
405
|
+
approvalId: approval.approvalId,
|
|
406
|
+
user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${buildRetryInstruction(ctx.config.tokenPrefix, approval.approvalId)} For details, run belay explain or /belay why.`,
|
|
407
|
+
agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
411
|
+
const approvalId = approvalCommandMatch(prompt, ctx.config.tokenPrefix);
|
|
412
|
+
if (!approvalId) {
|
|
413
|
+
return { continue: true };
|
|
414
|
+
}
|
|
415
|
+
if (ctx.config.approvalSigning.required) {
|
|
416
|
+
const message = `Signed approval token required for ${approvalId}. Editor prompt approval is disabled in this configuration. ` +
|
|
417
|
+
`Use belay approve --approval-id ${approvalId} --token <signed-token>.`;
|
|
418
|
+
await deps.appendAudit(ctx, {
|
|
419
|
+
event: 'approval',
|
|
420
|
+
kind: 'approval',
|
|
421
|
+
verdict: 'deny_pending_approval',
|
|
422
|
+
approvalId,
|
|
423
|
+
reason: 'approval_prompt_signing_required',
|
|
424
|
+
summary: prompt,
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
continue: false,
|
|
428
|
+
user_message: message,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const recorded = await recordApproval({
|
|
432
|
+
approvalId,
|
|
433
|
+
config: ctx.config,
|
|
434
|
+
requireSignedToken: ctx.config.approvalSigning.required,
|
|
435
|
+
store: {
|
|
436
|
+
loadPending: () => deps.loadApprovals(ctx, 'pending-approvals.json'),
|
|
437
|
+
loadApproved: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
438
|
+
writePending: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
439
|
+
writeApproved: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
await deps.appendAudit(ctx, {
|
|
443
|
+
event: 'approval',
|
|
444
|
+
kind: 'approval',
|
|
445
|
+
verdict: recorded.ok ? 'allow' : 'deny_pending_approval',
|
|
446
|
+
approvalId,
|
|
447
|
+
reason: recorded.ok ? 'approval_recorded' : 'approval_missing',
|
|
448
|
+
summary: prompt,
|
|
449
|
+
});
|
|
450
|
+
if (!recorded.ok) {
|
|
451
|
+
return {
|
|
452
|
+
continue: false,
|
|
453
|
+
user_message: recorded.message,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
continue: false,
|
|
458
|
+
user_message: recorded.message,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
export function gateVerdictToCursorResponse(verdict) {
|
|
462
|
+
return {
|
|
463
|
+
permission: verdict.permission,
|
|
464
|
+
user_message: verdict.user_message,
|
|
465
|
+
agent_message: verdict.agent_message,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
export function gateVerdictToClaudePreToolUseResponse(verdict) {
|
|
469
|
+
if (verdict.permission === 'allow') {
|
|
470
|
+
return {};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
hookSpecificOutput: {
|
|
474
|
+
hookEventName: 'PreToolUse',
|
|
475
|
+
permissionDecision: 'deny',
|
|
476
|
+
permissionDecisionReason: verdict.user_message ??
|
|
477
|
+
verdict.agent_message ??
|
|
478
|
+
`Belay denied this action (${verdict.reason}).`,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
export function gateVerdictToClaudeUserPromptResponse(verdict) {
|
|
483
|
+
if (verdict.continue) {
|
|
484
|
+
return {};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
hookSpecificOutput: {
|
|
488
|
+
hookEventName: 'UserPromptSubmit',
|
|
489
|
+
continue: false,
|
|
490
|
+
user_message: verdict.user_message,
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// Codex PreToolUse deny contract is identical to Claude's
|
|
495
|
+
// (`hookSpecificOutput.permissionDecision: "deny"` / exit 2). Reuse the same shape.
|
|
496
|
+
export function gateVerdictToCodexPreToolUseResponse(verdict) {
|
|
497
|
+
return gateVerdictToClaudePreToolUseResponse(verdict);
|
|
498
|
+
}
|
|
499
|
+
// Codex UserPromptSubmit blocks via `decision: "block"` (per developers.openai.com/codex/hooks).
|
|
500
|
+
export function gateVerdictToCodexUserPromptResponse(verdict) {
|
|
501
|
+
if (verdict.continue) {
|
|
502
|
+
return {};
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
decision: 'block',
|
|
506
|
+
reason: verdict.user_message,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
export async function appendObservedAudit(ctx, deps, eventName, payload) {
|
|
510
|
+
await deps.appendAudit(ctx, {
|
|
511
|
+
event: eventName,
|
|
512
|
+
kind: 'audit',
|
|
513
|
+
verdict: 'allow',
|
|
514
|
+
reason: 'observed',
|
|
515
|
+
summary: canonicalStringify(payload),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
export { GateNormalizationError };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function findRepoRoot(startPath, layout) {
|
|
4
|
+
let current = path.resolve(startPath);
|
|
5
|
+
while (true) {
|
|
6
|
+
for (const marker of layout.repoRootMarkers) {
|
|
7
|
+
if (existsSync(path.join(current, marker))) {
|
|
8
|
+
return current;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const parent = path.dirname(current);
|
|
12
|
+
if (parent === current) {
|
|
13
|
+
return path.resolve(startPath);
|
|
14
|
+
}
|
|
15
|
+
current = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ManagedHookDefinition } from '../defaults.js';
|
|
2
|
+
import type { DoctorOptions, DoctorReport, InitOptions, UpgradeOptions } from '../types.js';
|
|
3
|
+
import type { AdapterLayout, AdapterName } from './layouts/types.js';
|
|
4
|
+
export interface BelayAdapter {
|
|
5
|
+
name: AdapterName;
|
|
6
|
+
layout: AdapterLayout;
|
|
7
|
+
install(repoRoot: string, options: InitOptions): Promise<{
|
|
8
|
+
repoRoot: string;
|
|
9
|
+
withSkill: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
upgrade(repoRoot: string, options: UpgradeOptions): Promise<{
|
|
12
|
+
repoRoot: string;
|
|
13
|
+
}>;
|
|
14
|
+
doctor(options: DoctorOptions): Promise<DoctorReport>;
|
|
15
|
+
hookEvents(): Array<{
|
|
16
|
+
event: string;
|
|
17
|
+
definition: ManagedHookDefinition;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/branding.js
ADDED