@guilz-dev/belay 0.7.0 → 0.8.1
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 +19 -19
- package/dist/adapters/shared/gate-runtime.js +23 -5
- package/dist/bundle/claude-runtime.mjs +4909 -3504
- package/dist/bundle/codex-runtime.mjs +5050 -3645
- package/dist/bundle/cursor-runtime.mjs +5050 -3645
- package/dist/commands/doctor.js +24 -21
- package/dist/commands/dogfood.js +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/harvest.d.ts +0 -1
- package/dist/commands/harvest.js +1 -4
- package/dist/commands/metrics.js +39 -4
- package/dist/commands/quality.js +3 -9
- package/dist/commands/status.js +1 -1
- package/dist/conformance/guarantee-table.js +133 -11
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/audit-metrics.d.ts +24 -0
- package/dist/core/audit-metrics.js +82 -16
- package/dist/core/audit-types.d.ts +3 -0
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/policy-engine.d.ts +31 -1
- package/dist/core/capability/policy-engine.js +186 -0
- package/dist/core/capability/request.d.ts +9 -0
- package/dist/core/capability/resolver.d.ts +0 -1
- package/dist/core/capability/resolver.js +0 -1
- package/dist/core/config.js +0 -2
- package/dist/core/effect-ir/audit.js +1 -0
- package/dist/core/effect-ir/build.js +5 -1
- package/dist/core/effect-ir/index.d.ts +6 -1
- package/dist/core/effect-ir/index.js +2 -0
- package/dist/core/effect-ir/normalize.js +9 -3
- package/dist/core/effect-ir/policy.d.ts +2 -1
- package/dist/core/effect-ir/policy.js +93 -6
- package/dist/core/effect-ir/shell-build.d.ts +43 -0
- package/dist/core/effect-ir/shell-build.js +77 -0
- package/dist/core/effect-ir/shell-lower.d.ts +14 -0
- package/dist/core/effect-ir/shell-lower.js +1509 -0
- package/dist/core/effect-ir/types.d.ts +9 -2
- package/dist/core/gate-engine.js +63 -23
- package/dist/core/git-resource-identity.d.ts +26 -0
- package/dist/core/git-resource-identity.js +284 -0
- package/dist/core/harvest.d.ts +3 -7
- package/dist/core/harvest.js +4 -27
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/path-utils.js +42 -23
- package/dist/core/shell-substitution.d.ts +1 -0
- package/dist/core/shell-substitution.js +61 -0
- package/dist/core/standing-allow.d.ts +1 -2
- package/dist/core/standing-allow.js +3 -20
- package/dist/core/types.d.ts +4 -1
- package/dist/core/verdict/adapter.js +15 -15
- package/dist/core/verdict/containment.js +4 -0
- package/dist/core/verdict/egress-classify.d.ts +12 -0
- package/dist/core/verdict/egress-classify.js +746 -0
- package/dist/core/verdict/git-classifier.d.ts +12 -0
- package/dist/core/verdict/git-classifier.js +320 -0
- package/dist/core/verdict/launcher-resolve.js +77 -17
- package/dist/core/verdict/parser.d.ts +2 -0
- package/dist/core/verdict/parser.js +95 -1
- package/dist/core/verdict/types.d.ts +2 -3
- package/dist/core/verdict/verdict.js +103 -952
- package/dist/corpus/must-allow-commands.d.ts +2 -1
- package/dist/corpus/must-allow-commands.js +2 -1
- package/dist/corpus/runtime-match.d.ts +2 -1
- package/dist/corpus/runtime-match.js +2 -1
- package/dist/corpus/types.d.ts +2 -2
- package/dist/operational-insights.d.ts +3 -0
- package/dist/operational-insights.js +7 -3
- package/dist/runtime-provenance.d.ts +8 -0
- package/dist/runtime-provenance.js +34 -0
- package/dist/templates.js +7 -2
- package/dist/types.d.ts +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/core/verdict/overrides.d.ts +0 -7
- package/dist/core/verdict/overrides.js +0 -37
- package/dist/core/verdict/shell-policy.d.ts +0 -26
- package/dist/core/verdict/shell-policy.js +0 -40
- package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
- package/dist/corpus/standing-allow-catalog.generated.js +0 -99
|
@@ -30,6 +30,7 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
30
30
|
const byOpacity = {};
|
|
31
31
|
const byEffect = {};
|
|
32
32
|
const byConfidence = {};
|
|
33
|
+
const gateEventsByRuntime = {};
|
|
33
34
|
const summaryCounts = new Map();
|
|
34
35
|
let gateEvents = 0;
|
|
35
36
|
let wouldBlockCount = 0;
|
|
@@ -44,6 +45,12 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
44
45
|
continue;
|
|
45
46
|
}
|
|
46
47
|
gateEvents += 1;
|
|
48
|
+
const runtimeBuild = typeof record.runtimeBuildStamp === 'string'
|
|
49
|
+
? record.runtimeBuildStamp
|
|
50
|
+
: typeof record.runtimeVersion === 'string'
|
|
51
|
+
? record.runtimeVersion
|
|
52
|
+
: 'unrecorded';
|
|
53
|
+
increment(gateEventsByRuntime, runtimeBuild);
|
|
47
54
|
const reason = typeof record.reason === 'string' ? record.reason : 'unknown';
|
|
48
55
|
const kind = typeof record.kind === 'string' ? record.kind : 'unknown';
|
|
49
56
|
increment(byReason, reason);
|
|
@@ -88,33 +95,90 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
88
95
|
const topWouldBlockSummaries = [...summaryCounts.values()]
|
|
89
96
|
.sort((left, right) => right.count - left.count)
|
|
90
97
|
.slice(0, 10);
|
|
98
|
+
const activeCohort = options.activeCohort ?? null;
|
|
99
|
+
const cohortRecords = activeCohort
|
|
100
|
+
? auditRecords.filter((record) => record.runtimeBuildStamp === activeCohort.runtimeBuildStamp &&
|
|
101
|
+
record.configFingerprint === activeCohort.configFingerprint)
|
|
102
|
+
: [];
|
|
103
|
+
const cohortGateRecords = cohortRecords.filter((record) => {
|
|
104
|
+
const event = typeof record.event === 'string' ? record.event : '';
|
|
105
|
+
return GATE_EVENTS.has(event) && !isApprovalRecorded(record);
|
|
106
|
+
});
|
|
107
|
+
const cohortGateEvents = cohortGateRecords.length;
|
|
108
|
+
const cohortWouldBlockCount = cohortGateRecords.filter(inferWouldBlock).length;
|
|
109
|
+
const cohortWouldBlockRate = cohortGateEvents > 0 ? cohortWouldBlockCount / cohortGateEvents : 0;
|
|
110
|
+
const cohortApprovalRecordedCount = cohortRecords.filter(isApprovalRecorded).length;
|
|
111
|
+
const cohortAvailabilityAsks = computeAvailabilityAskCounts(cohortRecords);
|
|
112
|
+
const cohortClassifierWouldBlockCount = Math.max(0, cohortWouldBlockCount - cohortAvailabilityAsks.total);
|
|
113
|
+
const cohortClassifierWouldBlockRate = cohortGateEvents > 0 ? cohortClassifierWouldBlockCount / cohortGateEvents : 0;
|
|
114
|
+
const cohortRoundTrips = buildApprovalRoundTrips(cohortRecords);
|
|
115
|
+
const cohortRepeatedFingerprintAsks = computeRepeatedFingerprintAsks(cohortRecords);
|
|
116
|
+
const cohortNoisyRuleCandidates = detectNoisyRules(cohortRecords, cohortRoundTrips);
|
|
117
|
+
const cohortWouldBlockByReason = computeWouldBlockByReason(cohortRecords);
|
|
118
|
+
const cohortSummaryCounts = new Map();
|
|
119
|
+
for (const record of cohortGateRecords) {
|
|
120
|
+
if (!inferWouldBlock(record)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const reason = typeof record.reason === 'string' ? record.reason : 'unknown';
|
|
124
|
+
const summary = typeof record.summary === 'string' ? record.summary : '';
|
|
125
|
+
const key = `${reason}::${summary}`;
|
|
126
|
+
const existing = cohortSummaryCounts.get(key);
|
|
127
|
+
if (existing) {
|
|
128
|
+
existing.count += 1;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
cohortSummaryCounts.set(key, { summary, reason, count: 1 });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const cohortTopWouldBlockSummaries = [...cohortSummaryCounts.values()]
|
|
135
|
+
.sort((left, right) => right.count - left.count)
|
|
136
|
+
.slice(0, 10);
|
|
137
|
+
const currentCohort = {
|
|
138
|
+
identity: activeCohort,
|
|
139
|
+
gateEvents: cohortGateEvents,
|
|
140
|
+
excludedGateEvents: gateEvents - cohortGateEvents,
|
|
141
|
+
wouldBlockCount: cohortWouldBlockCount,
|
|
142
|
+
wouldBlockRate: cohortWouldBlockRate,
|
|
143
|
+
classifierWouldBlockCount: cohortClassifierWouldBlockCount,
|
|
144
|
+
classifierWouldBlockRate: cohortClassifierWouldBlockRate,
|
|
145
|
+
approvalRecordedCount: cohortApprovalRecordedCount,
|
|
146
|
+
availabilityAsks: cohortAvailabilityAsks,
|
|
147
|
+
wouldBlockByReason: cohortWouldBlockByReason,
|
|
148
|
+
topWouldBlockSummaries: cohortTopWouldBlockSummaries,
|
|
149
|
+
};
|
|
91
150
|
const mode = options.mode ?? null;
|
|
92
151
|
const unknownLocalEffect = options.unknownLocalEffect ?? null;
|
|
93
152
|
const notes = [];
|
|
94
153
|
let readyForEnforce = false;
|
|
95
154
|
if (mode === 'audit' && unknownLocalEffect === 'deny') {
|
|
96
155
|
notes.push('Dogfood config detected: audit mode with fail-closed shell policy.');
|
|
97
|
-
if (
|
|
98
|
-
notes.push('
|
|
156
|
+
if (!activeCohort) {
|
|
157
|
+
notes.push('Active runtime provenance is unavailable — readiness cannot use historical audit evidence.');
|
|
158
|
+
}
|
|
159
|
+
else if (cohortGateEvents === 0) {
|
|
160
|
+
notes.push('No gate events for the active runtime/config cohort — run normal agent work, then re-check metrics.');
|
|
99
161
|
}
|
|
100
|
-
else if (
|
|
101
|
-
if (
|
|
162
|
+
else if (cohortWouldBlockRate === 0) {
|
|
163
|
+
if (cohortGateEvents >= MIN_GATE_EVENTS_FOR_ENFORCE) {
|
|
102
164
|
readyForEnforce = true;
|
|
103
165
|
notes.push('No would-block events recorded — safe to try mode: "enforce".');
|
|
104
166
|
}
|
|
105
167
|
else {
|
|
106
|
-
notes.push(`Only ${
|
|
168
|
+
notes.push(`Only ${cohortGateEvents} active-cohort gate event(s) recorded — collect at least ${MIN_GATE_EVENTS_FOR_ENFORCE} before enforce.`);
|
|
107
169
|
}
|
|
108
170
|
}
|
|
109
171
|
else {
|
|
110
|
-
notes.push(`${
|
|
111
|
-
if (
|
|
112
|
-
notes.push(`${
|
|
172
|
+
notes.push(`${cohortWouldBlockCount} active-cohort would-block event(s) (${(cohortWouldBlockRate * 100).toFixed(1)}% of gate traffic; classifier-quality ${(cohortClassifierWouldBlockRate * 100).toFixed(1)}%). Review top summaries and correct EffectPlan semantics or resource scope; use exact approval only when the modeled effects are correct.`);
|
|
173
|
+
if (cohortApprovalRecordedCount > 0) {
|
|
174
|
+
notes.push(`${cohortApprovalRecordedCount} active-cohort approval(s) recorded — these likely indicate actions operators wanted.`);
|
|
113
175
|
}
|
|
114
176
|
else {
|
|
115
|
-
notes.push('Review top would-block summaries and
|
|
177
|
+
notes.push('Review top would-block summaries and correct EffectPlan semantics or resource scope before switching to enforce.');
|
|
116
178
|
}
|
|
117
|
-
if (
|
|
179
|
+
if (cohortClassifierWouldBlockRate < 0.05 &&
|
|
180
|
+
cohortGateEvents >= 20 &&
|
|
181
|
+
cohortAvailabilityAsks.total === 0) {
|
|
118
182
|
readyForEnforce = true;
|
|
119
183
|
notes.push('Classifier-quality would-block rate is below 5% with sufficient sample size — consider enforce mode.');
|
|
120
184
|
}
|
|
@@ -126,16 +190,16 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
126
190
|
else {
|
|
127
191
|
notes.push('Set policy.unknownLocalEffect to "deny" to dogfood fail-closed defaults.');
|
|
128
192
|
}
|
|
129
|
-
if (
|
|
193
|
+
if (cohortAvailabilityAsks.total > 0) {
|
|
130
194
|
readyForEnforce = false;
|
|
131
|
-
notes.push(`${
|
|
195
|
+
notes.push(`${cohortAvailabilityAsks.total} active-cohort availability-caused ask(s) — tune infrastructure before changing Effect semantics.`);
|
|
132
196
|
notes.push('Ready for enforce withheld while availability-caused asks are present.');
|
|
133
197
|
}
|
|
134
|
-
if (
|
|
135
|
-
notes.push(`${
|
|
198
|
+
if (cohortRepeatedFingerprintAsks.length > 0) {
|
|
199
|
+
notes.push(`${cohortRepeatedFingerprintAsks.length} active-cohort repeated fingerprint ask pattern(s) — review EffectPlan semantics and exact approval history.`);
|
|
136
200
|
}
|
|
137
|
-
if (
|
|
138
|
-
notes.push(`${
|
|
201
|
+
if (cohortNoisyRuleCandidates.length > 0) {
|
|
202
|
+
notes.push(`${cohortNoisyRuleCandidates.length} active-cohort noisy rule candidate(s) — high deny-then-approve rate.`);
|
|
139
203
|
}
|
|
140
204
|
return {
|
|
141
205
|
schemaVersion: AUDIT_METRICS_SCHEMA_VERSION,
|
|
@@ -158,6 +222,8 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
158
222
|
byOpacity,
|
|
159
223
|
byEffect,
|
|
160
224
|
byConfidence,
|
|
225
|
+
gateEventsByRuntime,
|
|
226
|
+
currentCohort,
|
|
161
227
|
approvalRecordedCount,
|
|
162
228
|
topWouldBlockSummaries,
|
|
163
229
|
approvalLatency,
|
|
@@ -16,6 +16,9 @@ export interface AuditRecord {
|
|
|
16
16
|
judgeFallbackReason?: string;
|
|
17
17
|
permission?: string;
|
|
18
18
|
mode?: string;
|
|
19
|
+
runtimeVersion?: string;
|
|
20
|
+
runtimeBuildStamp?: string;
|
|
21
|
+
configFingerprint?: string;
|
|
19
22
|
assessment?: Assessment;
|
|
20
23
|
predictedAssessment?: Assessment;
|
|
21
24
|
observedAssessment?: Assessment;
|
|
@@ -9,6 +9,6 @@ export { type CapabilityAuthorizationMetadata, type PolicyLegacyReasonHints, pol
|
|
|
9
9
|
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, type FileMutationCapabilityAnalysis, getDefaultPolicyEngine, policyDecisionRequiresAsk, type ShellCapabilityAnalysis, type SubagentCapabilityAnalysis, } from './policy-engine.js';
|
|
10
10
|
export type { AuthorizationContext, PolicyDecision, PolicyEngine, PolicyOutcome, } from './policy-types.js';
|
|
11
11
|
export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from './reasons.js';
|
|
12
|
-
export { CAPABILITY_REQUEST_VERSION, type CapabilityAction, type CapabilityEvidence, type CapabilityEvidenceLevel, type CapabilityHookKind, type CapabilityPrincipal, type CapabilityRequestContext, type CapabilityRequestV1, type CapabilityResource, } from './request.js';
|
|
12
|
+
export { CAPABILITY_REQUEST_VERSION, type CapabilityAction, type CapabilityEvidence, type CapabilityEvidenceLevel, type CapabilityHookKind, type CapabilityPrincipal, type CapabilityRequestContext, type CapabilityRequestV1, type CapabilityResource, type GitRefScope, type NetworkMode, type NetworkPayload, type ProcessOperation, } from './request.js';
|
|
13
13
|
export { addTrustedWorkspaceRoot, isBroadTrustedWorkspaceRoot, isHighStakesTrustedWorkspaceRoot, isPathWithinTrustedWorkspaceRoots, loadTrustedWorkspaceRoots, loadTrustedWorkspaceRootsSync, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from './trusted-workspace-roots.js';
|
|
14
14
|
export type { CapabilityApprovalScope, FsScopeAllowlistEntry, FsScopeAllowlistFile, TrustedWorkspaceRootEntry, TrustedWorkspaceRootsFile, } from './types.js';
|
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from '../config.js';
|
|
2
2
|
import type { VerdictEffect, VerdictLocation, VerdictOpacity } from '../verdict/types.js';
|
|
3
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
3
4
|
import type { AuthorizationContext, PolicyDecision, PolicyEngine } from './policy-types.js';
|
|
4
|
-
import { type CapabilityHookKind, type CapabilityRequestV1 } from './request.js';
|
|
5
|
+
import { type CapabilityAction, type CapabilityEvidenceLevel, type CapabilityHookKind, type CapabilityRequestV1, type CapabilityResource } from './request.js';
|
|
5
6
|
export type PolicyAuthExtras = Pick<AuthorizationContext, 'grants' | 'attestation'> & {
|
|
6
7
|
egressProxyActive?: boolean;
|
|
7
8
|
sensitivePaths?: string[];
|
|
8
9
|
};
|
|
10
|
+
export type EffectPolicyDisposition = 'allow' | 'allow_flagged' | 'ask';
|
|
11
|
+
export interface EffectRequirementPolicyInput {
|
|
12
|
+
tag: string;
|
|
13
|
+
action: CapabilityAction;
|
|
14
|
+
resource: CapabilityResource;
|
|
15
|
+
evidence: {
|
|
16
|
+
level: CapabilityEvidenceLevel;
|
|
17
|
+
signals: readonly string[];
|
|
18
|
+
basis: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
provenance?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface EffectRequirementPolicyContext {
|
|
23
|
+
cwd: string;
|
|
24
|
+
repoRoot: string;
|
|
25
|
+
trustedWorkspaceRoots?: readonly string[];
|
|
26
|
+
sensitivePaths?: readonly string[];
|
|
27
|
+
protectedArtifactRoots?: readonly string[];
|
|
28
|
+
grants?: readonly CapabilityGrantV1[];
|
|
29
|
+
capabilityRequest?: CapabilityRequestV1;
|
|
30
|
+
}
|
|
31
|
+
export interface EffectRequirementPolicyDecision extends PolicyDecision {
|
|
32
|
+
effectDisposition: EffectPolicyDisposition;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Policy for a lowered shell effect. It intentionally ignores executable
|
|
36
|
+
* command names, segment heads, command text, and config allowlists.
|
|
37
|
+
*/
|
|
38
|
+
export declare function evaluateEffectRequirementPolicy(requirement: EffectRequirementPolicyInput, context: EffectRequirementPolicyContext): EffectRequirementPolicyDecision;
|
|
9
39
|
export interface ShellCapabilityAnalysis {
|
|
10
40
|
command: string;
|
|
11
41
|
hookKind: CapabilityHookKind;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { isGitMetadataPath } from '../git-resource-identity.js';
|
|
3
4
|
import { matchesSensitivePath } from '../glob.js';
|
|
4
5
|
import { parseNetworkEndpoint } from '../network-endpoint.js';
|
|
5
6
|
import { canonicalPath, pathWithinRoot, resolveWorkspaceRootMatch } from '../path-utils.js';
|
|
@@ -9,6 +10,191 @@ import { boundaryVerifiedAllowEnabled } from './boundary-profile.js';
|
|
|
9
10
|
import { isGrantScopeTooBroad } from './grant.js';
|
|
10
11
|
import { broadGrantTargetsRequest, grantMatchesRequest } from './grant-match.js';
|
|
11
12
|
import { CAPABILITY_REQUEST_VERSION, } from './request.js';
|
|
13
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1']);
|
|
14
|
+
const DESTRUCTIVE_EFFECT_SIGNALS = new Set([
|
|
15
|
+
'git_history_destructive',
|
|
16
|
+
'git.destructive_history_write',
|
|
17
|
+
'git.destructive_worktree_write',
|
|
18
|
+
]);
|
|
19
|
+
const HIGH_STAKES_EFFECT_PATHS = [
|
|
20
|
+
/(?:^|[/\\])(?:\.env(?:\..*)?|credentials?(?:\.json)?|secrets?|authorized_keys|id_(?:rsa|dsa|ecdsa|ed25519)|[^/\\]+\.pem|\.ssh[/\\]config|\.zshrc|\.bashrc|\.git-credentials|\.npmrc|\.netrc|\.kube[/\\]config|\.docker[/\\]config\.json|\.gnupg(?:[/\\].*)?|\.pypirc)(?:$|[/\\])/i,
|
|
21
|
+
/(?:^|[/\\])application_default_credentials\.json$/i,
|
|
22
|
+
/(?:^|[/\\])\.config[/\\]gcloud[/\\](?:application_default_credentials\.json|credentials\.db|access_tokens\.db|legacy_credentials(?:[/\\].*)?)$/i,
|
|
23
|
+
/(?:^|[/\\])\.aws[/\\](?:credentials|config)$/i,
|
|
24
|
+
/(?:^|[/\\])\.azure[/\\](?:accessTokens\.json|azureProfile\.json|msal_token_cache\.(?:bin|json)|service_principal_entries\.json)$/i,
|
|
25
|
+
/(?:^|[/\\])\.terraform\.d[/\\]credentials\.tfrc\.json$/i,
|
|
26
|
+
/(?:^|[/\\])\.config[/\\]gh[/\\]hosts\.ya?ml$/i,
|
|
27
|
+
/(?:^|[/\\])\.oci[/\\]config$/i,
|
|
28
|
+
/(?:^|[/\\])\.config[/\\]doctl[/\\]config\.ya?ml$/i,
|
|
29
|
+
];
|
|
30
|
+
function effectDecision(effectDisposition, reason, matchedRule, signals) {
|
|
31
|
+
return {
|
|
32
|
+
effectDisposition,
|
|
33
|
+
outcome: effectDisposition === 'ask' ? 'require_approval' : 'allow',
|
|
34
|
+
reason,
|
|
35
|
+
signals: [...signals],
|
|
36
|
+
matchedRule,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function effectPathIsLocal(target, context) {
|
|
40
|
+
const resolved = resolveCapabilityPath(target, context.cwd);
|
|
41
|
+
return (resolveWorkspaceRootMatch(context.repoRoot, [...(context.trustedWorkspaceRoots ?? [])], resolved) !== null);
|
|
42
|
+
}
|
|
43
|
+
function effectPathIsHighStakes(target, context) {
|
|
44
|
+
const resolved = resolveCapabilityPath(target, context.cwd);
|
|
45
|
+
if (HIGH_STAKES_EFFECT_PATHS.some((pattern) => pattern.test(resolved))) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (context.protectedArtifactRoots?.some((root) => pathWithinRoot(canonicalPath(root), resolved))) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (isGitMetadataPath(resolved, context.repoRoot)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const workspaceMatch = resolveWorkspaceRootMatch(context.repoRoot, [...(context.trustedWorkspaceRoots ?? [])], resolved);
|
|
55
|
+
if (!workspaceMatch) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return matchesSensitivePath(workspaceMatch.relativePath.replaceAll('\\', '/'), [
|
|
59
|
+
...(context.sensitivePaths ?? []),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Policy for a lowered shell effect. It intentionally ignores executable
|
|
64
|
+
* command names, segment heads, command text, and config allowlists.
|
|
65
|
+
*/
|
|
66
|
+
export function evaluateEffectRequirementPolicy(requirement, context) {
|
|
67
|
+
const signals = [...requirement.evidence.signals];
|
|
68
|
+
const capabilityRequest = context.capabilityRequest ?? {
|
|
69
|
+
version: CAPABILITY_REQUEST_VERSION,
|
|
70
|
+
principal: { repoRoot: context.repoRoot },
|
|
71
|
+
action: requirement.action,
|
|
72
|
+
resource: requirement.resource,
|
|
73
|
+
context: {
|
|
74
|
+
cwd: context.cwd,
|
|
75
|
+
inputFingerprint: 'effect-plan',
|
|
76
|
+
hookKind: 'shell',
|
|
77
|
+
analysisBasis: [...requirement.evidence.basis],
|
|
78
|
+
},
|
|
79
|
+
evidence: {
|
|
80
|
+
level: requirement.evidence.level,
|
|
81
|
+
signals,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
if (signals.includes('forged_grant')) {
|
|
85
|
+
return effectDecision('ask', 'grant_forgery', 'forbid.grant_forgery', [
|
|
86
|
+
...signals,
|
|
87
|
+
'grant_forgery',
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
if (context.grants?.some((grant) => isGrantScopeTooBroad(grant) && broadGrantTargetsRequest(grant, capabilityRequest))) {
|
|
91
|
+
return effectDecision('ask', 'grant_scope_too_broad', 'forbid.broad_grant', [
|
|
92
|
+
...signals,
|
|
93
|
+
'grant_scope_too_broad',
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
const matchedGrant = requirement.resource.kind === 'executable'
|
|
97
|
+
? null
|
|
98
|
+
: findFreshGrant(capabilityRequest, [...(context.grants ?? [])]);
|
|
99
|
+
if (matchedGrant) {
|
|
100
|
+
return effectDecision('allow', 'capability_grant', 'grant.exact', grantDecisionSignals(capabilityRequest, matchedGrant));
|
|
101
|
+
}
|
|
102
|
+
if (requirement.evidence.level === 'indeterminate' ||
|
|
103
|
+
requirement.action === 'indeterminate' ||
|
|
104
|
+
requirement.tag === 'indeterminate') {
|
|
105
|
+
return effectDecision('ask', 'indeterminate_effect', 'effect.indeterminate', [
|
|
106
|
+
...signals,
|
|
107
|
+
'indeterminate',
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
if (signals.some((signal) => DESTRUCTIVE_EFFECT_SIGNALS.has(signal))) {
|
|
111
|
+
return effectDecision('ask', 'git_history_destructive', 'effect.destructive', signals);
|
|
112
|
+
}
|
|
113
|
+
if (signals.includes('belay_control_plane_command')) {
|
|
114
|
+
return effectDecision('allow', 'belay_control_plane_command', 'effect.belay_control_plane_command', signals);
|
|
115
|
+
}
|
|
116
|
+
switch (requirement.action) {
|
|
117
|
+
case 'fs.read':
|
|
118
|
+
if (requirement.resource.kind === 'path' &&
|
|
119
|
+
effectPathIsHighStakes(requirement.resource.path, context)) {
|
|
120
|
+
return effectDecision('ask', 'high_stakes_path', 'effect.fs_read_high_stakes', [
|
|
121
|
+
...signals,
|
|
122
|
+
'sensitive_path_read',
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
return effectDecision('allow', 'read_only', 'effect.fs_read', signals);
|
|
126
|
+
case 'fs.write':
|
|
127
|
+
if (requirement.resource.kind === 'package-cache') {
|
|
128
|
+
return effectDecision('allow_flagged', 'repo_local_mutation', 'effect.package_cache_write', signals);
|
|
129
|
+
}
|
|
130
|
+
if (requirement.resource.kind !== 'path') {
|
|
131
|
+
return effectDecision('ask', 'indeterminate_effect', 'effect.fs_write_unknown', signals);
|
|
132
|
+
}
|
|
133
|
+
if (effectPathIsHighStakes(requirement.resource.path, context)) {
|
|
134
|
+
return effectDecision('ask', 'high_stakes_path', 'effect.fs_write_high_stakes', [
|
|
135
|
+
...signals,
|
|
136
|
+
'tier1_catastrophic',
|
|
137
|
+
...(!effectPathIsLocal(requirement.resource.path, context)
|
|
138
|
+
? ['outside_repo_secret_credential_path']
|
|
139
|
+
: []),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
return effectPathIsLocal(requirement.resource.path, context)
|
|
143
|
+
? effectDecision('allow_flagged', 'repo_local_mutation', 'effect.fs_write_local', signals)
|
|
144
|
+
: effectDecision('ask', 'outside_repo_mutation', 'effect.fs_write_outside', signals);
|
|
145
|
+
case 'process.exec':
|
|
146
|
+
if (requirement.resource.kind !== 'executable') {
|
|
147
|
+
return effectDecision('ask', 'indeterminate_effect', 'effect.process_unknown', signals);
|
|
148
|
+
}
|
|
149
|
+
if (requirement.resource.operation === 'inspect') {
|
|
150
|
+
return effectDecision('allow', 'read_only', 'effect.process_inspect', signals);
|
|
151
|
+
}
|
|
152
|
+
if (requirement.resource.operation === 'spawn') {
|
|
153
|
+
return effectDecision('allow_flagged', 'repo_local_mutation', 'effect.process_spawn', signals);
|
|
154
|
+
}
|
|
155
|
+
return effectDecision('ask', 'high_stakes_path', 'effect.process_signal', signals);
|
|
156
|
+
case 'network.connect': {
|
|
157
|
+
if (requirement.tag === 'network.acquire' ||
|
|
158
|
+
requirement.resource.kind !== 'network' ||
|
|
159
|
+
!requirement.resource.mode ||
|
|
160
|
+
!requirement.resource.payload) {
|
|
161
|
+
return effectDecision('ask', 'external_effect', 'effect.network_unknown', signals);
|
|
162
|
+
}
|
|
163
|
+
const loopback = LOOPBACK_HOSTS.has(requirement.resource.host.replace(/^\[|\]$/g, '').toLowerCase());
|
|
164
|
+
if (requirement.resource.payload === 'secret') {
|
|
165
|
+
return effectDecision('ask', 'high_stakes_path', 'effect.network_secret_payload', [
|
|
166
|
+
...signals,
|
|
167
|
+
'secret_payload_send',
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
if (loopback && requirement.resource.mode === 'mutate') {
|
|
171
|
+
return effectDecision('allow_flagged', 'repo_local_mutation', 'effect.network_loopback_mutation', signals);
|
|
172
|
+
}
|
|
173
|
+
if (requirement.resource.mode === 'read' && requirement.resource.payload === 'none') {
|
|
174
|
+
return effectDecision('allow', 'read_only', 'effect.network_read', signals);
|
|
175
|
+
}
|
|
176
|
+
return effectDecision('ask', 'external_effect', 'effect.network_remote_mutation', signals);
|
|
177
|
+
}
|
|
178
|
+
case 'git.ref.write':
|
|
179
|
+
if (requirement.resource.kind !== 'git-ref' || !requirement.resource.scope) {
|
|
180
|
+
return effectDecision('ask', 'indeterminate_effect', 'effect.git_ref_unknown', signals);
|
|
181
|
+
}
|
|
182
|
+
if (requirement.resource.scope === 'remote') {
|
|
183
|
+
return effectDecision('ask', 'external_effect', 'effect.git_ref_remote', signals);
|
|
184
|
+
}
|
|
185
|
+
if (requirement.resource.repoPath &&
|
|
186
|
+
!effectPathIsLocal(requirement.resource.repoPath, context)) {
|
|
187
|
+
return effectDecision('ask', 'outside_repo_mutation', 'effect.git_ref_outside', signals);
|
|
188
|
+
}
|
|
189
|
+
return effectDecision('allow_flagged', 'repo_local_mutation', 'effect.git_ref_local', signals);
|
|
190
|
+
case 'secret.read':
|
|
191
|
+
return effectDecision('ask', 'high_stakes_path', 'effect.secret_read', signals);
|
|
192
|
+
case 'control_plane.write':
|
|
193
|
+
return effectDecision('ask', 'control_plane_mutation', 'effect.control_plane_write', signals);
|
|
194
|
+
default:
|
|
195
|
+
return effectDecision('ask', 'indeterminate_effect', 'effect.unknown', signals);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
12
198
|
function enrichAuthWithMaterializedGrants(request, config, auth) {
|
|
13
199
|
if (!auth?.attestation || !boundaryVerifiedAllowEnabled(auth.attestation)) {
|
|
14
200
|
return auth;
|
|
@@ -2,6 +2,10 @@ export declare const CAPABILITY_REQUEST_VERSION: 1;
|
|
|
2
2
|
export type CapabilityAction = 'fs.read' | 'fs.write' | 'process.exec' | 'network.connect' | 'secret.read' | 'git.ref.write' | 'control_plane.write' | 'indeterminate';
|
|
3
3
|
export type CapabilityEvidenceLevel = 'certain' | 'possible' | 'indeterminate';
|
|
4
4
|
export type CapabilityHookKind = 'shell' | 'tool' | 'subagent';
|
|
5
|
+
export type NetworkMode = 'read' | 'mutate' | 'ambiguous';
|
|
6
|
+
export type NetworkPayload = 'none' | 'present' | 'secret';
|
|
7
|
+
export type ProcessOperation = 'inspect' | 'spawn' | 'signal';
|
|
8
|
+
export type GitRefScope = 'local' | 'remote';
|
|
5
9
|
export interface CapabilityPrincipal {
|
|
6
10
|
adapter?: string;
|
|
7
11
|
repoRoot: string;
|
|
@@ -15,15 +19,20 @@ export type CapabilityResource = {
|
|
|
15
19
|
host: string;
|
|
16
20
|
port?: number;
|
|
17
21
|
protocol?: string;
|
|
22
|
+
mode?: NetworkMode;
|
|
23
|
+
payload?: NetworkPayload;
|
|
18
24
|
} | {
|
|
19
25
|
kind: 'executable';
|
|
20
26
|
command: string;
|
|
27
|
+
operation?: ProcessOperation;
|
|
21
28
|
} | {
|
|
22
29
|
kind: 'package-cache';
|
|
23
30
|
manager: 'npm' | 'pnpm';
|
|
24
31
|
} | {
|
|
25
32
|
kind: 'git-ref';
|
|
26
33
|
ref: string;
|
|
34
|
+
scope?: GitRefScope;
|
|
35
|
+
repoPath?: string;
|
|
27
36
|
} | {
|
|
28
37
|
kind: 'unknown';
|
|
29
38
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { evaluateSegmentShellPolicy, type SegmentShellPolicyInput, } from '../verdict/shell-policy.js';
|
|
2
1
|
export { BOUNDARY_PROFILE_L3_POLICY, checkGatedActionLimits } from './limits.js';
|
|
3
2
|
export { type CapabilityAuthorizationMetadata, policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
|
|
4
3
|
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, buildSubagentCapabilityRequest, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, type FileMutationCapabilityAnalysis, policyDecisionRequiresAsk, type ShellCapabilityAnalysis, type SubagentCapabilityAnalysis, } from './policy-engine.js';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { evaluateSegmentShellPolicy, } from '../verdict/shell-policy.js';
|
|
2
1
|
export { BOUNDARY_PROFILE_L3_POLICY, checkGatedActionLimits } from './limits.js';
|
|
3
2
|
export { policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
|
|
4
3
|
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, buildSubagentCapabilityRequest, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, policyDecisionRequiresAsk, } from './policy-engine.js';
|
package/dist/core/config.js
CHANGED
|
@@ -937,8 +937,6 @@ export function scrubOptionsFromConfig(config) {
|
|
|
937
937
|
export function classifierOptionsFromConfig(config) {
|
|
938
938
|
return {
|
|
939
939
|
strictChains: config.classifier.strictChains,
|
|
940
|
-
customExternalCommands: config.overrides.external,
|
|
941
|
-
customAllowCommands: config.overrides.allow,
|
|
942
940
|
sensitivePaths: config.classifier.sensitivePaths,
|
|
943
941
|
unknownLocalEffect: config.policy.unknownLocalEffect,
|
|
944
942
|
unparseableShell: config.policy.unparseableShell,
|
|
@@ -72,7 +72,11 @@ function delegateExecNode(peel, innerRecipe, segmentHead, resolvedLocalPath) {
|
|
|
72
72
|
{
|
|
73
73
|
tag: 'process.exec',
|
|
74
74
|
action: 'process.exec',
|
|
75
|
-
resource: {
|
|
75
|
+
resource: {
|
|
76
|
+
kind: 'executable',
|
|
77
|
+
command: resolvedLocalPath ?? segmentHead,
|
|
78
|
+
operation: 'spawn',
|
|
79
|
+
},
|
|
76
80
|
evidence: {
|
|
77
81
|
level: resolvedLocalPath ? 'certain' : peel.forceAcquire ? 'possible' : 'certain',
|
|
78
82
|
signals: [
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export type { GitRefScope, NetworkMode, NetworkPayload, ProcessOperation, } from '../capability/request.js';
|
|
1
2
|
export { effectPlanAuditFields, hashEffectPlan } from './audit.js';
|
|
2
3
|
export { buildCapabilityEffectPlan, buildEffectPlan, buildPackageExecEffectNode, collectRequirements, flattenRequirementsToCapabilityRequests, } from './build.js';
|
|
3
4
|
export { mergeEffectPlans, mergeRequirements, normalizeEffectTags } from './normalize.js';
|
|
4
5
|
export { innerRecipeFromPeel, isPackageExecLauncher, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
|
|
5
6
|
export { evaluatePackageExecSegment } from './package-exec-eval.js';
|
|
6
7
|
export { capabilityRequestsFromEffectPlan, capabilityRequestsToEffectRequirements, effectPlanPolicyLegacyReason, effectPlanPolicyRequiresAsk, evaluateEffectPlanPolicy, } from './policy.js';
|
|
7
|
-
export type {
|
|
8
|
+
export type { BuildShellEffectPlanParams, ShellEffectRequirement, ShellEffectResource, ShellEffectSegment, } from './shell-build.js';
|
|
9
|
+
export { buildShellEffectPlan } from './shell-build.js';
|
|
10
|
+
export type { LowerShellEffectPlanParams } from './shell-lower.js';
|
|
11
|
+
export { lowerShellEffectPlan } from './shell-lower.js';
|
|
12
|
+
export type { AnalysisCompleteness, EffectEvidence, EffectNode, EffectPlan, EffectPlanDisposition, EffectPlanPolicyProjection, EffectProvenance, EffectRequirement, EffectTag, ExecEffectNode, LauncherEffectNode, LauncherPhase, MergeEffectNode, PackageExecLauncher, } from './types.js';
|
|
8
13
|
export { EFFECT_PLAN_VERSION } from './types.js';
|
|
@@ -4,4 +4,6 @@ export { mergeEffectPlans, mergeRequirements, normalizeEffectTags } from './norm
|
|
|
4
4
|
export { innerRecipeFromPeel, isPackageExecLauncher, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
|
|
5
5
|
export { evaluatePackageExecSegment } from './package-exec-eval.js';
|
|
6
6
|
export { capabilityRequestsFromEffectPlan, capabilityRequestsToEffectRequirements, effectPlanPolicyLegacyReason, effectPlanPolicyRequiresAsk, evaluateEffectPlanPolicy, } from './policy.js';
|
|
7
|
+
export { buildShellEffectPlan } from './shell-build.js';
|
|
8
|
+
export { lowerShellEffectPlan } from './shell-lower.js';
|
|
7
9
|
export { EFFECT_PLAN_VERSION } from './types.js';
|
|
@@ -77,13 +77,19 @@ function resourceKey(resource) {
|
|
|
77
77
|
case 'path':
|
|
78
78
|
return resource.path;
|
|
79
79
|
case 'network':
|
|
80
|
-
return
|
|
80
|
+
return [
|
|
81
|
+
resource.host,
|
|
82
|
+
resource.port ?? '',
|
|
83
|
+
resource.protocol ?? '',
|
|
84
|
+
resource.mode ?? '',
|
|
85
|
+
resource.payload ?? '',
|
|
86
|
+
].join(':');
|
|
81
87
|
case 'executable':
|
|
82
|
-
return resource.command
|
|
88
|
+
return `${resource.command}:${resource.operation ?? ''}`;
|
|
83
89
|
case 'package-cache':
|
|
84
90
|
return resource.manager;
|
|
85
91
|
case 'git-ref':
|
|
86
|
-
return resource.ref
|
|
92
|
+
return `${resource.ref}:${resource.scope ?? ''}:${resource.repoPath ?? ''}`;
|
|
87
93
|
case 'unknown':
|
|
88
94
|
return 'unknown';
|
|
89
95
|
default:
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { PolicyDecision } from '../capability/policy-types.js';
|
|
2
2
|
import type { CapabilityRequestV1 } from '../capability/request.js';
|
|
3
3
|
import type { VerdictContext } from '../verdict/types.js';
|
|
4
|
-
import type { EffectPlan, EffectRequirement } from './types.js';
|
|
4
|
+
import type { EffectPlan, EffectPlanPolicyProjection, EffectRequirement } from './types.js';
|
|
5
5
|
export declare function capabilityRequestsToEffectRequirements(requests: readonly CapabilityRequestV1[]): EffectRequirement[];
|
|
6
6
|
export declare function capabilityRequestsFromEffectPlan(plan: EffectPlan, context: VerdictContext): CapabilityRequestV1[];
|
|
7
7
|
export declare function evaluateEffectPlanPolicy(plan: EffectPlan, context: VerdictContext): {
|
|
8
8
|
capabilityRequests: CapabilityRequestV1[];
|
|
9
9
|
decisions: PolicyDecision[];
|
|
10
10
|
authorizationDecision: PolicyDecision;
|
|
11
|
+
projection: EffectPlanPolicyProjection;
|
|
11
12
|
};
|
|
12
13
|
export declare function effectPlanPolicyRequiresAsk(decision: PolicyDecision): boolean;
|
|
13
14
|
export declare function effectPlanPolicyLegacyReason(decision: PolicyDecision): string;
|