@guilz-dev/belay 0.6.0 → 0.8.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 (183) hide show
  1. package/README.md +39 -20
  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 +443 -225
  5. package/dist/bundle/claude-runtime.mjs +13027 -7271
  6. package/dist/bundle/codex-runtime.mjs +12700 -6977
  7. package/dist/bundle/cursor-runtime.mjs +12695 -6977
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +58 -0
  11. package/dist/commands/dogfood.js +1 -1
  12. package/dist/commands/explain.js +2 -2
  13. package/dist/commands/harvest.d.ts +0 -1
  14. package/dist/commands/harvest.js +1 -4
  15. package/dist/commands/metrics.js +6 -0
  16. package/dist/commands/quality.js +1 -3
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/status.js +7 -0
  21. package/dist/config-io.js +49 -18
  22. package/dist/conformance/guarantee-table.js +137 -9
  23. package/dist/conformance/types.d.ts +2 -0
  24. package/dist/core/approval-replay.d.ts +1 -1
  25. package/dist/core/approval-replay.js +4 -1
  26. package/dist/core/approval-service.d.ts +6 -1
  27. package/dist/core/approval-service.js +86 -27
  28. package/dist/core/approval.d.ts +2 -0
  29. package/dist/core/approval.js +6 -3
  30. package/dist/core/audit-metrics.d.ts +1 -0
  31. package/dist/core/audit-metrics.js +12 -4
  32. package/dist/core/audit-types.d.ts +16 -0
  33. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  34. package/dist/core/capability/approval-state-mutation.js +76 -3
  35. package/dist/core/capability/approval-v3.d.ts +18 -1
  36. package/dist/core/capability/approval-v3.js +63 -23
  37. package/dist/core/capability/attestation.d.ts +3 -0
  38. package/dist/core/capability/attestation.js +7 -0
  39. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  40. package/dist/core/capability/boundary-driver-container.js +101 -34
  41. package/dist/core/capability/boundary-driver.js +1 -0
  42. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  43. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  44. package/dist/core/capability/boundary-run.d.ts +25 -0
  45. package/dist/core/capability/boundary-run.js +37 -9
  46. package/dist/core/capability/boundary-session.d.ts +4 -0
  47. package/dist/core/capability/boundary-session.js +9 -0
  48. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  49. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  50. package/dist/core/capability/gate-policy-shadow.js +23 -1
  51. package/dist/core/capability/grant-consumption.d.ts +4 -0
  52. package/dist/core/capability/grant-consumption.js +11 -0
  53. package/dist/core/capability/grant-lease.d.ts +36 -1
  54. package/dist/core/capability/grant-lease.js +213 -16
  55. package/dist/core/capability/grant-match.js +3 -0
  56. package/dist/core/capability/index.d.ts +2 -2
  57. package/dist/core/capability/index.js +1 -1
  58. package/dist/core/capability/policy-engine.d.ts +38 -1
  59. package/dist/core/capability/policy-engine.js +388 -15
  60. package/dist/core/capability/request.d.ts +12 -0
  61. package/dist/core/capability/resolver.d.ts +0 -1
  62. package/dist/core/capability/resolver.js +0 -1
  63. package/dist/core/config.d.ts +20 -0
  64. package/dist/core/config.js +62 -2
  65. package/dist/core/effect-ir/audit.d.ts +9 -0
  66. package/dist/core/effect-ir/audit.js +87 -0
  67. package/dist/core/effect-ir/build.d.ts +31 -0
  68. package/dist/core/effect-ir/build.js +281 -0
  69. package/dist/core/effect-ir/index.d.ts +13 -0
  70. package/dist/core/effect-ir/index.js +9 -0
  71. package/dist/core/effect-ir/normalize.d.ts +4 -0
  72. package/dist/core/effect-ir/normalize.js +110 -0
  73. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  74. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  75. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  76. package/dist/core/effect-ir/package-exec.js +292 -0
  77. package/dist/core/effect-ir/policy.d.ts +14 -0
  78. package/dist/core/effect-ir/policy.js +164 -0
  79. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  80. package/dist/core/effect-ir/shell-build.js +77 -0
  81. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  82. package/dist/core/effect-ir/shell-lower.js +1509 -0
  83. package/dist/core/effect-ir/types.d.ts +62 -0
  84. package/dist/core/effect-ir/types.js +1 -0
  85. package/dist/core/egress/allowlist.d.ts +1 -0
  86. package/dist/core/egress/allowlist.js +8 -0
  87. package/dist/core/egress-approval.js +45 -51
  88. package/dist/core/gate-contract.d.ts +2 -0
  89. package/dist/core/gate-contract.js +2 -0
  90. package/dist/core/gate-engine.js +82 -25
  91. package/dist/core/git-resource-identity.d.ts +26 -0
  92. package/dist/core/git-resource-identity.js +284 -0
  93. package/dist/core/harvest.d.ts +3 -7
  94. package/dist/core/harvest.js +4 -27
  95. package/dist/core/index.d.ts +5 -1
  96. package/dist/core/index.js +3 -1
  97. package/dist/core/judge-cloud-consent.js +29 -15
  98. package/dist/core/network-endpoint.d.ts +10 -0
  99. package/dist/core/network-endpoint.js +51 -0
  100. package/dist/core/path-utils.js +42 -23
  101. package/dist/core/process-runner.d.ts +10 -0
  102. package/dist/core/process-runner.js +124 -0
  103. package/dist/core/recover-advice.js +2 -1
  104. package/dist/core/recovery/artifact-store.d.ts +29 -0
  105. package/dist/core/recovery/artifact-store.js +336 -0
  106. package/dist/core/recovery/checkpoint.d.ts +25 -0
  107. package/dist/core/recovery/checkpoint.js +260 -0
  108. package/dist/core/recovery/index.d.ts +3 -1
  109. package/dist/core/recovery/index.js +2 -0
  110. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  111. package/dist/core/recovery/operator-guidance.js +39 -0
  112. package/dist/core/recovery/reconcile.d.ts +3 -0
  113. package/dist/core/recovery/reconcile.js +48 -0
  114. package/dist/core/recovery/resource-identity.d.ts +4 -0
  115. package/dist/core/recovery/resource-identity.js +34 -0
  116. package/dist/core/recovery/restore.d.ts +13 -0
  117. package/dist/core/recovery/restore.js +114 -0
  118. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  119. package/dist/core/recovery/snapshot-node.js +261 -0
  120. package/dist/core/recovery/types.d.ts +85 -1
  121. package/dist/core/shell-substitution.d.ts +1 -0
  122. package/dist/core/shell-substitution.js +61 -0
  123. package/dist/core/standing-allow.d.ts +1 -2
  124. package/dist/core/standing-allow.js +3 -20
  125. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  126. package/dist/core/transactional/apply-observed-changes.js +275 -0
  127. package/dist/core/transactional/backend-selector.d.ts +8 -0
  128. package/dist/core/transactional/backend-selector.js +118 -0
  129. package/dist/core/transactional/backend.d.ts +55 -0
  130. package/dist/core/transactional/backend.js +1 -0
  131. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  132. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  133. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  134. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  135. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  136. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  137. package/dist/core/transactional/file-clone.d.ts +15 -0
  138. package/dist/core/transactional/file-clone.js +139 -0
  139. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  140. package/dist/core/transactional/file-tree-path.js +50 -0
  141. package/dist/core/transactional/file-tree.d.ts +41 -0
  142. package/dist/core/transactional/file-tree.js +186 -0
  143. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  144. package/dist/core/transactional/git-worktree-backend.js +50 -0
  145. package/dist/core/transactional/git-worktree.d.ts +9 -11
  146. package/dist/core/transactional/git-worktree.js +24 -119
  147. package/dist/core/transactional/index.d.ts +11 -0
  148. package/dist/core/transactional/index.js +10 -0
  149. package/dist/core/transactional/reasons.js +1 -0
  150. package/dist/core/transactional/runner.js +157 -25
  151. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  152. package/dist/core/transactional/snapshot-node.js +87 -0
  153. package/dist/core/transactional/types.d.ts +17 -1
  154. package/dist/core/types.d.ts +13 -1
  155. package/dist/core/verdict/adapter.js +23 -15
  156. package/dist/core/verdict/containment.js +4 -0
  157. package/dist/core/verdict/egress-classify.d.ts +12 -0
  158. package/dist/core/verdict/egress-classify.js +746 -0
  159. package/dist/core/verdict/git-classifier.d.ts +12 -0
  160. package/dist/core/verdict/git-classifier.js +320 -0
  161. package/dist/core/verdict/launcher-resolve.js +79 -37
  162. package/dist/core/verdict/parser.d.ts +2 -0
  163. package/dist/core/verdict/parser.js +95 -1
  164. package/dist/core/verdict/types.d.ts +6 -2
  165. package/dist/core/verdict/verdict.js +103 -935
  166. package/dist/corpus/must-allow-commands.d.ts +2 -1
  167. package/dist/corpus/must-allow-commands.js +2 -1
  168. package/dist/corpus/runtime-match.d.ts +2 -1
  169. package/dist/corpus/runtime-match.js +2 -1
  170. package/dist/corpus/types.d.ts +2 -2
  171. package/dist/templates.js +7 -2
  172. package/dist/types.d.ts +11 -0
  173. package/dist/version.d.ts +1 -1
  174. package/dist/version.js +1 -1
  175. package/package.json +2 -2
  176. package/skills/belay/SKILL.md +8 -3
  177. package/skills/belay/belay-approve.md +10 -7
  178. package/dist/core/verdict/overrides.d.ts +0 -7
  179. package/dist/core/verdict/overrides.js +0 -37
  180. package/dist/core/verdict/shell-policy.d.ts +0 -25
  181. package/dist/core/verdict/shell-policy.js +0 -40
  182. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  183. package/dist/corpus/standing-allow-catalog.generated.js +0 -99
