@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,129 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { compactApprovals, createApprovalRecord } from './approval.js';
|
|
3
|
+
import { issueApprovalToken } from './approval-token.js';
|
|
4
|
+
import { configuredControlPlaneDir } from './config.js';
|
|
5
|
+
import { addDomainToAllowlist, loadEgressAllowlist, saveEgressAllowlist, } from './egress/allowlist.js';
|
|
6
|
+
import { parseHostFromSummary } from './egress/fingerprint.js';
|
|
7
|
+
import { notifyDeny } from './notify.js';
|
|
8
|
+
export async function ensurePendingEgressApproval(params) {
|
|
9
|
+
const { config, repoRoot, policyResult, store } = params;
|
|
10
|
+
const pending = await store.loadPending();
|
|
11
|
+
pending.state = compactApprovals(pending.state);
|
|
12
|
+
const existing = pending.state.approvals.find((approval) => approval.kind === 'egress' &&
|
|
13
|
+
approval.fingerprint === policyResult.fingerprint &&
|
|
14
|
+
approval.repoRoot === repoRoot);
|
|
15
|
+
if (existing) {
|
|
16
|
+
await store.writePending(pending.filePath, pending.state);
|
|
17
|
+
return { approvalId: existing.approvalId, approval: existing, created: false };
|
|
18
|
+
}
|
|
19
|
+
const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
20
|
+
const approval = createApprovalRecord({
|
|
21
|
+
kind: 'egress',
|
|
22
|
+
fingerprint: policyResult.fingerprint,
|
|
23
|
+
repoRoot,
|
|
24
|
+
reason: policyResult.reason,
|
|
25
|
+
summary: policyResult.summary,
|
|
26
|
+
approvalTtlMinutes: config.approvalTtlMinutes,
|
|
27
|
+
approvalId,
|
|
28
|
+
});
|
|
29
|
+
pending.state.approvals.push(approval);
|
|
30
|
+
await store.writePending(pending.filePath, pending.state);
|
|
31
|
+
return { approvalId, approval, created: true };
|
|
32
|
+
}
|
|
33
|
+
const consumeLocks = new Map();
|
|
34
|
+
async function withConsumeLock(key, fn) {
|
|
35
|
+
const previous = consumeLocks.get(key) ?? Promise.resolve();
|
|
36
|
+
let release;
|
|
37
|
+
const gate = new Promise((resolve) => {
|
|
38
|
+
release = resolve;
|
|
39
|
+
});
|
|
40
|
+
consumeLocks.set(key, previous.then(() => gate));
|
|
41
|
+
await previous;
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
release();
|
|
47
|
+
if (consumeLocks.get(key) === gate) {
|
|
48
|
+
consumeLocks.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function consumeApprovedEgress(params) {
|
|
53
|
+
const lockKey = `${params.repoRoot}:${params.fingerprint}`;
|
|
54
|
+
return withConsumeLock(lockKey, async () => {
|
|
55
|
+
const approved = await params.store.loadApproved();
|
|
56
|
+
approved.state = compactApprovals(approved.state);
|
|
57
|
+
const index = approved.state.approvals.findIndex((approval) => approval.kind === 'egress' &&
|
|
58
|
+
approval.fingerprint === params.fingerprint &&
|
|
59
|
+
approval.repoRoot === params.repoRoot);
|
|
60
|
+
if (index === -1) {
|
|
61
|
+
await params.store.writeApproved(approved.filePath, approved.state);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const [approval] = approved.state.approvals.splice(index, 1);
|
|
65
|
+
await params.store.writeApproved(approved.filePath, approved.state);
|
|
66
|
+
return approval;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function notifyEgressDeny(params) {
|
|
70
|
+
if (!params.config.notifications.webhookUrl && !params.config.notifications.commandHook) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let approvalToken;
|
|
74
|
+
if (params.config.approvalSigning.required) {
|
|
75
|
+
try {
|
|
76
|
+
approvalToken = await issueApprovalToken({
|
|
77
|
+
approvalId: params.approval.approvalId,
|
|
78
|
+
fingerprint: params.approval.fingerprint,
|
|
79
|
+
repoRoot: params.approval.repoRoot,
|
|
80
|
+
issuedAt: params.approval.createdAt,
|
|
81
|
+
expiresAt: params.approval.expiresAt,
|
|
82
|
+
}, configuredControlPlaneDir(params.config));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
approvalToken = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await notifyDeny(params.config.notifications, {
|
|
89
|
+
approvalId: params.approval.approvalId,
|
|
90
|
+
reason: params.policyResult.reason,
|
|
91
|
+
summary: params.policyResult.summary,
|
|
92
|
+
repoRoot: params.repoRoot,
|
|
93
|
+
fingerprint: params.policyResult.fingerprint,
|
|
94
|
+
approvalToken,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export async function recordEgressApproval(params) {
|
|
98
|
+
const { recordApproval } = await import('./approval-service.js');
|
|
99
|
+
const pending = await params.store.loadPending();
|
|
100
|
+
const match = pending.state.approvals.find((approval) => approval.approvalId === params.approvalId);
|
|
101
|
+
const host = match ? parseHostFromSummary(match.summary) : null;
|
|
102
|
+
if (params.scope === 'domain' && match && !host) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
message: `Cannot add domain to egress allowlist: could not parse host from summary "${match.summary}".`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const result = await recordApproval({
|
|
109
|
+
approvalId: params.approvalId,
|
|
110
|
+
config: params.config,
|
|
111
|
+
token: params.token,
|
|
112
|
+
requireSignedToken: params.requireSignedToken ?? false,
|
|
113
|
+
store: params.store,
|
|
114
|
+
});
|
|
115
|
+
if (!result.ok || params.scope !== 'domain' || !host) {
|
|
116
|
+
return { ok: result.ok, message: result.message };
|
|
117
|
+
}
|
|
118
|
+
const allowlist = await loadEgressAllowlist(params.store.allowlistPath);
|
|
119
|
+
const updated = addDomainToAllowlist(allowlist, {
|
|
120
|
+
host,
|
|
121
|
+
approvedAt: new Date().toISOString(),
|
|
122
|
+
approvalId: params.approvalId,
|
|
123
|
+
});
|
|
124
|
+
await saveEgressAllowlist(params.store.allowlistPath, updated);
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
message: `${result.message} Domain ${host} added to egress allowlist.`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function canonicalStringify(value: unknown): string;
|
|
2
|
+
export declare function hashValue(value: string): string;
|
|
3
|
+
export declare function shellFingerprint(cwdRelative: string, normalizedCommand: string): string;
|
|
4
|
+
export declare function subagentFingerprint(kind: string, scrubbed: unknown, repoRoot: string): string;
|
|
5
|
+
export declare function toolFingerprint(toolName: string, scrubbed: unknown, repoRoot: string): string;
|
|
6
|
+
export { egressFingerprint } from './egress/fingerprint.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function canonicalStringify(value) {
|
|
3
|
+
if (value === null || typeof value !== 'object') {
|
|
4
|
+
return JSON.stringify(value);
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return `[${value.map((item) => canonicalStringify(item)).join(',')}]`;
|
|
8
|
+
}
|
|
9
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
10
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonicalStringify(child)}`).join(',')}}`;
|
|
11
|
+
}
|
|
12
|
+
export function hashValue(value) {
|
|
13
|
+
return createHash('sha256').update(value).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
export function shellFingerprint(cwdRelative, normalizedCommand) {
|
|
16
|
+
return hashValue(`shell:${cwdRelative}:${normalizedCommand}`);
|
|
17
|
+
}
|
|
18
|
+
export function subagentFingerprint(kind, scrubbed, repoRoot) {
|
|
19
|
+
return hashValue(`subagent:${kind}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
20
|
+
}
|
|
21
|
+
export function toolFingerprint(toolName, scrubbed, repoRoot) {
|
|
22
|
+
return hashValue(`tool:${toolName}:${canonicalStringify(scrubbed)}:${repoRoot}`);
|
|
23
|
+
}
|
|
24
|
+
export { egressFingerprint } from './egress/fingerprint.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Assessment, ClassifyResult, HookVerdict } from './types.js';
|
|
2
|
+
export declare const GATE_CONTRACT_VERSION: 1;
|
|
3
|
+
export type GatedActionKind = 'shell' | 'subagent' | 'tool';
|
|
4
|
+
export interface GatedAction {
|
|
5
|
+
contractVersion: typeof GATE_CONTRACT_VERSION;
|
|
6
|
+
kind: GatedActionKind;
|
|
7
|
+
repoRoot: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
payload?: Record<string, unknown>;
|
|
11
|
+
toolName?: string;
|
|
12
|
+
/** Reserved for v0.5 agent-side assessment ingestion. */
|
|
13
|
+
agentAssessment?: Assessment;
|
|
14
|
+
}
|
|
15
|
+
export interface GatePermissionResponse {
|
|
16
|
+
permission: 'allow' | 'deny';
|
|
17
|
+
user_message?: string;
|
|
18
|
+
agent_message?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface GateVerdict extends GatePermissionResponse {
|
|
21
|
+
contractVersion: typeof GATE_CONTRACT_VERSION;
|
|
22
|
+
verdict: HookVerdict;
|
|
23
|
+
reason: string;
|
|
24
|
+
fingerprint: string;
|
|
25
|
+
assessment: Assessment;
|
|
26
|
+
normalizedCommand?: string;
|
|
27
|
+
summary?: string;
|
|
28
|
+
approvalId?: string;
|
|
29
|
+
wouldBlock: boolean;
|
|
30
|
+
mode: 'enforce' | 'audit';
|
|
31
|
+
v2?: ClassifyResult['v2'];
|
|
32
|
+
}
|
|
33
|
+
export declare function isGatedAction(value: unknown): value is GatedAction;
|
|
34
|
+
export declare function classifyResultToGateVerdict(params: {
|
|
35
|
+
result: ClassifyResult;
|
|
36
|
+
mode: 'enforce' | 'audit';
|
|
37
|
+
permission: 'allow' | 'deny';
|
|
38
|
+
wouldBlock: boolean;
|
|
39
|
+
approvalId?: string;
|
|
40
|
+
user_message?: string;
|
|
41
|
+
agent_message?: string;
|
|
42
|
+
}): GateVerdict;
|
|
43
|
+
export declare function unnormalizedGateVerdict(params: {
|
|
44
|
+
reason: string;
|
|
45
|
+
mode: 'enforce' | 'audit';
|
|
46
|
+
user_message: string;
|
|
47
|
+
agent_message?: string;
|
|
48
|
+
}): GateVerdict;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const GATE_CONTRACT_VERSION = 1;
|
|
2
|
+
export function isGatedAction(value) {
|
|
3
|
+
if (typeof value !== 'object' || value === null) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const record = value;
|
|
7
|
+
return (record.contractVersion === GATE_CONTRACT_VERSION &&
|
|
8
|
+
(record.kind === 'shell' || record.kind === 'subagent' || record.kind === 'tool') &&
|
|
9
|
+
typeof record.repoRoot === 'string' &&
|
|
10
|
+
typeof record.cwd === 'string');
|
|
11
|
+
}
|
|
12
|
+
export function classifyResultToGateVerdict(params) {
|
|
13
|
+
const { result, mode, permission, wouldBlock, approvalId, user_message, agent_message } = params;
|
|
14
|
+
return {
|
|
15
|
+
contractVersion: GATE_CONTRACT_VERSION,
|
|
16
|
+
verdict: result.verdict,
|
|
17
|
+
reason: result.reason,
|
|
18
|
+
fingerprint: result.fingerprint,
|
|
19
|
+
assessment: result.assessment,
|
|
20
|
+
normalizedCommand: result.normalizedCommand,
|
|
21
|
+
summary: result.summary,
|
|
22
|
+
permission,
|
|
23
|
+
wouldBlock,
|
|
24
|
+
mode,
|
|
25
|
+
approvalId,
|
|
26
|
+
user_message,
|
|
27
|
+
agent_message,
|
|
28
|
+
v2: result.v2,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function unnormalizedGateVerdict(params) {
|
|
32
|
+
return {
|
|
33
|
+
contractVersion: GATE_CONTRACT_VERSION,
|
|
34
|
+
verdict: 'deny_pending_approval',
|
|
35
|
+
reason: params.reason,
|
|
36
|
+
fingerprint: 'unnormalized',
|
|
37
|
+
assessment: {
|
|
38
|
+
reversibility: 'irreversible',
|
|
39
|
+
external: true,
|
|
40
|
+
blastRadius: 'unknown',
|
|
41
|
+
confidence: 0,
|
|
42
|
+
signals: ['normalization_failed'],
|
|
43
|
+
},
|
|
44
|
+
permission: 'deny',
|
|
45
|
+
wouldBlock: true,
|
|
46
|
+
mode: params.mode,
|
|
47
|
+
user_message: params.user_message,
|
|
48
|
+
agent_message: params.agent_message,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BelayConfigV3 } from './config.js';
|
|
2
|
+
import type { GatedAction, GatedActionKind } from './gate-contract.js';
|
|
3
|
+
import type { Assessment, ClassifierOptions, ClassifyResult } from './types.js';
|
|
4
|
+
export declare class GateNormalizationError extends Error {
|
|
5
|
+
readonly reason = "normalization_failed";
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function extractAgentAssessment(payload?: Record<string, unknown>): Assessment | undefined;
|
|
9
|
+
export declare function normalizeGatedAction(params: {
|
|
10
|
+
kind: GatedActionKind;
|
|
11
|
+
repoRoot: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
command?: string;
|
|
14
|
+
payload?: Record<string, unknown>;
|
|
15
|
+
toolName?: string;
|
|
16
|
+
agentAssessment?: GatedAction['agentAssessment'];
|
|
17
|
+
}): GatedAction;
|
|
18
|
+
export declare function classifyGatedAction(action: GatedAction, config: BelayConfigV3, extraOptions?: ClassifierOptions): Promise<ClassifyResult>;
|
|
19
|
+
export declare function classifyGatedActionAsync(action: GatedAction, config: BelayConfigV3, extraOptions?: ClassifierOptions): Promise<ClassifyResult>;
|
|
20
|
+
export declare function gateEnabledForAction(config: BelayConfigV3, action: GatedAction): boolean;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { allPathsAllowlisted } from './capability/allowlist.js';
|
|
2
|
+
import { collectOutsideRepoPaths } from './capability/paths.js';
|
|
3
|
+
import { classifySubagent } from './classify-subagent.js';
|
|
4
|
+
import { classifyToolUse } from './classify-tool.js';
|
|
5
|
+
import { classifierOptionsFromConfig } from './config.js';
|
|
6
|
+
import { GATE_CONTRACT_VERSION } from './gate-contract.js';
|
|
7
|
+
import { mergeAgentAssessment } from './judgment.js';
|
|
8
|
+
import { classifyShell } from './v2/adapter.js';
|
|
9
|
+
export class GateNormalizationError extends Error {
|
|
10
|
+
reason = 'normalization_failed';
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'GateNormalizationError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function parseAssessment(value) {
|
|
17
|
+
if (!value || typeof value !== 'object') {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const record = value;
|
|
21
|
+
if ((record.reversibility === 'reversible' ||
|
|
22
|
+
record.reversibility === 'recoverable_with_cost' ||
|
|
23
|
+
record.reversibility === 'irreversible') &&
|
|
24
|
+
typeof record.external === 'boolean' &&
|
|
25
|
+
typeof record.blastRadius === 'string' &&
|
|
26
|
+
typeof record.confidence === 'number' &&
|
|
27
|
+
Array.isArray(record.signals) &&
|
|
28
|
+
record.signals.every((signal) => typeof signal === 'string')) {
|
|
29
|
+
return {
|
|
30
|
+
reversibility: record.reversibility,
|
|
31
|
+
external: record.external,
|
|
32
|
+
blastRadius: record.blastRadius,
|
|
33
|
+
confidence: record.confidence,
|
|
34
|
+
signals: record.signals,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export function extractAgentAssessment(payload) {
|
|
40
|
+
if (!payload) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
for (const key of ['agentAssessment', 'assessment']) {
|
|
44
|
+
const parsed = parseAssessment(payload[key]);
|
|
45
|
+
if (parsed) {
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const toolInput = payload.tool_input;
|
|
50
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
51
|
+
return extractAgentAssessment(toolInput);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function shellCommandFromPayload(payload) {
|
|
56
|
+
const direct = payload.command;
|
|
57
|
+
if (typeof direct === 'string' && direct.trim()) {
|
|
58
|
+
return direct.trim();
|
|
59
|
+
}
|
|
60
|
+
const toolInput = payload.tool_input;
|
|
61
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
62
|
+
const command = toolInput.command;
|
|
63
|
+
if (typeof command === 'string' && command.trim()) {
|
|
64
|
+
return command.trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
export function normalizeGatedAction(params) {
|
|
70
|
+
const { kind, repoRoot, cwd, payload, toolName, agentAssessment } = params;
|
|
71
|
+
let command = params.command?.trim() ?? '';
|
|
72
|
+
if (kind === 'shell' && !command && payload) {
|
|
73
|
+
command = shellCommandFromPayload(payload);
|
|
74
|
+
}
|
|
75
|
+
if (kind === 'shell' && !command) {
|
|
76
|
+
throw new GateNormalizationError('Shell gated action requires a command.');
|
|
77
|
+
}
|
|
78
|
+
if (kind === 'tool' && !payload) {
|
|
79
|
+
throw new GateNormalizationError('Tool gated action requires a payload.');
|
|
80
|
+
}
|
|
81
|
+
if (kind === 'subagent' && !payload) {
|
|
82
|
+
throw new GateNormalizationError('Subagent gated action requires a payload.');
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
contractVersion: GATE_CONTRACT_VERSION,
|
|
86
|
+
kind,
|
|
87
|
+
repoRoot,
|
|
88
|
+
cwd,
|
|
89
|
+
command: command || undefined,
|
|
90
|
+
payload,
|
|
91
|
+
toolName,
|
|
92
|
+
agentAssessment,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function applyShellPeripheralPolicy(command, action, result, options) {
|
|
96
|
+
if (options.brokerFsScope &&
|
|
97
|
+
result.verdict === 'deny_pending_approval' &&
|
|
98
|
+
(result.reason === 'outside_repo_mutation' ||
|
|
99
|
+
result.reason === 'outside_repo_redirect' ||
|
|
100
|
+
result.reason === 'repo_outside_mutation' ||
|
|
101
|
+
result.v2?.location === 'repo_outside')) {
|
|
102
|
+
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
|
|
103
|
+
if (outsideRepoPaths.length > 0 &&
|
|
104
|
+
options.fsScopeAllowlist &&
|
|
105
|
+
allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
|
|
106
|
+
return {
|
|
107
|
+
...result,
|
|
108
|
+
verdict: 'allow_flagged',
|
|
109
|
+
reason: 'capability_fs_hint',
|
|
110
|
+
assessment: {
|
|
111
|
+
...result.assessment,
|
|
112
|
+
signals: [
|
|
113
|
+
...result.assessment.signals,
|
|
114
|
+
'capability_fs_hint',
|
|
115
|
+
'sandbox_boundary_expected',
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
124
|
+
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
125
|
+
if (action.kind === 'shell') {
|
|
126
|
+
const command = action.command ?? shellCommandFromPayload(action.payload ?? {});
|
|
127
|
+
if (!command) {
|
|
128
|
+
throw new GateNormalizationError('Shell gated action requires a command.');
|
|
129
|
+
}
|
|
130
|
+
let result = await classifyShell(command, action.cwd, action.repoRoot, config, options);
|
|
131
|
+
result = applyShellPeripheralPolicy(command, action, result, options);
|
|
132
|
+
if (!action.agentAssessment) {
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
136
|
+
if (!merged.mismatch) {
|
|
137
|
+
return { ...result, assessment: merged.assessment };
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...result,
|
|
141
|
+
verdict: 'deny_pending_approval',
|
|
142
|
+
reason: 'agent_assessment_mismatch',
|
|
143
|
+
assessment: merged.assessment,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (action.kind === 'subagent') {
|
|
147
|
+
return classifySubagent(action.payload ?? {}, action.repoRoot, options);
|
|
148
|
+
}
|
|
149
|
+
return classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
|
|
150
|
+
}
|
|
151
|
+
export async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
|
152
|
+
return classifyGatedAction(action, config, extraOptions);
|
|
153
|
+
}
|
|
154
|
+
export function gateEnabledForAction(config, action) {
|
|
155
|
+
if (action.kind === 'shell') {
|
|
156
|
+
return config.gates.shell;
|
|
157
|
+
}
|
|
158
|
+
if (action.kind === 'subagent') {
|
|
159
|
+
return config.gates.subagent;
|
|
160
|
+
}
|
|
161
|
+
const toolName = action.toolName ?? String(action.payload?.tool_name ?? '');
|
|
162
|
+
if (toolName === 'Shell') {
|
|
163
|
+
return config.gates.toolShell;
|
|
164
|
+
}
|
|
165
|
+
if (toolName === 'Write' || toolName === 'StrReplace' || toolName === 'Delete') {
|
|
166
|
+
return config.gates.fileMutation;
|
|
167
|
+
}
|
|
168
|
+
if (toolName === 'Task') {
|
|
169
|
+
return config.gates.subagent;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function matchesSensitivePath(filePath: string, patterns: string[]): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function matchesSensitivePath(filePath, patterns) {
|
|
2
|
+
const normalized = filePath.replaceAll('\\', '/');
|
|
3
|
+
const segments = normalized.split('/');
|
|
4
|
+
const baseName = segments.at(-1) ?? normalized;
|
|
5
|
+
for (const pattern of patterns) {
|
|
6
|
+
const normalizedPattern = pattern.replaceAll('\\', '/');
|
|
7
|
+
if (normalizedPattern.includes('**')) {
|
|
8
|
+
const parts = normalizedPattern.split('**').map((part) => part.replace(/^\/+|\/+$/g, ''));
|
|
9
|
+
const prefix = parts[0]?.replace(/\/+$/, '') ?? '';
|
|
10
|
+
const suffix = parts[1]?.replace(/^\/+/, '') ?? '';
|
|
11
|
+
if (prefix && !normalized.startsWith(prefix)) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (suffix && !normalized.includes(suffix)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (prefix || suffix) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (normalizedPattern.includes('*')) {
|
|
22
|
+
const regex = new RegExp(`^${normalizedPattern.replaceAll('.', '\\.').replaceAll('*', '.*')}$`);
|
|
23
|
+
if (regex.test(normalized) || regex.test(baseName)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (normalized === normalizedPattern || baseName === normalizedPattern) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (normalized.endsWith(`/${normalizedPattern}`)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (segments.includes(normalizedPattern)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
|
|
2
|
+
export type { AuditMetricsReport } from './audit-metrics.js';
|
|
3
|
+
export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
|
|
4
|
+
export { classifySubagent } from './classify-subagent.js';
|
|
5
|
+
export { classifyToolUse } from './classify-tool.js';
|
|
6
|
+
export { approvedApprovalsFile, type BelayConfig, type BelayConfigV1, type BelayConfigV2, type BelayConfigV3, type BelayConfigV4, type BelayControlPlaneConfig, type BelayJudgeConfig, type BelayOverridesConfig, type BelayPolicyConfig, type BelayRedactionConfig, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
|
|
7
|
+
export { matchesCustomCommand } from './custom-command-match.js';
|
|
8
|
+
export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
|
|
9
|
+
export type { GatedAction, GatedActionKind, GatePermissionResponse, GateVerdict, } from './gate-contract.js';
|
|
10
|
+
export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
|
|
11
|
+
export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
|
|
12
|
+
export { matchesSensitivePath } from './glob.js';
|
|
13
|
+
export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
|
|
14
|
+
export { scrubString, scrubValue } from './scrub.js';
|
|
15
|
+
export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
|
|
16
|
+
export type { TransactionalDiffEvaluation, TransactionalExecutionResult, } from './transactional/index.js';
|
|
17
|
+
export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
|
|
18
|
+
export type { ApprovalRecord, ApprovalStateFile, Assessment, ClassifierOptions, ClassifyResult, HookVerdict, Reversibility, ScrubOptions, UnknownLocalEffectPolicy, } from './types.js';
|
|
19
|
+
export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './v2/index.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
|
|
2
|
+
export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
|
|
3
|
+
export { classifySubagent } from './classify-subagent.js';
|
|
4
|
+
export { classifyToolUse } from './classify-tool.js';
|
|
5
|
+
export { approvedApprovalsFile, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
|
|
6
|
+
export { matchesCustomCommand } from './custom-command-match.js';
|
|
7
|
+
export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
|
|
8
|
+
export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
|
|
9
|
+
export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
|
|
10
|
+
export { matchesSensitivePath } from './glob.js';
|
|
11
|
+
export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
|
|
12
|
+
export { scrubString, scrubValue } from './scrub.js';
|
|
13
|
+
export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
|
|
14
|
+
export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
|
|
15
|
+
export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './v2/index.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScopedPaths } from '../adapters/layouts/scope.js';
|
|
2
|
+
import type { AdapterLayout } from '../adapters/layouts/types.js';
|
|
3
|
+
export interface IntegrityManifest {
|
|
4
|
+
version: 1;
|
|
5
|
+
generatedAt: string;
|
|
6
|
+
files: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export declare function sha256File(filePath: string): Promise<string>;
|
|
9
|
+
export declare function integrityManifestPath(layout: AdapterLayout, repoRoot: string): string;
|
|
10
|
+
export declare function runtimeIntegrityFiles(_layout: AdapterLayout, paths: ScopedPaths): string[];
|
|
11
|
+
export declare function writeIntegrityManifest(repoRoot: string, layout: AdapterLayout, filePaths: string[]): Promise<void>;
|
|
12
|
+
export declare function verifyIntegrityManifest(repoRoot: string, layout: AdapterLayout): Promise<{
|
|
13
|
+
ok: boolean;
|
|
14
|
+
mismatches: string[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } 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
|
+
export async function sha256File(filePath) {
|
|
6
|
+
const content = await readFile(filePath);
|
|
7
|
+
return createHash('sha256').update(content).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
export function integrityManifestPath(layout, repoRoot) {
|
|
10
|
+
return path.join(layout.repoLocalStateDir(repoRoot), 'integrity-manifest.json');
|
|
11
|
+
}
|
|
12
|
+
export function runtimeIntegrityFiles(_layout, paths) {
|
|
13
|
+
const files = [paths.configPath];
|
|
14
|
+
if (paths.scope !== 'project') {
|
|
15
|
+
return files;
|
|
16
|
+
}
|
|
17
|
+
const hooksDir = paths.hooksDir;
|
|
18
|
+
const runtimeDir = paths.runtimeDir;
|
|
19
|
+
return [
|
|
20
|
+
...files,
|
|
21
|
+
paths.hooksSettingsPath,
|
|
22
|
+
path.join(hooksDir, 'belay-before-submit.mjs'),
|
|
23
|
+
path.join(hooksDir, 'belay-shell-gate.mjs'),
|
|
24
|
+
path.join(hooksDir, 'belay-tool-gate.mjs'),
|
|
25
|
+
path.join(hooksDir, 'belay-audit.mjs'),
|
|
26
|
+
path.join(hooksDir, 'belay-runner'),
|
|
27
|
+
path.join(hooksDir, 'belay-runner.cmd'),
|
|
28
|
+
path.join(runtimeDir, 'core.mjs'),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export async function writeIntegrityManifest(repoRoot, layout, filePaths) {
|
|
32
|
+
const files = {};
|
|
33
|
+
for (const filePath of filePaths) {
|
|
34
|
+
if (!existsSync(filePath)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const relativePath = path.relative(repoRoot, filePath);
|
|
38
|
+
files[relativePath] = await sha256File(filePath);
|
|
39
|
+
}
|
|
40
|
+
const manifest = {
|
|
41
|
+
version: 1,
|
|
42
|
+
generatedAt: new Date().toISOString(),
|
|
43
|
+
files,
|
|
44
|
+
};
|
|
45
|
+
const manifestPath = integrityManifestPath(layout, repoRoot);
|
|
46
|
+
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
47
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
48
|
+
}
|
|
49
|
+
export async function verifyIntegrityManifest(repoRoot, layout) {
|
|
50
|
+
const manifestPath = integrityManifestPath(layout, repoRoot);
|
|
51
|
+
if (!existsSync(manifestPath)) {
|
|
52
|
+
return { ok: false, mismatches: ['missing integrity-manifest.json'] };
|
|
53
|
+
}
|
|
54
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
55
|
+
const mismatches = [];
|
|
56
|
+
for (const [relativePath, expectedHash] of Object.entries(manifest.files ?? {})) {
|
|
57
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
58
|
+
if (!existsSync(absolutePath)) {
|
|
59
|
+
mismatches.push(`missing ${relativePath}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const actualHash = await sha256File(absolutePath);
|
|
63
|
+
if (actualHash !== expectedHash) {
|
|
64
|
+
mismatches.push(`hash mismatch ${relativePath}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { ok: mismatches.length === 0, mismatches };
|
|
68
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function resolveJudgeApiKey(env = process.env) {
|
|
2
|
+
const belay = env.BELAY_JUDGE_API_KEY?.trim();
|
|
3
|
+
if (belay) {
|
|
4
|
+
return { key: belay, source: 'BELAY_JUDGE_API_KEY' };
|
|
5
|
+
}
|
|
6
|
+
const openai = env.OPENAI_API_KEY?.trim();
|
|
7
|
+
if (openai) {
|
|
8
|
+
return { key: openai, source: 'OPENAI_API_KEY' };
|
|
9
|
+
}
|
|
10
|
+
return { key: null, source: null };
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BelayJudgeConfig } from './config.js';
|
|
2
|
+
export type JudgeProfileName = 'local-ollama';
|
|
3
|
+
export declare const JUDGE_PROFILE_LOCAL_OLLAMA: BelayJudgeConfig;
|
|
4
|
+
export declare const JUDGE_PROFILES: Record<JudgeProfileName, BelayJudgeConfig>;
|
|
5
|
+
export interface ResolveJudgeConfigInput {
|
|
6
|
+
judgeProfile?: JudgeProfileName;
|
|
7
|
+
judgeProvider?: 'ollama' | 'openai-compatible' | 'cursor';
|
|
8
|
+
judgeModel?: string;
|
|
9
|
+
judgeEndpoint?: string;
|
|
10
|
+
existingJudge?: BelayJudgeConfig;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveJudgeConfig(input?: ResolveJudgeConfigInput): BelayJudgeConfig;
|
|
13
|
+
export declare class CloudJudgeConsentRequiredError extends Error {
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
export declare class JudgeEndpointRequiredError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
export declare function assertJudgeEndpoint(judge: BelayJudgeConfig): void;
|
|
20
|
+
export declare function resolveInitJudgeConfig(input: {
|
|
21
|
+
isFresh: boolean;
|
|
22
|
+
hasExplicitJudgeFlags: boolean;
|
|
23
|
+
judgeProfile?: JudgeProfileName;
|
|
24
|
+
judgeProvider?: 'ollama' | 'openai-compatible' | 'cursor';
|
|
25
|
+
judgeModel?: string;
|
|
26
|
+
judgeEndpoint?: string;
|
|
27
|
+
acceptCloudJudge?: boolean;
|
|
28
|
+
existingJudge?: BelayJudgeConfig;
|
|
29
|
+
}): BelayJudgeConfig;
|