@guilz-dev/belay 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +20 -1
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/shared/gate-runtime.d.ts +7 -0
  4. package/dist/adapters/shared/gate-runtime.js +420 -220
  5. package/dist/bundle/claude-runtime.mjs +13215 -8864
  6. package/dist/bundle/codex-runtime.mjs +10771 -6453
  7. package/dist/bundle/cursor-runtime.mjs +10797 -6484
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +51 -0
  11. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  12. package/dist/commands/recovery-checkpoints.js +347 -0
  13. package/dist/commands/revoke.js +19 -7
  14. package/dist/commands/status.js +7 -0
  15. package/dist/config-io.js +49 -18
  16. package/dist/conformance/guarantee-table.js +6 -0
  17. package/dist/conformance/types.d.ts +1 -0
  18. package/dist/core/approval-replay.d.ts +1 -1
  19. package/dist/core/approval-replay.js +4 -1
  20. package/dist/core/approval-service.d.ts +6 -1
  21. package/dist/core/approval-service.js +86 -27
  22. package/dist/core/approval.d.ts +2 -0
  23. package/dist/core/approval.js +6 -3
  24. package/dist/core/audit-types.d.ts +13 -0
  25. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  26. package/dist/core/capability/approval-state-mutation.js +76 -3
  27. package/dist/core/capability/approval-v3.d.ts +18 -1
  28. package/dist/core/capability/approval-v3.js +63 -23
  29. package/dist/core/capability/attestation.d.ts +3 -0
  30. package/dist/core/capability/attestation.js +7 -0
  31. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  32. package/dist/core/capability/boundary-driver-container.js +101 -34
  33. package/dist/core/capability/boundary-driver.js +1 -0
  34. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  35. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  36. package/dist/core/capability/boundary-run.d.ts +25 -0
  37. package/dist/core/capability/boundary-run.js +37 -9
  38. package/dist/core/capability/boundary-session.d.ts +4 -0
  39. package/dist/core/capability/boundary-session.js +9 -0
  40. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  41. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  42. package/dist/core/capability/gate-policy-shadow.js +23 -1
  43. package/dist/core/capability/grant-consumption.d.ts +4 -0
  44. package/dist/core/capability/grant-consumption.js +11 -0
  45. package/dist/core/capability/grant-lease.d.ts +36 -1
  46. package/dist/core/capability/grant-lease.js +213 -16
  47. package/dist/core/capability/grant-match.js +3 -0
  48. package/dist/core/capability/index.d.ts +1 -1
  49. package/dist/core/capability/index.js +1 -1
  50. package/dist/core/capability/policy-engine.d.ts +7 -0
  51. package/dist/core/capability/policy-engine.js +202 -15
  52. package/dist/core/capability/request.d.ts +3 -0
  53. package/dist/core/config.d.ts +20 -0
  54. package/dist/core/config.js +62 -0
  55. package/dist/core/effect-ir/audit.d.ts +9 -0
  56. package/dist/core/effect-ir/audit.js +86 -0
  57. package/dist/core/effect-ir/build.d.ts +31 -0
  58. package/dist/core/effect-ir/build.js +277 -0
  59. package/dist/core/effect-ir/index.d.ts +8 -0
  60. package/dist/core/effect-ir/index.js +7 -0
  61. package/dist/core/effect-ir/normalize.d.ts +4 -0
  62. package/dist/core/effect-ir/normalize.js +104 -0
  63. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  64. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  65. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  66. package/dist/core/effect-ir/package-exec.js +292 -0
  67. package/dist/core/effect-ir/policy.d.ts +13 -0
  68. package/dist/core/effect-ir/policy.js +77 -0
  69. package/dist/core/effect-ir/types.d.ts +55 -0
  70. package/dist/core/effect-ir/types.js +1 -0
  71. package/dist/core/egress/allowlist.d.ts +1 -0
  72. package/dist/core/egress/allowlist.js +8 -0
  73. package/dist/core/egress-approval.js +45 -51
  74. package/dist/core/gate-contract.d.ts +2 -0
  75. package/dist/core/gate-contract.js +2 -0
  76. package/dist/core/gate-engine.js +26 -9
  77. package/dist/core/index.d.ts +3 -1
  78. package/dist/core/index.js +2 -1
  79. package/dist/core/judge-cloud-consent.js +29 -15
  80. package/dist/core/network-endpoint.d.ts +10 -0
  81. package/dist/core/network-endpoint.js +51 -0
  82. package/dist/core/process-runner.d.ts +10 -0
  83. package/dist/core/process-runner.js +124 -0
  84. package/dist/core/recover-advice.js +2 -1
  85. package/dist/core/recovery/artifact-store.d.ts +29 -0
  86. package/dist/core/recovery/artifact-store.js +336 -0
  87. package/dist/core/recovery/checkpoint.d.ts +25 -0
  88. package/dist/core/recovery/checkpoint.js +260 -0
  89. package/dist/core/recovery/index.d.ts +3 -1
  90. package/dist/core/recovery/index.js +2 -0
  91. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  92. package/dist/core/recovery/operator-guidance.js +39 -0
  93. package/dist/core/recovery/reconcile.d.ts +3 -0
  94. package/dist/core/recovery/reconcile.js +48 -0
  95. package/dist/core/recovery/resource-identity.d.ts +4 -0
  96. package/dist/core/recovery/resource-identity.js +34 -0
  97. package/dist/core/recovery/restore.d.ts +13 -0
  98. package/dist/core/recovery/restore.js +114 -0
  99. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  100. package/dist/core/recovery/snapshot-node.js +261 -0
  101. package/dist/core/recovery/types.d.ts +85 -1
  102. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  103. package/dist/core/transactional/apply-observed-changes.js +275 -0
  104. package/dist/core/transactional/backend-selector.d.ts +8 -0
  105. package/dist/core/transactional/backend-selector.js +118 -0
  106. package/dist/core/transactional/backend.d.ts +55 -0
  107. package/dist/core/transactional/backend.js +1 -0
  108. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  109. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  110. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  111. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  112. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  113. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  114. package/dist/core/transactional/file-clone.d.ts +15 -0
  115. package/dist/core/transactional/file-clone.js +139 -0
  116. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  117. package/dist/core/transactional/file-tree-path.js +50 -0
  118. package/dist/core/transactional/file-tree.d.ts +41 -0
  119. package/dist/core/transactional/file-tree.js +186 -0
  120. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  121. package/dist/core/transactional/git-worktree-backend.js +50 -0
  122. package/dist/core/transactional/git-worktree.d.ts +9 -11
  123. package/dist/core/transactional/git-worktree.js +24 -119
  124. package/dist/core/transactional/index.d.ts +11 -0
  125. package/dist/core/transactional/index.js +10 -0
  126. package/dist/core/transactional/reasons.js +1 -0
  127. package/dist/core/transactional/runner.js +157 -25
  128. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  129. package/dist/core/transactional/snapshot-node.js +87 -0
  130. package/dist/core/transactional/types.d.ts +17 -1
  131. package/dist/core/types.d.ts +10 -1
  132. package/dist/core/verdict/adapter.js +8 -0
  133. package/dist/core/verdict/launcher-resolve.js +2 -20
  134. package/dist/core/verdict/shell-policy.d.ts +1 -0
  135. package/dist/core/verdict/shell-policy.js +1 -1
  136. package/dist/core/verdict/types.d.ts +5 -0
  137. package/dist/core/verdict/verdict.js +37 -20
  138. package/dist/types.d.ts +11 -0
  139. package/dist/version.d.ts +1 -1
  140. package/dist/version.js +1 -1
  141. package/package.json +1 -1
  142. package/skills/belay/SKILL.md +8 -3
  143. package/skills/belay/belay-approve.md +10 -7
@@ -0,0 +1,152 @@
1
+ import { policyDecisionToLegacyReason } from '../capability/policy-bridge.js';
2
+ import { policyDecisionRequiresAsk } from '../capability/policy-engine.js';
3
+ import { cwdRelative } from '../verdict/containment.js';
4
+ import { verdictFingerprint } from '../verdict/fingerprint.js';
5
+ import { redactCommand } from '../verdict/parser.js';
6
+ import { buildEffectPlan } from './build.js';
7
+ import { innerRecipeFromPeel, peelPackageExecArgv } from './package-exec.js';
8
+ import { capabilityRequestsToEffectRequirements, evaluateEffectPlanPolicy } from './policy.js';
9
+ function askFromEffectPlan(params) {
10
+ const { capabilityRequests, authorizationDecision, decisions } = evaluateEffectPlanPolicy(params.plan, params.context);
11
+ const legacyReason = policyDecisionRequiresAsk(authorizationDecision)
12
+ ? policyDecisionToLegacyReason(authorizationDecision)
13
+ : params.reason;
14
+ return {
15
+ permission: 'ask',
16
+ location: params.inner?.location ?? 'unknown',
17
+ opacity: params.plan.opacity,
18
+ effect: params.inner?.effect ?? 'unknown',
19
+ confidence: 'deterministic',
20
+ reason: legacyReason,
21
+ signals: [...new Set([...params.signals, ...params.plan.signals])],
22
+ capabilityRequests,
23
+ authorizationDecision,
24
+ effectPlan: params.plan,
25
+ effectPlanPolicyDecisions: decisions,
26
+ };
27
+ }
28
+ function allowFromEffectPlan(params) {
29
+ const { capabilityRequests, authorizationDecision, decisions } = evaluateEffectPlanPolicy(params.plan, params.context);
30
+ return {
31
+ ...params.inner,
32
+ opacity: params.plan.opacity,
33
+ signals: [...new Set([...params.inner.signals, ...params.signals, ...params.plan.signals])],
34
+ capabilityRequests,
35
+ authorizationDecision,
36
+ effectPlan: params.plan,
37
+ effectPlanPolicyDecisions: decisions,
38
+ };
39
+ }
40
+ export async function evaluatePackageExecSegment(params) {
41
+ const peel = peelPackageExecArgv(params.peeled);
42
+ if (!peel) {
43
+ return {
44
+ permission: 'ask',
45
+ location: 'unknown',
46
+ opacity: 'opaque',
47
+ effect: 'unknown',
48
+ confidence: 'deterministic',
49
+ reason: 'launcher_unresolved',
50
+ signals: ['launcher_unresolved'],
51
+ };
52
+ }
53
+ const relative = cwdRelative(params.context.repoRoot, params.context.cwd);
54
+ const inputFingerprint = verdictFingerprint(relative, redactCommand(params.command));
55
+ if (peel.opaque) {
56
+ const plan = buildEffectPlan({
57
+ tokens: params.peeled,
58
+ cwd: params.context.cwd,
59
+ repoRoot: params.context.repoRoot,
60
+ inputFingerprint,
61
+ }) ??
62
+ {
63
+ version: 1,
64
+ root: { kind: 'merge', children: [] },
65
+ inputFingerprint,
66
+ opacity: 'opaque',
67
+ disposition: 'effects',
68
+ completeness: 'partial',
69
+ signals: [peel.reason],
70
+ };
71
+ return askFromEffectPlan({
72
+ plan,
73
+ context: params.context,
74
+ reason: peel.reason,
75
+ signals: [peel.reason, ...peel.signals],
76
+ });
77
+ }
78
+ const innerRecipe = innerRecipeFromPeel(peel);
79
+ let innerVerdict = null;
80
+ let innerRequirements;
81
+ if (innerRecipe) {
82
+ innerVerdict = await params.evaluateInner(innerRecipe);
83
+ if (innerVerdict.capabilityRequests?.length) {
84
+ innerRequirements = capabilityRequestsToEffectRequirements(innerVerdict.capabilityRequests);
85
+ }
86
+ else if (innerVerdict.permission === 'allow' && innerVerdict.effect === 'read_only') {
87
+ innerRequirements = [];
88
+ }
89
+ }
90
+ const plan = buildEffectPlan({
91
+ tokens: params.peeled,
92
+ cwd: params.context.cwd,
93
+ repoRoot: params.context.repoRoot,
94
+ inputFingerprint,
95
+ innerRequirements,
96
+ });
97
+ if (!plan) {
98
+ return {
99
+ permission: 'ask',
100
+ location: innerVerdict?.location ?? 'unknown',
101
+ opacity: 'opaque',
102
+ effect: innerVerdict?.effect ?? 'unknown',
103
+ confidence: 'deterministic',
104
+ reason: peel.reason,
105
+ signals: [peel.reason, ...peel.signals],
106
+ capabilityRequests: innerVerdict?.capabilityRequests,
107
+ authorizationDecision: innerVerdict?.authorizationDecision,
108
+ };
109
+ }
110
+ const { capabilityRequests, authorizationDecision, decisions } = evaluateEffectPlanPolicy(plan, params.context);
111
+ if (policyDecisionRequiresAsk(authorizationDecision)) {
112
+ return askFromEffectPlan({
113
+ plan,
114
+ context: params.context,
115
+ reason: policyDecisionToLegacyReason(authorizationDecision),
116
+ signals: [peel.reason, ...peel.signals],
117
+ inner: innerVerdict ?? undefined,
118
+ });
119
+ }
120
+ if (innerVerdict?.permission === 'ask') {
121
+ return {
122
+ ...innerVerdict,
123
+ opacity: plan.opacity,
124
+ signals: [...new Set([...innerVerdict.signals, peel.reason, ...plan.signals])],
125
+ capabilityRequests,
126
+ authorizationDecision,
127
+ effectPlan: plan,
128
+ effectPlanPolicyDecisions: decisions,
129
+ };
130
+ }
131
+ if (!innerVerdict) {
132
+ return {
133
+ permission: 'allow',
134
+ location: 'repo_local',
135
+ opacity: plan.opacity,
136
+ effect: 'read_only',
137
+ confidence: 'deterministic',
138
+ reason: peel.reason,
139
+ signals: [peel.reason, ...plan.signals],
140
+ capabilityRequests,
141
+ authorizationDecision,
142
+ effectPlan: plan,
143
+ effectPlanPolicyDecisions: decisions,
144
+ };
145
+ }
146
+ return allowFromEffectPlan({
147
+ plan,
148
+ context: params.context,
149
+ inner: innerVerdict,
150
+ signals: [peel.reason, ...peel.signals],
151
+ });
152
+ }
@@ -0,0 +1,29 @@
1
+ import type { PackageExecLauncher } from './types.js';
2
+ export interface PackageExecPeelResult {
3
+ launcher: PackageExecLauncher;
4
+ innerTokens: string[];
5
+ acquisitionSpecs: string[];
6
+ opaque: boolean;
7
+ reason: string;
8
+ forceAcquire: boolean;
9
+ signals: string[];
10
+ }
11
+ export type PackageAcquisitionSource = {
12
+ kind: 'local';
13
+ } | {
14
+ kind: 'registry';
15
+ } | {
16
+ kind: 'network';
17
+ host: string;
18
+ port?: number;
19
+ protocol: string;
20
+ };
21
+ export declare function isPackageExecLauncher(tokens: string[]): boolean;
22
+ export declare function peelPackageExecArgv(tokens: string[]): PackageExecPeelResult | null;
23
+ export declare function classifyPackageAcquisitionSpec(spec: string): PackageAcquisitionSource;
24
+ export declare function resolveLocalBin(binName: string, cwd: string, repoRoot: string): {
25
+ path: string;
26
+ proven: true;
27
+ } | null;
28
+ export declare function innerRecipeFromPeel(peel: PackageExecPeelResult): string | null;
29
+ export declare function isPackageExecHeadToken(head: string): boolean;
@@ -0,0 +1,292 @@
1
+ import { existsSync, realpathSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseNetworkEndpoint } from '../network-endpoint.js';
4
+ import { pathWithinRoot } from '../path-utils.js';
5
+ const READ_ONLY_SUFFIXES = new Set(['--version', '-v', '--help', '-h']);
6
+ function isPackageExecHead(head) {
7
+ return head === 'npx' || head === 'npm' || head === 'pnpm';
8
+ }
9
+ export function isPackageExecLauncher(tokens) {
10
+ const head = tokens[0] ?? '';
11
+ if (head === 'npx') {
12
+ return true;
13
+ }
14
+ if (head === 'npm' && tokens[1] === 'exec') {
15
+ return true;
16
+ }
17
+ if (head === 'pnpm' && tokens[1] === 'dlx') {
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+ export function peelPackageExecArgv(tokens) {
23
+ const head = tokens[0] ?? '';
24
+ if (head === 'npx') {
25
+ return peelNpx(tokens);
26
+ }
27
+ if (head === 'npm' && tokens[1] === 'exec') {
28
+ return peelNpmExec(tokens);
29
+ }
30
+ if (head === 'pnpm' && tokens[1] === 'dlx') {
31
+ return peelPnpmDlx(tokens);
32
+ }
33
+ return null;
34
+ }
35
+ function peelNpx(tokens) {
36
+ const signals = ['package_exec:npx'];
37
+ const acquisitionSpecs = [];
38
+ let index = 1;
39
+ let forceAcquire = false;
40
+ while (index < tokens.length) {
41
+ const token = tokens[index] ?? '';
42
+ if (index === 1 && tokens.length === 2 && READ_ONLY_SUFFIXES.has(token)) {
43
+ return {
44
+ launcher: 'npx',
45
+ innerTokens: [token],
46
+ acquisitionSpecs,
47
+ opaque: false,
48
+ reason: 'npx_wrapper_readonly',
49
+ forceAcquire: false,
50
+ signals: [...signals, 'npx_wrapper_readonly'],
51
+ };
52
+ }
53
+ if (token === '--') {
54
+ index += 1;
55
+ break;
56
+ }
57
+ if (token === '-y' || token === '--yes') {
58
+ forceAcquire = true;
59
+ signals.push('npx.force_yes');
60
+ index += 1;
61
+ continue;
62
+ }
63
+ if (token === '-p' || token === '--package') {
64
+ const packageSpec = tokens[index + 1];
65
+ if (!packageSpec) {
66
+ return opaquePackageExec('npx', 'npx_package_missing', signals, acquisitionSpecs);
67
+ }
68
+ acquisitionSpecs.push(packageSpec);
69
+ forceAcquire = true;
70
+ signals.push('npx.package_flag');
71
+ index += 2;
72
+ continue;
73
+ }
74
+ if (token.startsWith('--package=')) {
75
+ acquisitionSpecs.push(token.slice('--package='.length));
76
+ forceAcquire = true;
77
+ signals.push('npx.package_flag');
78
+ index += 1;
79
+ continue;
80
+ }
81
+ if (token === '-c' || token === '--call') {
82
+ return {
83
+ launcher: 'npx',
84
+ innerTokens: [],
85
+ acquisitionSpecs,
86
+ opaque: true,
87
+ reason: 'npx_call_script',
88
+ forceAcquire: true,
89
+ signals: [...signals, 'npx_call_script'],
90
+ };
91
+ }
92
+ if (token.startsWith('-')) {
93
+ return opaquePackageExec('npx', 'npx_unknown_option', [...signals, 'npx_unknown_option'], acquisitionSpecs);
94
+ }
95
+ break;
96
+ }
97
+ const innerTokens = tokens.slice(index);
98
+ if (innerTokens.length === 0) {
99
+ return {
100
+ launcher: 'npx',
101
+ innerTokens: [],
102
+ acquisitionSpecs,
103
+ opaque: true,
104
+ reason: 'npx_target_missing',
105
+ forceAcquire,
106
+ signals: [...signals, 'npx_target_missing'],
107
+ };
108
+ }
109
+ if (/^@[^/]+\/[^/]+/.test(innerTokens[0] ?? '')) {
110
+ if (acquisitionSpecs.length === 0 && innerTokens[0]) {
111
+ acquisitionSpecs.push(innerTokens[0]);
112
+ }
113
+ return opaquePackageExec('npx', 'npx_scoped_package_bin_indeterminate', [...signals, 'npx_scoped_package_bin_indeterminate'], acquisitionSpecs);
114
+ }
115
+ if (requiresExplicitPackageAcquisition(innerTokens[0] ?? '')) {
116
+ forceAcquire = true;
117
+ signals.push('npx.remote_spec');
118
+ }
119
+ if (acquisitionSpecs.length === 0 && innerTokens[0]) {
120
+ acquisitionSpecs.push(innerTokens[0]);
121
+ }
122
+ return {
123
+ launcher: 'npx',
124
+ innerTokens,
125
+ acquisitionSpecs,
126
+ opaque: false,
127
+ reason: 'npx_exec',
128
+ forceAcquire,
129
+ signals,
130
+ };
131
+ }
132
+ function peelNpmExec(tokens) {
133
+ return peelForcedPackageExec(tokens, 2, 'npm', 'npm_exec');
134
+ }
135
+ function peelPnpmDlx(tokens) {
136
+ return peelForcedPackageExec(tokens, 2, 'pnpm', 'pnpm_dlx');
137
+ }
138
+ function peelForcedPackageExec(tokens, startIndex, launcher, reason) {
139
+ const signals = [`package_exec:${reason}`, `${reason}_acquire`];
140
+ const acquisitionSpecs = [];
141
+ let index = startIndex;
142
+ while (index < tokens.length) {
143
+ const token = tokens[index] ?? '';
144
+ if (token === '--') {
145
+ index += 1;
146
+ break;
147
+ }
148
+ if (launcher === 'npm' && (token === '-y' || token === '--yes')) {
149
+ index += 1;
150
+ continue;
151
+ }
152
+ if (launcher === 'npm' && (token === '-p' || token === '--package')) {
153
+ const packageSpec = tokens[index + 1];
154
+ if (!packageSpec) {
155
+ return opaquePackageExec(launcher, `${reason}_package_missing`, signals, acquisitionSpecs);
156
+ }
157
+ acquisitionSpecs.push(packageSpec);
158
+ index += 2;
159
+ continue;
160
+ }
161
+ if (launcher === 'npm' && token.startsWith('--package=')) {
162
+ acquisitionSpecs.push(token.slice('--package='.length));
163
+ index += 1;
164
+ continue;
165
+ }
166
+ if (launcher === 'npm' && (token === '-c' || token === '--call')) {
167
+ return opaquePackageExec(launcher, `${reason}_call_script`, signals, acquisitionSpecs);
168
+ }
169
+ if (token.startsWith('-')) {
170
+ return opaquePackageExec(launcher, `${reason}_unknown_option`, [...signals, `${reason}_unknown_option`], acquisitionSpecs);
171
+ }
172
+ break;
173
+ }
174
+ const innerTokens = tokens.slice(index);
175
+ if (acquisitionSpecs.length === 0 && innerTokens[0]) {
176
+ acquisitionSpecs.push(innerTokens[0]);
177
+ }
178
+ return {
179
+ launcher,
180
+ innerTokens,
181
+ acquisitionSpecs,
182
+ opaque: innerTokens.length === 0,
183
+ reason: innerTokens.length === 0 ? `${reason}_missing` : reason,
184
+ forceAcquire: true,
185
+ signals,
186
+ };
187
+ }
188
+ function opaquePackageExec(launcher, reason, signals, acquisitionSpecs = []) {
189
+ return {
190
+ launcher,
191
+ innerTokens: [],
192
+ acquisitionSpecs,
193
+ opaque: true,
194
+ reason,
195
+ forceAcquire: true,
196
+ signals,
197
+ };
198
+ }
199
+ function requiresExplicitPackageAcquisition(token) {
200
+ if (!token || token.startsWith('-')) {
201
+ return false;
202
+ }
203
+ if (classifyPackageAcquisitionSpec(token).kind !== 'registry') {
204
+ return true;
205
+ }
206
+ if (token.includes('@') && !token.startsWith('@')) {
207
+ return true;
208
+ }
209
+ if (/^@[^/]+\/[^/]+@/.test(token)) {
210
+ return true;
211
+ }
212
+ if (token.startsWith('npm:')) {
213
+ return true;
214
+ }
215
+ return false;
216
+ }
217
+ export function classifyPackageAcquisitionSpec(spec) {
218
+ const normalized = spec.trim();
219
+ if (normalized.startsWith('file:') ||
220
+ normalized.startsWith('git+file:') ||
221
+ normalized.startsWith('link:') ||
222
+ normalized.startsWith('workspace:') ||
223
+ normalized.startsWith('./') ||
224
+ normalized.startsWith('../') ||
225
+ normalized.startsWith('/') ||
226
+ normalized.startsWith('~/') ||
227
+ /^[A-Za-z]:[\\/]/.test(normalized)) {
228
+ return { kind: 'local' };
229
+ }
230
+ const transportSpec = normalized.startsWith('git+') ? normalized.slice(4) : normalized;
231
+ const endpoint = parseNetworkEndpoint(transportSpec, {
232
+ allowHostedGitShorthand: true,
233
+ allowScpStyle: true,
234
+ });
235
+ if (endpoint) {
236
+ return { kind: 'network', ...endpoint };
237
+ }
238
+ if (/^[A-Za-z0-9][A-Za-z0-9._-]*\/[^/]+(?:#.*)?$/.test(normalized)) {
239
+ return { kind: 'network', host: 'github.com', protocol: 'git' };
240
+ }
241
+ if (/(?:\.tgz|\.tar|\.tar\.gz)$/i.test(normalized)) {
242
+ return { kind: 'local' };
243
+ }
244
+ if ((normalized.match(/\//g)?.length ?? 0) > 1) {
245
+ return { kind: 'local' };
246
+ }
247
+ return { kind: 'registry' };
248
+ }
249
+ export function resolveLocalBin(binName, cwd, repoRoot) {
250
+ const base = path.basename(binName);
251
+ if (!base || base === '.' || base === '..') {
252
+ return null;
253
+ }
254
+ let current = path.resolve(cwd);
255
+ const stop = path.resolve(repoRoot);
256
+ if (!pathWithinRoot(stop, current)) {
257
+ return null;
258
+ }
259
+ while (true) {
260
+ const candidate = path.join(current, 'node_modules', '.bin', base);
261
+ if (existsSync(candidate)) {
262
+ try {
263
+ const resolvedCandidate = realpathSync.native(candidate);
264
+ if (!pathWithinRoot(stop, resolvedCandidate) || !statSync(resolvedCandidate).isFile()) {
265
+ return null;
266
+ }
267
+ return { path: resolvedCandidate, proven: true };
268
+ }
269
+ catch {
270
+ return null;
271
+ }
272
+ }
273
+ if (current === stop) {
274
+ break;
275
+ }
276
+ const parent = path.dirname(current);
277
+ if (parent === current) {
278
+ break;
279
+ }
280
+ current = parent;
281
+ }
282
+ return null;
283
+ }
284
+ export function innerRecipeFromPeel(peel) {
285
+ if (peel.opaque || peel.reason === 'npx_wrapper_readonly' || peel.innerTokens.length === 0) {
286
+ return null;
287
+ }
288
+ return peel.innerTokens.join(' ');
289
+ }
290
+ export function isPackageExecHeadToken(head) {
291
+ return isPackageExecHead(head);
292
+ }
@@ -0,0 +1,13 @@
1
+ import type { PolicyDecision } from '../capability/policy-types.js';
2
+ import type { CapabilityRequestV1 } from '../capability/request.js';
3
+ import type { VerdictContext } from '../verdict/types.js';
4
+ import type { EffectPlan, EffectRequirement } from './types.js';
5
+ export declare function capabilityRequestsToEffectRequirements(requests: readonly CapabilityRequestV1[]): EffectRequirement[];
6
+ export declare function capabilityRequestsFromEffectPlan(plan: EffectPlan, context: VerdictContext): CapabilityRequestV1[];
7
+ export declare function evaluateEffectPlanPolicy(plan: EffectPlan, context: VerdictContext): {
8
+ capabilityRequests: CapabilityRequestV1[];
9
+ decisions: PolicyDecision[];
10
+ authorizationDecision: PolicyDecision;
11
+ };
12
+ export declare function effectPlanPolicyRequiresAsk(decision: PolicyDecision): boolean;
13
+ export declare function effectPlanPolicyLegacyReason(decision: PolicyDecision): string;
@@ -0,0 +1,77 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { policyDecisionToLegacyReason } from '../capability/policy-bridge.js';
3
+ import { evaluateCapabilityRequestsPolicy, policyDecisionRequiresAsk, } from '../capability/policy-engine.js';
4
+ import { collectRequirements, flattenRequirementsToCapabilityRequests } from './build.js';
5
+ function shellPrincipal(repoRoot, cwd) {
6
+ return {
7
+ repoRoot,
8
+ sessionHash: createHash('sha256').update(`${repoRoot}:${cwd}`).digest('hex').slice(0, 16),
9
+ };
10
+ }
11
+ function capabilityActionToEffectTag(action) {
12
+ switch (action) {
13
+ case 'fs.read':
14
+ return 'fs.read';
15
+ case 'fs.write':
16
+ return 'fs.write';
17
+ case 'process.exec':
18
+ return 'process.exec';
19
+ case 'network.connect':
20
+ return 'network.connect';
21
+ case 'secret.read':
22
+ return 'secret.read';
23
+ case 'git.ref.write':
24
+ return 'git.ref.write';
25
+ case 'control_plane.write':
26
+ return 'control_plane.write';
27
+ case 'indeterminate':
28
+ return 'indeterminate';
29
+ default:
30
+ return 'indeterminate';
31
+ }
32
+ }
33
+ export function capabilityRequestsToEffectRequirements(requests) {
34
+ return requests.map((request) => ({
35
+ tag: capabilityActionToEffectTag(request.action),
36
+ action: request.action,
37
+ resource: request.resource,
38
+ evidence: {
39
+ level: request.evidence.level,
40
+ signals: [...request.evidence.signals],
41
+ basis: [...request.context.analysisBasis],
42
+ },
43
+ provenance: {
44
+ innerCommand: request.resource.kind === 'executable' ? request.resource.command : undefined,
45
+ },
46
+ provenances: [
47
+ {
48
+ innerCommand: request.resource.kind === 'executable' ? request.resource.command : undefined,
49
+ },
50
+ ],
51
+ }));
52
+ }
53
+ export function capabilityRequestsFromEffectPlan(plan, context) {
54
+ const requirements = collectRequirements(plan.root);
55
+ return flattenRequirementsToCapabilityRequests(requirements, {
56
+ principal: shellPrincipal(context.repoRoot, context.cwd),
57
+ cwd: context.cwd,
58
+ repoRoot: context.repoRoot,
59
+ hookKind: 'shell',
60
+ inputFingerprint: plan.inputFingerprint,
61
+ });
62
+ }
63
+ export function evaluateEffectPlanPolicy(plan, context) {
64
+ const capabilityRequests = capabilityRequestsFromEffectPlan(plan, context);
65
+ const { decisions, decision } = evaluateCapabilityRequestsPolicy(capabilityRequests, context.config, {
66
+ grants: context.grants,
67
+ attestation: context.attestation,
68
+ egressProxyActive: context.egressProxyActive,
69
+ }, context.trustedWorkspaceRoots);
70
+ return { capabilityRequests, decisions, authorizationDecision: decision };
71
+ }
72
+ export function effectPlanPolicyRequiresAsk(decision) {
73
+ return policyDecisionRequiresAsk(decision);
74
+ }
75
+ export function effectPlanPolicyLegacyReason(decision) {
76
+ return policyDecisionToLegacyReason(decision);
77
+ }
@@ -0,0 +1,55 @@
1
+ import type { CapabilityAction, CapabilityEvidenceLevel, CapabilityResource } from '../capability/request.js';
2
+ import type { VerdictOpacity } from '../verdict/types.js';
3
+ export declare const EFFECT_PLAN_VERSION: 1;
4
+ export type EffectTag = 'fs.read' | 'fs.write' | 'process.exec' | 'network.connect' | 'network.acquire' | 'git.ref.write' | 'secret.read' | 'control_plane.write' | 'read_only' | 'indeterminate';
5
+ export type LauncherPhase = 'invoke' | 'resolve' | 'acquire' | 'cache_read' | 'cache_write' | 'delegate';
6
+ export type PackageExecLauncher = 'npx' | 'npm' | 'pnpm';
7
+ export interface EffectEvidence {
8
+ level: CapabilityEvidenceLevel;
9
+ signals: readonly string[];
10
+ basis: readonly string[];
11
+ }
12
+ export interface EffectProvenance {
13
+ segment?: string;
14
+ launcher?: PackageExecLauncher;
15
+ phase?: LauncherPhase;
16
+ innerCommand?: string;
17
+ innerArgv?: readonly string[];
18
+ }
19
+ export interface EffectRequirement {
20
+ tag: EffectTag;
21
+ action: CapabilityAction;
22
+ resource: CapabilityResource;
23
+ evidence: EffectEvidence;
24
+ provenance: EffectProvenance;
25
+ /** All contributing sources after requirement de-duplication. */
26
+ provenances?: readonly EffectProvenance[];
27
+ }
28
+ export interface ExecEffectNode {
29
+ kind: 'exec';
30
+ commandRedacted: string;
31
+ segmentHead: string;
32
+ requirements: readonly EffectRequirement[];
33
+ }
34
+ export interface LauncherEffectNode {
35
+ kind: 'launcher';
36
+ launcher: PackageExecLauncher | 'npm' | 'pnpm' | 'make';
37
+ phase: LauncherPhase;
38
+ opaque: boolean;
39
+ reason: string;
40
+ children: readonly EffectNode[];
41
+ }
42
+ export interface MergeEffectNode {
43
+ kind: 'merge';
44
+ children: readonly EffectNode[];
45
+ }
46
+ export type EffectNode = ExecEffectNode | LauncherEffectNode | MergeEffectNode;
47
+ export interface EffectPlan {
48
+ version: typeof EFFECT_PLAN_VERSION;
49
+ root: EffectNode;
50
+ inputFingerprint: string;
51
+ opacity: VerdictOpacity;
52
+ disposition: 'effects' | 'effect_free';
53
+ completeness: 'complete' | 'partial';
54
+ signals: readonly string[];
55
+ }
@@ -0,0 +1 @@
1
+ export const EFFECT_PLAN_VERSION = 1;
@@ -3,5 +3,6 @@ import type { EgressAllowlistEntry, EgressAllowlistFile } from './types.js';
3
3
  export declare function egressAllowlistPath(config: BelayConfigV3, repoLocalStateDir: string): string;
4
4
  export declare function loadEgressAllowlist(filePath: string): Promise<EgressAllowlistFile>;
5
5
  export declare function saveEgressAllowlist(filePath: string, state: EgressAllowlistFile): Promise<void>;
6
+ export declare function mutateEgressAllowlist(filePath: string, mutate: (state: EgressAllowlistFile) => EgressAllowlistFile): Promise<EgressAllowlistFile>;
6
7
  export declare function isHostAllowlisted(host: string, allowlist: EgressAllowlistFile): boolean;
7
8
  export declare function addDomainToAllowlist(allowlist: EgressAllowlistFile, entry: EgressAllowlistEntry): EgressAllowlistFile;
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { withStateFileLock } from '../capability/approval-state-mutation.js';
4
5
  import { belayStateDir } from '../config.js';
5
6
  export function egressAllowlistPath(config, repoLocalStateDir) {
6
7
  return path.join(belayStateDir(config, repoLocalStateDir), 'egress-allowlist.json');
@@ -19,6 +20,13 @@ export async function saveEgressAllowlist(filePath, state) {
19
20
  await mkdir(path.dirname(filePath), { recursive: true });
20
21
  await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
21
22
  }
23
+ export async function mutateEgressAllowlist(filePath, mutate) {
24
+ return withStateFileLock(filePath, async () => {
25
+ const updated = mutate(await loadEgressAllowlist(filePath));
26
+ await saveEgressAllowlist(filePath, updated);
27
+ return updated;
28
+ });
29
+ }
22
30
  export function isHostAllowlisted(host, allowlist) {
23
31
  const normalized = host.toLowerCase();
24
32
  return allowlist.domains.some((entry) => entry.host.toLowerCase() === normalized);