@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
@@ -51,6 +51,46 @@ export const DEFAULT_MODEL_ASSIST = {
51
51
  enabled: false,
52
52
  timeoutMs: 3000,
53
53
  };
54
+ export const DEFAULT_FILE_CHECKPOINT = {
55
+ enabled: false,
56
+ allowNonGit: false,
57
+ maxFiles: 100_000,
58
+ maxSourceBytes: 2_147_483_648,
59
+ maxWorkspaceBytes: 4_294_967_296,
60
+ prepareTimeoutMs: 30_000,
61
+ copyConcurrency: 8,
62
+ };
63
+ function clampCopyConcurrency(value) {
64
+ return Math.min(32, Math.max(1, Math.floor(value)));
65
+ }
66
+ export function normalizeFileCheckpointConfig(raw) {
67
+ return {
68
+ enabled: raw?.enabled === true,
69
+ allowNonGit: raw?.allowNonGit === true,
70
+ maxFiles: typeof raw?.maxFiles === 'number' && raw.maxFiles > 0
71
+ ? Math.floor(raw.maxFiles)
72
+ : DEFAULT_FILE_CHECKPOINT.maxFiles,
73
+ maxSourceBytes: typeof raw?.maxSourceBytes === 'number' && raw.maxSourceBytes > 0
74
+ ? Math.floor(raw.maxSourceBytes)
75
+ : DEFAULT_FILE_CHECKPOINT.maxSourceBytes,
76
+ maxWorkspaceBytes: typeof raw?.maxWorkspaceBytes === 'number' && raw.maxWorkspaceBytes > 0
77
+ ? Math.floor(raw.maxWorkspaceBytes)
78
+ : DEFAULT_FILE_CHECKPOINT.maxWorkspaceBytes,
79
+ prepareTimeoutMs: typeof raw?.prepareTimeoutMs === 'number' && raw.prepareTimeoutMs > 0
80
+ ? Math.floor(raw.prepareTimeoutMs)
81
+ : DEFAULT_FILE_CHECKPOINT.prepareTimeoutMs,
82
+ copyConcurrency: typeof raw?.copyConcurrency === 'number' && raw.copyConcurrency > 0
83
+ ? clampCopyConcurrency(raw.copyConcurrency)
84
+ : DEFAULT_FILE_CHECKPOINT.copyConcurrency,
85
+ };
86
+ }
87
+ export const DEFAULT_RECOVERY_CHECKPOINT = {
88
+ enabled: false,
89
+ appliedRetentionHours: 7 * 24,
90
+ restoredRetentionHours: 24,
91
+ maxCheckpoints: 20,
92
+ maxBytes: 1024 * 1024 * 1024,
93
+ };
54
94
  export const DEFAULT_TRANSACTIONAL_V3 = {
55
95
  enabled: false,
56
96
  minConfidence: DEFAULT_CONFIDENCE_THRESHOLDS.flag,
@@ -60,6 +100,8 @@ export const DEFAULT_TRANSACTIONAL_V3 = {
60
100
  gates: {
61
101
  shell: true,
62
102
  },
103
+ fileCheckpoint: { ...DEFAULT_FILE_CHECKPOINT },
104
+ checkpoint: { ...DEFAULT_RECOVERY_CHECKPOINT },
63
105
  };
64
106
  export const LEGACY_POLICY_V3 = {
65
107
  unknownLocalEffect: 'allow_flagged',
@@ -696,6 +738,26 @@ export function normalizeConfig(config) {
696
738
  gates: {
697
739
  shell: v4.policy?.transactional?.gates?.shell !== false,
698
740
  },
741
+ fileCheckpoint: normalizeFileCheckpointConfig(v4.policy?.transactional?.fileCheckpoint),
742
+ checkpoint: {
743
+ enabled: v4.policy?.transactional?.checkpoint?.enabled === true,
744
+ appliedRetentionHours: typeof v4.policy?.transactional?.checkpoint?.appliedRetentionHours === 'number' &&
745
+ v4.policy.transactional.checkpoint.appliedRetentionHours > 0
746
+ ? v4.policy.transactional.checkpoint.appliedRetentionHours
747
+ : DEFAULT_RECOVERY_CHECKPOINT.appliedRetentionHours,
748
+ restoredRetentionHours: typeof v4.policy?.transactional?.checkpoint?.restoredRetentionHours === 'number' &&
749
+ v4.policy.transactional.checkpoint.restoredRetentionHours > 0
750
+ ? v4.policy.transactional.checkpoint.restoredRetentionHours
751
+ : DEFAULT_RECOVERY_CHECKPOINT.restoredRetentionHours,
752
+ maxCheckpoints: typeof v4.policy?.transactional?.checkpoint?.maxCheckpoints === 'number' &&
753
+ v4.policy.transactional.checkpoint.maxCheckpoints > 0
754
+ ? Math.floor(v4.policy.transactional.checkpoint.maxCheckpoints)
755
+ : DEFAULT_RECOVERY_CHECKPOINT.maxCheckpoints,
756
+ maxBytes: typeof v4.policy?.transactional?.checkpoint?.maxBytes === 'number' &&
757
+ v4.policy.transactional.checkpoint.maxBytes > 0
758
+ ? Math.floor(v4.policy.transactional.checkpoint.maxBytes)
759
+ : DEFAULT_RECOVERY_CHECKPOINT.maxBytes,
760
+ },
699
761
  };
700
762
  })(),
