@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
@@ -51,6 +51,9 @@ export function hostIntegrationBoundaryContext(repoRoot) {
51
51
  egressProxyActive: false,
52
52
  proxyEnv: {},
53
53
  },
54
+ attestationPath: '',
55
+ attestation: null,
56
+ attestationFresh: false,
54
57
  };
55
58
  }
56
59
  export async function resolveBoundaryDriverContext(params) {
@@ -74,6 +77,8 @@ export async function resolveBoundaryDriverContext(params) {
74
77
  egressProxyEnv: proxyEnv,
75
78
  repoRoot: params.repoRoot,
76
79
  });
80
+ const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
81
+ const attestation = await loadBoundaryAttestation(attestationPath, params.repoRoot, configuredControlPlaneDir(params.config));
77
82
  return {
78
83
  driver,
79
84
  driverId,
@@ -84,6 +89,9 @@ export async function resolveBoundaryDriverContext(params) {
84
89
  egressProxyActive: proxyActive,
85
90
  proxyEnv,
86
91
  },
92
+ attestationPath,
93
+ attestation,
94
+ attestationFresh: attestation ? isAttestationFresh(attestation) : false,
87
95
  };
88
96
  }
89
97
  export async function runWithBoundaryDriver(params) {
@@ -124,6 +132,7 @@ export async function runBoundaryAgentCommand(params) {
124
132
  command: params.command,
125
133
  cwd: params.cwd ?? params.repoRoot,
126
134
  timeoutMs: params.timeoutMs ?? 30 * 60_000,
135
+ runOptions: params.runOptions,
127
136
  });
128
137
  }
129
138
  export { dockerNetworkArgs };
@@ -0,0 +1,5 @@
1
+ import type { BoundaryWorkspaceMount } from './boundary-run.js';
2
+ export declare function validateWorkspaceMount(mount: BoundaryWorkspaceMount): void;
3
+ export declare function resolveGuestWorkdir(mount: BoundaryWorkspaceMount): string;
4
+ export declare function buildWorkspaceMountSpec(mount: BoundaryWorkspaceMount): string;
5
+ export declare function workspaceMountEnvArgs(mount: BoundaryWorkspaceMount): string[];
@@ -0,0 +1,68 @@
1
+ import path from 'node:path';
2
+ import { canonicalPath, pathWithinRoot } from '../path-utils.js';
3
+ const HOST_PATH_ENV_VARS = [
4
+ 'BELAY_EGRESS_REPO_ROOT',
5
+ 'BELAY_JUDGE_BROKER_REPO_ROOT',
6
+ 'BELAY_JUDGE_BROKER_STATE_DIR',
7
+ 'BELAY_REPO_ROOT',
8
+ ];
9
+ export function validateWorkspaceMount(mount) {
10
+ const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
11
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
12
+ if (!hostSourceRoot || !guestTargetRoot) {
13
+ throw new Error('boundary_workspace_mount_invalid_root');
14
+ }
15
+ if (hostSourceRoot.includes(',') || guestTargetRoot.includes(',')) {
16
+ throw new Error('boundary_workspace_mount_invalid_root');
17
+ }
18
+ if (hostSourceRoot === guestTargetRoot) {
19
+ throw new Error('boundary_workspace_mount_source_equals_target');
20
+ }
21
+ if (pathWithinRoot(hostSourceRoot, guestTargetRoot)) {
22
+ throw new Error('boundary_workspace_mount_source_contains_target');
23
+ }
24
+ if (mount.cwdRelative.includes('\0')) {
25
+ throw new Error('boundary_workspace_mount_invalid_cwd');
26
+ }
27
+ const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
28
+ if (normalizedRelative === '..' || normalizedRelative.startsWith('../')) {
29
+ throw new Error('boundary_workspace_mount_invalid_cwd');
30
+ }
31
+ if (path.posix.isAbsolute(normalizedRelative)) {
32
+ throw new Error('boundary_workspace_mount_invalid_cwd');
33
+ }
34
+ }
35
+ export function resolveGuestWorkdir(mount) {
36
+ validateWorkspaceMount(mount);
37
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
38
+ const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
39
+ if (normalizedRelative === '.' || normalizedRelative === '') {
40
+ return guestTargetRoot;
41
+ }
42
+ const segments = normalizedRelative.split('/').filter(Boolean);
43
+ return path.posix.join(guestTargetRoot, ...segments);
44
+ }
45
+ export function buildWorkspaceMountSpec(mount) {
46
+ validateWorkspaceMount(mount);
47
+ const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
48
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
49
+ const readonly = mount.writable ? '' : ',readonly';
50
+ return `type=bind,src=${hostSourceRoot},dst=${guestTargetRoot}${readonly}`;
51
+ }
52
+ export function workspaceMountEnvArgs(mount) {
53
+ const workdir = resolveGuestWorkdir(mount);
54
+ if (!mount.hideHostSourcePath) {
55
+ return ['-e', `PWD=${workdir}`];
56
+ }
57
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
58
+ const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
59
+ const args = [];
60
+ for (const key of HOST_PATH_ENV_VARS) {
61
+ args.push('-e', `${key}=`);
62
+ }
63
+ args.push('-e', `PWD=${workdir}`, '-e', `OLDPWD=${workdir}`, '-e', `BELAY_REPO_ROOT=${guestTargetRoot}`);
64
+ if (hostSourceRoot !== guestTargetRoot) {
65
+ args.push('-e', `BELAY_EGRESS_REPO_ROOT=${guestTargetRoot}`);
66
+ }
67
+ return args;
68
+ }
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { belayStateDir } from '../config.js';
3
+ import { effectPlanAuditFields } from '../effect-ir/audit.js';
3
4
  import { buildTier1Prompt } from '../verdict/judge.js';
