@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.
Files changed (81) hide show
  1. package/README.md +19 -19
  2. package/dist/adapters/shared/gate-runtime.js +23 -5
  3. package/dist/bundle/claude-runtime.mjs +4909 -3504
  4. package/dist/bundle/codex-runtime.mjs +5050 -3645
  5. package/dist/bundle/cursor-runtime.mjs +5050 -3645
  6. package/dist/commands/doctor.js +24 -21
  7. package/dist/commands/dogfood.js +1 -1
  8. package/dist/commands/explain.js +2 -2
  9. package/dist/commands/harvest.d.ts +0 -1
  10. package/dist/commands/harvest.js +1 -4
  11. package/dist/commands/metrics.js +39 -4
  12. package/dist/commands/quality.js +3 -9
  13. package/dist/commands/status.js +1 -1
  14. package/dist/conformance/guarantee-table.js +133 -11
  15. package/dist/conformance/types.d.ts +1 -0
  16. package/dist/core/audit-metrics.d.ts +24 -0
  17. package/dist/core/audit-metrics.js +82 -16
  18. package/dist/core/audit-types.d.ts +3 -0
  19. package/dist/core/capability/index.d.ts +1 -1
  20. package/dist/core/capability/policy-engine.d.ts +31 -1
  21. package/dist/core/capability/policy-engine.js +186 -0
  22. package/dist/core/capability/request.d.ts +9 -0
  23. package/dist/core/capability/resolver.d.ts +0 -1
  24. package/dist/core/capability/resolver.js +0 -1
  25. package/dist/core/config.js +0 -2
  26. package/dist/core/effect-ir/audit.js +1 -0
  27. package/dist/core/effect-ir/build.js +5 -1
  28. package/dist/core/effect-ir/index.d.ts +6 -1
  29. package/dist/core/effect-ir/index.js +2 -0
  30. package/dist/core/effect-ir/normalize.js +9 -3
  31. package/dist/core/effect-ir/policy.d.ts +2 -1
  32. package/dist/core/effect-ir/policy.js +93 -6
  33. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  34. package/dist/core/effect-ir/shell-build.js +77 -0
  35. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  36. package/dist/core/effect-ir/shell-lower.js +1509 -0
  37. package/dist/core/effect-ir/types.d.ts +9 -2
  38. package/dist/core/gate-engine.js +63 -23
  39. package/dist/core/git-resource-identity.d.ts +26 -0
  40. package/dist/core/git-resource-identity.js +284 -0
  41. package/dist/core/harvest.d.ts +3 -7
  42. package/dist/core/harvest.js +4 -27
  43. package/dist/core/index.d.ts +2 -0
  44. package/dist/core/index.js +1 -0
  45. package/dist/core/path-utils.js +42 -23
  46. package/dist/core/shell-substitution.d.ts +1 -0
  47. package/dist/core/shell-substitution.js +61 -0
  48. package/dist/core/standing-allow.d.ts +1 -2
  49. package/dist/core/standing-allow.js +3 -20
  50. package/dist/core/types.d.ts +4 -1
  51. package/dist/core/verdict/adapter.js +15 -15
  52. package/dist/core/verdict/containment.js +4 -0
  53. package/dist/core/verdict/egress-classify.d.ts +12 -0
  54. package/dist/core/verdict/egress-classify.js +746 -0
  55. package/dist/core/verdict/git-classifier.d.ts +12 -0
  56. package/dist/core/verdict/git-classifier.js +320 -0
  57. package/dist/core/verdict/launcher-resolve.js +77 -17
  58. package/dist/core/verdict/parser.d.ts +2 -0
  59. package/dist/core/verdict/parser.js +95 -1
  60. package/dist/core/verdict/types.d.ts +2 -3
  61. package/dist/core/verdict/verdict.js +103 -952
  62. package/dist/corpus/must-allow-commands.d.ts +2 -1
  63. package/dist/corpus/must-allow-commands.js +2 -1
  64. package/dist/corpus/runtime-match.d.ts +2 -1
  65. package/dist/corpus/runtime-match.js +2 -1
  66. package/dist/corpus/types.d.ts +2 -2
  67. package/dist/operational-insights.d.ts +3 -0
  68. package/dist/operational-insights.js +7 -3
  69. package/dist/runtime-provenance.d.ts +8 -0
  70. package/dist/runtime-provenance.js +34 -0
  71. package/dist/templates.js +7 -2
  72. package/dist/types.d.ts +3 -0
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/package.json +2 -2
  76. package/dist/core/verdict/overrides.d.ts +0 -7
  77. package/dist/core/verdict/overrides.js +0 -37
  78. package/dist/core/verdict/shell-policy.d.ts +0 -26
  79. package/dist/core/verdict/shell-policy.js +0 -40
  80. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  81. 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 (gateEvents === 0) {
98
- notes.push('No gate events yetrun normal agent work, then re-check metrics.');
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 (wouldBlockRate === 0) {
101
- if (gateEvents >= MIN_GATE_EVENTS_FOR_ENFORCE) {
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 ${gateEvents} gate event(s) recorded — collect at least ${MIN_GATE_EVENTS_FOR_ENFORCE} before enforce.`);
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(`${wouldBlockCount} would-block event(s) (${(wouldBlockRate * 100).toFixed(1)}% of gate traffic; classifier-quality ${(classifierWouldBlockRate * 100).toFixed(1)}%). Review top summaries and add overrides.allow where appropriate.`);
111
- if (approvalRecordedCount > 0) {
112
- notes.push(`${approvalRecordedCount} approval(s) recorded — these likely indicate actions operators wanted.`);
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 add overrides.allow for legitimate commands before switching to enforce.');
177
+ notes.push('Review top would-block summaries and correct EffectPlan semantics or resource scope before switching to enforce.');
116
178
  }
117
- if (classifierWouldBlockRate < 0.05 && gateEvents >= 20 && availabilityAsks.total === 0) {
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 (availabilityAsks.total > 0) {
193
+ if (cohortAvailabilityAsks.total > 0) {
130
194
  readyForEnforce = false;
131
- notes.push(`${availabilityAsks.total} availability-caused ask(s) — tune infrastructure before corpus overrides.`);
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 (repeatedFingerprintAsks.length > 0) {
135
- notes.push(`${repeatedFingerprintAsks.length} repeated fingerprint ask pattern(s) — review standing-allow / cache candidates.`);
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 (noisyRuleCandidates.length > 0) {
138
- notes.push(`${noisyRuleCandidates.length} noisy rule candidate(s) — high deny-then-approve rate.`);
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';
@@ -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,
@@ -12,6 +12,7 @@ function auditableResource(resource) {
12
12
  return {
13
13
  kind: 'executable',
14
14
  commandHash: hashValue(resource.command),
15
+ ...(resource.operation ? { operation: resource.operation } : {}),
15
16
  };
16
17
  }
17
18
  return resource;
@@ -72,7 +72,11 @@ function delegateExecNode(peel, innerRecipe, segmentHead, resolvedLocalPath) {
72
72
  {
73
73
  tag: 'process.exec',
74
74
  action: 'process.exec',
75
- resource: { kind: 'executable', command: resolvedLocalPath ?? segmentHead },
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 { EffectEvidence, EffectNode, EffectPlan, EffectProvenance, EffectRequirement, EffectTag, ExecEffectNode, LauncherEffectNode, LauncherPhase, MergeEffectNode, PackageExecLauncher, } from './types.js';
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 `${resource.host}:${resource.port ?? ''}:${resource.protocol ?? ''}`;
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;