@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,15 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { BoundaryAttestation } from './attestation.js';
|
|
3
|
+
/** Policy-only boundary (L3/L4). No attested L1 enforcement. */
|
|
4
|
+
export declare const BOUNDARY_PROFILE_L3_POLICY: "l3-policy-engine";
|
|
5
|
+
/** Editor-facing label when only prediction + approval layers are active. */
|
|
6
|
+
export declare const BOUNDARY_PROFILE_L3_L4_ONLY: "l3-l4-only";
|
|
7
|
+
/** Attested container / sandbox boundary with grant materialization. */
|
|
8
|
+
export declare const BOUNDARY_PROFILE_L1_ATTESTED: "l1-attested-boundary";
|
|
9
|
+
export type BoundaryProfileId = typeof BOUNDARY_PROFILE_L3_POLICY | typeof BOUNDARY_PROFILE_L3_L4_ONLY | typeof BOUNDARY_PROFILE_L1_ATTESTED;
|
|
10
|
+
export declare function isAttestedBoundary(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
11
|
+
export declare function resolveBoundaryProfile(params: {
|
|
12
|
+
config: BelayConfigV4;
|
|
13
|
+
attestation?: BoundaryAttestation | null;
|
|
14
|
+
}): BoundaryProfileId;
|
|
15
|
+
export declare function boundaryVerifiedAllowEnabled(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isAttestationFresh } from './attestation.js';
|
|
2
|
+
/** Policy-only boundary (L3/L4). No attested L1 enforcement. */
|
|
3
|
+
export const BOUNDARY_PROFILE_L3_POLICY = 'l3-policy-engine';
|
|
4
|
+
/** Editor-facing label when only prediction + approval layers are active. */
|
|
5
|
+
export const BOUNDARY_PROFILE_L3_L4_ONLY = 'l3-l4-only';
|
|
6
|
+
/** Attested container / sandbox boundary with grant materialization. */
|
|
7
|
+
export const BOUNDARY_PROFILE_L1_ATTESTED = 'l1-attested-boundary';
|
|
8
|
+
export function isAttestedBoundary(attestation) {
|
|
9
|
+
if (!attestation || !isAttestationFresh(attestation)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return attestation.deniesUngrantedEffects && attestation.materializesGrants;
|
|
13
|
+
}
|
|
14
|
+
export function resolveBoundaryProfile(params) {
|
|
15
|
+
if (!isAttestedBoundary(params.attestation)) {
|
|
16
|
+
return BOUNDARY_PROFILE_L3_L4_ONLY;
|
|
17
|
+
}
|
|
18
|
+
if (params.attestation?.driver === 'container') {
|
|
19
|
+
return BOUNDARY_PROFILE_L1_ATTESTED;
|
|
20
|
+
}
|
|
21
|
+
return BOUNDARY_PROFILE_L3_POLICY;
|
|
22
|
+
}
|
|
23
|
+
export function boundaryVerifiedAllowEnabled(attestation) {
|
|
24
|
+
return isAttestedBoundary(attestation);
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ShellRunResult } from '../transactional/git-worktree.js';
|
|
2
|
+
import type { ClassifyResult } from '../types.js';
|
|
3
|
+
export interface BoundaryRunOptions {
|
|
4
|
+
/** When true, container driver mounts the working directory read-only. */
|
|
5
|
+
mountReadOnly?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface BoundaryPrepareContext {
|
|
8
|
+
repoRoot?: string;
|
|
9
|
+
egressProxyActive: boolean;
|
|
10
|
+
proxyEnv: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export interface BoundaryRunnable {
|
|
13
|
+
prepare?(context: BoundaryPrepareContext): Promise<void>;
|
|
14
|
+
run(command: string, cwd: string, timeoutMs: number, options?: BoundaryRunOptions): Promise<ShellRunResult>;
|
|
15
|
+
}
|
|
16
|
+
export declare function boundaryMountReadOnlyFromPrediction(predicted: ClassifyResult): boolean;
|
|
17
|
+
export declare function runWithBoundaryRunnable(target: BoundaryRunnable, params: {
|
|
18
|
+
prepareContext: BoundaryPrepareContext;
|
|
19
|
+
command: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
timeoutMs: number;
|
|
22
|
+
runOptions?: BoundaryRunOptions;
|
|
23
|
+
}): Promise<ShellRunResult>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function boundaryMountReadOnlyFromPrediction(predicted) {
|
|
2
|
+
const effect = predicted.axes?.effect;
|
|
3
|
+
if (effect === 'local_mutation' || effect === 'remote_mutation') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const action = predicted.capabilityRequests?.[0]?.action;
|
|
7
|
+
if (action === 'fs.write' || action === 'git.ref.write' || action === 'control_plane.write') {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (action === 'fs.read') {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (action === 'network.connect' || action === 'process.exec' || action === 'indeterminate') {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export async function runWithBoundaryRunnable(target, params) {
|
|
19
|
+
if (target.prepare) {
|
|
20
|
+
await target.prepare(params.prepareContext);
|
|
21
|
+
}
|
|
22
|
+
return target.run(params.command, params.cwd, params.timeoutMs, params.runOptions);
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { BoundaryAttestation, BoundaryDriverId } from './attestation.js';
|
|
3
|
+
import { type BoundaryDriver } from './boundary-driver.js';
|
|
4
|
+
import { dockerNetworkArgs } from './boundary-egress.js';
|
|
5
|
+
import { type BoundaryPrepareContext, type BoundaryRunOptions } from './boundary-run.js';
|
|
6
|
+
export interface BoundarySessionOptions {
|
|
7
|
+
egressProxyRunning?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ResolvedBoundaryDriverContext {
|
|
10
|
+
driver: BoundaryDriver;
|
|
11
|
+
driverId: BoundaryDriverId;
|
|
12
|
+
proxyActive: boolean;
|
|
13
|
+
proxyEnv: Record<string, string>;
|
|
14
|
+
prepareContext: BoundaryPrepareContext;
|
|
15
|
+
}
|
|
16
|
+
export declare function boundaryAttestationPath(repoRoot: string, config: BelayConfigV4): string;
|
|
17
|
+
export declare function loadBoundaryAttestation(filePath: string, expectedRepoRoot?: string, controlPlaneDir?: string): Promise<BoundaryAttestation | null>;
|
|
18
|
+
export declare function saveBoundaryAttestation(filePath: string, attestation: BoundaryAttestation, repoRoot: string, controlPlaneDir: string): Promise<void>;
|
|
19
|
+
export declare function hostIntegrationBoundaryContext(repoRoot: string): ResolvedBoundaryDriverContext;
|
|
20
|
+
export declare function resolveBoundaryDriverContext(params: {
|
|
21
|
+
repoRoot: string;
|
|
22
|
+
config: BelayConfigV4;
|
|
23
|
+
driverId?: BoundaryDriverId;
|
|
24
|
+
egressProxyRunning?: boolean;
|
|
25
|
+
}): Promise<ResolvedBoundaryDriverContext>;
|
|
26
|
+
export declare function runWithBoundaryDriver(params: {
|
|
27
|
+
context: ResolvedBoundaryDriverContext;
|
|
28
|
+
command: string;
|
|
29
|
+
cwd: string;
|
|
30
|
+
timeoutMs: number;
|
|
31
|
+
runOptions?: BoundaryRunOptions;
|
|
32
|
+
}): Promise<Awaited<ReturnType<BoundaryDriver['run']>>>;
|
|
33
|
+
export declare function startBoundarySession(params: {
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
config: BelayConfigV4;
|
|
36
|
+
driverId?: BoundaryDriverId;
|
|
37
|
+
egressProxyRunning?: boolean;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
attestation: BoundaryAttestation;
|
|
40
|
+
attestationPath: string;
|
|
41
|
+
}>;
|
|
42
|
+
export declare function boundarySessionStatus(params: {
|
|
43
|
+
repoRoot: string;
|
|
44
|
+
config: BelayConfigV4;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
attestationPath: string;
|
|
47
|
+
attestation: BoundaryAttestation | null;
|
|
48
|
+
fresh: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
export declare function runBoundaryAgentCommand(params: {
|
|
51
|
+
repoRoot: string;
|
|
52
|
+
config: BelayConfigV4;
|
|
53
|
+
command: string;
|
|
54
|
+
cwd?: string;
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
}): Promise<Awaited<ReturnType<BoundaryDriver['run']>>>;
|
|
57
|
+
export { dockerNetworkArgs };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { egressStatus } from '../../services/egress-service.js';
|
|
4
|
+
import { configuredControlPlaneDir } from '../config.js';
|
|
5
|
+
import { isAttestationFresh } from './attestation.js';
|
|
6
|
+
import { readSignedAttestationFile, signBoundaryAttestation, verifySignedBoundaryAttestation, } from './boundary-attestation-sign.js';
|
|
7
|
+
import { getDefaultBoundaryDriver } from './boundary-driver.js';
|
|
8
|
+
import { dockerNetworkArgs, isEgressProxyActive, resolveBoundaryEgressProxyEnv, } from './boundary-egress.js';
|
|
9
|
+
import { runWithBoundaryRunnable, } from './boundary-run.js';
|
|
10
|
+
export function boundaryAttestationPath(repoRoot, config) {
|
|
11
|
+
const rel = config.capability?.attestationRelPath ?? '.belay/attestation.json';
|
|
12
|
+
return path.isAbsolute(rel) ? rel : path.join(repoRoot, rel);
|
|
13
|
+
}
|
|
14
|
+
export async function loadBoundaryAttestation(filePath, expectedRepoRoot, controlPlaneDir) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readSignedAttestationFile(filePath);
|
|
17
|
+
if (expectedRepoRoot && controlPlaneDir) {
|
|
18
|
+
return verifySignedBoundaryAttestation({
|
|
19
|
+
file: raw,
|
|
20
|
+
expectedRepoRoot,
|
|
21
|
+
controlPlaneDir,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function saveBoundaryAttestation(filePath, attestation, repoRoot, controlPlaneDir) {
|
|
31
|
+
const signed = await signBoundaryAttestation({
|
|
32
|
+
repoRoot,
|
|
33
|
+
attestation,
|
|
34
|
+
controlPlaneDir,
|
|
35
|
+
});
|
|
36
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
37
|
+
await writeFile(filePath, `${JSON.stringify(signed, null, 2)}\n`, {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function hostIntegrationBoundaryContext(repoRoot) {
|
|
43
|
+
const driver = getDefaultBoundaryDriver('host-integration', { repoRoot });
|
|
44
|
+
return {
|
|
45
|
+
driver,
|
|
46
|
+
driverId: 'host-integration',
|
|
47
|
+
proxyActive: false,
|
|
48
|
+
proxyEnv: {},
|
|
49
|
+
prepareContext: {
|
|
50
|
+
repoRoot,
|
|
51
|
+
egressProxyActive: false,
|
|
52
|
+
proxyEnv: {},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function resolveBoundaryDriverContext(params) {
|
|
57
|
+
const driverId = params.driverId ?? params.config.capability?.boundaryDriver ?? 'host-integration';
|
|
58
|
+
let proxyActive = params.egressProxyRunning === true;
|
|
59
|
+
if (params.egressProxyRunning === undefined) {
|
|
60
|
+
const egress = await egressStatus({ targetDir: params.repoRoot });
|
|
61
|
+
proxyActive = isEgressProxyActive({
|
|
62
|
+
config: params.config,
|
|
63
|
+
running: egress.running,
|
|
64
|
+
foreignProxy: egress.foreignProxy,
|
|
65
|
+
repoRootMismatch: egress.repoRootMismatch,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const proxyEnv = resolveBoundaryEgressProxyEnv({
|
|
69
|
+
driverId,
|
|
70
|
+
config: params.config,
|
|
71
|
+
proxyActive,
|
|
72
|
+
});
|
|
73
|
+
const driver = getDefaultBoundaryDriver(driverId, {
|
|
74
|
+
egressProxyEnv: proxyEnv,
|
|
75
|
+
repoRoot: params.repoRoot,
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
driver,
|
|
79
|
+
driverId,
|
|
80
|
+
proxyActive,
|
|
81
|
+
proxyEnv,
|
|
82
|
+
prepareContext: {
|
|
83
|
+
repoRoot: params.repoRoot,
|
|
84
|
+
egressProxyActive: proxyActive,
|
|
85
|
+
proxyEnv,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function runWithBoundaryDriver(params) {
|
|
90
|
+
return runWithBoundaryRunnable(params.context.driver, {
|
|
91
|
+
prepareContext: params.context.prepareContext,
|
|
92
|
+
command: params.command,
|
|
93
|
+
cwd: params.cwd,
|
|
94
|
+
timeoutMs: params.timeoutMs,
|
|
95
|
+
runOptions: params.runOptions,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export async function startBoundarySession(params) {
|
|
99
|
+
const resolved = await resolveBoundaryDriverContext(params);
|
|
100
|
+
const attestation = await resolved.driver.probe();
|
|
101
|
+
if (resolved.driver.prepare) {
|
|
102
|
+
await resolved.driver.prepare(resolved.prepareContext);
|
|
103
|
+
}
|
|
104
|
+
const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
|
|
105
|
+
await saveBoundaryAttestation(attestationPath, attestation, params.repoRoot, configuredControlPlaneDir(params.config));
|
|
106
|
+
return { attestation, attestationPath };
|
|
107
|
+
}
|
|
108
|
+
export async function boundarySessionStatus(params) {
|
|
109
|
+
const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
|
|
110
|
+
const attestation = await loadBoundaryAttestation(attestationPath, params.repoRoot, configuredControlPlaneDir(params.config));
|
|
111
|
+
return {
|
|
112
|
+
attestationPath,
|
|
113
|
+
attestation,
|
|
114
|
+
fresh: attestation ? isAttestationFresh(attestation) : false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export async function runBoundaryAgentCommand(params) {
|
|
118
|
+
const context = await resolveBoundaryDriverContext({
|
|
119
|
+
repoRoot: params.repoRoot,
|
|
120
|
+
config: params.config,
|
|
121
|
+
});
|
|
122
|
+
return runWithBoundaryDriver({
|
|
123
|
+
context,
|
|
124
|
+
command: params.command,
|
|
125
|
+
cwd: params.cwd ?? params.repoRoot,
|
|
126
|
+
timeoutMs: params.timeoutMs ?? 30 * 60_000,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export { dockerNetworkArgs };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { canonicalStringify, hashValue } from '../fingerprint.js';
|
|
2
|
+
export function hashCapabilityRequests(requests) {
|
|
3
|
+
if (requests.length === 0) {
|
|
4
|
+
return '';
|
|
5
|
+
}
|
|
6
|
+
const normalized = [...requests].sort((left, right) => canonicalStringify(left).localeCompare(canonicalStringify(right)));
|
|
7
|
+
return hashValue(`capability-requests:v1:${canonicalStringify(normalized)}`);
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GATE_LATENCY_BUDGET } from '../../corpus/gate-latency-budget.js';
|
|
2
|
+
export type GateLatencyKind = keyof typeof GATE_LATENCY_BUDGET;
|
|
3
|
+
/**
|
|
4
|
+
* Advisory warnings when CI budgets still exceed PLAN acceptance targets.
|
|
5
|
+
* Surfaced via sandbox status `advisories` and doctor warnings — not `issues` or gate exit codes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function evaluateGateLatencyRatchetAdvisories(): string[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GATE_LATENCY_BUDGET, GATE_LATENCY_PLAN_TARGET } from '../../corpus/gate-latency-budget.js';
|
|
2
|
+
/**
|
|
3
|
+
* Advisory warnings when CI budgets still exceed PLAN acceptance targets.
|
|
4
|
+
* Surfaced via sandbox status `advisories` and doctor warnings — not `issues` or gate exit codes.
|
|
5
|
+
*/
|
|
6
|
+
export function evaluateGateLatencyRatchetAdvisories() {
|
|
7
|
+
const warnings = [];
|
|
8
|
+
for (const kind of Object.keys(GATE_LATENCY_BUDGET)) {
|
|
9
|
+
const budget = GATE_LATENCY_BUDGET[kind];
|
|
10
|
+
if (budget.p95Ms > GATE_LATENCY_PLAN_TARGET.p95Ms) {
|
|
11
|
+
warnings.push(`Gate ${kind} p95 CI budget ${budget.p95Ms}ms exceeds PLAN target ${GATE_LATENCY_PLAN_TARGET.p95Ms}ms — tighten via quality-loop ratchet.`);
|
|
12
|
+
}
|
|
13
|
+
if (budget.maxMs > GATE_LATENCY_PLAN_TARGET.maxMs) {
|
|
14
|
+
warnings.push(`Gate ${kind} max CI budget ${budget.maxMs}ms exceeds PLAN target ${GATE_LATENCY_PLAN_TARGET.maxMs}ms — tighten via quality-loop ratchet.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return warnings;
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { ClassifyResult } from '../types.js';
|
|
3
|
+
import type { JudgeProviderId } from '../verdict/judge-catalog.js';
|
|
4
|
+
import type { Tier1Verdict } from '../verdict/types.js';
|
|
5
|
+
export { recordGateApprovalAsk } from './gate-shadow-ratchet.js';
|
|
6
|
+
export declare function capabilityDecisionAuditFields(result: ClassifyResult): Record<string, unknown>;
|
|
7
|
+
export declare function policyWouldBlock(result: ClassifyResult): boolean;
|
|
8
|
+
export declare function judgeWouldBlock(verdict: Tier1Verdict): boolean;
|
|
9
|
+
export declare function policyJudgeMismatch(result: ClassifyResult, judgeVerdict: Tier1Verdict): boolean;
|
|
10
|
+
export declare function scheduleGateShadowAudit(params: {
|
|
11
|
+
repoRoot: string;
|
|
12
|
+
config: BelayConfigV4;
|
|
13
|
+
providerId: string;
|
|
14
|
+
result: ClassifyResult;
|
|
15
|
+
command?: string;
|
|
16
|
+
stateDir?: string;
|
|
17
|
+
}): Record<string, unknown>;
|
|
18
|
+
export declare function runGatePolicyShadowComparison(params: {
|
|
19
|
+
repoRoot: string;
|
|
20
|
+
config: BelayConfigV4;
|
|
21
|
+
providerId: Exclude<JudgeProviderId, 'ollama'>;
|
|
22
|
+
command: string;
|
|
23
|
+
result: ClassifyResult;
|
|
24
|
+
stateDir?: string;
|
|
25
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { belayStateDir } from '../config.js';
|
|
3
|
+
import { buildTier1Prompt } from '../verdict/judge.js';
|
|
4
|
+
import { normalizeJudgeRuntimeConfig } from '../verdict/judge-runtime-config.js';
|
|
5
|
+
import { judgeShadowAuditFields, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, } from '../verdict/judge-shadow.js';
|
|
6
|
+
import { evaluateWithJudgeTransport } from '../verdict/judge-transport.js';
|
|
7
|
+
import { recordPolicyJudgeComparison } from './gate-shadow-ratchet.js';
|
|
8
|
+
export { recordGateApprovalAsk } from './gate-shadow-ratchet.js';
|
|
9
|
+
export function capabilityDecisionAuditFields(result) {
|
|
10
|
+
const fields = {};
|
|
11
|
+
if (result.capabilityRequests?.length) {
|
|
12
|
+
fields.capabilityRequestCount = result.capabilityRequests.length;
|
|
13
|
+
fields.capabilityActions = result.capabilityRequests.map((request) => request.action);
|
|
14
|
+
}
|
|
15
|
+
if (result.authorizationDecision) {
|
|
16
|
+
fields.policyOutcome = result.authorizationDecision.outcome;
|
|
17
|
+
fields.policyReason = result.authorizationDecision.reason;
|
|
18
|
+
if (result.authorizationDecision.matchedRule) {
|
|
19
|
+
fields.policyMatchedRule = result.authorizationDecision.matchedRule;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (result.boundaryProfile) {
|
|
23
|
+
fields.boundaryProfile = result.boundaryProfile;
|
|
24
|
+
}
|
|
25
|
+
if (result.authorizationDecision?.signals.includes('boundary_materialized_grant')) {
|
|
26
|
+
fields.boundaryGrantMaterialized = true;
|
|
27
|
+
}
|
|
28
|
+
return fields;
|
|
29
|
+
}
|
|
30
|
+
export function policyWouldBlock(result) {
|
|
31
|
+
const outcome = result.authorizationDecision?.outcome;
|
|
32
|
+
if (outcome === 'allow') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (outcome === 'require_approval' || outcome === 'deny') {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return result.verdict === 'deny_pending_approval';
|
|
39
|
+
}
|
|
40
|
+
export function judgeWouldBlock(verdict) {
|
|
41
|
+
return !verdict.local_recoverable || verdict.destroys_history_or_secrets === true;
|
|
42
|
+
}
|
|
43
|
+
export function policyJudgeMismatch(result, judgeVerdict) {
|
|
44
|
+
return policyWouldBlock(result) !== judgeWouldBlock(judgeVerdict);
|
|
45
|
+
}
|
|
46
|
+
function queueDeferredGatePolicyShadow(params) {
|
|
47
|
+
setImmediate(() => {
|
|
48
|
+
void runGatePolicyShadowComparison(params).catch(() => { });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function scheduleGateShadowAudit(params) {
|
|
52
|
+
const trace = capabilityDecisionAuditFields(params.result);
|
|
53
|
+
const judge = params.config.judge;
|
|
54
|
+
const mode = judge?.mode ?? 'shadow';
|
|
55
|
+
if (mode !== 'shadow' || !judge.runtime?.shadow?.enabled) {
|
|
56
|
+
return trace;
|
|
57
|
+
}
|
|
58
|
+
const deferredTrace = {
|
|
59
|
+
...trace,
|
|
60
|
+
...judgeShadowAuditFields(params.repoRoot),
|
|
61
|
+
judgeShadowScheduled: false,
|
|
62
|
+
judgeShadowDeferred: true,
|
|
63
|
+
};
|
|
64
|
+
const command = params.command?.trim();
|
|
65
|
+
if (!command ||
|
|
66
|
+
params.providerId === 'ollama' ||
|
|
67
|
+
!shouldRunShadowComparison(params.repoRoot, params.providerId, judge.runtime.shadow)) {
|
|
68
|
+
return deferredTrace;
|
|
69
|
+
}
|
|
70
|
+
queueDeferredGatePolicyShadow({
|
|
71
|
+
repoRoot: params.repoRoot,
|
|
72
|
+
config: params.config,
|
|
73
|
+
providerId: params.providerId,
|
|
74
|
+
command,
|
|
75
|
+
result: params.result,
|
|
76
|
+
stateDir: params.stateDir,
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
...deferredTrace,
|
|
80
|
+
judgeShadowQueued: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function runGatePolicyShadowComparison(params) {
|
|
84
|
+
const judge = params.config.judge;
|
|
85
|
+
const runtime = normalizeJudgeRuntimeConfig(judge.runtime);
|
|
86
|
+
const stateDir = params.stateDir ?? belayStateDir(params.config, path.join(params.repoRoot, '.belay'));
|
|
87
|
+
const transport = await evaluateWithJudgeTransport({
|
|
88
|
+
prompt: buildTier1Prompt(params.command),
|
|
89
|
+
context: {
|
|
90
|
+
providerId: params.providerId,
|
|
91
|
+
model: judge.model ?? 'default',
|
|
92
|
+
repoRoot: params.repoRoot,
|
|
93
|
+
stateDir,
|
|
94
|
+
judgeMode: judge.mode ?? 'shadow',
|
|
95
|
+
runtime,
|
|
96
|
+
judgeTimeoutMs: judge.timeoutMs ?? 25_000,
|
|
97
|
+
},
|
|
98
|
+
}, { skipTransportShadow: true, spawnOnly: true });
|
|
99
|
+
if (!transport.verdict) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const mismatch = policyJudgeMismatch(params.result, transport.verdict);
|
|
103
|
+
const shadow = recordShadowComparison(params.repoRoot, runtime.shadow, mismatch);
|
|
104
|
+
await recordPolicyJudgeComparison(params.repoRoot, stateDir, mismatch);
|
|
105
|
+
if (shadow.killSwitchTriggered) {
|
|
106
|
+
await triggerJudgeSessionKillSwitch(params.repoRoot, stateDir);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { capabilityDecisionAuditFields, judgeWouldBlock, policyJudgeMismatch, policyWouldBlock, recordGateApprovalAsk, runGatePolicyShadowComparison, scheduleGateShadowAudit, } from './gate-policy-shadow.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { capabilityDecisionAuditFields, judgeWouldBlock, policyJudgeMismatch, policyWouldBlock, recordGateApprovalAsk, runGatePolicyShadowComparison, scheduleGateShadowAudit, } from './gate-policy-shadow.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface GateShadowRatchetState {
|
|
2
|
+
version: 1;
|
|
3
|
+
policyJudgeComparisons: number;
|
|
4
|
+
policyJudgeMismatches: number;
|
|
5
|
+
approvalByReason: Record<string, {
|
|
6
|
+
asks: number;
|
|
7
|
+
approved: number;
|
|
8
|
+
}>;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function recordPolicyJudgeComparison(repoRoot: string, stateDir: string, mismatch: boolean): Promise<void>;
|
|
12
|
+
export declare function recordGateApprovalAsk(stateDir: string, reason: string, approved: boolean): Promise<void>;
|
|
13
|
+
export declare function evaluateShadowRatchetWarnings(state: GateShadowRatchetState): string[];
|
|
14
|
+
export declare function loadShadowRatchetWarnings(stateDir: string): Promise<string[]>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const DEFAULT_STATE = {
|
|
5
|
+
version: 1,
|
|
6
|
+
policyJudgeComparisons: 0,
|
|
7
|
+
policyJudgeMismatches: 0,
|
|
8
|
+
approvalByReason: {},
|
|
9
|
+
updatedAt: new Date(0).toISOString(),
|
|
10
|
+
};
|
|
11
|
+
const MISMATCH_RATE_THRESHOLD = 0.25;
|
|
12
|
+
const APPROVAL_RATE_THRESHOLD = 0.5;
|
|
13
|
+
const MIN_SAMPLES = 10;
|
|
14
|
+
function ratchetPath(stateDir) {
|
|
15
|
+
return path.join(stateDir, 'gate-shadow-ratchet.json');
|
|
16
|
+
}
|
|
17
|
+
async function loadState(stateDir) {
|
|
18
|
+
const filePath = ratchetPath(stateDir);
|
|
19
|
+
if (!existsSync(filePath)) {
|
|
20
|
+
return { ...DEFAULT_STATE, updatedAt: new Date().toISOString() };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(await readFile(filePath, 'utf8'));
|
|
24
|
+
return {
|
|
25
|
+
...DEFAULT_STATE,
|
|
26
|
+
...raw,
|
|
27
|
+
approvalByReason: raw.approvalByReason ?? {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { ...DEFAULT_STATE, updatedAt: new Date().toISOString() };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function saveState(stateDir, state) {
|
|
35
|
+
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
36
|
+
await writeFile(ratchetPath(stateDir), `${JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
export async function recordPolicyJudgeComparison(repoRoot, stateDir, mismatch) {
|
|
39
|
+
void repoRoot;
|
|
40
|
+
const state = await loadState(stateDir);
|
|
41
|
+
state.policyJudgeComparisons += 1;
|
|
42
|
+
if (mismatch) {
|
|
43
|
+
state.policyJudgeMismatches += 1;
|
|
44
|
+
}
|
|
45
|
+
await saveState(stateDir, state);
|
|
46
|
+
}
|
|
47
|
+
export async function recordGateApprovalAsk(stateDir, reason, approved) {
|
|
48
|
+
const state = await loadState(stateDir);
|
|
49
|
+
const entry = state.approvalByReason[reason] ?? { asks: 0, approved: 0 };
|
|
50
|
+
if (approved) {
|
|
51
|
+
entry.approved += 1;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
entry.asks += 1;
|
|
55
|
+
}
|
|
56
|
+
state.approvalByReason[reason] = entry;
|
|
57
|
+
await saveState(stateDir, state);
|
|
58
|
+
}
|
|
59
|
+
export function evaluateShadowRatchetWarnings(state) {
|
|
60
|
+
const warnings = [];
|
|
61
|
+
if (state.policyJudgeComparisons >= MIN_SAMPLES) {
|
|
62
|
+
const rate = state.policyJudgeMismatches / state.policyJudgeComparisons;
|
|
63
|
+
if (rate > MISMATCH_RATE_THRESHOLD) {
|
|
64
|
+
warnings.push(`Policy/judge shadow mismatch rate ${(rate * 100).toFixed(0)}% exceeds ${(MISMATCH_RATE_THRESHOLD * 100).toFixed(0)}% threshold (${state.policyJudgeMismatches}/${state.policyJudgeComparisons}).`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const [reason, counts] of Object.entries(state.approvalByReason)) {
|
|
68
|
+
if (counts.asks < MIN_SAMPLES) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const approvalRate = counts.approved / counts.asks;
|
|
72
|
+
if (approvalRate >= APPROVAL_RATE_THRESHOLD) {
|
|
73
|
+
warnings.push(`High approval rate for "${reason}": ${(approvalRate * 100).toFixed(0)}% (${counts.approved}/${counts.asks}). Consider tightening policy.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return warnings;
|
|
77
|
+
}
|
|
78
|
+
export async function loadShadowRatchetWarnings(stateDir) {
|
|
79
|
+
const state = await loadState(stateDir);
|
|
80
|
+
return evaluateShadowRatchetWarnings(state);
|
|
81
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ApprovalStateFile } from '../types.js';
|
|
2
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
3
|
+
import type { CapabilityRequestV1 } from './request.js';
|
|
4
|
+
export declare function grantsFromApprovedState(state: ApprovalStateFile, repoRoot: string): CapabilityGrantV1[];
|
|
5
|
+
export declare function consumeGrantLease(state: ApprovalStateFile, grantId: string): {
|
|
6
|
+
state: ApprovalStateFile;
|
|
7
|
+
consumed: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function findMatchingGrant(grants: CapabilityGrantV1[] | undefined, request: CapabilityRequestV1): CapabilityGrantV1 | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isGrantScopeTooBroad } from './grant.js';
|
|
2
|
+
import { grantMatchesRequest } from './grant-match.js';
|
|
3
|
+
export function grantsFromApprovedState(state, repoRoot) {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
return state.approvals
|
|
6
|
+
.filter((approval) => approval.repoRoot === repoRoot)
|
|
7
|
+
.map((approval) => approval.grant)
|
|
8
|
+
.filter((grant) => {
|
|
9
|
+
if (!grant) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (isGrantScopeTooBroad(grant)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const expires = Date.parse(grant.expiresAt);
|
|
16
|
+
return Number.isFinite(expires) && expires > now && grant.usesRemaining > 0;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function consumeGrantLease(state, grantId) {
|
|
20
|
+
let consumed = false;
|
|
21
|
+
const approvals = state.approvals.map((approval) => {
|
|
22
|
+
const grant = approval.grant;
|
|
23
|
+
if (!grant || grant.grantId !== grantId || grant.usesRemaining <= 0) {
|
|
24
|
+
return approval;
|
|
25
|
+
}
|
|
26
|
+
consumed = true;
|
|
27
|
+
const usesRemaining = grant.usesRemaining - 1;
|
|
28
|
+
return {
|
|
29
|
+
...approval,
|
|
30
|
+
grant: {
|
|
31
|
+
...grant,
|
|
32
|
+
usesRemaining,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
state: { ...state, approvals },
|
|
38
|
+
consumed,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function findMatchingGrant(grants, request) {
|
|
42
|
+
if (!grants?.length) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return grants.find((grant) => grantMatchesRequest(grant, request));
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { ApprovalStateFile, ClassifierOptions } from '../types.js';
|
|
3
|
+
import type { BoundaryAttestation } from './attestation.js';
|
|
4
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
5
|
+
export declare function capabilityGrantsEnabled(config: BelayConfigV4): boolean;
|
|
6
|
+
export declare function grantsFromApprovalState(state: ApprovalStateFile, repoRoot: string, config: BelayConfigV4): CapabilityGrantV1[] | undefined;
|
|
7
|
+
export declare function loadClassifierAuthorization(params: {
|
|
8
|
+
repoRoot: string;
|
|
9
|
+
config: BelayConfigV4;
|
|
10
|
+
approvedState: ApprovalStateFile;
|
|
11
|
+
}): Promise<Pick<ClassifierOptions, 'grants' | 'attestation' | 'boundaryProfile'>>;
|
|
12
|
+
export type { BoundaryAttestation, CapabilityGrantV1 };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { configuredControlPlaneDir } from '../config.js';
|
|
2
|
+
import { resolveBoundaryProfile } from './boundary-profile.js';
|
|
3
|
+
import { boundaryAttestationPath, loadBoundaryAttestation } from './boundary-session.js';
|
|
4
|
+
import { grantsFromApprovedState } from './grant-lease.js';
|
|
5
|
+
export function capabilityGrantsEnabled(config) {
|
|
6
|
+
return config.capability?.grantsEnabled !== false;
|
|
7
|
+
}
|
|
8
|
+
export function grantsFromApprovalState(state, repoRoot, config) {
|
|
9
|
+
if (!capabilityGrantsEnabled(config)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const grants = grantsFromApprovedState(state, repoRoot);
|
|
13
|
+
return grants.length > 0 ? grants : undefined;
|
|
14
|
+
}
|
|
15
|
+
export async function loadClassifierAuthorization(params) {
|
|
16
|
+
const grants = grantsFromApprovalState(params.approvedState, params.repoRoot, params.config);
|
|
17
|
+
const attestation = await loadBoundaryAttestation(boundaryAttestationPath(params.repoRoot, params.config), params.repoRoot, configuredControlPlaneDir(params.config));
|
|
18
|
+
const boundaryProfile = resolveBoundaryProfile({
|
|
19
|
+
config: params.config,
|
|
20
|
+
attestation,
|
|
21
|
+
});
|
|
22
|
+
return { grants, attestation, boundaryProfile };
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
2
|
+
import type { CapabilityRequestV1 } from './request.js';
|
|
3
|
+
export declare function grantTargetsRequest(grant: CapabilityGrantV1, request: CapabilityRequestV1): boolean;
|
|
4
|
+
/** Repo-scoped target match for forged broad-grant detection (principal may be incomplete). */
|
|
5
|
+
export declare function broadGrantTargetsRequest(grant: CapabilityGrantV1, request: CapabilityRequestV1): boolean;
|
|
6
|
+
export declare function grantMatchesRequest(grant: CapabilityGrantV1, request: CapabilityRequestV1): boolean;
|