4
5
  import { normalizeJudgeRuntimeConfig } from '../verdict/judge-runtime-config.js';
5
6
  import { judgeShadowAuditFields, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, } from '../verdict/judge-shadow.js';
@@ -7,7 +8,16 @@ import { evaluateWithJudgeTransport } from '../verdict/judge-transport.js';
7
8
  import { recordPolicyJudgeComparison } from './gate-shadow-ratchet.js';
8
9
  export { recordGateApprovalAsk } from './gate-shadow-ratchet.js';
9
10
  export function capabilityDecisionAuditFields(result) {
10
- const fields = {};
11
+ const fields = {
12
+ ...effectPlanAuditFields(result.effectPlan, {
13
+ capabilityRequests: result.capabilityRequests,
14
+ decisions: result.effectPlanPolicyDecisions,
15
+ authorizationDecision: result.authorizationDecision,
16
+ }),
17
+ executionRoute: 'downstream_host',
18
+ enforcementStatus: 'prediction_only',
19
+ boundaryEnforcement: 'prediction_only',
20
+ };
11
21
  if (result.capabilityRequests?.length) {
12
22
  fields.capabilityRequestCount = result.capabilityRequests.length;
13
23
  fields.capabilityActions = result.capabilityRequests.map((request) => request.action);
@@ -19,6 +29,18 @@ export function capabilityDecisionAuditFields(result) {
19
29
  fields.policyMatchedRule = result.authorizationDecision.matchedRule;
20
30
  }
21
31
  }
32
+ const decisions = result.effectPlanPolicyDecisions ?? [];
33
+ if (decisions.length > 0 &&
34
+ decisions.every((decision) => decision.matchedRule === 'boundary.verified')) {
35
+ fields.authorizationMode = 'boundary_policy';
36
+ }
37
+ else if (decisions.length > 0 &&
38
+ decisions.every((decision) => decision.matchedRule === 'grant.exact')) {
39
+ fields.authorizationMode = 'exact_grant';
40
+ }
41
+ else {
42
+ fields.authorizationMode = result.authorizationDecision ? 'policy_decision' : 'none';
43
+ }
22
44
  if (result.boundaryProfile) {
23
45
  fields.boundaryProfile = result.boundaryProfile;
24
46
  }
@@ -0,0 +1,4 @@
1
+ import type { ClassifyResult } from '../types.js';
2
+ export declare function capabilityGrantLeaseRequired(result: Pick<ClassifyResult, 'reason' | 'authorizationDecision'>): boolean;
3
+ /** Fail closed when a grant lease is required but capability requests are missing. */
4
+ export declare function canConsumeCapabilityGrantLease(result: ClassifyResult): boolean;
@@ -0,0 +1,11 @@
1
+ export function capabilityGrantLeaseRequired(result) {
2
+ return (result.reason === 'capability_grant' ||
3
+ result.authorizationDecision?.matchedRule === 'grant.exact');
4
+ }
5
+ /** Fail closed when a grant lease is required but capability requests are missing. */
6
+ export function canConsumeCapabilityGrantLease(result) {
7
+ if (!capabilityGrantLeaseRequired(result)) {
8
+ return true;
9
+ }
10
+ return (result.capabilityRequests?.length ?? 0) > 0;
11
+ }
@@ -1,9 +1,44 @@
1
- import type { ApprovalStateFile } from '../types.js';
1
+ import type { ApprovalRecord, ApprovalStateFile } from '../types.js';
2
2
  import type { CapabilityGrantV1 } from './grant.js';
3
3
  import type { CapabilityRequestV1 } from './request.js';
4
+ export type GrantBundleValidationFailureReason = 'legacy_record' | 'empty_requests' | 'cardinality_mismatch' | 'grant_scope_too_broad' | 'grant_inactive' | 'grant_mismatch';
5
+ export type GrantBundleValidationResult = {
6
+ ok: true;
7
+ approval: ApprovalRecord;
8
+ } | {
9
+ ok: false;
10
+ reason: GrantBundleValidationFailureReason;
11
+ };
12
+ export declare function grantsFromApproval(approval: {
13
+ grant?: CapabilityGrantV1;
14
+ grants?: CapabilityGrantV1[];
15
+ }): CapabilityGrantV1[];
16
+ export declare function approvalGrantBundleExhausted(approval: {
17
+ grant?: CapabilityGrantV1;
18
+ grants?: CapabilityGrantV1[];
19
+ }): boolean;
20
+ export declare function consumeApprovalGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[]): {
21
+ approval: ApprovalRecord;
22
+ consumed: boolean;
23
+ };
24
+ /** Validate and consume a marked exact bundle without mutating on any failure. */
25
+ export declare function validateAndConsumeGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
26
+ /** Revalidate a marked bundle during an active replay lease without consuming it twice. */
27
+ export declare function validateGrantBundleForLeaseReuse(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
28
+ /** Consume every grant in an approved record after fingerprint/hash replay validation. */
29
+ export declare function consumeApprovedRecordGrantBundle(approval: ApprovalRecord): {
30
+ approval: ApprovalRecord;
31
+ consumed: boolean;
32
+ };
33
+ export declare function decrementApprovalLegacyGrant(approval: ApprovalRecord): ApprovalRecord;
4
34
  export declare function grantsFromApprovedState(state: ApprovalStateFile, repoRoot: string): CapabilityGrantV1[];
5
35
  export declare function consumeGrantLease(state: ApprovalStateFile, grantId: string): {
6
36
  state: ApprovalStateFile;
7
37
  consumed: boolean;
8
38
  };
39
+ /** Consume one lease for each request; all must succeed (atomic bundle consumption). */
40
+ export declare function consumeGrantLeasesForRequests(state: ApprovalStateFile, requests: CapabilityRequestV1[]): {
41
+ state: ApprovalStateFile;
42
+ consumed: boolean;
43
+ };
9
44
  export declare function findMatchingGrant(grants: CapabilityGrantV1[] | undefined, request: CapabilityRequestV1): CapabilityGrantV1 | undefined;
@@ -1,36 +1,212 @@
1
+ import { canonicalStringify } from '../fingerprint.js';
1
2
  import { isGrantScopeTooBroad } from './grant.js';
2
3
  import { grantMatchesRequest } from './grant-match.js';
3
- export function grantsFromApprovedState(state, repoRoot) {
4
+ function isActiveGrant(grant, now) {
5
+ if (isGrantScopeTooBroad(grant)) {
6
+ return false;
7
+ }
8
+ const expires = Date.parse(grant.expiresAt);
9
+ return Number.isFinite(expires) && expires > now && grant.usesRemaining > 0;
10
+ }
11
+ export function grantsFromApproval(approval) {
12
+ if (approval.grants?.length) {
13
+ return approval.grants;
14
+ }
15
+ if (approval.grant) {
16
+ return [approval.grant];
17
+ }
18
+ return [];
19
+ }
20
+ export function approvalGrantBundleExhausted(approval) {
21
+ const bundle = grantsFromApproval(approval);
22
+ if (!bundle.length) {
23
+ return false;
24
+ }
25
+ return bundle.every((grant) => grant.usesRemaining <= 0);
26
+ }
27
+ export function consumeApprovalGrantBundle(approval, requests) {
28
+ const bundle = grantsFromApproval(approval);
29
+ if (!bundle.length || !requests.length) {
30
+ return { approval, consumed: false };
31
+ }
32
+ const grants = [...bundle];
4
33
  const now = Date.now();
5
- return state.approvals
6
- .filter((approval) => approval.repoRoot === repoRoot)
7
- .map((approval) => approval.grant)
8
- .filter((grant) => {
34
+ for (const request of requests) {
35
+ const index = grants.findIndex((grant) => {
36
+ return isActiveGrant(grant, now) && grantMatchesRequest(grant, request);
37
+ });
38
+ if (index === -1) {
39
+ return { approval, consumed: false };
40
+ }
41
+ const grant = grants[index];
9
42
  if (!grant) {
10
- return false;
43
+ return { approval, consumed: false };
11
44
  }
12
- if (isGrantScopeTooBroad(grant)) {
45
+ grants[index] = decrementGrant(grant);
46
+ }
47
+ return {
48
+ approval: {
49
+ ...approval,
50
+ grants,
51
+ grant: grants[0],
52
+ },
53
+ consumed: true,
54
+ };
55
+ }
56
+ function exactGrantMatchesRequest(grant, request) {
57
+ return (persistedCanonicalStringify(grant.principal) ===
58
+ persistedCanonicalStringify(request.principal) &&
59
+ grant.action === request.action &&
60
+ persistedCanonicalStringify(grant.resource) === persistedCanonicalStringify(request.resource) &&
61
+ grant.inputFingerprint === request.context.inputFingerprint);
62
+ }
63
+ function validateExactGrantBundle(approval, requests, now, usage) {
64
+ if (approval.grantBundleVersion !== 1) {
65
+ return { ok: false, reason: 'legacy_record' };
66
+ }
67
+ if (!requests.length) {
68
+ return { ok: false, reason: 'empty_requests' };
69
+ }
70
+ const bundle = grantsFromApproval(approval);
71
+ if (bundle.length !== requests.length) {
72
+ return { ok: false, reason: 'cardinality_mismatch' };
73
+ }
74
+ if (bundle.some((grant) => isGrantScopeTooBroad(grant))) {
75
+ return { ok: false, reason: 'grant_scope_too_broad' };
76
+ }
77
+ const usageIsValid = bundle.every((grant) => {
78
+ const expires = Date.parse(grant.expiresAt);
79
+ if (!Number.isFinite(expires) || expires <= now) {
13
80
  return false;
14
81
  }
15
- const expires = Date.parse(grant.expiresAt);
16
- return Number.isFinite(expires) && expires > now && grant.usesRemaining > 0;
82
+ return usage === 'available'
83
+ ? grant.usesRemaining > 0
84
+ : grant.usesRemaining >= 0 && grant.usesRemaining < grant.maxUses;
17
85
  });
86
+ if (!usageIsValid) {
87
+ return { ok: false, reason: 'grant_inactive' };
88
+ }
89
+ const unmatched = [...bundle];
90
+ for (const request of requests) {
91
+ const index = unmatched.findIndex((grant) => exactGrantMatchesRequest(grant, request));
92
+ if (index === -1) {
93
+ return { ok: false, reason: 'grant_mismatch' };
94
+ }
95
+ unmatched.splice(index, 1);
96
+ }
97
+ if (unmatched.length > 0) {
98
+ return { ok: false, reason: 'grant_mismatch' };
99
+ }
100
+ return { ok: true, approval };
101
+ }
102
+ function persistedCanonicalStringify(value) {
103
+ return canonicalStringify(omitUndefined(value));
104
+ }
105
+ function omitUndefined(value) {
106
+ if (Array.isArray(value)) {
107
+ return value.map((entry) => omitUndefined(entry));
108
+ }
109
+ if (!value || typeof value !== 'object') {
110
+ return value;
111
+ }
112
+ return Object.fromEntries(Object.entries(value)
113
+ .filter(([, entry]) => entry !== undefined)
114
+ .map(([key, entry]) => [key, omitUndefined(entry)]));
115
+ }
116
+ /** Validate and consume a marked exact bundle without mutating on any failure. */
117
+ export function validateAndConsumeGrantBundle(approval, requests, now = Date.now()) {
118
+ const validated = validateExactGrantBundle(approval, requests, now, 'available');
119
+ if (!validated.ok) {
120
+ return validated;
121
+ }
122
+ const grants = grantsFromApproval(approval).map((grant) => decrementGrant(grant));
123
+ return {
124
+ ok: true,
125
+ approval: {
126
+ ...approval,
127
+ grants,
128
+ grant: grants[0],
129
+ },
130
+ };
131
+ }
132
+ /** Revalidate a marked bundle during an active replay lease without consuming it twice. */
133
+ export function validateGrantBundleForLeaseReuse(approval, requests, now = Date.now()) {
134
+ return validateExactGrantBundle(approval, requests, now, 'previously_consumed');
135
+ }
136
+ /** Consume every grant in an approved record after fingerprint/hash replay validation. */
137
+ export function consumeApprovedRecordGrantBundle(approval) {
138
+ const bundle = grantsFromApproval(approval);
139
+ const now = Date.now();
140
+ if (!bundle.length ||
141
+ bundle.some((grant) => {
142
+ const expires = Date.parse(grant.expiresAt);
143
+ return !Number.isFinite(expires) || expires <= now || grant.usesRemaining <= 0;
144
+ })) {
145
+ return { approval, consumed: false };
146
+ }
147
+ const grants = bundle.map((grant) => decrementGrant(grant));
148
+ return {
149
+ approval: {
150
+ ...approval,
151
+ grants,
152
+ grant: grants[0],
153
+ },
154
+ consumed: true,
155
+ };
156
+ }
157
+ export function decrementApprovalLegacyGrant(approval) {
158
+ const bundle = grantsFromApproval(approval);
159
+ if (!bundle.length) {
160
+ return approval;
161
+ }
162
+ const grants = bundle.map((grant, index) => (index === 0 ? decrementGrant(grant) : grant));
163
+ return {
164
+ ...approval,
165
+ grants,
166
+ grant: grants[0],
167
+ };
168
+ }
169
+ export function grantsFromApprovedState(state, repoRoot) {
170
+ const now = Date.now();
171
+ const collected = [];
172
+ for (const approval of state.approvals) {
173
+ if (approval.repoRoot !== repoRoot) {
174
+ continue;
175
+ }
176
+ for (const grant of grantsFromApproval(approval)) {
177
+ if (isActiveGrant(grant, now)) {
178
+ collected.push(grant);
179
+ }
180
+ }
181
+ }
182
+ return collected;
183
+ }
184
+ function decrementGrant(grant) {
185
+ return {
186
+ ...grant,
187
+ usesRemaining: grant.usesRemaining - 1,
188
+ };
18
189
  }
19
190
  export function consumeGrantLease(state, grantId) {
20
191
  let consumed = false;
21
192
  const approvals = state.approvals.map((approval) => {
22
- const grant = approval.grant;
23
- if (!grant || grant.grantId !== grantId || grant.usesRemaining <= 0) {
193
+ const bundle = grantsFromApproval(approval);
194
+ const index = bundle.findIndex((g) => g.grantId === grantId);
195
+ if (index === -1) {
196
+ return approval;
197
+ }
198
+ const grant = bundle[index];
199
+ if (!grant || grant.usesRemaining <= 0) {
24
200
  return approval;
25
201
  }
26
202
  consumed = true;
27
- const usesRemaining = grant.usesRemaining - 1;
203
+ const updated = decrementGrant(grant);
204
+ const grants = [...bundle];
205
+ grants[index] = updated;
28
206
  return {
29
207
  ...approval,
30
- grant: {
31
- ...grant,
32
- usesRemaining,
33
- },
208
+ grants,
209
+ grant: grants[0],
34
210
  };
35
211
  });
36
212
  return {
@@ -38,6 +214,27 @@ export function consumeGrantLease(state, grantId) {
38
214
  consumed,
39
215
  };
40
216
  }
217
+ /** Consume one lease for each request; all must succeed (atomic bundle consumption). */
218
+ export function consumeGrantLeasesForRequests(state, requests) {
219
+ const repoRoot = requests[0]?.principal.repoRoot;
220
+ if (!repoRoot || requests.some((request) => request.principal.repoRoot !== repoRoot)) {
221
+ return { state, consumed: false };
222
+ }
223
+ for (let index = 0; index < state.approvals.length; index += 1) {
224
+ const approval = state.approvals[index];
225
+ if (!approval || approval.repoRoot !== repoRoot) {
226
+ continue;
227
+ }
228
+ const consumed = consumeApprovalGrantBundle(approval, requests);
229
+ if (!consumed.consumed) {
230
+ continue;
231
+ }
232
+ const approvals = [...state.approvals];
233
+ approvals[index] = consumed.approval;
234
+ return { state: { ...state, approvals }, consumed: true };
235
+ }
236
+ return { state, consumed: false };
237
+ }
41
238
  export function findMatchingGrant(grants, request) {
42
239
  if (!grants?.length) {
43
240
  return undefined;
@@ -53,6 +53,9 @@ function grantTargetsResource(grant, request) {
53
53
  if (grant.resource.kind === 'executable' && request.resource.kind === 'executable') {
54
54
  return grant.resource.command === request.resource.command;
55
55
  }
56
+ if (grant.resource.kind === 'package-cache' && request.resource.kind === 'package-cache') {
57
+ return grant.resource.manager === request.resource.manager;
58
+ }
56
59
  if (grant.resource.kind === 'git-ref' && request.resource.kind === 'git-ref') {
57
60
  return grant.resource.ref === request.resource.ref;
58
61
  }
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, isAttestationFresh, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, type CapabilityGrantV1, isGrantScopeTooBroad, } from './grant.js';
@@ -9,6 +9,6 @@ export { type CapabilityAuthorizationMetadata, type PolicyLegacyReasonHints, pol
9
9
  export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, type FileMutationCapabilityAnalysis, getDefaultPolicyEngine, policyDecisionRequiresAsk, type ShellCapabilityAnalysis, type SubagentCapabilityAnalysis, } from './policy-engine.js';
10
10
  export type { AuthorizationContext, PolicyDecision, PolicyEngine, PolicyOutcome, } from './policy-types.js';
11
11
  export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from './reasons.js';
12
- export { CAPABILITY_REQUEST_VERSION, type CapabilityAction, type CapabilityEvidence, type CapabilityEvidenceLevel, type CapabilityHookKind, type CapabilityPrincipal, type CapabilityRequestContext, type CapabilityRequestV1, type CapabilityResource, } from './request.js';
12
+ export { CAPABILITY_REQUEST_VERSION, type CapabilityAction, type CapabilityEvidence, type CapabilityEvidenceLevel, type CapabilityHookKind, type CapabilityPrincipal, type CapabilityRequestContext, type CapabilityRequestV1, type CapabilityResource, type GitRefScope, type NetworkMode, type NetworkPayload, type ProcessOperation, } from './request.js';
13
13
  export { addTrustedWorkspaceRoot, isBroadTrustedWorkspaceRoot, isHighStakesTrustedWorkspaceRoot, isPathWithinTrustedWorkspaceRoots, loadTrustedWorkspaceRoots, loadTrustedWorkspaceRootsSync, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from './trusted-workspace-roots.js';
14
14
  export type { CapabilityApprovalScope, FsScopeAllowlistEntry, FsScopeAllowlistFile, TrustedWorkspaceRootEntry, TrustedWorkspaceRootsFile, } from './types.js';
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, isGrantScopeTooBroad, } from './grant.js';
@@ -1,11 +1,41 @@
1
1
  import type { BelayConfigV4 } from '../config.js';
2
2
  import type { VerdictEffect, VerdictLocation, VerdictOpacity } from '../verdict/types.js';
3
+ import type { CapabilityGrantV1 } from './grant.js';
3
4
  import type { AuthorizationContext, PolicyDecision, PolicyEngine } from './policy-types.js';
4
- import { type CapabilityHookKind, type CapabilityRequestV1 } from './request.js';
5
+ import { type CapabilityAction, type CapabilityEvidenceLevel, type CapabilityHookKind, type CapabilityRequestV1, type CapabilityResource } from './request.js';
5
6
  export type PolicyAuthExtras = Pick<AuthorizationContext, 'grants' | 'attestation'> & {
6
7
  egressProxyActive?: boolean;
7
8
  sensitivePaths?: string[];
8
9
  };
10
+ export type EffectPolicyDisposition = 'allow' | 'allow_flagged' | 'ask';
11
+ export interface EffectRequirementPolicyInput {
12
+ tag: string;
13
+ action: CapabilityAction;
14
+ resource: CapabilityResource;
15
+ evidence: {
16
+ level: CapabilityEvidenceLevel;
17
+ signals: readonly string[];
18
+ basis: readonly string[];
19
+ };
20
+ provenance?: unknown;
21
+ }
22
+ export interface EffectRequirementPolicyContext {
23
+ cwd: string;
24
+ repoRoot: string;
25
+ trustedWorkspaceRoots?: readonly string[];
26
+ sensitivePaths?: readonly string[];
27
+ protectedArtifactRoots?: readonly string[];
28
+ grants?: readonly CapabilityGrantV1[];
29
+ capabilityRequest?: CapabilityRequestV1;
30
+ }
31
+ export interface EffectRequirementPolicyDecision extends PolicyDecision {
32
+ effectDisposition: EffectPolicyDisposition;
33
+ }
34
+ /**
35
+ * Policy for a lowered shell effect. It intentionally ignores executable
36
+ * command names, segment heads, command text, and config allowlists.
37
+ */
38
+ export declare function evaluateEffectRequirementPolicy(requirement: EffectRequirementPolicyInput, context: EffectRequirementPolicyContext): EffectRequirementPolicyDecision;
9
39
  export interface ShellCapabilityAnalysis {
10
40
  command: string;
11
41
  hookKind: CapabilityHookKind;
@@ -56,12 +86,19 @@ export declare function evaluateSubagentPolicy(analysis: SubagentCapabilityAnaly
56
86
  decision: PolicyDecision;
57
87
  };
58
88
  export declare function buildShellCapabilityRequest(analysis: ShellCapabilityAnalysis): CapabilityRequestV1;
89
+ export declare function buildShellCapabilityRequests(analysis: ShellCapabilityAnalysis): CapabilityRequestV1[];
59
90
  export declare function buildFileMutationCapabilityRequest(analysis: FileMutationCapabilityAnalysis): CapabilityRequestV1;
60
91
  export declare function createTypeScriptPolicyEngine(): PolicyEngine;
61
92
  export declare function getDefaultPolicyEngine(): PolicyEngine;
62
93
  export declare function policyDecisionRequiresAsk(decision: PolicyDecision): boolean;
94
+ export declare function combinePolicyDecisions(decisions: readonly PolicyDecision[]): PolicyDecision;
95
+ export declare function evaluateCapabilityRequestsPolicy(requests: readonly CapabilityRequestV1[], config: BelayConfigV4, auth?: PolicyAuthExtras, trustedWorkspaceRoots?: string[]): {
96
+ decisions: PolicyDecision[];
97
+ decision: PolicyDecision;
98
+ };
63
99
  export declare function evaluateShellPolicy(analysis: ShellCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {
64
100
  request: CapabilityRequestV1;
101
+ requests: CapabilityRequestV1[];
65
102
  decision: PolicyDecision;
66
103
  };
67
104
  export declare function evaluateFileMutationPolicy(analysis: FileMutationCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {