@guilz-dev/belay 0.4.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/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +1 -2
- package/dist/adapters/shared/gate-runtime.js +226 -28
- package/dist/bundle/claude-runtime.mjs +6967 -4078
- package/dist/bundle/codex-runtime.mjs +5951 -3064
- package/dist/bundle/cursor-runtime.mjs +5951 -3064
- package/dist/cli.js +43 -1
- 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 +36 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/config-io.js +49 -12
- 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-service.js +6 -2
- package/dist/core/approval.d.ts +2 -0
- package/dist/core/approval.js +14 -3
- 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 +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 +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/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 +14 -3
- package/dist/core/config.js +56 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +6 -1
- 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/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- 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/standing-allow.js +3 -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 +21 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +16 -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 +16 -0
- 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 +13 -1
- package/dist/core/verdict/verdict.js +296 -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 +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { VerdictEffect, VerdictLocation, VerdictOpacity } from '../verdict/types.js';
|
|
3
|
+
import type { AuthorizationContext, PolicyDecision, PolicyEngine } from './policy-types.js';
|
|
4
|
+
import { type CapabilityHookKind, type CapabilityRequestV1 } from './request.js';
|
|
5
|
+
export type PolicyAuthExtras = Pick<AuthorizationContext, 'grants' | 'attestation'> & {
|
|
6
|
+
egressProxyActive?: boolean;
|
|
7
|
+
sensitivePaths?: string[];
|
|
8
|
+
};
|
|
9
|
+
export interface ShellCapabilityAnalysis {
|
|
10
|
+
command: string;
|
|
11
|
+
hookKind: CapabilityHookKind;
|
|
12
|
+
segmentHead: string;
|
|
13
|
+
effect: VerdictEffect;
|
|
14
|
+
location: VerdictLocation;
|
|
15
|
+
opacity: VerdictOpacity;
|
|
16
|
+
egressClass?: 'read' | 'destructive' | 'ambiguous';
|
|
17
|
+
pathArgs: string[];
|
|
18
|
+
/** Verdict-resolved canonical paths aligned with pathArgs (preferred for policy resource selection). */
|
|
19
|
+
resolvedPathTargets?: string[];
|
|
20
|
+
signals: string[];
|
|
21
|
+
cwd: string;
|
|
22
|
+
repoRoot: string;
|
|
23
|
+
inputFingerprint: string;
|
|
24
|
+
adapter?: string;
|
|
25
|
+
trustedWorkspaceRoots?: string[];
|
|
26
|
+
sensitivePaths?: string[];
|
|
27
|
+
protectedArtifactRoots?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface FileMutationCapabilityAnalysis {
|
|
30
|
+
hookKind: CapabilityHookKind;
|
|
31
|
+
toolKind: string;
|
|
32
|
+
filePath: string;
|
|
33
|
+
resolvedPath: string;
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
cwd: string;
|
|
36
|
+
inputFingerprint: string;
|
|
37
|
+
signals: string[];
|
|
38
|
+
isDelete: boolean;
|
|
39
|
+
locationLabel: 'repo_local' | 'outside_repo' | 'sensitive_path' | 'control_plane';
|
|
40
|
+
trustedWorkspaceRoots?: string[];
|
|
41
|
+
sensitivePaths?: string[];
|
|
42
|
+
adapter?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SubagentCapabilityAnalysis {
|
|
45
|
+
subagentType: string;
|
|
46
|
+
summary: string;
|
|
47
|
+
repoRoot: string;
|
|
48
|
+
cwd: string;
|
|
49
|
+
inputFingerprint: string;
|
|
50
|
+
signals: string[];
|
|
51
|
+
adapter?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function buildSubagentCapabilityRequest(analysis: SubagentCapabilityAnalysis): CapabilityRequestV1;
|
|
54
|
+
export declare function evaluateSubagentPolicy(analysis: SubagentCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {
|
|
55
|
+
request: CapabilityRequestV1;
|
|
56
|
+
decision: PolicyDecision;
|
|
57
|
+
};
|
|
58
|
+
export declare function buildShellCapabilityRequest(analysis: ShellCapabilityAnalysis): CapabilityRequestV1;
|
|
59
|
+
export declare function buildFileMutationCapabilityRequest(analysis: FileMutationCapabilityAnalysis): CapabilityRequestV1;
|
|
60
|
+
export declare function createTypeScriptPolicyEngine(): PolicyEngine;
|
|
61
|
+
export declare function getDefaultPolicyEngine(): PolicyEngine;
|
|
62
|
+
export declare function policyDecisionRequiresAsk(decision: PolicyDecision): boolean;
|
|
63
|
+
export declare function evaluateShellPolicy(analysis: ShellCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {
|
|
64
|
+
request: CapabilityRequestV1;
|
|
65
|
+
decision: PolicyDecision;
|
|
66
|
+
};
|
|
67
|
+
export declare function evaluateFileMutationPolicy(analysis: FileMutationCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {
|
|
68
|
+
request: CapabilityRequestV1;
|
|
69
|
+
decision: PolicyDecision;
|
|
70
|
+
};
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { matchesSensitivePath } from '../glob.js';
|
|
4
|
+
import { canonicalPath, pathWithinRoot, resolveWorkspaceRootMatch } from '../path-utils.js';
|
|
5
|
+
import { BOUNDARY_GRANT_ISSUER_CONTAINER, isPathWithinBoundaryMount, materializeContainerBoundaryGrant, } from './boundary-grant-materialize.js';
|
|
6
|
+
import { boundaryVerifiedAllowEnabled } from './boundary-profile.js';
|
|
7
|
+
import { isGrantScopeTooBroad } from './grant.js';
|
|
8
|
+
import { broadGrantTargetsRequest, grantMatchesRequest } from './grant-match.js';
|
|
9
|
+
import { CAPABILITY_REQUEST_VERSION, } from './request.js';
|
|
10
|
+
function enrichAuthWithMaterializedGrants(request, config, auth) {
|
|
11
|
+
if (!auth?.attestation || !boundaryVerifiedAllowEnabled(auth.attestation)) {
|
|
12
|
+
return auth;
|
|
13
|
+
}
|
|
14
|
+
const materialized = materializeContainerBoundaryGrant(request, {
|
|
15
|
+
attestation: auth.attestation,
|
|
16
|
+
mountRoot: request.context.cwd,
|
|
17
|
+
egressProxyActive: auth.egressProxyActive === true,
|
|
18
|
+
existingGrants: auth.grants,
|
|
19
|
+
sensitivePaths: auth.sensitivePaths ?? config.classifier.sensitivePaths,
|
|
20
|
+
});
|
|
21
|
+
if (!materialized) {
|
|
22
|
+
return auth;
|
|
23
|
+
}
|
|
24
|
+
return { ...auth, grants: [...(auth.grants ?? []), materialized] };
|
|
25
|
+
}
|
|
26
|
+
function buildAuthorizationContext(config, trustedWorkspaceRoots, auth) {
|
|
27
|
+
return {
|
|
28
|
+
config,
|
|
29
|
+
grants: auth?.grants,
|
|
30
|
+
attestation: auth?.attestation ?? null,
|
|
31
|
+
trustedWorkspaceRoots,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function buildSubagentCapabilityRequest(analysis) {
|
|
35
|
+
return {
|
|
36
|
+
version: CAPABILITY_REQUEST_VERSION,
|
|
37
|
+
principal: {
|
|
38
|
+
adapter: analysis.adapter,
|
|
39
|
+
repoRoot: analysis.repoRoot,
|
|
40
|
+
sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
|
|
41
|
+
},
|
|
42
|
+
action: 'process.exec',
|
|
43
|
+
resource: { kind: 'executable', command: analysis.subagentType },
|
|
44
|
+
context: {
|
|
45
|
+
cwd: analysis.cwd,
|
|
46
|
+
inputFingerprint: analysis.inputFingerprint,
|
|
47
|
+
hookKind: 'subagent',
|
|
48
|
+
analysisBasis: [`subagent:${analysis.subagentType}`],
|
|
49
|
+
},
|
|
50
|
+
evidence: {
|
|
51
|
+
level: 'possible',
|
|
52
|
+
signals: [...analysis.signals],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function evaluateSubagentPolicy(analysis, config, auth) {
|
|
57
|
+
const request = buildSubagentCapabilityRequest(analysis);
|
|
58
|
+
const enriched = enrichAuthWithMaterializedGrants(request, config, auth);
|
|
59
|
+
const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, undefined, enriched));
|
|
60
|
+
return { request, decision };
|
|
61
|
+
}
|
|
62
|
+
function hashSession(value) {
|
|
63
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
64
|
+
}
|
|
65
|
+
function resolveCapabilityPath(targetPath, cwd) {
|
|
66
|
+
const joined = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
67
|
+
return canonicalPath(joined);
|
|
68
|
+
}
|
|
69
|
+
function evidenceLevelForOpacity(opacity) {
|
|
70
|
+
if (opacity === 'transparent' || opacity === 'recursive') {
|
|
71
|
+
return 'certain';
|
|
72
|
+
}
|
|
73
|
+
if (opacity === 'opaque') {
|
|
74
|
+
return 'possible';
|
|
75
|
+
}
|
|
76
|
+
return 'indeterminate';
|
|
77
|
+
}
|
|
78
|
+
function isGitRefWrite(analysis) {
|
|
79
|
+
if (analysis.segmentHead !== 'git') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return analysis.signals.includes('git.push');
|
|
83
|
+
}
|
|
84
|
+
function actionForShellAnalysis(analysis) {
|
|
85
|
+
if (isGitRefWrite(analysis)) {
|
|
86
|
+
return 'git.ref.write';
|
|
87
|
+
}
|
|
88
|
+
if (analysis.effect === 'remote_mutation' ||
|
|
89
|
+
analysis.egressClass === 'ambiguous' ||
|
|
90
|
+
analysis.egressClass === 'read' ||
|
|
91
|
+
analysis.egressClass === 'destructive') {
|
|
92
|
+
return 'network.connect';
|
|
93
|
+
}
|
|
94
|
+
if (analysis.opacity === 'unparseable' || analysis.effect === 'unknown') {
|
|
95
|
+
return 'indeterminate';
|
|
96
|
+
}
|
|
97
|
+
if (analysis.effect === 'read_only') {
|
|
98
|
+
return 'fs.read';
|
|
99
|
+
}
|
|
100
|
+
if (analysis.location === 'external') {
|
|
101
|
+
return 'network.connect';
|
|
102
|
+
}
|
|
103
|
+
if (analysis.effect === 'local_mutation') {
|
|
104
|
+
return 'fs.write';
|
|
105
|
+
}
|
|
106
|
+
return 'indeterminate';
|
|
107
|
+
}
|
|
108
|
+
function resourcePathForShellAnalysis(analysis) {
|
|
109
|
+
const targets = analysis.resolvedPathTargets && analysis.resolvedPathTargets.length > 0
|
|
110
|
+
? analysis.resolvedPathTargets
|
|
111
|
+
: analysis.pathArgs;
|
|
112
|
+
if (targets.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const repoRoot = canonicalPath(analysis.repoRoot);
|
|
116
|
+
const resolved = targets.map((target) => analysis.resolvedPathTargets?.length
|
|
117
|
+
? canonicalPath(target)
|
|
118
|
+
: resolveCapabilityPath(target, analysis.cwd));
|
|
119
|
+
if (analysis.effect === 'local_mutation') {
|
|
120
|
+
const outside = resolved.find((candidate) => !pathWithinRoot(repoRoot, candidate));
|
|
121
|
+
return outside ?? resolved[resolved.length - 1];
|
|
122
|
+
}
|
|
123
|
+
return resolved[0];
|
|
124
|
+
}
|
|
125
|
+
function resourceForShellAnalysis(analysis) {
|
|
126
|
+
if (isGitRefWrite(analysis)) {
|
|
127
|
+
return { kind: 'git-ref', ref: 'push' };
|
|
128
|
+
}
|
|
129
|
+
if (analysis.egressClass === 'ambiguous' ||
|
|
130
|
+
analysis.egressClass === 'read' ||
|
|
131
|
+
analysis.egressClass === 'destructive' ||
|
|
132
|
+
analysis.location === 'external') {
|
|
133
|
+
return { kind: 'network', host: '*', protocol: 'unknown' };
|
|
134
|
+
}
|
|
135
|
+
const resourcePath = resourcePathForShellAnalysis(analysis);
|
|
136
|
+
if (resourcePath) {
|
|
137
|
+
return { kind: 'path', path: resourcePath };
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'executable', command: analysis.segmentHead };
|
|
140
|
+
}
|
|
141
|
+
export function buildShellCapabilityRequest(analysis) {
|
|
142
|
+
return {
|
|
143
|
+
version: CAPABILITY_REQUEST_VERSION,
|
|
144
|
+
principal: {
|
|
145
|
+
adapter: analysis.adapter,
|
|
146
|
+
repoRoot: analysis.repoRoot,
|
|
147
|
+
sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
|
|
148
|
+
},
|
|
149
|
+
action: actionForShellAnalysis(analysis),
|
|
150
|
+
resource: resourceForShellAnalysis(analysis),
|
|
151
|
+
context: {
|
|
152
|
+
cwd: analysis.cwd,
|
|
153
|
+
inputFingerprint: analysis.inputFingerprint,
|
|
154
|
+
hookKind: analysis.hookKind,
|
|
155
|
+
analysisBasis: [
|
|
156
|
+
`segment:${analysis.segmentHead}`,
|
|
157
|
+
`effect:${analysis.effect}`,
|
|
158
|
+
`location:${analysis.location}`,
|
|
159
|
+
`opacity:${analysis.opacity}`,
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
evidence: {
|
|
163
|
+
level: evidenceLevelForOpacity(analysis.opacity),
|
|
164
|
+
signals: [...analysis.signals],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function actionForFileMutation(analysis) {
|
|
169
|
+
if (analysis.locationLabel === 'control_plane') {
|
|
170
|
+
return 'control_plane.write';
|
|
171
|
+
}
|
|
172
|
+
if (analysis.locationLabel === 'sensitive_path') {
|
|
173
|
+
return 'fs.write';
|
|
174
|
+
}
|
|
175
|
+
if (analysis.isDelete) {
|
|
176
|
+
return 'fs.write';
|
|
177
|
+
}
|
|
178
|
+
return 'fs.write';
|
|
179
|
+
}
|
|
180
|
+
export function buildFileMutationCapabilityRequest(analysis) {
|
|
181
|
+
return {
|
|
182
|
+
version: CAPABILITY_REQUEST_VERSION,
|
|
183
|
+
principal: {
|
|
184
|
+
adapter: analysis.adapter,
|
|
185
|
+
repoRoot: analysis.repoRoot,
|
|
186
|
+
sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
|
|
187
|
+
},
|
|
188
|
+
action: actionForFileMutation(analysis),
|
|
189
|
+
resource: { kind: 'path', path: analysis.resolvedPath },
|
|
190
|
+
context: {
|
|
191
|
+
cwd: analysis.cwd,
|
|
192
|
+
inputFingerprint: analysis.inputFingerprint,
|
|
193
|
+
hookKind: analysis.hookKind,
|
|
194
|
+
analysisBasis: [
|
|
195
|
+
`tool:${analysis.toolKind}`,
|
|
196
|
+
`location:${analysis.locationLabel}`,
|
|
197
|
+
analysis.isDelete ? 'delete' : 'write',
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
evidence: {
|
|
201
|
+
level: analysis.locationLabel === 'repo_local' ? 'certain' : 'possible',
|
|
202
|
+
signals: [...analysis.signals],
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function findFreshGrant(request, grants) {
|
|
207
|
+
if (!grants?.length) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
for (const grant of grants) {
|
|
212
|
+
const expires = Date.parse(grant.expiresAt);
|
|
213
|
+
if (Number.isFinite(expires) && expires > now && grantMatchesRequest(grant, request)) {
|
|
214
|
+
return grant;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function hasFreshGrant(request, grants) {
|
|
220
|
+
return findFreshGrant(request, grants) !== null;
|
|
221
|
+
}
|
|
222
|
+
function grantDecisionSignals(request, grant) {
|
|
223
|
+
const signals = [...request.evidence.signals, 'capability_grant'];
|
|
224
|
+
if (grant.issuer === BOUNDARY_GRANT_ISSUER_CONTAINER) {
|
|
225
|
+
signals.push('boundary_materialized_grant');
|
|
226
|
+
}
|
|
227
|
+
return signals;
|
|
228
|
+
}
|
|
229
|
+
function shellLocationFromRequest(request) {
|
|
230
|
+
const entry = request.context.analysisBasis.find((basis) => basis.startsWith('location:'));
|
|
231
|
+
return entry?.slice('location:'.length) ?? null;
|
|
232
|
+
}
|
|
233
|
+
function isRepoLocalShellLocation(request) {
|
|
234
|
+
return shellLocationFromRequest(request) === 'repo_local';
|
|
235
|
+
}
|
|
236
|
+
function isRepoLocalRoutineWrite(request, sensitivePaths) {
|
|
237
|
+
if (!isRepoLocalShellLocation(request)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (request.action !== 'fs.write' && request.action !== 'fs.read') {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (request.resource.kind !== 'path') {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
|
|
247
|
+
const repoRoot = canonicalPath(request.principal.repoRoot);
|
|
248
|
+
if (!pathWithinRoot(repoRoot, resolved)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const relative = resolved.slice(repoRoot.length).replace(/^[/\\]/, '');
|
|
252
|
+
return !matchesSensitivePath(relative, sensitivePaths);
|
|
253
|
+
}
|
|
254
|
+
function isTrustedWorkspaceWrite(request, trustedWorkspaceRoots) {
|
|
255
|
+
if (!trustedWorkspaceRoots?.length || request.resource.kind !== 'path') {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
const match = resolveWorkspaceRootMatch(request.principal.repoRoot, trustedWorkspaceRoots, request.resource.path);
|
|
259
|
+
return match?.kind === 'trusted';
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* boundary.verified widens only when builtin already allows, or a boundary-materialized
|
|
263
|
+
* grant is present for an approved high-risk action (network via egress chokepoint).
|
|
264
|
+
*/
|
|
265
|
+
function verifiedBoundaryAllows(request, context) {
|
|
266
|
+
if (!boundaryVerifiedAllowEnabled(context.attestation)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (request.evidence.level !== 'certain') {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const builtin = builtInRule(request, context);
|
|
273
|
+
if (builtin?.outcome === 'allow') {
|
|
274
|
+
return isPathWithinBoundaryMount(request);
|
|
275
|
+
}
|
|
276
|
+
if (builtin?.outcome === 'deny') {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
return hasFreshGrant(request, context.grants?.filter((grant) => grant.issuer === BOUNDARY_GRANT_ISSUER_CONTAINER));
|
|
280
|
+
}
|
|
281
|
+
function builtInRule(request, context) {
|
|
282
|
+
const trustedWorkspaceRoots = context.trustedWorkspaceRoots;
|
|
283
|
+
const sensitivePaths = context.config.classifier.sensitivePaths;
|
|
284
|
+
if (request.action === 'control_plane.write') {
|
|
285
|
+
return {
|
|
286
|
+
outcome: 'require_approval',
|
|
287
|
+
reason: 'control_plane_mutation',
|
|
288
|
+
signals: [...request.evidence.signals, 'control_plane_path'],
|
|
289
|
+
matchedRule: 'builtin.control_plane',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (request.action === 'process.exec' && request.context.hookKind === 'subagent') {
|
|
293
|
+
return {
|
|
294
|
+
outcome: 'allow',
|
|
295
|
+
reason: 'subagent_review',
|
|
296
|
+
signals: [...request.evidence.signals],
|
|
297
|
+
matchedRule: 'builtin.subagent',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (request.action === 'secret.read' || request.action === 'git.ref.write') {
|
|
301
|
+
return {
|
|
302
|
+
outcome: 'require_approval',
|
|
303
|
+
reason: 'high_stakes_path',
|
|
304
|
+
signals: [...request.evidence.signals, 'secret_path'],
|
|
305
|
+
matchedRule: 'builtin.secret',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (request.action === 'network.connect') {
|
|
309
|
+
return {
|
|
310
|
+
outcome: 'require_approval',
|
|
311
|
+
reason: 'external_effect',
|
|
312
|
+
signals: [...request.evidence.signals, 'network_connect'],
|
|
313
|
+
matchedRule: 'builtin.network',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (request.action === 'indeterminate' || request.evidence.level === 'indeterminate') {
|
|
317
|
+
return {
|
|
318
|
+
outcome: 'require_approval',
|
|
319
|
+
reason: 'indeterminate_effect',
|
|
320
|
+
signals: [...request.evidence.signals, 'indeterminate'],
|
|
321
|
+
matchedRule: 'builtin.indeterminate',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (request.resource.kind === 'path' && sensitivePaths?.length) {
|
|
325
|
+
const repoRoot = canonicalPath(request.principal.repoRoot);
|
|
326
|
+
const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
|
|
327
|
+
if (pathWithinRoot(repoRoot, resolved)) {
|
|
328
|
+
const relative = resolved.slice(repoRoot.length).replace(/^[/\\]/, '');
|
|
329
|
+
if (matchesSensitivePath(relative, sensitivePaths)) {
|
|
330
|
+
return {
|
|
331
|
+
outcome: 'require_approval',
|
|
332
|
+
reason: 'high_stakes_path',
|
|
333
|
+
signals: [...request.evidence.signals, 'sensitive_path'],
|
|
334
|
+
matchedRule: 'builtin.sensitive_path',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (request.resource.kind === 'path') {
|
|
340
|
+
const repoRoot = canonicalPath(request.principal.repoRoot);
|
|
341
|
+
const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
|
|
342
|
+
const insideRepo = pathWithinRoot(repoRoot, resolved);
|
|
343
|
+
if (!insideRepo && !isTrustedWorkspaceWrite(request, trustedWorkspaceRoots)) {
|
|
344
|
+
return {
|
|
345
|
+
outcome: 'require_approval',
|
|
346
|
+
reason: 'outside_repo_mutation',
|
|
347
|
+
signals: [...request.evidence.signals, 'outside_repo_path'],
|
|
348
|
+
matchedRule: 'builtin.outside_repo',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (request.action === 'fs.read' || request.action === 'fs.write') {
|
|
353
|
+
if (request.action === 'fs.read' &&
|
|
354
|
+
request.resource.kind === 'executable' &&
|
|
355
|
+
isRepoLocalShellLocation(request)) {
|
|
356
|
+
return {
|
|
357
|
+
outcome: 'allow',
|
|
358
|
+
reason: 'read_only',
|
|
359
|
+
signals: [...request.evidence.signals],
|
|
360
|
+
matchedRule: 'builtin.repo_local',
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (isRepoLocalRoutineWrite(request, sensitivePaths)) {
|
|
364
|
+
return {
|
|
365
|
+
outcome: 'allow',
|
|
366
|
+
reason: request.action === 'fs.read' ? 'read_only' : 'repo_local_mutation',
|
|
367
|
+
signals: [...request.evidence.signals],
|
|
368
|
+
matchedRule: 'builtin.repo_local',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (isTrustedWorkspaceWrite(request, trustedWorkspaceRoots)) {
|
|
372
|
+
return {
|
|
373
|
+
outcome: 'allow',
|
|
374
|
+
reason: 'trusted_workspace_root',
|
|
375
|
+
signals: [...request.evidence.signals, 'trusted_workspace_root'],
|
|
376
|
+
matchedRule: 'builtin.trusted_workspace',
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (request.evidence.signals.includes('unparseable_shell')) {
|
|
381
|
+
const deny = context.config.policy.unparseableShell === 'deny';
|
|
382
|
+
return {
|
|
383
|
+
outcome: deny ? 'require_approval' : 'allow',
|
|
384
|
+
reason: 'unparseable_shell',
|
|
385
|
+
signals: [...request.evidence.signals],
|
|
386
|
+
matchedRule: deny ? 'builtin.unparseable' : 'builtin.unparseable_allow_flagged',
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (request.evidence.signals.includes('opaque_execution')) {
|
|
390
|
+
return {
|
|
391
|
+
outcome: 'require_approval',
|
|
392
|
+
reason: 'opaque_execution',
|
|
393
|
+
signals: [...request.evidence.signals],
|
|
394
|
+
matchedRule: 'builtin.opaque',
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
export function createTypeScriptPolicyEngine() {
|
|
400
|
+
return {
|
|
401
|
+
evaluate(request, context) {
|
|
402
|
+
if (request.evidence.signals.includes('forged_grant')) {
|
|
403
|
+
return {
|
|
404
|
+
outcome: 'deny',
|
|
405
|
+
reason: 'grant_forgery',
|
|
406
|
+
signals: [...request.evidence.signals, 'grant_forgery'],
|
|
407
|
+
matchedRule: 'forbid.grant_forgery',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const grants = context.grants;
|
|
411
|
+
if (grants?.some((grant) => isGrantScopeTooBroad(grant) && broadGrantTargetsRequest(grant, request))) {
|
|
412
|
+
return {
|
|
413
|
+
outcome: 'deny',
|
|
414
|
+
reason: 'grant_scope_too_broad',
|
|
415
|
+
signals: [...request.evidence.signals, 'grant_scope_too_broad'],
|
|
416
|
+
matchedRule: 'forbid.broad_grant',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const matchedGrant = findFreshGrant(request, grants);
|
|
420
|
+
if (matchedGrant) {
|
|
421
|
+
return {
|
|
422
|
+
outcome: 'allow',
|
|
423
|
+
reason: 'capability_grant',
|
|
424
|
+
signals: grantDecisionSignals(request, matchedGrant),
|
|
425
|
+
matchedRule: 'grant.exact',
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (verifiedBoundaryAllows(request, context)) {
|
|
429
|
+
return {
|
|
430
|
+
outcome: 'allow',
|
|
431
|
+
reason: 'verified_boundary',
|
|
432
|
+
signals: [...request.evidence.signals, 'verified_boundary'],
|
|
433
|
+
matchedRule: 'boundary.verified',
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const builtin = builtInRule(request, context);
|
|
437
|
+
if (builtin) {
|
|
438
|
+
return builtin;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
outcome: 'require_approval',
|
|
442
|
+
reason: 'policy_default',
|
|
443
|
+
signals: [...request.evidence.signals, 'default_deny'],
|
|
444
|
+
matchedRule: 'default.require_approval',
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
let defaultPolicyEngine = null;
|
|
450
|
+
export function getDefaultPolicyEngine() {
|
|
451
|
+
if (!defaultPolicyEngine) {
|
|
452
|
+
defaultPolicyEngine = createTypeScriptPolicyEngine();
|
|
453
|
+
}
|
|
454
|
+
return defaultPolicyEngine;
|
|
455
|
+
}
|
|
456
|
+
export function policyDecisionRequiresAsk(decision) {
|
|
457
|
+
return decision.outcome === 'require_approval' || decision.outcome === 'deny';
|
|
458
|
+
}
|
|
459
|
+
export function evaluateShellPolicy(analysis, config, auth) {
|
|
460
|
+
const request = buildShellCapabilityRequest(analysis);
|
|
461
|
+
const enriched = enrichAuthWithMaterializedGrants(request, config, {
|
|
462
|
+
...auth,
|
|
463
|
+
sensitivePaths: auth?.sensitivePaths ?? analysis.sensitivePaths,
|
|
464
|
+
});
|
|
465
|
+
const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, analysis.trustedWorkspaceRoots, enriched));
|
|
466
|
+
return { request, decision };
|
|
467
|
+
}
|
|
468
|
+
export function evaluateFileMutationPolicy(analysis, config, auth) {
|
|
469
|
+
const request = buildFileMutationCapabilityRequest(analysis);
|
|
470
|
+
const enriched = enrichAuthWithMaterializedGrants(request, config, {
|
|
471
|
+
...auth,
|
|
472
|
+
sensitivePaths: auth?.sensitivePaths ?? analysis.sensitivePaths,
|
|
473
|
+
});
|
|
474
|
+
const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, analysis.trustedWorkspaceRoots, enriched));
|
|
475
|
+
return { request, decision };
|
|
476
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { BoundaryAttestation } from './attestation.js';
|
|
3
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
4
|
+
import type { CapabilityRequestV1 } from './request.js';
|
|
5
|
+
export type PolicyOutcome = 'allow' | 'require_approval' | 'deny';
|
|
6
|
+
export interface PolicyDecision {
|
|
7
|
+
outcome: PolicyOutcome;
|
|
8
|
+
reason: string;
|
|
9
|
+
signals: string[];
|
|
10
|
+
matchedRule?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AuthorizationContext {
|
|
13
|
+
config: BelayConfigV4;
|
|
14
|
+
grants?: CapabilityGrantV1[];
|
|
15
|
+
attestation?: BoundaryAttestation | null;
|
|
16
|
+
trustedWorkspaceRoots?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface PolicyEngine {
|
|
19
|
+
evaluate(request: CapabilityRequestV1, context: AuthorizationContext): PolicyDecision;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const CAPABILITY_REQUEST_VERSION: 1;
|
|
2
|
+
export type CapabilityAction = 'fs.read' | 'fs.write' | 'process.exec' | 'network.connect' | 'secret.read' | 'git.ref.write' | 'control_plane.write' | 'indeterminate';
|
|
3
|
+
export type CapabilityEvidenceLevel = 'certain' | 'possible' | 'indeterminate';
|
|
4
|
+
export type CapabilityHookKind = 'shell' | 'tool' | 'subagent';
|
|
5
|
+
export interface CapabilityPrincipal {
|
|
6
|
+
adapter?: string;
|
|
7
|
+
repoRoot: string;
|
|
8
|
+
sessionHash?: string;
|
|
9
|
+
}
|
|
10
|
+
export type CapabilityResource = {
|
|
11
|
+
kind: 'path';
|
|
12
|
+
path: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'network';
|
|
15
|
+
host: string;
|
|
16
|
+
port?: number;
|
|
17
|
+
protocol?: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'executable';
|
|
20
|
+
command: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'git-ref';
|
|
23
|
+
ref: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'unknown';
|
|
26
|
+
};
|
|
27
|
+
export interface CapabilityRequestContext {
|
|
28
|
+
cwd: string;
|
|
29
|
+
inputFingerprint: string;
|
|
30
|
+
hookKind: CapabilityHookKind;
|
|
31
|
+
analysisBasis: string[];
|
|
32
|
+
boundaryProfile?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CapabilityEvidence {
|
|
35
|
+
level: CapabilityEvidenceLevel;
|
|
36
|
+
signals: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface CapabilityRequestV1 {
|
|
39
|
+
version: typeof CAPABILITY_REQUEST_VERSION;
|
|
40
|
+
principal: CapabilityPrincipal;
|
|
41
|
+
action: CapabilityAction;
|
|
42
|
+
resource: CapabilityResource;
|
|
43
|
+
context: CapabilityRequestContext;
|
|
44
|
+
evidence: CapabilityEvidence;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CAPABILITY_REQUEST_VERSION = 1;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { evaluateSegmentShellPolicy, type SegmentShellPolicyInput, } from '../verdict/shell-policy.js';
|
|
2
|
+
export { BOUNDARY_PROFILE_L3_POLICY, checkGatedActionLimits } from './limits.js';
|
|
3
|
+
export { type CapabilityAuthorizationMetadata, policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
|
|
4
|
+
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, buildSubagentCapabilityRequest, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, type FileMutationCapabilityAnalysis, policyDecisionRequiresAsk, type ShellCapabilityAnalysis, type SubagentCapabilityAnalysis, } from './policy-engine.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { evaluateSegmentShellPolicy, } from '../verdict/shell-policy.js';
|
|
2
|
+
export { BOUNDARY_PROFILE_L3_POLICY, checkGatedActionLimits } from './limits.js';
|
|
3
|
+
export { policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
|
|
4
|
+
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, buildSubagentCapabilityRequest, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, policyDecisionRequiresAsk, } from './policy-engine.js';
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import type { BelayConfigV3 } from './config.js';
|
|
1
2
|
import type { ClassifierOptions, ClassifyResult } from './types.js';
|
|
2
|
-
export declare function classifySubagent(payload: Record<string, unknown>, repoRoot: string, options
|
|
3
|
+
export declare function classifySubagent(payload: Record<string, unknown>, repoRoot: string, options: ClassifierOptions | undefined, config: BelayConfigV3): ClassifyResult;
|