@guilz-dev/belay 0.3.0 → 0.6.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 +7 -1
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +12 -16
- package/dist/adapters/shared/gate-runtime.js +499 -43
- package/dist/bundle/claude-runtime.mjs +8004 -2676
- package/dist/bundle/codex-runtime.mjs +8021 -2683
- package/dist/bundle/cursor-runtime.mjs +7993 -2667
- package/dist/cli.js +180 -6
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +264 -58
- package/dist/commands/doctor.js +47 -2
- package/dist/commands/explain.js +17 -2
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +24 -4
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +59 -17
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +14 -9
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +53 -4
- package/dist/core/approval.d.ts +26 -3
- package/dist/core/approval.js +61 -7
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/approval-state-mutation.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.js +92 -0
- package/dist/core/capability/approval-v3.d.ts +15 -0
- package/dist/core/capability/approval-v3.js +74 -0
- package/dist/core/capability/attestation.d.ts +13 -0
- package/dist/core/capability/attestation.js +55 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +7 -0
- package/dist/core/capability/boundary-driver-container.js +122 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +43 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
- package/dist/core/capability/boundary-grant-materialize.js +78 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +23 -0
- package/dist/core/capability/boundary-run.js +23 -0
- package/dist/core/capability/boundary-session.d.ts +57 -0
- package/dist/core/capability/boundary-session.js +129 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +108 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-lease.d.ts +9 -0
- package/dist/core/capability/grant-lease.js +46 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +93 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +10 -1
- package/dist/core/capability/index.js +8 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +70 -0
- package/dist/core/capability/policy-engine.js +476 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +45 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +40 -22
- package/dist/core/classify-tool.js +157 -64
- package/dist/core/config.d.ts +32 -3
- package/dist/core/config.js +105 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +12 -7
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +90 -11
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +60 -7
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +3 -0
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/types.d.ts +20 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +178 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/git-worktree.d.ts +10 -3
- package/dist/core/transactional/git-worktree.js +81 -11
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +4 -0
- package/dist/core/transactional/runner.js +22 -6
- package/dist/core/transactional/types.d.ts +4 -0
- package/dist/core/types.d.ts +36 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +19 -6
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +33 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +49 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +291 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +139 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
- package/dist/core/verdict/judge-runtime-config.js +96 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +200 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +44 -0
- package/dist/core/verdict/judge-transport.js +268 -0
- package/dist/core/verdict/judge.d.ts +14 -14
- package/dist/core/verdict/judge.js +5 -67
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +16 -0
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +25 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +26 -1
- package/dist/core/verdict/verdict.js +300 -179
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +61 -8
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +162 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +43 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +99 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +137 -0
- package/dist/services/sandbox-service.d.ts +5 -0
- package/dist/services/sandbox-service.js +30 -2
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from './config.js';
|
|
2
|
+
import type { GatedActionKind } from './gate-contract.js';
|
|
3
|
+
import type { ClassifyResult } from './types.js';
|
|
4
|
+
export type StandingAllowSource = 'provably-benign-corpus' | 'must-allow-catalog' | 'operator' | 'availability-reconfirmed';
|
|
5
|
+
export interface StandingAllowEntry {
|
|
6
|
+
kind: GatedActionKind;
|
|
7
|
+
fingerprint: string;
|
|
8
|
+
source: StandingAllowSource;
|
|
9
|
+
reason: string;
|
|
10
|
+
summary?: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
repoRoot?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface StandingAllowFile {
|
|
16
|
+
version: 1;
|
|
17
|
+
entries: StandingAllowEntry[];
|
|
18
|
+
}
|
|
19
|
+
export interface StandingAllowMatch {
|
|
20
|
+
source: StandingAllowSource;
|
|
21
|
+
catalogCommand?: string;
|
|
22
|
+
entryId?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
25
|
+
export declare const DEFAULT_STANDING_ALLOW_TTL_MS: number;
|
|
26
|
+
export declare function standingAllowFile(config: BelayConfigV4, repoLocalStateDir: string): string;
|
|
27
|
+
export declare function isTier0StandingAllowBlocked(result: ClassifyResult): boolean;
|
|
28
|
+
export declare function compactStandingAllow(state: StandingAllowFile, now?: number): StandingAllowFile;
|
|
29
|
+
export declare function loadStandingAllow(filePath: string): Promise<StandingAllowFile>;
|
|
30
|
+
export declare function saveStandingAllow(filePath: string, state: StandingAllowFile): Promise<void>;
|
|
31
|
+
export declare function resolveStandingAllowMatch(params: {
|
|
32
|
+
kind: GatedActionKind;
|
|
33
|
+
result: ClassifyResult;
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
state: StandingAllowFile;
|
|
36
|
+
now?: number;
|
|
37
|
+
}): StandingAllowMatch | null;
|
|
38
|
+
export declare function addStandingAllowEntry(state: StandingAllowFile, entry: Omit<StandingAllowEntry, 'createdAt' | 'expiresAt'> & {
|
|
39
|
+
createdAt?: string;
|
|
40
|
+
expiresAt?: string;
|
|
41
|
+
ttlMs?: number;
|
|
42
|
+
}): StandingAllowFile;
|
|
43
|
+
export declare function revokeStandingAllowEntry(state: StandingAllowFile, params: {
|
|
44
|
+
kind: GatedActionKind;
|
|
45
|
+
fingerprint: string;
|
|
46
|
+
repoRoot?: string;
|
|
47
|
+
}): {
|
|
48
|
+
state: StandingAllowFile;
|
|
49
|
+
removed: boolean;
|
|
50
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { STANDING_ALLOW_CATALOG } from '../corpus/standing-allow-catalog.generated.js';
|
|
5
|
+
import { belayStateDir } from './config.js';
|
|
6
|
+
const EMPTY_STANDING_ALLOW = {
|
|
7
|
+
version: 1,
|
|
8
|
+
entries: [],
|
|
9
|
+
};
|
|
10
|
+
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
11
|
+
export const DEFAULT_STANDING_ALLOW_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
12
|
+
const PROVABLY_BENIGN_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.provablyBenign.map((entry) => entry.normalizedCommand));
|
|
13
|
+
const MUST_ALLOW_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.mustAllow.map((entry) => entry.normalizedCommand));
|
|
14
|
+
/** Reasons that must never be silenced via standing-allow (defense in depth with signal checks). */
|
|
15
|
+
const STANDING_ALLOW_BLOCKED_REASONS = new Set([
|
|
16
|
+
'external_effect',
|
|
17
|
+
'tier1_catastrophic',
|
|
18
|
+
'protected_artifact',
|
|
19
|
+
'pipe_to_shell',
|
|
20
|
+
'command_substitution',
|
|
21
|
+
'outside_repo_mutation',
|
|
22
|
+
'outside_repo_redirect',
|
|
23
|
+
'control_plane_mutation',
|
|
24
|
+
]);
|
|
25
|
+
export function standingAllowFile(config, repoLocalStateDir) {
|
|
26
|
+
return `${belayStateDir(config, repoLocalStateDir)}/standing-allow.json`;
|
|
27
|
+
}
|
|
28
|
+
export function isTier0StandingAllowBlocked(result) {
|
|
29
|
+
if (result.reason.startsWith('tier0_') || STANDING_ALLOW_BLOCKED_REASONS.has(result.reason)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const signals = result.assessment?.signals ?? [];
|
|
33
|
+
return signals.some((signal) => signal === 'tier0_external' || signal === 'tier1_catastrophic');
|
|
34
|
+
}
|
|
35
|
+
function isExpired(iso, now = Date.now()) {
|
|
36
|
+
const parsed = Date.parse(iso);
|
|
37
|
+
return Number.isNaN(parsed) || parsed <= now;
|
|
38
|
+
}
|
|
39
|
+
export function compactStandingAllow(state, now = Date.now()) {
|
|
40
|
+
return {
|
|
41
|
+
version: 1,
|
|
42
|
+
entries: state.entries.filter((entry) => !isExpired(entry.expiresAt, now)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function sanitizeStandingAllowEntries(input) {
|
|
46
|
+
if (!Array.isArray(input)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return input.flatMap((entry) => {
|
|
50
|
+
if (!entry || typeof entry !== 'object') {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const record = entry;
|
|
54
|
+
if ((record.kind !== 'shell' && record.kind !== 'tool' && record.kind !== 'subagent') ||
|
|
55
|
+
typeof record.fingerprint !== 'string' ||
|
|
56
|
+
!record.fingerprint.trim() ||
|
|
57
|
+
typeof record.expiresAt !== 'string' ||
|
|
58
|
+
typeof record.createdAt !== 'string' ||
|
|
59
|
+
typeof record.reason !== 'string') {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const source = record.source;
|
|
63
|
+
if (source !== 'operator' && source !== 'availability-reconfirmed') {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
kind: record.kind,
|
|
69
|
+
fingerprint: record.fingerprint,
|
|
70
|
+
source,
|
|
71
|
+
reason: record.reason,
|
|
72
|
+
createdAt: record.createdAt,
|
|
73
|
+
expiresAt: record.expiresAt,
|
|
74
|
+
...(typeof record.summary === 'string' ? { summary: record.summary } : {}),
|
|
75
|
+
...(typeof record.repoRoot === 'string' ? { repoRoot: record.repoRoot } : {}),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export async function loadStandingAllow(filePath) {
|
|
81
|
+
if (!existsSync(filePath)) {
|
|
82
|
+
return { ...EMPTY_STANDING_ALLOW };
|
|
83
|
+
}
|
|
84
|
+
const raw = JSON.parse(await readFile(filePath, 'utf8'));
|
|
85
|
+
const rawCount = Array.isArray(raw.entries) ? raw.entries.length : 0;
|
|
86
|
+
const entries = sanitizeStandingAllowEntries(raw.entries);
|
|
87
|
+
const compacted = compactStandingAllow({ version: 1, entries });
|
|
88
|
+
if (rawCount !== compacted.entries.length || entries.length !== compacted.entries.length) {
|
|
89
|
+
await saveStandingAllow(filePath, compacted);
|
|
90
|
+
}
|
|
91
|
+
return compacted;
|
|
92
|
+
}
|
|
93
|
+
export async function saveStandingAllow(filePath, state) {
|
|
94
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
95
|
+
const compacted = compactStandingAllow(state);
|
|
96
|
+
await writeFile(filePath, `${JSON.stringify(compacted, null, 2)}\n`, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
function matchBundledCatalog(kind, normalizedCommand) {
|
|
99
|
+
if (kind !== 'shell' || !normalizedCommand) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (PROVABLY_BENIGN_COMMANDS.has(normalizedCommand)) {
|
|
103
|
+
return { source: 'provably-benign-corpus', catalogCommand: normalizedCommand };
|
|
104
|
+
}
|
|
105
|
+
if (MUST_ALLOW_COMMANDS.has(normalizedCommand)) {
|
|
106
|
+
return { source: 'must-allow-catalog', catalogCommand: normalizedCommand };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function matchStateEntry(params) {
|
|
111
|
+
const now = params.now ?? Date.now();
|
|
112
|
+
const match = params.state.entries.find((entry) => entry.kind === params.kind &&
|
|
113
|
+
entry.fingerprint === params.fingerprint &&
|
|
114
|
+
!isExpired(entry.expiresAt, now) &&
|
|
115
|
+
(entry.repoRoot === undefined || entry.repoRoot === params.repoRoot));
|
|
116
|
+
if (!match) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return { source: match.source, entryId: match.fingerprint };
|
|
120
|
+
}
|
|
121
|
+
export function resolveStandingAllowMatch(params) {
|
|
122
|
+
if (params.result.verdict !== 'deny_pending_approval') {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (isTier0StandingAllowBlocked(params.result)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const normalizedCommand = params.result.normalizedCommand ?? params.result.summary ?? '';
|
|
129
|
+
const catalogMatch = matchBundledCatalog(params.kind, normalizedCommand);
|
|
130
|
+
if (catalogMatch) {
|
|
131
|
+
return catalogMatch;
|
|
132
|
+
}
|
|
133
|
+
return matchStateEntry({
|
|
134
|
+
kind: params.kind,
|
|
135
|
+
fingerprint: params.result.fingerprint,
|
|
136
|
+
repoRoot: params.repoRoot,
|
|
137
|
+
state: params.state,
|
|
138
|
+
now: params.now,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export function addStandingAllowEntry(state, entry) {
|
|
142
|
+
const createdAt = entry.createdAt ?? new Date().toISOString();
|
|
143
|
+
const ttlMs = entry.ttlMs ?? DEFAULT_STANDING_ALLOW_TTL_MS;
|
|
144
|
+
const expiresAt = entry.expiresAt ?? new Date(Date.parse(createdAt) + ttlMs).toISOString();
|
|
145
|
+
const next = {
|
|
146
|
+
kind: entry.kind,
|
|
147
|
+
fingerprint: entry.fingerprint,
|
|
148
|
+
source: entry.source,
|
|
149
|
+
reason: entry.reason,
|
|
150
|
+
createdAt,
|
|
151
|
+
expiresAt,
|
|
152
|
+
...(entry.summary ? { summary: entry.summary } : {}),
|
|
153
|
+
...(entry.repoRoot ? { repoRoot: entry.repoRoot } : {}),
|
|
154
|
+
};
|
|
155
|
+
const filtered = state.entries.filter((existing) => !(existing.kind === next.kind &&
|
|
156
|
+
existing.fingerprint === next.fingerprint &&
|
|
157
|
+
existing.repoRoot === next.repoRoot));
|
|
158
|
+
return compactStandingAllow({
|
|
159
|
+
version: 1,
|
|
160
|
+
entries: [...filtered, next],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export function revokeStandingAllowEntry(state, params) {
|
|
164
|
+
const before = state.entries.length;
|
|
165
|
+
const entries = state.entries.filter((entry) => {
|
|
166
|
+
if (entry.kind !== params.kind || entry.fingerprint !== params.fingerprint) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (params.repoRoot && entry.repoRoot && entry.repoRoot !== params.repoRoot) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
state: { version: 1, entries },
|
|
176
|
+
removed: entries.length < before,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -51,7 +51,10 @@ export function evaluateTransactionalDiff(changes, ctx) {
|
|
|
51
51
|
categories.add('large_deletion');
|
|
52
52
|
}
|
|
53
53
|
const categoryList = [...categories];
|
|
54
|
-
const dangerous = categoryList.includes('large_deletion')
|
|
54
|
+
const dangerous = categoryList.includes('large_deletion') ||
|
|
55
|
+
categoryList.includes('repo_outside') ||
|
|
56
|
+
categoryList.includes('sensitive_path') ||
|
|
57
|
+
categoryList.includes('control_plane');
|
|
55
58
|
const base = {
|
|
56
59
|
categories: categoryList,
|
|
57
60
|
changes,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { capabilityRequestsBlockRecovery } from '../recovery/capability.js';
|
|
1
2
|
const EXCLUDED_REASONS = new Set([
|
|
2
3
|
'unparseable_shell',
|
|
3
4
|
'external_effect',
|
|
@@ -26,6 +27,9 @@ export function isTransactionalEligible(config, kind, result) {
|
|
|
26
27
|
if (EXCLUDED_REASONS.has(result.reason)) {
|
|
27
28
|
return false;
|
|
28
29
|
}
|
|
30
|
+
if (capabilityRequestsBlockRecovery(result.capabilityRequests)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
29
33
|
const { assessment } = result;
|
|
30
34
|
if (assessment.external) {
|
|
31
35
|
return false;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { TransactionalFileChange, TransactionalSnapshot } from './types.js';
|
|
2
|
+
export declare const TRANSACTIONAL_APPLY_TOCTOU = "transactional_apply_toctou";
|
|
3
|
+
export declare function hashRepoFile(filePath: string): Promise<string>;
|
|
4
|
+
export declare function captureRepoFileHashes(repoRoot: string, changes: TransactionalFileChange[]): Promise<Map<string, string | null>>;
|
|
2
5
|
export declare function isGitWorktreeAvailable(repoRoot: string): Promise<boolean>;
|
|
3
|
-
export declare function isDirtyWorktree(repoRoot: string
|
|
4
|
-
|
|
6
|
+
export declare function isDirtyWorktree(repoRoot: string, options?: {
|
|
7
|
+
ignoreRoots?: string[];
|
|
8
|
+
}): Promise<boolean>;
|
|
9
|
+
export declare function createGitWorktreeSnapshot(repoRoot: string, _stateDir: string): Promise<TransactionalSnapshot>;
|
|
5
10
|
export declare function resolveWorktreeCwd(repoRoot: string, worktreePath: string, cwd: string): string;
|
|
6
11
|
export interface ShellRunResult {
|
|
7
12
|
exitCode: number | null;
|
|
@@ -10,4 +15,6 @@ export interface ShellRunResult {
|
|
|
10
15
|
}
|
|
11
16
|
export declare function runShellCommand(command: string, cwd: string, timeoutMs: number): Promise<ShellRunResult>;
|
|
12
17
|
export declare function collectWorktreeChanges(worktreePath: string): Promise<TransactionalFileChange[]>;
|
|
13
|
-
export declare function applyWorktreeChanges(worktreePath: string, repoRoot: string, changes: TransactionalFileChange[]
|
|
18
|
+
export declare function applyWorktreeChanges(worktreePath: string, repoRoot: string, changes: TransactionalFileChange[], options?: {
|
|
19
|
+
baseHashes?: Map<string, string | null>;
|
|
20
|
+
}): Promise<void>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { copyFile, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { canonicalPath } from '../path-utils.js';
|
|
7
|
+
import { canonicalPath, isPathOutsideRoot, pathWithinRoot } from '../path-utils.js';
|
|
8
|
+
export const TRANSACTIONAL_APPLY_TOCTOU = 'transactional_apply_toctou';
|
|
8
9
|
function execGit(repoRoot, args) {
|
|
9
10
|
return new Promise((resolve, reject) => {
|
|
10
11
|
const child = spawn('git', ['-C', repoRoot, ...args], {
|
|
@@ -24,6 +25,38 @@ function execGit(repoRoot, args) {
|
|
|
24
25
|
});
|
|
25
26
|
});
|
|
26
27
|
}
|
|
28
|
+
function porcelainRelativePath(line) {
|
|
29
|
+
if (line.length < 4) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const relativePath = line.slice(3).trim();
|
|
33
|
+
return relativePath || null;
|
|
34
|
+
}
|
|
35
|
+
function isIgnoredDirtyPath(repoRoot, relativePath, ignoreRoots) {
|
|
36
|
+
if (ignoreRoots.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const absolutePath = canonicalPath(path.join(repoRoot, relativePath));
|
|
40
|
+
return ignoreRoots.some((root) => pathWithinRoot(root, absolutePath) ||
|
|
41
|
+
root === absolutePath ||
|
|
42
|
+
pathWithinRoot(absolutePath, root));
|
|
43
|
+
}
|
|
44
|
+
export async function hashRepoFile(filePath) {
|
|
45
|
+
const content = await readFile(filePath);
|
|
46
|
+
return createHash('sha256').update(content).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
export async function captureRepoFileHashes(repoRoot, changes) {
|
|
49
|
+
const hashes = new Map();
|
|
50
|
+
for (const change of changes) {
|
|
51
|
+
const target = path.join(repoRoot, change.relativePath);
|
|
52
|
+
if (!existsSync(target)) {
|
|
53
|
+
hashes.set(change.relativePath, null);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
hashes.set(change.relativePath, await hashRepoFile(target));
|
|
57
|
+
}
|
|
58
|
+
return hashes;
|
|
59
|
+
}
|
|
27
60
|
export async function isGitWorktreeAvailable(repoRoot) {
|
|
28
61
|
try {
|
|
29
62
|
await execGit(repoRoot, ['rev-parse', '--git-dir']);
|
|
@@ -33,18 +66,32 @@ export async function isGitWorktreeAvailable(repoRoot) {
|
|
|
33
66
|
return false;
|
|
34
67
|
}
|
|
35
68
|
}
|
|
36
|
-
export async function isDirtyWorktree(repoRoot) {
|
|
69
|
+
export async function isDirtyWorktree(repoRoot, options) {
|
|
37
70
|
try {
|
|
38
|
-
const status = await execGit(repoRoot, ['status', '--porcelain'
|
|
39
|
-
|
|
71
|
+
const status = await execGit(repoRoot, ['status', '--porcelain']);
|
|
72
|
+
const ignoreRoots = (options?.ignoreRoots ?? []).map((root) => canonicalPath(root));
|
|
73
|
+
const repoRootCanonical = canonicalPath(repoRoot);
|
|
74
|
+
for (const line of status.split('\n')) {
|
|
75
|
+
if (!line.trim()) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const relativePath = porcelainRelativePath(line);
|
|
79
|
+
if (!relativePath) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (isIgnoredDirtyPath(repoRootCanonical, relativePath, ignoreRoots)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
40
88
|
}
|
|
41
89
|
catch {
|
|
42
90
|
return true;
|
|
43
91
|
}
|
|
44
92
|
}
|
|
45
|
-
export async function createGitWorktreeSnapshot(repoRoot,
|
|
46
|
-
const worktreePath = path.join(
|
|
47
|
-
await mkdir(stateDir, { recursive: true });
|
|
93
|
+
export async function createGitWorktreeSnapshot(repoRoot, _stateDir) {
|
|
94
|
+
const worktreePath = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-'));
|
|
48
95
|
await execGit(repoRoot, ['worktree', 'add', '--detach', worktreePath, 'HEAD']);
|
|
49
96
|
return {
|
|
50
97
|
worktreePath,
|
|
@@ -67,7 +114,7 @@ export async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
|
67
114
|
export function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
|
|
68
115
|
const resolvedCwd = canonicalPath(cwd);
|
|
69
116
|
const relative = path.relative(canonicalPath(repoRoot), resolvedCwd);
|
|
70
|
-
if (relative
|
|
117
|
+
if (isPathOutsideRoot(relative) || path.isAbsolute(relative)) {
|
|
71
118
|
return worktreePath;
|
|
72
119
|
}
|
|
73
120
|
if (relative === '') {
|
|
@@ -155,7 +202,30 @@ async function rollbackAppliedChanges(actions) {
|
|
|
155
202
|
}
|
|
156
203
|
}
|
|
157
204
|
}
|
|
158
|
-
|
|
205
|
+
async function assertRepoFilesUnchanged(repoRoot, changes, baseHashes) {
|
|
206
|
+
for (const change of changes) {
|
|
207
|
+
const target = path.join(repoRoot, change.relativePath);
|
|
208
|
+
const expected = baseHashes.get(change.relativePath);
|
|
209
|
+
const exists = existsSync(target);
|
|
210
|
+
if (expected === null) {
|
|
211
|
+
if (exists) {
|
|
212
|
+
throw new Error(TRANSACTIONAL_APPLY_TOCTOU);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!exists) {
|
|
217
|
+
throw new Error(TRANSACTIONAL_APPLY_TOCTOU);
|
|
218
|
+
}
|
|
219
|
+
const current = await hashRepoFile(target);
|
|
220
|
+
if (current !== expected) {
|
|
221
|
+
throw new Error(TRANSACTIONAL_APPLY_TOCTOU);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
export async function applyWorktreeChanges(worktreePath, repoRoot, changes, options) {
|
|
226
|
+
if (options?.baseHashes) {
|
|
227
|
+
await assertRepoFilesUnchanged(repoRoot, changes, options.baseHashes);
|
|
228
|
+
}
|
|
159
229
|
const backupRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-rollback-'));
|
|
160
230
|
const rollbackActions = [];
|
|
161
231
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const TRANSACTIONAL_ALREADY_APPLIED = "transactional_already_applied";
|
|
2
2
|
export declare const TRANSACTIONAL_OBSERVED_RISK = "transactional_observed_risk";
|
|
3
3
|
export declare const TRANSACTIONAL_APPLY_FAILED = "transactional_apply_failed";
|
|
4
|
+
export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, } from '../recovery/fail-closed.js';
|
|
4
5
|
export declare const TRANSACTIONAL_APPROVAL_BYPASS_REASONS: Set<string>;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export const TRANSACTIONAL_ALREADY_APPLIED = 'transactional_already_applied';
|
|
2
2
|
export const TRANSACTIONAL_OBSERVED_RISK = 'transactional_observed_risk';
|
|
3
3
|
export const TRANSACTIONAL_APPLY_FAILED = 'transactional_apply_failed';
|
|
4
|
+
export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, } from '../recovery/fail-closed.js';
|
|
4
5
|
export const TRANSACTIONAL_APPROVAL_BYPASS_REASONS = new Set([
|
|
5
6
|
TRANSACTIONAL_OBSERVED_RISK,
|
|
6
7
|
TRANSACTIONAL_ALREADY_APPLIED,
|
|
7
8
|
TRANSACTIONAL_APPLY_FAILED,
|
|
9
|
+
'recovery_substrate_unavailable',
|
|
10
|
+
'recovery_execution_failed',
|
|
11
|
+
'recovery_dirty_worktree',
|
|
8
12
|
]);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { boundaryMountReadOnlyFromPrediction, runWithBoundaryRunnable, } from '../capability/boundary-run.js';
|
|
2
|
+
import { hostIntegrationBoundaryContext } from '../capability/boundary-session.js';
|
|
1
3
|
import { evaluateTransactionalDiff } from './diff-evaluator.js';
|
|
2
|
-
import { applyWorktreeChanges, collectWorktreeChanges, createGitWorktreeSnapshot, isDirtyWorktree, isGitWorktreeAvailable, resolveWorktreeCwd,
|
|
4
|
+
import { applyWorktreeChanges, captureRepoFileHashes, collectWorktreeChanges, createGitWorktreeSnapshot, isDirtyWorktree, isGitWorktreeAvailable, resolveWorktreeCwd, TRANSACTIONAL_APPLY_TOCTOU, } from './git-worktree.js';
|
|
3
5
|
import { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPLY_FAILED, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
|
|
4
6
|
export async function runTransactionalExecution(params) {
|
|
5
7
|
const { predicted, repoRoot, stateDir, command, cwd, timeoutMs, diffContext } = params;
|
|
@@ -12,7 +14,7 @@ export async function runTransactionalExecution(params) {
|
|
|
12
14
|
result: predicted,
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
|
-
if (await isDirtyWorktree(repoRoot)) {
|
|
17
|
+
if (await isDirtyWorktree(repoRoot, { ignoreRoots: params.dirtyIgnoreRoots })) {
|
|
16
18
|
return {
|
|
17
19
|
ok: false,
|
|
18
20
|
skipped: true,
|
|
@@ -25,7 +27,16 @@ export async function runTransactionalExecution(params) {
|
|
|
25
27
|
try {
|
|
26
28
|
snapshot = await createGitWorktreeSnapshot(repoRoot, stateDir);
|
|
27
29
|
const execCwd = resolveWorktreeCwd(repoRoot, snapshot.worktreePath, cwd);
|
|
28
|
-
const
|
|
30
|
+
const boundaryContext = params.boundaryContext ?? hostIntegrationBoundaryContext(params.repoRoot);
|
|
31
|
+
const shellResult = await runWithBoundaryRunnable(boundaryContext.driver, {
|
|
32
|
+
prepareContext: boundaryContext.prepareContext,
|
|
33
|
+
command,
|
|
34
|
+
cwd: execCwd,
|
|
35
|
+
timeoutMs,
|
|
36
|
+
runOptions: {
|
|
37
|
+
mountReadOnly: boundaryMountReadOnlyFromPrediction(predicted),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
29
40
|
if (shellResult.timedOut) {
|
|
30
41
|
return {
|
|
31
42
|
ok: false,
|
|
@@ -52,10 +63,12 @@ export async function runTransactionalExecution(params) {
|
|
|
52
63
|
const changes = await collectWorktreeChanges(snapshot.worktreePath);
|
|
53
64
|
const observed = evaluateTransactionalDiff(changes, diffContext);
|
|
54
65
|
if (observed.verdict === 'allow') {
|
|
66
|
+
const baseHashes = await captureRepoFileHashes(repoRoot, changes);
|
|
55
67
|
try {
|
|
56
|
-
await applyWorktreeChanges(snapshot.worktreePath, repoRoot, changes);
|
|
68
|
+
await applyWorktreeChanges(snapshot.worktreePath, repoRoot, changes, { baseHashes });
|
|
57
69
|
}
|
|
58
|
-
catch {
|
|
70
|
+
catch (error) {
|
|
71
|
+
const toctou = error instanceof Error && error.message === TRANSACTIONAL_APPLY_TOCTOU;
|
|
59
72
|
const result = {
|
|
60
73
|
...predicted,
|
|
61
74
|
verdict: 'deny_pending_approval',
|
|
@@ -64,7 +77,10 @@ export async function runTransactionalExecution(params) {
|
|
|
64
77
|
...observed.assessment,
|
|
65
78
|
reversibility: 'irreversible',
|
|
66
79
|
confidence: 1,
|
|
67
|
-
signals: [
|
|
80
|
+
signals: [
|
|
81
|
+
...observed.assessment.signals,
|
|
82
|
+
toctou ? 'transactional_apply_toctou' : 'transactional_apply_failed',
|
|
83
|
+
],
|
|
68
84
|
},
|
|
69
85
|
};
|
|
70
86
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResolvedBoundaryDriverContext } from '../capability/boundary-session.js';
|
|
1
2
|
import type { Assessment, ClassifyResult, HookVerdict } from '../types.js';
|
|
2
3
|
export type TransactionalFileChangeKind = 'added' | 'modified' | 'deleted';
|
|
3
4
|
export interface TransactionalFileChange {
|
|
@@ -43,4 +44,7 @@ export interface TransactionalRunnerParams {
|
|
|
43
44
|
timeoutMs: number;
|
|
44
45
|
predicted: ClassifyResult;
|
|
45
46
|
diffContext: TransactionalDiffContext;
|
|
47
|
+
boundaryContext?: ResolvedBoundaryDriverContext;
|
|
48
|
+
/** Repo-local Belay paths excluded from dirty-worktree gating (init artifacts). */
|
|
49
|
+
dirtyIgnoreRoots?: string[];
|
|
46
50
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { BoundaryAttestation } from './capability/attestation.js';
|
|
2
|
+
import type { CapabilityGrantV1 } from './capability/grant.js';
|
|
3
|
+
import type { PolicyDecision } from './capability/policy-types.js';
|
|
4
|
+
import type { CapabilityRequestV1 } from './capability/request.js';
|
|
1
5
|
import type { FsScopeAllowlistFile } from './capability/types.js';
|
|
2
6
|
export type BelayMode = 'enforce' | 'audit';
|
|
3
7
|
export type HookVerdict = 'allow' | 'allow_flagged' | 'deny_pending_approval';
|
|
@@ -34,6 +38,9 @@ export interface ClassifyResult {
|
|
|
34
38
|
normalizedCommand?: string;
|
|
35
39
|
summary?: string;
|
|
36
40
|
axes?: VerdictAxes;
|
|
41
|
+
capabilityRequests?: CapabilityRequestV1[];
|
|
42
|
+
authorizationDecision?: PolicyDecision;
|
|
43
|
+
boundaryProfile?: string;
|
|
37
44
|
}
|
|
38
45
|
export type UnknownLocalEffectPolicy = 'allow_flagged' | 'deny';
|
|
39
46
|
export type UnparseableShellPolicy = 'allow_flagged' | 'deny';
|
|
@@ -67,10 +74,18 @@ export interface ClassifierOptions {
|
|
|
67
74
|
/** When true with sandbox enabled, outside-repo rules defer to fs-scope allowlist. */
|
|
68
75
|
brokerFsScope?: boolean;
|
|
69
76
|
fsScopeAllowlist?: FsScopeAllowlistFile;
|
|
70
|
-
|
|
71
|
-
tier1Judge?: import('./verdict/types.js').Tier1Judge;
|
|
77
|
+
trustedWorkspaceRoots?: string[];
|
|
72
78
|
/** When false, path resolution stays fail-closed (opaque cd chains). Default: Boolean(cwd). */
|
|
73
79
|
trustedCwd?: boolean;
|
|
80
|
+
grants?: CapabilityGrantV1[];
|
|
81
|
+
attestation?: BoundaryAttestation | null;
|
|
82
|
+
boundaryProfile?: string;
|
|
83
|
+
/** True when egress proxy is running and bound to this repository. */
|
|
84
|
+
egressProxyActive?: boolean;
|
|
85
|
+
}
|
|
86
|
+
export interface ApprovalScopeHint {
|
|
87
|
+
scope: 'workspace-root';
|
|
88
|
+
path: string;
|
|
74
89
|
}
|
|
75
90
|
export interface ApprovalRecord {
|
|
76
91
|
approvalId: string;
|
|
@@ -87,8 +102,26 @@ export interface ApprovalRecord {
|
|
|
87
102
|
/** Original gated input for explain-last-ask (ApprovalState v2). */
|
|
88
103
|
input?: string;
|
|
89
104
|
inputKind?: 'shell' | 'tool' | 'subagent';
|
|
105
|
+
/** Replay envelope: working directory when the action was gated. */
|
|
106
|
+
cwd?: string;
|
|
107
|
+
/** Replay envelope: tool name for tool/subagent actions. */
|
|
108
|
+
toolName?: string;
|
|
109
|
+
/** Replay envelope: canonical payload hash for strict replay validation. */
|
|
110
|
+
payloadHash?: string;
|
|
111
|
+
/** Replay envelope: scrubbed payload JSON for explain (size-capped at write time). */
|
|
112
|
+
payloadJson?: string;
|
|
113
|
+
/** Optional scope hint for structured capability approvals. */
|
|
114
|
+
scopeHint?: ApprovalScopeHint;
|
|
115
|
+
/** Approval state v3: normalized capability requests at ask time. */
|
|
116
|
+
capabilityRequests?: CapabilityRequestV1[];
|
|
117
|
+
/** Approval state v3: hash of capabilityRequests for replay binding. */
|
|
118
|
+
capabilityRequestHash?: string;
|
|
119
|
+
/** Approval state v3: minted grant after human approval. */
|
|
120
|
+
grant?: CapabilityGrantV1;
|
|
90
121
|
}
|
|
91
122
|
export interface ApprovalStateFile {
|
|
92
|
-
version: 1 | 2;
|
|
123
|
+
version: 1 | 2 | 3;
|
|
124
|
+
/** Optimistic concurrency revision (v3). */
|
|
125
|
+
revision?: number;
|
|
93
126
|
approvals: ApprovalRecord[];
|
|
94
127
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from '../config.js';
|
|
2
2
|
import type { ClassifierOptions, ClassifyResult } from '../types.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { VerdictContext, VerdictResult } from './types.js';
|
|
4
4
|
export declare function resolveClassifierTrustedCwd(cwd: string, options?: Pick<ClassifierOptions, 'trustedCwd'>, explicit?: boolean): boolean;
|
|
5
5
|
export declare function buildVerdictContext(params: {
|
|
6
6
|
cwd: string;
|
|
7
7
|
repoRoot: string;
|
|
8
8
|
config: BelayConfigV4;
|
|
9
9
|
options?: ClassifierOptions;
|
|
10
|
-
judge?: Tier1Judge;
|
|
11
10
|
trustedCwd?: boolean;
|
|
12
11
|
}): VerdictContext;
|
|
13
|
-
export declare function classifyShell(command: string, cwd: string, repoRoot: string, config: BelayConfigV4, options?: ClassifierOptions
|
|
14
|
-
export declare function verdictToClassifyResult(result: VerdictResult): ClassifyResult;
|
|
12
|
+
export declare function classifyShell(command: string, cwd: string, repoRoot: string, config: BelayConfigV4, options?: ClassifierOptions): Promise<ClassifyResult>;
|
|
13
|
+
export declare function verdictToClassifyResult(result: VerdictResult, config?: BelayConfigV4, options?: ClassifierOptions): ClassifyResult;
|
|
15
14
|
export declare function verdictAuditFields(result: VerdictResult): Record<string, unknown>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { BOUNDARY_PROFILE_L3_L4_ONLY, resolveBoundaryProfile, } from '../capability/boundary-profile.js';
|
|
1
2
|
import { judgeTraceAuditFields } from './judge-audit.js';
|
|
2
|
-
import {
|
|
3
|
+
import { recordJudgeLatency } from './judge-baseline.js';
|
|
3
4
|
import { verdict } from './verdict.js';
|
|
4
5
|
export function resolveClassifierTrustedCwd(cwd, options, explicit) {
|
|
5
6
|
if (explicit !== undefined) {
|
|
@@ -15,21 +16,27 @@ export function buildVerdictContext(params) {
|
|
|
15
16
|
return {
|
|
16
17
|
cwd: params.cwd,
|
|
17
18
|
repoRoot: params.repoRoot,
|
|
19
|
+
config: params.config,
|
|
18
20
|
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
21
|
+
trustedWorkspaceRoots: params.options?.trustedWorkspaceRoots ?? [],
|
|
19
22
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
20
23
|
protectedArtifactRoots: protectedArtifactRoots.length > 0 ? [...new Set(protectedArtifactRoots)] : undefined,
|
|
21
24
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
22
25
|
customExternalCommands: params.options?.customExternalCommands ?? params.config.overrides.external,
|
|
23
|
-
judge: params.judge ?? params.options?.tier1Judge ?? createJudgeFromConfig(params.config),
|
|
24
26
|
mode: params.config.mode,
|
|
25
27
|
unknownLocalEffect: params.options?.unknownLocalEffect ?? params.config.policy.unknownLocalEffect,
|
|
26
28
|
unparseableShell: params.options?.unparseableShell ?? params.config.policy.unparseableShell,
|
|
29
|
+
grants: params.options?.grants,
|
|
30
|
+
attestation: params.options?.attestation,
|
|
31
|
+
egressProxyActive: params.options?.egressProxyActive,
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
|
-
export async function classifyShell(command, cwd, repoRoot, config, options = {}
|
|
30
|
-
const context = buildVerdictContext({ cwd, repoRoot, config, options
|
|
34
|
+
export async function classifyShell(command, cwd, repoRoot, config, options = {}) {
|
|
35
|
+
const context = buildVerdictContext({ cwd, repoRoot, config, options });
|
|
36
|
+
const started = Date.now();
|
|
31
37
|
const result = await verdict(command, context);
|
|
32
|
-
|
|
38
|
+
recordJudgeLatency('gate', Date.now() - started);
|
|
39
|
+
return verdictToClassifyResult(result, config, options);
|
|
33
40
|
}
|
|
34
41
|
function mapLegacyReason(result) {
|
|
35
42
|
if (result.reason === 'repo_outside_mutation') {
|
|
@@ -62,7 +69,7 @@ function mapLegacyReason(result) {
|
|
|
62
69
|
}
|
|
63
70
|
return result.reason;
|
|
64
71
|
}
|
|
65
|
-
export function verdictToClassifyResult(result) {
|
|
72
|
+
export function verdictToClassifyResult(result, config, options = {}) {
|
|
66
73
|
const external = result.location === 'external' ||
|
|
67
74
|
result.location === 'repo_outside' ||
|
|
68
75
|
result.effect === 'remote_mutation';
|
|
@@ -111,6 +118,12 @@ export function verdictToClassifyResult(result) {
|
|
|
111
118
|
signals: result.signals,
|
|
112
119
|
...judgeTraceAuditFields(result.judgeTrace),
|
|
113
120
|
},
|
|
121
|
+
capabilityRequests: result.capabilityRequests,
|
|
122
|
+
authorizationDecision: result.authorizationDecision,
|
|
123
|
+
boundaryProfile: options.boundaryProfile ??
|
|
124
|
+
(config
|
|
125
|
+
? resolveBoundaryProfile({ config, attestation: options.attestation })
|
|
126
|
+
: BOUNDARY_PROFILE_L3_L4_ONLY),
|
|
114
127
|
};
|
|
115
128
|
}
|
|
116
129
|
export function verdictAuditFields(result) {
|