@@ -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,14 @@
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, EffectPlanPolicyProjection, 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
+ projection: EffectPlanPolicyProjection;
12
+ };
13
+ export declare function effectPlanPolicyRequiresAsk(decision: PolicyDecision): boolean;
14
+ export declare function effectPlanPolicyLegacyReason(decision: PolicyDecision): string;
@@ -0,0 +1,164 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { policyDecisionToLegacyReason } from '../capability/policy-bridge.js';
3
+ import { evaluateEffectRequirementPolicy, 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 requirements = collectRequirements(plan.root);
65
+ const capabilityRequests = capabilityRequestsFromEffectPlan(plan, context);
66
+ const decisions = requirements.map((requirement, index) => evaluateEffectRequirementPolicy({
67
+ tag: requirement.tag,
68
+ action: requirement.action,
69
+ resource: requirement.resource,
70
+ evidence: requirement.evidence,
71
+ provenance: requirement.provenance,
72
+ }, {
73
+ cwd: context.cwd,
74
+ repoRoot: context.repoRoot,
75
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
76
+ sensitivePaths: context.sensitivePaths,
77
+ protectedArtifactRoots: context.protectedArtifactRoots,
78
+ grants: context.grants,
79
+ capabilityRequest: capabilityRequests[index],
80
+ }));
81
+ const indeterminateIndexes = requirements.flatMap((requirement, index) => requirement.action === 'indeterminate' ||
82
+ requirement.tag === 'indeterminate' ||
83
+ requirement.evidence.level === 'indeterminate'
84
+ ? [index]
85
+ : []);
86
+ const unresolvedPartial = indeterminateIndexes.length === 0 ||
87
+ indeterminateIndexes.some((index) => decisions[index]?.matchedRule !== 'grant.exact');
88
+ if (plan.completeness === 'partial' && unresolvedPartial) {
89
+ decisions.push({
90
+ effectDisposition: 'ask',
91
+ outcome: 'require_approval',
92
+ reason: 'indeterminate_effect',
93
+ signals: [...plan.signals, 'effect_plan_partial'],
94
+ matchedRule: 'effect.plan_partial',
95
+ });
96
+ }
97
+ const worstDecision = worstEffectDecision(decisions);
98
+ const authorizationDecision = (worstDecision?.effectDisposition === 'ask'
99
+ ? worstDecision
100
+ : (decisions.find((decision) => decision.matchedRule === 'grant.exact') ?? worstDecision)) ??
101
+ {
102
+ outcome: 'allow',
103
+ reason: 'read_only',
104
+ signals: [],
105
+ matchedRule: 'effect.effect_free',
106
+ };
107
+ const projection = projectEffectPlanPolicy(decisions, authorizationDecision);
108
+ return {
109
+ capabilityRequests,
110
+ decisions,
111
+ authorizationDecision,
112
+ projection,
113
+ };
114
+ }
115
+ function dispositionRank(disposition) {
116
+ switch (disposition) {
117
+ case 'ask':
118
+ return 2;
119
+ case 'allow_flagged':
120
+ return 1;
121
+ case 'allow':
122
+ return 0;
123
+ default:
124
+ return 2;
125
+ }
126
+ }
127
+ function projectEffectPlanPolicy(decisions, authorizationDecision) {
128
+ const worst = worstEffectDecision(decisions);
129
+ if (!worst) {
130
+ return {
131
+ permission: 'allow',
132
+ hookVerdict: 'allow',
133
+ reason: 'read_only',
134
+ };
135
+ }
136
+ if (worst.effectDisposition === 'ask') {
137
+ return {
138
+ permission: 'ask',
139
+ hookVerdict: 'deny_pending_approval',
140
+ reason: policyDecisionToLegacyReason(authorizationDecision),
141
+ };
142
+ }
143
+ return {
144
+ permission: 'allow',
145
+ hookVerdict: worst.effectDisposition === 'allow_flagged' ? 'allow_flagged' : 'allow',
146
+ reason: policyDecisionToLegacyReason(worst),
147
+ };
148
+ }
149
+ function worstEffectDecision(decisions) {
150
+ let worst;
151
+ for (const decision of decisions) {
152
+ if (!worst ||
153
+ dispositionRank(decision.effectDisposition) > dispositionRank(worst.effectDisposition)) {
154
+ worst = decision;
155
+ }
156
+ }
157
+ return worst;
158
+ }
159
+ export function effectPlanPolicyRequiresAsk(decision) {
160
+ return policyDecisionRequiresAsk(decision);
161
+ }
162
+ export function effectPlanPolicyLegacyReason(decision) {
163
+ return policyDecisionToLegacyReason(decision);
164
+ }
@@ -0,0 +1,43 @@
1
+ import type { CapabilityResource } from '../capability/request.js';
2
+ import type { AnalysisCompleteness, EffectPlan, EffectRequirement } from './types.js';
3
+ type ShellNetworkResource = Extract<CapabilityResource, {
4
+ kind: 'network';
5
+ }> & Required<Pick<Extract<CapabilityResource, {
6
+ kind: 'network';
7
+ }>, 'mode' | 'payload'>>;
8
+ type ShellExecutableResource = Extract<CapabilityResource, {
9
+ kind: 'executable';
10
+ }> & Required<Pick<Extract<CapabilityResource, {
11
+ kind: 'executable';
12
+ }>, 'operation'>>;
13
+ type ShellGitRefResource = Extract<CapabilityResource, {
14
+ kind: 'git-ref';
15
+ }> & Required<Pick<Extract<CapabilityResource, {
16
+ kind: 'git-ref';
17
+ }>, 'scope'>>;
18
+ export type ShellEffectResource = Exclude<CapabilityResource, {
19
+ kind: 'network' | 'executable' | 'git-ref';
20
+ }> | ShellNetworkResource | ShellExecutableResource | ShellGitRefResource;
21
+ export type ShellEffectRequirement = Omit<EffectRequirement, 'resource'> & {
22
+ resource: ShellEffectResource;
23
+ };
24
+ export interface ShellEffectSegment {
25
+ commandRedacted: string;
26
+ segmentHead: string;
27
+ requirements: readonly ShellEffectRequirement[];
28
+ completeness: AnalysisCompleteness;
29
+ opacity: EffectPlan['opacity'];
30
+ signals: readonly string[];
31
+ }
32
+ export interface BuildShellEffectPlanParams {
33
+ inputFingerprint: string;
34
+ segments: readonly ShellEffectSegment[];
35
+ signals?: readonly string[];
36
+ }
37
+ /**
38
+ * Build a canonical EffectPlan from shell segments that have already been
39
+ * semantically analyzed. This function only assembles and normalizes effects;
40
+ * command-specific lowering belongs to the segment producers.
41
+ */
42
+ export declare function buildShellEffectPlan(params: BuildShellEffectPlanParams): EffectPlan;
43
+ export {};