@guilz-dev/belay 0.4.0 → 0.7.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 +27 -2
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +8 -2
- package/dist/adapters/shared/gate-runtime.js +579 -181
- package/dist/bundle/claude-runtime.mjs +13574 -6334
- package/dist/bundle/codex-runtime.mjs +13108 -5903
- package/dist/bundle/cursor-runtime.mjs +12676 -5476
- package/dist/cli.js +80 -2
- package/dist/commands/approve.js +34 -10
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +134 -11
- package/dist/commands/doctor.js +87 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- package/dist/commands/recovery-checkpoints.d.ts +164 -0
- package/dist/commands/recovery-checkpoints.js +347 -0
- package/dist/commands/revoke.js +19 -7
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/status.js +7 -0
- package/dist/config-io.js +91 -23
- 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 +20 -9
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/approval-replay.d.ts +1 -1
- package/dist/core/approval-replay.js +4 -1
- package/dist/core/approval-service.d.ts +6 -1
- package/dist/core/approval-service.js +87 -24
- package/dist/core/approval.d.ts +4 -0
- package/dist/core/approval.js +19 -5
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +32 -0
- package/dist/core/capability/approval-state-mutation.js +165 -0
- package/dist/core/capability/approval-v3.d.ts +32 -0
- package/dist/core/capability/approval-v3.js +114 -0
- package/dist/core/capability/attestation.d.ts +16 -0
- package/dist/core/capability/attestation.js +62 -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 +19 -0
- package/dist/core/capability/boundary-driver-container.js +189 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +44 -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 +14 -0
- package/dist/core/capability/boundary-grant-materialize.js +93 -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 +48 -0
- package/dist/core/capability/boundary-run.js +51 -0
- package/dist/core/capability/boundary-session.d.ts +61 -0
- package/dist/core/capability/boundary-session.js +138 -0
- package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
- package/dist/core/capability/boundary-workspace-mount.js +68 -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 +130 -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-consumption.d.ts +4 -0
- package/dist/core/capability/grant-consumption.js +11 -0
- package/dist/core/capability/grant-lease.d.ts +44 -0
- package/dist/core/capability/grant-lease.js +243 -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 +96 -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 +8 -0
- package/dist/core/capability/index.js +7 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- 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 +77 -0
- package/dist/core/capability/policy-engine.js +663 -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 +48 -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/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +38 -2
- package/dist/core/classify-tool.js +126 -62
- package/dist/core/config.d.ts +34 -3
- package/dist/core/config.js +118 -3
- package/dist/core/effect-ir/audit.d.ts +9 -0
- package/dist/core/effect-ir/audit.js +86 -0
- package/dist/core/effect-ir/build.d.ts +31 -0
- package/dist/core/effect-ir/build.js +277 -0
- package/dist/core/effect-ir/index.d.ts +8 -0
- package/dist/core/effect-ir/index.js +7 -0
- package/dist/core/effect-ir/normalize.d.ts +4 -0
- package/dist/core/effect-ir/normalize.js +104 -0
- package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
- package/dist/core/effect-ir/package-exec-eval.js +152 -0
- package/dist/core/effect-ir/package-exec.d.ts +29 -0
- package/dist/core/effect-ir/package-exec.js +292 -0
- package/dist/core/effect-ir/policy.d.ts +13 -0
- package/dist/core/effect-ir/policy.js +77 -0
- package/dist/core/effect-ir/types.d.ts +55 -0
- package/dist/core/effect-ir/types.js +1 -0
- package/dist/core/egress/allowlist.d.ts +1 -0
- package/dist/core/egress/allowlist.js +8 -0
- package/dist/core/egress-approval.js +45 -51
- package/dist/core/gate-contract.d.ts +5 -0
- package/dist/core/gate-contract.js +5 -0
- package/dist/core/gate-engine.js +30 -8
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -1
- package/dist/core/judge-cloud-consent.js +29 -15
- package/dist/core/judge-doctor.js +39 -14
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- package/dist/core/process-runner.d.ts +10 -0
- package/dist/core/process-runner.js +124 -0
- package/dist/core/recover-advice.js +2 -1
- package/dist/core/recovery/artifact-store.d.ts +29 -0
- package/dist/core/recovery/artifact-store.js +336 -0
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -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 +5 -0
- package/dist/core/recovery/index.js +4 -0
- package/dist/core/recovery/operator-guidance.d.ts +7 -0
- package/dist/core/recovery/operator-guidance.js +39 -0
- package/dist/core/recovery/reconcile.d.ts +3 -0
- package/dist/core/recovery/reconcile.js +48 -0
- package/dist/core/recovery/resource-identity.d.ts +4 -0
- package/dist/core/recovery/resource-identity.js +34 -0
- package/dist/core/recovery/restore.d.ts +13 -0
- package/dist/core/recovery/restore.js +114 -0
- package/dist/core/recovery/snapshot-node.d.ts +27 -0
- package/dist/core/recovery/snapshot-node.js +261 -0
- package/dist/core/recovery/types.d.ts +104 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/standing-allow.js +3 -0
- package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
- package/dist/core/transactional/apply-observed-changes.js +275 -0
- package/dist/core/transactional/backend-selector.d.ts +8 -0
- package/dist/core/transactional/backend-selector.js +118 -0
- package/dist/core/transactional/backend.d.ts +55 -0
- package/dist/core/transactional/backend.js +1 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +278 -0
- package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
- package/dist/core/transactional/file-checkpoint-git.js +217 -0
- package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
- package/dist/core/transactional/file-checkpoint-staging.js +54 -0
- package/dist/core/transactional/file-clone.d.ts +15 -0
- package/dist/core/transactional/file-clone.js +139 -0
- package/dist/core/transactional/file-tree-path.d.ts +7 -0
- package/dist/core/transactional/file-tree-path.js +50 -0
- package/dist/core/transactional/file-tree.d.ts +41 -0
- package/dist/core/transactional/file-tree.js +186 -0
- package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
- package/dist/core/transactional/git-worktree-backend.js +50 -0
- package/dist/core/transactional/git-worktree.d.ts +14 -9
- package/dist/core/transactional/git-worktree.js +62 -87
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +5 -0
- package/dist/core/transactional/runner.js +171 -23
- package/dist/core/transactional/snapshot-node.d.ts +26 -0
- package/dist/core/transactional/snapshot-node.js +87 -0
- package/dist/core/transactional/types.d.ts +21 -1
- package/dist/core/types.d.ts +30 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +24 -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.js +1 -0
- package/dist/core/verdict/judge-baseline.d.ts +1 -1
- package/dist/core/verdict/judge-baseline.js +1 -0
- package/dist/core/verdict/judge-broker-service.js +12 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
- package/dist/core/verdict/judge-cli.js +5 -3
- package/dist/core/verdict/judge-provider-matrix.js +1 -1
- package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
- package/dist/core/verdict/judge-runtime-config.js +10 -6
- package/dist/core/verdict/judge-session-broker.d.ts +1 -1
- package/dist/core/verdict/judge-session-broker.js +10 -5
- package/dist/core/verdict/judge-transport.d.ts +5 -1
- package/dist/core/verdict/judge-transport.js +24 -14
- package/dist/core/verdict/judge.d.ts +2 -15
- package/dist/core/verdict/judge.js +5 -68
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +14 -16
- 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 +26 -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 +18 -1
- package/dist/core/verdict/verdict.js +313 -178
- package/dist/corpus/evaluate.js +3 -7
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/mutators.d.ts +1 -1
- package/dist/corpus/mutators.js +11 -0
- package/dist/corpus/runtime-match.js +1 -3
- package/dist/corpus/standing-allow-catalog.generated.js +29 -9
- package/dist/judge-broker-daemon.js +15 -1
- package/dist/services/sandbox-service.d.ts +4 -0
- package/dist/services/sandbox-service.js +28 -2
- package/dist/types.d.ts +13 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -3
- package/skills/belay/belay-approve.md +10 -7
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
attestationPath: string;
|
|
16
|
+
attestation: BoundaryAttestation | null;
|
|
17
|
+
attestationFresh: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function boundaryAttestationPath(repoRoot: string, config: BelayConfigV4): string;
|
|
20
|
+
export declare function loadBoundaryAttestation(filePath: string, expectedRepoRoot?: string, controlPlaneDir?: string): Promise<BoundaryAttestation | null>;
|
|
21
|
+
export declare function saveBoundaryAttestation(filePath: string, attestation: BoundaryAttestation, repoRoot: string, controlPlaneDir: string): Promise<void>;
|
|
22
|
+
export declare function hostIntegrationBoundaryContext(repoRoot: string): ResolvedBoundaryDriverContext;
|
|
23
|
+
export declare function resolveBoundaryDriverContext(params: {
|
|
24
|
+
repoRoot: string;
|
|
25
|
+
config: BelayConfigV4;
|
|
26
|
+
driverId?: BoundaryDriverId;
|
|
27
|
+
egressProxyRunning?: boolean;
|
|
28
|
+
}): Promise<ResolvedBoundaryDriverContext>;
|
|
29
|
+
export declare function runWithBoundaryDriver(params: {
|
|
30
|
+
context: ResolvedBoundaryDriverContext;
|
|
31
|
+
command: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
timeoutMs: number;
|
|
34
|
+
runOptions?: BoundaryRunOptions;
|
|
35
|
+
}): Promise<Awaited<ReturnType<BoundaryDriver['run']>>>;
|
|
36
|
+
export declare function startBoundarySession(params: {
|
|
37
|
+
repoRoot: string;
|
|
38
|
+
config: BelayConfigV4;
|
|
39
|
+
driverId?: BoundaryDriverId;
|
|
40
|
+
egressProxyRunning?: boolean;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
attestation: BoundaryAttestation;
|
|
43
|
+
attestationPath: string;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function boundarySessionStatus(params: {
|
|
46
|
+
repoRoot: string;
|
|
47
|
+
config: BelayConfigV4;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
attestationPath: string;
|
|
50
|
+
attestation: BoundaryAttestation | null;
|
|
51
|
+
fresh: boolean;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function runBoundaryAgentCommand(params: {
|
|
54
|
+
repoRoot: string;
|
|
55
|
+
config: BelayConfigV4;
|
|
56
|
+
command: string;
|
|
57
|
+
cwd?: string;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
runOptions?: BoundaryRunOptions;
|
|
60
|
+
}): Promise<Awaited<ReturnType<BoundaryDriver['run']>>>;
|
|
61
|
+
export { dockerNetworkArgs };
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
attestationPath: '',
|
|
55
|
+
attestation: null,
|
|
56
|
+
attestationFresh: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function resolveBoundaryDriverContext(params) {
|
|
60
|
+
const driverId = params.driverId ?? params.config.capability?.boundaryDriver ?? 'host-integration';
|
|
61
|
+
let proxyActive = params.egressProxyRunning === true;
|
|
62
|
+
if (params.egressProxyRunning === undefined) {
|
|
63
|
+
const egress = await egressStatus({ targetDir: params.repoRoot });
|
|
64
|
+
proxyActive = isEgressProxyActive({
|
|
65
|
+
config: params.config,
|
|
66
|
+
running: egress.running,
|
|
67
|
+
foreignProxy: egress.foreignProxy,
|
|
68
|
+
repoRootMismatch: egress.repoRootMismatch,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const proxyEnv = resolveBoundaryEgressProxyEnv({
|
|
72
|
+
driverId,
|
|
73
|
+
config: params.config,
|
|
74
|
+
proxyActive,
|
|
75
|
+
});
|
|
76
|
+
const driver = getDefaultBoundaryDriver(driverId, {
|
|
77
|
+
egressProxyEnv: proxyEnv,
|
|
78
|
+
repoRoot: params.repoRoot,
|
|
79
|
+
});
|
|
80
|
+
const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
|
|
81
|
+
const attestation = await loadBoundaryAttestation(attestationPath, params.repoRoot, configuredControlPlaneDir(params.config));
|
|
82
|
+
return {
|
|
83
|
+
driver,
|
|
84
|
+
driverId,
|
|
85
|
+
proxyActive,
|
|
86
|
+
proxyEnv,
|
|
87
|
+
prepareContext: {
|
|
88
|
+
repoRoot: params.repoRoot,
|
|
89
|
+
egressProxyActive: proxyActive,
|
|
90
|
+
proxyEnv,
|
|
91
|
+
},
|
|
92
|
+
attestationPath,
|
|
93
|
+
attestation,
|
|
94
|
+
attestationFresh: attestation ? isAttestationFresh(attestation) : false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export async function runWithBoundaryDriver(params) {
|
|
98
|
+
return runWithBoundaryRunnable(params.context.driver, {
|
|
99
|
+
prepareContext: params.context.prepareContext,
|
|
100
|
+
command: params.command,
|
|
101
|
+
cwd: params.cwd,
|
|
102
|
+
timeoutMs: params.timeoutMs,
|
|
103
|
+
runOptions: params.runOptions,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export async function startBoundarySession(params) {
|
|
107
|
+
const resolved = await resolveBoundaryDriverContext(params);
|
|
108
|
+
const attestation = await resolved.driver.probe();
|
|
109
|
+
if (resolved.driver.prepare) {
|
|
110
|
+
await resolved.driver.prepare(resolved.prepareContext);
|
|
111
|
+
}
|
|
112
|
+
const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
|
|
113
|
+
await saveBoundaryAttestation(attestationPath, attestation, params.repoRoot, configuredControlPlaneDir(params.config));
|
|
114
|
+
return { attestation, attestationPath };
|
|
115
|
+
}
|
|
116
|
+
export async function boundarySessionStatus(params) {
|
|
117
|
+
const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
|
|
118
|
+
const attestation = await loadBoundaryAttestation(attestationPath, params.repoRoot, configuredControlPlaneDir(params.config));
|
|
119
|
+
return {
|
|
120
|
+
attestationPath,
|
|
121
|
+
attestation,
|
|
122
|
+
fresh: attestation ? isAttestationFresh(attestation) : false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export async function runBoundaryAgentCommand(params) {
|
|
126
|
+
const context = await resolveBoundaryDriverContext({
|
|
127
|
+
repoRoot: params.repoRoot,
|
|
128
|
+
config: params.config,
|
|
129
|
+
});
|
|
130
|
+
return runWithBoundaryDriver({
|
|
131
|
+
context,
|
|
132
|
+
command: params.command,
|
|
133
|
+
cwd: params.cwd ?? params.repoRoot,
|
|
134
|
+
timeoutMs: params.timeoutMs ?? 30 * 60_000,
|
|
135
|
+
runOptions: params.runOptions,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
export { dockerNetworkArgs };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BoundaryWorkspaceMount } from './boundary-run.js';
|
|
2
|
+
export declare function validateWorkspaceMount(mount: BoundaryWorkspaceMount): void;
|
|
3
|
+
export declare function resolveGuestWorkdir(mount: BoundaryWorkspaceMount): string;
|
|
4
|
+
export declare function buildWorkspaceMountSpec(mount: BoundaryWorkspaceMount): string;
|
|
5
|
+
export declare function workspaceMountEnvArgs(mount: BoundaryWorkspaceMount): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonicalPath, pathWithinRoot } from '../path-utils.js';
|
|
3
|
+
const HOST_PATH_ENV_VARS = [
|
|
4
|
+
'BELAY_EGRESS_REPO_ROOT',
|
|
5
|
+
'BELAY_JUDGE_BROKER_REPO_ROOT',
|
|
6
|
+
'BELAY_JUDGE_BROKER_STATE_DIR',
|
|
7
|
+
'BELAY_REPO_ROOT',
|
|
8
|
+
];
|
|
9
|
+
export function validateWorkspaceMount(mount) {
|
|
10
|
+
const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
|
|
11
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
12
|
+
if (!hostSourceRoot || !guestTargetRoot) {
|
|
13
|
+
throw new Error('boundary_workspace_mount_invalid_root');
|
|
14
|
+
}
|
|
15
|
+
if (hostSourceRoot.includes(',') || guestTargetRoot.includes(',')) {
|
|
16
|
+
throw new Error('boundary_workspace_mount_invalid_root');
|
|
17
|
+
}
|
|
18
|
+
if (hostSourceRoot === guestTargetRoot) {
|
|
19
|
+
throw new Error('boundary_workspace_mount_source_equals_target');
|
|
20
|
+
}
|
|
21
|
+
if (pathWithinRoot(hostSourceRoot, guestTargetRoot)) {
|
|
22
|
+
throw new Error('boundary_workspace_mount_source_contains_target');
|
|
23
|
+
}
|
|
24
|
+
if (mount.cwdRelative.includes('\0')) {
|
|
25
|
+
throw new Error('boundary_workspace_mount_invalid_cwd');
|
|
26
|
+
}
|
|
27
|
+
const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
|
|
28
|
+
if (normalizedRelative === '..' || normalizedRelative.startsWith('../')) {
|
|
29
|
+
throw new Error('boundary_workspace_mount_invalid_cwd');
|
|
30
|
+
}
|
|
31
|
+
if (path.posix.isAbsolute(normalizedRelative)) {
|
|
32
|
+
throw new Error('boundary_workspace_mount_invalid_cwd');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function resolveGuestWorkdir(mount) {
|
|
36
|
+
validateWorkspaceMount(mount);
|
|
37
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
38
|
+
const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
|
|
39
|
+
if (normalizedRelative === '.' || normalizedRelative === '') {
|
|
40
|
+
return guestTargetRoot;
|
|
41
|
+
}
|
|
42
|
+
const segments = normalizedRelative.split('/').filter(Boolean);
|
|
43
|
+
return path.posix.join(guestTargetRoot, ...segments);
|
|
44
|
+
}
|
|
45
|
+
export function buildWorkspaceMountSpec(mount) {
|
|
46
|
+
validateWorkspaceMount(mount);
|
|
47
|
+
const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
|
|
48
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
49
|
+
const readonly = mount.writable ? '' : ',readonly';
|
|
50
|
+
return `type=bind,src=${hostSourceRoot},dst=${guestTargetRoot}${readonly}`;
|
|
51
|
+
}
|
|
52
|
+
export function workspaceMountEnvArgs(mount) {
|
|
53
|
+
const workdir = resolveGuestWorkdir(mount);
|
|
54
|
+
if (!mount.hideHostSourcePath) {
|
|
55
|
+
return ['-e', `PWD=${workdir}`];
|
|
56
|
+
}
|
|
57
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
58
|
+
const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
|
|
59
|
+
const args = [];
|
|
60
|
+
for (const key of HOST_PATH_ENV_VARS) {
|
|
61
|
+
args.push('-e', `${key}=`);
|
|
62
|
+
}
|
|
63
|
+
args.push('-e', `PWD=${workdir}`, '-e', `OLDPWD=${workdir}`, '-e', `BELAY_REPO_ROOT=${guestTargetRoot}`);
|
|
64
|
+
if (hostSourceRoot !== guestTargetRoot) {
|
|
65
|
+
args.push('-e', `BELAY_EGRESS_REPO_ROOT=${guestTargetRoot}`);
|
|
66
|
+
}
|
|
67
|
+
return args;
|
|
68
|
+
}
|
|
@@ -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,130 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { belayStateDir } from '../config.js';
|
|
3
|
+
import { effectPlanAuditFields } from '../effect-ir/audit.js';
|
|
4
|
+
import { buildTier1Prompt } from '../verdict/judge.js';
|
|
5
|
+
import { normalizeJudgeRuntimeConfig } from '../verdict/judge-runtime-config.js';
|
|
6
|
+
import { judgeShadowAuditFields, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, } from '../verdict/judge-shadow.js';
|
|
7
|
+
import { evaluateWithJudgeTransport } from '../verdict/judge-transport.js';
|
|
8
|
+
import { recordPolicyJudgeComparison } from './gate-shadow-ratchet.js';
|
|
9
|
+
export { recordGateApprovalAsk } from './gate-shadow-ratchet.js';
|
|
10
|
+
export function capabilityDecisionAuditFields(result) {
|
|
11
|
+
const fields = {
|
|
12
|
+
...effectPlanAuditFields(result.effectPlan, {
|
|
13
|
+
capabilityRequests: result.capabilityRequests,
|
|
14
|
+
decisions: result.effectPlanPolicyDecisions,
|
|
15
|
+
authorizationDecision: result.authorizationDecision,
|
|
16
|
+
}),
|
|
17
|
+
executionRoute: 'downstream_host',
|
|
18
|
+
enforcementStatus: 'prediction_only',
|
|
19
|
+
boundaryEnforcement: 'prediction_only',
|
|
20
|
+
};
|
|
21
|
+
if (result.capabilityRequests?.length) {
|
|
22
|
+
fields.capabilityRequestCount = result.capabilityRequests.length;
|
|
23
|
+
fields.capabilityActions = result.capabilityRequests.map((request) => request.action);
|
|
24
|
+
}
|
|
25
|
+
if (result.authorizationDecision) {
|
|
26
|
+
fields.policyOutcome = result.authorizationDecision.outcome;
|
|
27
|
+
fields.policyReason = result.authorizationDecision.reason;
|
|
28
|
+
if (result.authorizationDecision.matchedRule) {
|
|
29
|
+
fields.policyMatchedRule = result.authorizationDecision.matchedRule;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const decisions = result.effectPlanPolicyDecisions ?? [];
|
|
33
|
+
if (decisions.length > 0 &&
|
|
34
|
+
decisions.every((decision) => decision.matchedRule === 'boundary.verified')) {
|
|
35
|
+
fields.authorizationMode = 'boundary_policy';
|
|
36
|
+
}
|
|
37
|
+
else if (decisions.length > 0 &&
|
|
38
|
+
decisions.every((decision) => decision.matchedRule === 'grant.exact')) {
|
|
39
|
+
fields.authorizationMode = 'exact_grant';
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
fields.authorizationMode = result.authorizationDecision ? 'policy_decision' : 'none';
|
|
43
|
+
}
|
|
44
|
+
if (result.boundaryProfile) {
|
|
45
|
+
fields.boundaryProfile = result.boundaryProfile;
|
|
46
|
+
}
|
|
47
|
+
if (result.authorizationDecision?.signals.includes('boundary_materialized_grant')) {
|
|
48
|
+
fields.boundaryGrantMaterialized = true;
|
|
49
|
+
}
|
|
50
|
+
return fields;
|
|
51
|
+
}
|
|
52
|
+
export function policyWouldBlock(result) {
|
|
53
|
+
const outcome = result.authorizationDecision?.outcome;
|
|
54
|
+
if (outcome === 'allow') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (outcome === 'require_approval' || outcome === 'deny') {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return result.verdict === 'deny_pending_approval';
|
|
61
|
+
}
|
|
62
|
+
export function judgeWouldBlock(verdict) {
|
|
63
|
+
return !verdict.local_recoverable || verdict.destroys_history_or_secrets === true;
|
|
64
|
+
}
|
|
65
|
+
export function policyJudgeMismatch(result, judgeVerdict) {
|
|
66
|
+
return policyWouldBlock(result) !== judgeWouldBlock(judgeVerdict);
|
|
67
|
+
}
|
|
68
|
+
function queueDeferredGatePolicyShadow(params) {
|
|
69
|
+
setImmediate(() => {
|
|
70
|
+
void runGatePolicyShadowComparison(params).catch(() => { });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function scheduleGateShadowAudit(params) {
|
|
74
|
+
const trace = capabilityDecisionAuditFields(params.result);
|
|
75
|
+
const judge = params.config.judge;
|
|
76
|
+
const mode = judge?.mode ?? 'shadow';
|
|
77
|
+
if (mode !== 'shadow' || !judge.runtime?.shadow?.enabled) {
|
|
78
|
+
return trace;
|
|
79
|
+
}
|
|
80
|
+
const deferredTrace = {
|
|
81
|
+
...trace,
|
|
82
|
+
...judgeShadowAuditFields(params.repoRoot),
|
|
83
|
+
judgeShadowScheduled: false,
|
|
84
|
+
judgeShadowDeferred: true,
|
|
85
|
+
};
|
|
86
|
+
const command = params.command?.trim();
|
|
87
|
+
if (!command ||
|
|
88
|
+
params.providerId === 'ollama' ||
|
|
89
|
+
!shouldRunShadowComparison(params.repoRoot, params.providerId, judge.runtime.shadow)) {
|
|
90
|
+
return deferredTrace;
|
|
91
|
+
}
|
|
92
|
+
queueDeferredGatePolicyShadow({
|
|
93
|
+
repoRoot: params.repoRoot,
|
|
94
|
+
config: params.config,
|
|
95
|
+
providerId: params.providerId,
|
|
96
|
+
command,
|
|
97
|
+
result: params.result,
|
|
98
|
+
stateDir: params.stateDir,
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
...deferredTrace,
|
|
102
|
+
judgeShadowQueued: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export async function runGatePolicyShadowComparison(params) {
|
|
106
|
+
const judge = params.config.judge;
|
|
107
|
+
const runtime = normalizeJudgeRuntimeConfig(judge.runtime);
|
|
108
|
+
const stateDir = params.stateDir ?? belayStateDir(params.config, path.join(params.repoRoot, '.belay'));
|
|
109
|
+
const transport = await evaluateWithJudgeTransport({
|
|
110
|
+
prompt: buildTier1Prompt(params.command),
|
|
111
|
+
context: {
|
|
112
|
+
providerId: params.providerId,
|
|
113
|
+
model: judge.model ?? 'default',
|
|
114
|
+
repoRoot: params.repoRoot,
|
|
115
|
+
stateDir,
|
|
116
|
+
judgeMode: judge.mode ?? 'shadow',
|
|
117
|
+
runtime,
|
|
118
|
+
judgeTimeoutMs: judge.timeoutMs ?? 25_000,
|
|
119
|
+
},
|
|
120
|
+
}, { skipTransportShadow: true, spawnOnly: true });
|
|
121
|
+
if (!transport.verdict) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const mismatch = policyJudgeMismatch(params.result, transport.verdict);
|
|
125
|
+
const shadow = recordShadowComparison(params.repoRoot, runtime.shadow, mismatch);
|
|
126
|
+
await recordPolicyJudgeComparison(params.repoRoot, stateDir, mismatch);
|
|
127
|
+
if (shadow.killSwitchTriggered) {
|
|
128
|
+
await triggerJudgeSessionKillSwitch(params.repoRoot, stateDir);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -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,4 @@
|
|
|
1
|
+
import type { ClassifyResult } from '../types.js';
|
|
2
|
+
export declare function capabilityGrantLeaseRequired(result: Pick<ClassifyResult, 'reason' | 'authorizationDecision'>): boolean;
|
|
3
|
+
/** Fail closed when a grant lease is required but capability requests are missing. */
|
|
4
|
+
export declare function canConsumeCapabilityGrantLease(result: ClassifyResult): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function capabilityGrantLeaseRequired(result) {
|
|
2
|
+
return (result.reason === 'capability_grant' ||
|
|
3
|
+
result.authorizationDecision?.matchedRule === 'grant.exact');
|
|
4
|
+
}
|
|
5
|
+
/** Fail closed when a grant lease is required but capability requests are missing. */
|
|
6
|
+
export function canConsumeCapabilityGrantLease(result) {
|
|
7
|
+
if (!capabilityGrantLeaseRequired(result)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return (result.capabilityRequests?.length ?? 0) > 0;
|
|
11
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ApprovalRecord, ApprovalStateFile } from '../types.js';
|
|
2
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
3
|
+
import type { CapabilityRequestV1 } from './request.js';
|
|
4
|
+
export type GrantBundleValidationFailureReason = 'legacy_record' | 'empty_requests' | 'cardinality_mismatch' | 'grant_scope_too_broad' | 'grant_inactive' | 'grant_mismatch';
|
|
5
|
+
export type GrantBundleValidationResult = {
|
|
6
|
+
ok: true;
|
|
7
|
+
approval: ApprovalRecord;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: GrantBundleValidationFailureReason;
|
|
11
|
+
};
|
|
12
|
+
export declare function grantsFromApproval(approval: {
|
|
13
|
+
grant?: CapabilityGrantV1;
|
|
14
|
+
grants?: CapabilityGrantV1[];
|
|
15
|
+
}): CapabilityGrantV1[];
|
|
16
|
+
export declare function approvalGrantBundleExhausted(approval: {
|
|
17
|
+
grant?: CapabilityGrantV1;
|
|
18
|
+
grants?: CapabilityGrantV1[];
|
|
19
|
+
}): boolean;
|
|
20
|
+
export declare function consumeApprovalGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[]): {
|
|
21
|
+
approval: ApprovalRecord;
|
|
22
|
+
consumed: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** Validate and consume a marked exact bundle without mutating on any failure. */
|
|
25
|
+
export declare function validateAndConsumeGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
|
|
26
|
+
/** Revalidate a marked bundle during an active replay lease without consuming it twice. */
|
|
27
|
+
export declare function validateGrantBundleForLeaseReuse(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
|
|
28
|
+
/** Consume every grant in an approved record after fingerprint/hash replay validation. */
|
|
29
|
+
export declare function consumeApprovedRecordGrantBundle(approval: ApprovalRecord): {
|
|
30
|
+
approval: ApprovalRecord;
|
|
31
|
+
consumed: boolean;
|
|
32
|
+
};
|
|
33
|
+
export declare function decrementApprovalLegacyGrant(approval: ApprovalRecord): ApprovalRecord;
|
|
34
|
+
export declare function grantsFromApprovedState(state: ApprovalStateFile, repoRoot: string): CapabilityGrantV1[];
|
|
35
|
+
export declare function consumeGrantLease(state: ApprovalStateFile, grantId: string): {
|
|
36
|
+
state: ApprovalStateFile;
|
|
37
|
+
consumed: boolean;
|
|
38
|
+
};
|
|
39
|
+
/** Consume one lease for each request; all must succeed (atomic bundle consumption). */
|
|
40
|
+
export declare function consumeGrantLeasesForRequests(state: ApprovalStateFile, requests: CapabilityRequestV1[]): {
|
|
41
|
+
state: ApprovalStateFile;
|
|
42
|
+
consumed: boolean;
|
|
43
|
+
};
|
|
44
|
+
export declare function findMatchingGrant(grants: CapabilityGrantV1[] | undefined, request: CapabilityRequestV1): CapabilityGrantV1 | undefined;
|