701
763
  },
@@ -0,0 +1,9 @@
1
+ import type { PolicyDecision } from '../capability/policy-types.js';
2
+ import type { CapabilityRequestV1 } from '../capability/request.js';
3
+ import type { EffectPlan } from './types.js';
4
+ export declare function hashEffectPlan(plan: EffectPlan): string;
5
+ export declare function effectPlanAuditFields(plan: EffectPlan | undefined, policy?: {
6
+ capabilityRequests?: readonly CapabilityRequestV1[];
7
+ decisions?: readonly PolicyDecision[];
8
+ authorizationDecision?: PolicyDecision;
9
+ }): Record<string, unknown>;
@@ -0,0 +1,86 @@
1
+ import { canonicalStringify, hashValue } from '../fingerprint.js';
2
+ import { collectRequirements } from './build.js';
3
+ function auditableProvenance(provenance) {
4
+ return {
5
+ ...(provenance.segment ? { segment: provenance.segment } : {}),
6
+ ...(provenance.launcher ? { launcher: provenance.launcher } : {}),
7
+ ...(provenance.phase ? { phase: provenance.phase } : {}),
8
+ };
9
+ }
10
+ function auditableResource(resource) {
11
+ if (resource.kind === 'executable') {
12
+ return {
13
+ kind: 'executable',
14
+ commandHash: hashValue(resource.command),
15
+ };
16
+ }
17
+ return resource;
18
+ }
19
+ export function hashEffectPlan(plan) {
20
+ const requirements = collectRequirements(plan.root)
21
+ .map((requirement) => ({
22
+ tag: requirement.tag,
23
+ action: requirement.action,
24
+ resource: auditableResource(requirement.resource),
25
+ evidence: {
26
+ level: requirement.evidence.level,
27
+ signals: [...requirement.evidence.signals].sort(),
28
+ basis: [...requirement.evidence.basis].sort(),
29
+ },
30
+ provenance: requirement.provenance,
31
+ provenances: [...(requirement.provenances ?? [requirement.provenance])].sort((left, right) => canonicalStringify(left).localeCompare(canonicalStringify(right))),
32
+ }))
33
+ .sort((left, right) => canonicalStringify(left).localeCompare(canonicalStringify(right)));
34
+ return hashValue(`effect-plan:v1:${canonicalStringify({
35
+ version: plan.version,
36
+ inputFingerprint: plan.inputFingerprint,
37
+ opacity: plan.opacity,
38
+ disposition: plan.disposition,
39
+ completeness: plan.completeness,
40
+ signals: [...plan.signals].sort(),
41
+ requirements,
42
+ })}`);
43
+ }
44
+ export function effectPlanAuditFields(plan, policy) {
45
+ if (!plan) {
46
+ return {};
47
+ }
48
+ const fields = {
49
+ effectPlanVersion: plan.version,
50
+ effectIRHash: hashEffectPlan(plan),
51
+ effectPlanSignals: [...plan.signals],
52
+ effectPlanOpacity: plan.opacity,
53
+ effectPlanDisposition: plan.disposition,
54
+ effectPlanCompleteness: plan.completeness,
55
+ effectPlanRequirements: collectRequirements(plan.root).map((requirement) => ({
56
+ tag: requirement.tag,
57
+ action: requirement.action,
58
+ resource: auditableResource(requirement.resource),
59
+ evidence: requirement.evidence,
60
+ provenance: auditableProvenance(requirement.provenance),
61
+ provenances: (requirement.provenances ?? [requirement.provenance]).map((provenance) => auditableProvenance(provenance)),
62
+ })),
63
+ };
64
+ if (policy?.authorizationDecision) {
65
+ fields.effectPlanPolicyOutcome = policy.authorizationDecision.outcome;
66
+ fields.effectPlanPolicyReason = policy.authorizationDecision.reason;
67
+ if (policy.authorizationDecision.matchedRule) {
68
+ fields.effectPlanPolicyMatchedRule = policy.authorizationDecision.matchedRule;
69
+ }
70
+ }
71
+ if (policy?.capabilityRequests?.length) {
72
+ fields.effectPlanRequestActions = policy.capabilityRequests.map((request) => request.action);
73
+ fields.effectPlanRequestCount = policy.capabilityRequests.length;
74
+ if (policy.capabilityRequests.some((request) => request.evidence.signals.includes('package_cache_write'))) {
75
+ fields.effectPlanCacheWriteProjected = true;
76
+ }
77
+ }
78
+ if (policy?.decisions?.length) {
79
+ fields.effectPlanRequestDecisions = policy.decisions.map((decision) => ({
80
+ outcome: decision.outcome,
81
+ reason: decision.reason,
82
+ matchedRule: decision.matchedRule,
83
+ }));
84
+ }
85
+ return fields;
86
+ }
@@ -0,0 +1,31 @@
1
+ import type { CapabilityPrincipal, CapabilityRequestV1 } from '../capability/request.js';
2
+ import type { LauncherResolution } from '../verdict/launcher-resolve.js';
3
+ import type { EffectNode, EffectPlan, EffectRequirement, PackageExecLauncher } from './types.js';
4
+ export interface BuildEffectPlanParams {
5
+ tokens: string[];
6
+ cwd: string;
7
+ repoRoot: string;
8
+ inputFingerprint: string;
9
+ launcherResolution?: LauncherResolution | null;
10
+ innerRequirements?: readonly EffectRequirement[];
11
+ }
12
+ export interface BuildCapabilityEffectPlanParams {
13
+ actionKind: 'shell' | 'tool' | 'subagent';
14
+ summary: string;
15
+ inputFingerprint: string;
16
+ requests: readonly CapabilityRequestV1[];
17
+ effectFree: boolean;
18
+ }
19
+ export declare function buildPackageExecEffectNode(params: BuildEffectPlanParams): EffectNode | null;
20
+ export declare function collectRequirements(node: EffectNode): EffectRequirement[];
21
+ export declare function buildEffectPlan(params: BuildEffectPlanParams): EffectPlan | null;
22
+ /** Build the canonical plan envelope for non-package-exec gated actions. */
23
+ export declare function buildCapabilityEffectPlan(params: BuildCapabilityEffectPlanParams): EffectPlan;
24
+ export declare function flattenRequirementsToCapabilityRequests(requirements: readonly EffectRequirement[], opts: {
25
+ principal: CapabilityPrincipal;
26
+ cwd: string;
27
+ repoRoot: string;
28
+ hookKind: 'shell';
29
+ inputFingerprint: string;
30
+ }): CapabilityRequestV1[];
31
+ export type { PackageExecLauncher };
@@ -0,0 +1,277 @@
1
+ import { mergeRequirements } from './normalize.js';
2
+ import { classifyPackageAcquisitionSpec, innerRecipeFromPeel, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
3
+ export function buildPackageExecEffectNode(params) {
4
+ const peel = peelPackageExecArgv(params.tokens);
5
+ if (!peel) {
6
+ return null;
7
+ }
8
+ const children = [];
9
+ const invokeNode = {
10
+ kind: 'launcher',
11
+ launcher: peel.launcher,
12
+ phase: 'invoke',
13
+ opaque: peel.opaque,
14
+ reason: peel.reason,
15
+ children: [],
16
+ };
17
+ children.push(invokeNode);
18
+ if (peel.opaque) {
19
+ children.push(acquireRequirement(peel, 'indeterminate'));
20
+ return { kind: 'merge', children };
21
+ }
22
+ if (peel.reason === 'npx_wrapper_readonly') {
23
+ return { kind: 'merge', children };
24
+ }
25
+ const innerRecipe = innerRecipeFromPeel(peel);
26
+ const binHead = peel.innerTokens[0] ?? '';
27
+ const local = !peel.forceAcquire ? resolveLocalBin(binHead, params.cwd, params.repoRoot) : null;
28
+ if (local) {
29
+ children.push({
30
+ kind: 'launcher',
31
+ launcher: peel.launcher,
32
+ phase: 'resolve',
33
+ opaque: false,
34
+ reason: 'npx_local_bin_resolved',
35
+ children: [],
36
+ });
37
+ if (innerRecipe) {
38
+ children.push(delegateExecNode(peel, innerRecipe, binHead, local.path));
39
+ }
40
+ if (innerRecipe && params.innerRequirements !== undefined) {
41
+ if (params.innerRequirements.length > 0) {
42
+ children.push({
43
+ kind: 'exec',
44
+ commandRedacted: innerRecipe,
45
+ segmentHead: binHead,
46
+ requirements: [...params.innerRequirements],
47
+ });
48
+ }
49
+ }
50
+ }
51
+ else {
52
+ children.push(acquireRequirement(peel, 'possible'));
53
+ if (innerRecipe) {
54
+ children.push(delegateExecNode(peel, innerRecipe, binHead));
55
+ }
56
+ }
57
+ return { kind: 'merge', children };
58
+ }
59
+ function delegateExecNode(peel, innerRecipe, segmentHead, resolvedLocalPath) {
60
+ return {
61
+ kind: 'launcher',
62
+ launcher: peel.launcher,
63
+ phase: 'delegate',
64
+ opaque: false,
65
+ reason: 'package_exec_delegate',
66
+ children: [
67
+ {
68
+ kind: 'exec',
69
+ commandRedacted: innerRecipe,
70
+ segmentHead,
71
+ requirements: [
72
+ {
73
+ tag: 'process.exec',
74
+ action: 'process.exec',
75
+ resource: { kind: 'executable', command: resolvedLocalPath ?? segmentHead },
76
+ evidence: {
77
+ level: resolvedLocalPath ? 'certain' : peel.forceAcquire ? 'possible' : 'certain',
78
+ signals: [
79
+ 'package_exec.delegate',
80
+ ...(resolvedLocalPath ? ['package_exec.local_bin_resolved'] : []),
81
+ ],
82
+ basis: [`launcher:${peel.launcher}`],
83
+ },
84
+ provenance: {
85
+ launcher: peel.launcher,
86
+ phase: 'delegate',
87
+ innerCommand: innerRecipe,
88
+ innerArgv: [...peel.innerTokens],
89
+ },
90
+ },
91
+ ],
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ function cacheWriteRequirement(peel) {
97
+ return {
98
+ tag: 'fs.write',
99
+ action: 'fs.write',
100
+ resource: { kind: 'package-cache', manager: peel.launcher === 'pnpm' ? 'pnpm' : 'npm' },
101
+ evidence: {
102
+ level: 'possible',
103
+ signals: [...peel.signals, 'package_cache_write'],
104
+ basis: [`launcher:${peel.launcher}:cache_write`],
105
+ },
106
+ provenance: { launcher: peel.launcher, phase: 'cache_write' },
107
+ };
108
+ }
109
+ function acquireRequirement(peel, level) {
110
+ const knownResources = packageAcquisitionResources(peel.acquisitionSpecs);
111
+ const networkRequirements = knownResources.map((resource) => acquireNetworkRequirement(peel, resource, 'possible'));
112
+ if (level === 'indeterminate') {
113
+ networkRequirements.push(acquireNetworkRequirement(peel, { kind: 'unknown' }, level));
114
+ }
115
+ else if (networkRequirements.length === 0 && peel.acquisitionSpecs.length === 0) {
116
+ networkRequirements.push(acquireNetworkRequirement(peel, { kind: 'network', host: 'registry.npmjs.org', protocol: 'registry' }, level));
117
+ }
118
+ return {
119
+ kind: 'launcher',
120
+ launcher: peel.launcher,
121
+ phase: 'acquire',
122
+ opaque: level === 'indeterminate',
123
+ reason: peel.reason,
124
+ children: [
125
+ {
126
+ kind: 'exec',
127
+ commandRedacted: peel.innerTokens.join(' ') || peel.launcher,
128
+ segmentHead: peel.launcher,
129
+ requirements: [
130
+ ...networkRequirements,
131
+ ...(level === 'possible' || peel.acquisitionSpecs.length > 0
132
+ ? [cacheWriteRequirement(peel)]
133
+ : []),
134
+ ],
135
+ },
136
+ ],
137
+ };
138
+ }
139
+ function acquireNetworkRequirement(peel, resource, level) {
140
+ return {
141
+ tag: level === 'indeterminate' ? 'indeterminate' : 'network.acquire',
142
+ action: level === 'indeterminate' ? 'indeterminate' : 'network.connect',
143
+ resource,
144
+ evidence: {
145
+ level,
146
+ signals: [...peel.signals, 'package_acquire_possible'],
147
+ basis: [`launcher:${peel.launcher}:acquire`],
148
+ },
149
+ provenance: {
150
+ launcher: peel.launcher,
151
+ phase: 'acquire',
152
+ innerArgv: [...peel.acquisitionSpecs],
153
+ },
154
+ };
155
+ }
156
+ function packageAcquisitionResources(specs) {
157
+ const resources = new Map();
158
+ for (const spec of specs) {
159
+ const source = classifyPackageAcquisitionSpec(spec);
160
+ if (source.kind === 'local') {
161
+ continue;
162
+ }
163
+ const resource = source.kind === 'registry'
164
+ ? { kind: 'network', host: 'registry.npmjs.org', protocol: 'registry' }
165
+ : {
166
+ kind: 'network',
167
+ host: source.host,
168
+ ...(source.port ? { port: source.port } : {}),
169
+ protocol: source.protocol,
170
+ };
171
+ resources.set(`${resource.host}:${resource.port ?? ''}:${resource.protocol ?? ''}`, resource);
172
+ }
173
+ return [...resources.values()];
174
+ }
175
+ export function collectRequirements(node) {
176
+ switch (node.kind) {
177
+ case 'exec':
178
+ return [...node.requirements];
179
+ case 'launcher':
180
+ return node.children.flatMap((child) => collectRequirements(child));
181
+ case 'merge':
182
+ return mergeRequirements(...node.children.map((child) => collectRequirements(child)));
183
+ default:
184
+ return [];
185
+ }
186
+ }
187
+ export function buildEffectPlan(params) {
188
+ const root = buildPackageExecEffectNode(params);
189
+ if (!root) {
190
+ return null;
191
+ }
192
+ const requirements = collectRequirements(root);
193
+ const signals = [
194
+ ...new Set([
195
+ ...(params.launcherResolution?.reason ? [params.launcherResolution.reason] : []),
196
+ ...requirements.flatMap((r) => r.evidence.signals),
197
+ ]),
198
+ ];
199
+ const opacity = peelPackageExecArgv(params.tokens)?.opaque || params.launcherResolution?.opaque
200
+ ? 'opaque'
201
+ : 'recursive';
202
+ return {
203
+ version: 1,
204
+ root,
205
+ inputFingerprint: params.inputFingerprint,
206
+ opacity,
207
+ disposition: requirements.length > 0 ? 'effects' : 'effect_free',
208
+ completeness: opacity === 'recursive' ? 'complete' : 'partial',
209
+ signals,
210
+ };
211
+ }
212
+ function effectTagForAction(action) {
213
+ return action === 'indeterminate' ? 'indeterminate' : action;
214
+ }
215
+ /** Build the canonical plan envelope for non-package-exec gated actions. */
216
+ export function buildCapabilityEffectPlan(params) {
217
+ const provenance = { segment: params.actionKind };
218
+ const requirements = params.requests.map((request) => ({
219
+ tag: effectTagForAction(request.action),
220
+ action: request.action,
221
+ resource: request.resource,
222
+ evidence: {
223
+ level: request.evidence.level,
224
+ signals: [...request.evidence.signals],
225
+ basis: [...request.context.analysisBasis],
226
+ },
227
+ provenance,
228
+ provenances: [provenance],
229
+ }));
230
+ if (!params.effectFree && requirements.length === 0) {
231
+ requirements.push({
232
+ tag: 'indeterminate',
233
+ action: 'indeterminate',
234
+ resource: { kind: 'unknown' },
235
+ evidence: {
236
+ level: 'indeterminate',
237
+ signals: ['effect_plan.incomplete'],
238
+ basis: [`gated_action:${params.actionKind}`],
239
+ },
240
+ provenance,
241
+ provenances: [provenance],
242
+ });
243
+ }
244
+ const partial = requirements.some((requirement) => requirement.evidence.level === 'indeterminate');
245
+ return {
246
+ version: 1,
247
+ root: {
248
+ kind: 'exec',
249
+ commandRedacted: params.summary,
250
+ segmentHead: params.actionKind,
251
+ requirements,
252
+ },
253
+ inputFingerprint: params.inputFingerprint,
254
+ opacity: partial ? 'opaque' : 'transparent',
255
+ disposition: params.effectFree ? 'effect_free' : 'effects',
256
+ completeness: partial ? 'partial' : 'complete',
257
+ signals: [...new Set(requirements.flatMap((requirement) => requirement.evidence.signals))],
258
+ };
259
+ }
260
+ export function flattenRequirementsToCapabilityRequests(requirements, opts) {
261
+ return requirements.map((req) => ({
262
+ version: 1,
263
+ principal: opts.principal,
264
+ action: req.action,
265
+ resource: req.resource,
266
+ context: {
267
+ hookKind: opts.hookKind,
268
+ cwd: opts.cwd,
269
+ inputFingerprint: opts.inputFingerprint,
270
+ analysisBasis: [...req.evidence.basis],
271
+ },
272
+ evidence: {
273
+ level: req.evidence.level,
274
+ signals: [...req.evidence.signals],
275
+ },
276
+ }));
277
+ }
@@ -0,0 +1,8 @@
1
+ export { effectPlanAuditFields, hashEffectPlan } from './audit.js';
2
+ export { buildCapabilityEffectPlan, buildEffectPlan, buildPackageExecEffectNode, collectRequirements, flattenRequirementsToCapabilityRequests, } from './build.js';
3
+ export { mergeEffectPlans, mergeRequirements, normalizeEffectTags } from './normalize.js';
4
+ export { innerRecipeFromPeel, isPackageExecLauncher, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
5
+ export { evaluatePackageExecSegment } from './package-exec-eval.js';
6
+ 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 { EFFECT_PLAN_VERSION } from './types.js';
@@ -0,0 +1,7 @@
1
+ export { effectPlanAuditFields, hashEffectPlan } from './audit.js';
2
+ export { buildCapabilityEffectPlan, buildEffectPlan, buildPackageExecEffectNode, collectRequirements, flattenRequirementsToCapabilityRequests, } from './build.js';
3
+ export { mergeEffectPlans, mergeRequirements, normalizeEffectTags } from './normalize.js';
4
+ export { innerRecipeFromPeel, isPackageExecLauncher, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
5
+ export { evaluatePackageExecSegment } from './package-exec-eval.js';
6
+ export { capabilityRequestsFromEffectPlan, capabilityRequestsToEffectRequirements, effectPlanPolicyLegacyReason, effectPlanPolicyRequiresAsk, evaluateEffectPlanPolicy, } from './policy.js';
7
+ export { EFFECT_PLAN_VERSION } from './types.js';
@@ -0,0 +1,4 @@
1
+ import type { EffectPlan, EffectRequirement, EffectTag } from './types.js';
2
+ export declare function normalizeEffectTags(tags: Iterable<EffectTag>): readonly EffectTag[];
3
+ export declare function mergeRequirements(...groups: readonly (readonly EffectRequirement[])[]): EffectRequirement[];
4
+ export declare function mergeEffectPlans(...plans: readonly (EffectPlan | null | undefined)[]): EffectPlan | undefined;
@@ -0,0 +1,104 @@
1
+ import { canonicalStringify, hashValue } from '../fingerprint.js';
2
+ export function normalizeEffectTags(tags) {
3
+ const set = new Set(tags);
4
+ if (set.has('read_only') && set.size > 1) {
5
+ throw new Error('read_only must not co-occur with other effect tags');
6
+ }
7
+ return [...set].sort();
8
+ }
9
+ export function mergeRequirements(...groups) {
10
+ const seen = new Map();
11
+ for (const group of groups) {
12
+ for (const req of group) {
13
+ const key = `${req.action}:${req.resource.kind}:${resourceKey(req.resource)}`;
14
+ const existing = seen.get(key);
15
+ if (!existing) {
16
+ seen.set(key, {
17
+ ...req,
18
+ provenances: uniqueProvenances(req.provenances ?? [req.provenance]),
19
+ });
20
+ continue;
21
+ }
22
+ const strongest = evidenceRank(req.evidence.level) > evidenceRank(existing.evidence.level) ? req : existing;
23
+ seen.set(key, {
24
+ ...strongest,
25
+ evidence: {
26
+ level: strongest.evidence.level,
27
+ signals: [...new Set([...existing.evidence.signals, ...req.evidence.signals])],
28
+ basis: [...new Set([...existing.evidence.basis, ...req.evidence.basis])],
29
+ },
30
+ provenances: uniqueProvenances([
31
+ ...(existing.provenances ?? [existing.provenance]),
32
+ ...(req.provenances ?? [req.provenance]),
33
+ ]),
34
+ });
35
+ }
36
+ }
37
+ return [...seen.values()];
38
+ }
39
+ export function mergeEffectPlans(...plans) {
40
+ const present = plans.filter((plan) => plan !== null && plan !== undefined);
41
+ if (!present.length) {
42
+ return undefined;
43
+ }
44
+ if (present.length === 1) {
45
+ return present[0];
46
+ }
47
+ const children = present.flatMap((plan) => plan.root.kind === 'merge' ? [...plan.root.children] : [plan.root]);
48
+ const opacity = present.some((plan) => plan.opacity === 'unparseable')
49
+ ? 'unparseable'
50
+ : present.some((plan) => plan.opacity === 'opaque')
51
+ ? 'opaque'
52
+ : present.some((plan) => plan.opacity === 'recursive')
53
+ ? 'recursive'
54
+ : 'transparent';
55
+ const fingerprints = present.map((plan) => plan.inputFingerprint).sort();
56
+ return {
57
+ version: 1,
58
+ root: { kind: 'merge', children },
59
+ inputFingerprint: hashValue(`effect-plan-input:v1:${canonicalStringify(fingerprints)}`),
60
+ opacity,
61
+ disposition: present.some((plan) => plan.disposition === 'effects') ? 'effects' : 'effect_free',
62
+ completeness: present.every((plan) => plan.completeness === 'complete')
63
+ ? 'complete'
64
+ : 'partial',
65
+ signals: [...new Set(present.flatMap((plan) => [...plan.signals]))].sort(),
66
+ };
67
+ }
68
+ function uniqueProvenances(provenances) {
69
+ const seen = new Map();
70
+ for (const provenance of provenances) {
71
+ seen.set(canonicalStringify(provenance), provenance);
72
+ }
73
+ return [...seen.values()];
74
+ }
75
+ function resourceKey(resource) {
76
+ switch (resource.kind) {
77
+ case 'path':
78
+ return resource.path;
79
+ case 'network':
80
+ return `${resource.host}:${resource.port ?? ''}:${resource.protocol ?? ''}`;
81
+ case 'executable':
82
+ return resource.command;
83
+ case 'package-cache':
84
+ return resource.manager;
85
+ case 'git-ref':
86
+ return resource.ref;
87
+ case 'unknown':
88
+ return 'unknown';
89
+ default:
90
+ return 'unknown';
91
+ }
92
+ }
93
+ function evidenceRank(level) {
94
+ switch (level) {
95
+ case 'certain':
96
+ return 3;
97
+ case 'possible':
98
+ return 2;
99
+ case 'indeterminate':
100
+ return 1;
101
+ default:
102
+ return 0;
103
+ }
104
+ }
@@ -0,0 +1,8 @@
1
+ import type { InternalSegmentVerdict, VerdictContext } from '../verdict/types.js';
2
+ export interface PackageExecEvalParams {
3
+ command: string;
4
+ peeled: string[];
5
+ context: VerdictContext;
6
+ evaluateInner: (innerRecipe: string) => Promise<InternalSegmentVerdict>;
7
+ }
8
+ export declare function evaluatePackageExecSegment(params: PackageExecEvalParams): Promise<InternalSegmentVerdict>;