@guilz-dev/belay 0.4.0 → 0.6.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 (158) hide show
  1. package/README.md +7 -1
  2. package/dist/adapters/layouts/scope.js +3 -2
  3. package/dist/adapters/shared/gate-runtime.d.ts +1 -2
  4. package/dist/adapters/shared/gate-runtime.js +226 -28
  5. package/dist/bundle/claude-runtime.mjs +6967 -4078
  6. package/dist/bundle/codex-runtime.mjs +5951 -3064
  7. package/dist/bundle/cursor-runtime.mjs +5951 -3064
  8. package/dist/cli.js +43 -1
  9. package/dist/commands/classify-for-report.js +1 -1
  10. package/dist/commands/config.d.ts +8 -0
  11. package/dist/commands/config.js +134 -11
  12. package/dist/commands/doctor.js +36 -1
  13. package/dist/commands/explain.js +11 -1
  14. package/dist/commands/judge.js +3 -2
  15. package/dist/commands/session.d.ts +20 -0
  16. package/dist/commands/session.js +51 -0
  17. package/dist/config-io.js +49 -12
  18. package/dist/conformance/guarantee-posture.d.ts +21 -0
  19. package/dist/conformance/guarantee-posture.js +46 -0
  20. package/dist/conformance/guarantee-table.d.ts +1 -0
  21. package/dist/conformance/guarantee-table.js +14 -9
  22. package/dist/core/approval-service.js +6 -2
  23. package/dist/core/approval.d.ts +2 -0
  24. package/dist/core/approval.js +14 -3
  25. package/dist/core/capability/approval-state-mutation.d.ts +13 -0
  26. package/dist/core/capability/approval-state-mutation.js +92 -0
  27. package/dist/core/capability/approval-v3.d.ts +15 -0
  28. package/dist/core/capability/approval-v3.js +74 -0
  29. package/dist/core/capability/attestation.d.ts +13 -0
  30. package/dist/core/capability/attestation.js +55 -0
  31. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  32. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  33. package/dist/core/capability/boundary-driver-container.d.ts +7 -0
  34. package/dist/core/capability/boundary-driver-container.js +122 -0
  35. package/dist/core/capability/boundary-driver.d.ts +29 -0
  36. package/dist/core/capability/boundary-driver.js +43 -0
  37. package/dist/core/capability/boundary-egress.d.ts +21 -0
  38. package/dist/core/capability/boundary-egress.js +109 -0
  39. package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
  40. package/dist/core/capability/boundary-grant-materialize.js +78 -0
  41. package/dist/core/capability/boundary-profile.d.ts +15 -0
  42. package/dist/core/capability/boundary-profile.js +25 -0
  43. package/dist/core/capability/boundary-run.d.ts +23 -0
  44. package/dist/core/capability/boundary-run.js +23 -0
  45. package/dist/core/capability/boundary-session.d.ts +57 -0
  46. package/dist/core/capability/boundary-session.js +129 -0
  47. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  48. package/dist/core/capability/capability-request-hash.js +8 -0
  49. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  50. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  51. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  52. package/dist/core/capability/gate-policy-shadow.js +108 -0
  53. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  54. package/dist/core/capability/gate-shadow-audit.js +1 -0
  55. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  56. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  57. package/dist/core/capability/grant-lease.d.ts +9 -0
  58. package/dist/core/capability/grant-lease.js +46 -0
  59. package/dist/core/capability/grant-loader.d.ts +12 -0
  60. package/dist/core/capability/grant-loader.js +23 -0
  61. package/dist/core/capability/grant-match.d.ts +6 -0
  62. package/dist/core/capability/grant-match.js +93 -0
  63. package/dist/core/capability/grant.d.ts +16 -0
  64. package/dist/core/capability/grant.js +16 -0
  65. package/dist/core/capability/index.d.ts +8 -0
  66. package/dist/core/capability/index.js +7 -0
  67. package/dist/core/capability/limits.d.ts +7 -0
  68. package/dist/core/capability/limits.js +55 -0
  69. package/dist/core/capability/policy-bridge.d.ts +18 -0
  70. package/dist/core/capability/policy-bridge.js +51 -0
  71. package/dist/core/capability/policy-engine.d.ts +70 -0
  72. package/dist/core/capability/policy-engine.js +476 -0
  73. package/dist/core/capability/policy-types.d.ts +20 -0
  74. package/dist/core/capability/policy-types.js +1 -0
  75. package/dist/core/capability/request.d.ts +45 -0
  76. package/dist/core/capability/request.js +1 -0
  77. package/dist/core/capability/resolver.d.ts +4 -0
  78. package/dist/core/capability/resolver.js +4 -0
  79. package/dist/core/classify-subagent.d.ts +2 -1
  80. package/dist/core/classify-subagent.js +38 -2
  81. package/dist/core/classify-tool.js +126 -62
  82. package/dist/core/config.d.ts +14 -3
  83. package/dist/core/config.js +56 -3
  84. package/dist/core/gate-contract.d.ts +3 -0
  85. package/dist/core/gate-contract.js +3 -0
  86. package/dist/core/gate-engine.js +6 -1
  87. package/dist/core/judge-doctor.js +39 -14
  88. package/dist/core/judge-fallback-hints.d.ts +13 -0
  89. package/dist/core/judge-fallback-hints.js +108 -0
  90. package/dist/core/path-utils.d.ts +1 -0
  91. package/dist/core/path-utils.js +5 -2
  92. package/dist/core/recovery/capability.d.ts +3 -0
  93. package/dist/core/recovery/capability.js +22 -0
  94. package/dist/core/recovery/fail-closed.d.ts +6 -0
  95. package/dist/core/recovery/fail-closed.js +25 -0
  96. package/dist/core/recovery/index.d.ts +3 -0
  97. package/dist/core/recovery/index.js +2 -0
  98. package/dist/core/recovery/types.d.ts +20 -0
  99. package/dist/core/recovery/types.js +1 -0
  100. package/dist/core/standing-allow.js +3 -0
  101. package/dist/core/transactional/diff-evaluator.js +4 -1
  102. package/dist/core/transactional/eligibility.js +4 -0
  103. package/dist/core/transactional/git-worktree.d.ts +10 -3
  104. package/dist/core/transactional/git-worktree.js +81 -11
  105. package/dist/core/transactional/reasons.d.ts +1 -0
  106. package/dist/core/transactional/reasons.js +4 -0
  107. package/dist/core/transactional/runner.js +22 -6
  108. package/dist/core/transactional/types.d.ts +4 -0
  109. package/dist/core/types.d.ts +21 -3
  110. package/dist/core/verdict/adapter.d.ts +3 -4
  111. package/dist/core/verdict/adapter.js +16 -12
  112. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  113. package/dist/core/verdict/cursor-acp-client.js +345 -0
  114. package/dist/core/verdict/egress-classify.d.ts +2 -2
  115. package/dist/core/verdict/egress-classify.js +2 -2
  116. package/dist/core/verdict/git-classifier.d.ts +26 -0
  117. package/dist/core/verdict/git-classifier.js +533 -0
  118. package/dist/core/verdict/judge-audit.js +1 -0
  119. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  120. package/dist/core/verdict/judge-baseline.js +1 -0
  121. package/dist/core/verdict/judge-broker-service.js +12 -0
  122. package/dist/core/verdict/judge-catalog.js +1 -0
  123. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  124. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  125. package/dist/core/verdict/judge-cli.js +5 -3
  126. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  127. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  128. package/dist/core/verdict/judge-runtime-config.js +10 -6
  129. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  130. package/dist/core/verdict/judge-session-broker.js +10 -5
  131. package/dist/core/verdict/judge-transport.d.ts +5 -1
  132. package/dist/core/verdict/judge-transport.js +24 -14
  133. package/dist/core/verdict/judge.d.ts +2 -15
  134. package/dist/core/verdict/judge.js +5 -68
  135. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  136. package/dist/core/verdict/launcher-resolve.js +16 -0
  137. package/dist/core/verdict/prescan.d.ts +15 -0
  138. package/dist/core/verdict/prescan.js +68 -0
  139. package/dist/core/verdict/shell-policy.d.ts +25 -0
  140. package/dist/core/verdict/shell-policy.js +40 -0
  141. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  142. package/dist/core/verdict/shell-semantics.js +225 -0
  143. package/dist/core/verdict/types.d.ts +13 -1
  144. package/dist/core/verdict/verdict.js +296 -178
  145. package/dist/corpus/evaluate.js +3 -7
  146. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  147. package/dist/corpus/gate-latency-budget.js +33 -0
  148. package/dist/corpus/mutators.d.ts +1 -1
  149. package/dist/corpus/mutators.js +11 -0
  150. package/dist/corpus/runtime-match.js +1 -3
  151. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  152. package/dist/judge-broker-daemon.js +15 -1
  153. package/dist/services/sandbox-service.d.ts +4 -0
  154. package/dist/services/sandbox-service.js +28 -2
  155. package/dist/types.d.ts +2 -2
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ import path from 'node:path';
2
+ import { belayStateDir } from '../config.js';
3
+ import { buildTier1Prompt } from '../verdict/judge.js';
4
+ import { normalizeJudgeRuntimeConfig } from '../verdict/judge-runtime-config.js';
5
+ import { judgeShadowAuditFields, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, } from '../verdict/judge-shadow.js';
6
+ import { evaluateWithJudgeTransport } from '../verdict/judge-transport.js';
7
+ import { recordPolicyJudgeComparison } from './gate-shadow-ratchet.js';
8
+ export { recordGateApprovalAsk } from './gate-shadow-ratchet.js';
9
+ export function capabilityDecisionAuditFields(result) {
10
+ const fields = {};
11
+ if (result.capabilityRequests?.length) {
12
+ fields.capabilityRequestCount = result.capabilityRequests.length;
13
+ fields.capabilityActions = result.capabilityRequests.map((request) => request.action);
14
+ }
15
+ if (result.authorizationDecision) {
16
+ fields.policyOutcome = result.authorizationDecision.outcome;
17
+ fields.policyReason = result.authorizationDecision.reason;
18
+ if (result.authorizationDecision.matchedRule) {
19
+ fields.policyMatchedRule = result.authorizationDecision.matchedRule;
20
+ }
21
+ }
22
+ if (result.boundaryProfile) {
23
+ fields.boundaryProfile = result.boundaryProfile;
24
+ }
25
+ if (result.authorizationDecision?.signals.includes('boundary_materialized_grant')) {
26
+ fields.boundaryGrantMaterialized = true;
27
+ }
28
+ return fields;
29
+ }
30
+ export function policyWouldBlock(result) {
31
+ const outcome = result.authorizationDecision?.outcome;
32
+ if (outcome === 'allow') {
33
+ return false;
34
+ }
35
+ if (outcome === 'require_approval' || outcome === 'deny') {
36
+ return true;
37
+ }
38
+ return result.verdict === 'deny_pending_approval';
39
+ }
40
+ export function judgeWouldBlock(verdict) {
41
+ return !verdict.local_recoverable || verdict.destroys_history_or_secrets === true;
42
+ }
43
+ export function policyJudgeMismatch(result, judgeVerdict) {
44
+ return policyWouldBlock(result) !== judgeWouldBlock(judgeVerdict);
45
+ }
46
+ function queueDeferredGatePolicyShadow(params) {
47
+ setImmediate(() => {
48
+ void runGatePolicyShadowComparison(params).catch(() => { });
49
+ });
50
+ }
51
+ export function scheduleGateShadowAudit(params) {
52
+ const trace = capabilityDecisionAuditFields(params.result);
53
+ const judge = params.config.judge;
54
+ const mode = judge?.mode ?? 'shadow';
55
+ if (mode !== 'shadow' || !judge.runtime?.shadow?.enabled) {
56
+ return trace;
57
+ }
58
+ const deferredTrace = {
59
+ ...trace,
60
+ ...judgeShadowAuditFields(params.repoRoot),
61
+ judgeShadowScheduled: false,
62
+ judgeShadowDeferred: true,
63
+ };
64
+ const command = params.command?.trim();
65
+ if (!command ||
66
+ params.providerId === 'ollama' ||
67
+ !shouldRunShadowComparison(params.repoRoot, params.providerId, judge.runtime.shadow)) {
68
+ return deferredTrace;
69
+ }
70
+ queueDeferredGatePolicyShadow({
71
+ repoRoot: params.repoRoot,
72
+ config: params.config,
73
+ providerId: params.providerId,
74
+ command,
75
+ result: params.result,
76
+ stateDir: params.stateDir,
77
+ });
78
+ return {
79
+ ...deferredTrace,
80
+ judgeShadowQueued: true,
81
+ };
82
+ }
83
+ export async function runGatePolicyShadowComparison(params) {
84
+ const judge = params.config.judge;
85
+ const runtime = normalizeJudgeRuntimeConfig(judge.runtime);
86
+ const stateDir = params.stateDir ?? belayStateDir(params.config, path.join(params.repoRoot, '.belay'));
87
+ const transport = await evaluateWithJudgeTransport({
88
+ prompt: buildTier1Prompt(params.command),
89
+ context: {
90
+ providerId: params.providerId,
91
+ model: judge.model ?? 'default',
92
+ repoRoot: params.repoRoot,
93
+ stateDir,
94
+ judgeMode: judge.mode ?? 'shadow',
95
+ runtime,
96
+ judgeTimeoutMs: judge.timeoutMs ?? 25_000,
97
+ },
98
+ }, { skipTransportShadow: true, spawnOnly: true });
99
+ if (!transport.verdict) {
100
+ return;
101
+ }
102
+ const mismatch = policyJudgeMismatch(params.result, transport.verdict);
103
+ const shadow = recordShadowComparison(params.repoRoot, runtime.shadow, mismatch);
104
+ await recordPolicyJudgeComparison(params.repoRoot, stateDir, mismatch);
105
+ if (shadow.killSwitchTriggered) {
106
+ await triggerJudgeSessionKillSwitch(params.repoRoot, stateDir);
107
+ }
108
+ }
@@ -0,0 +1 @@
1
+ export { capabilityDecisionAuditFields, judgeWouldBlock, policyJudgeMismatch, policyWouldBlock, recordGateApprovalAsk, runGatePolicyShadowComparison, scheduleGateShadowAudit, } from './gate-policy-shadow.js';
@@ -0,0 +1 @@
1
+ export { capabilityDecisionAuditFields, judgeWouldBlock, policyJudgeMismatch, policyWouldBlock, recordGateApprovalAsk, runGatePolicyShadowComparison, scheduleGateShadowAudit, } from './gate-policy-shadow.js';
@@ -0,0 +1,14 @@
1
+ export interface GateShadowRatchetState {
2
+ version: 1;
3
+ policyJudgeComparisons: number;
4
+ policyJudgeMismatches: number;
5
+ approvalByReason: Record<string, {
6
+ asks: number;
7
+ approved: number;
8
+ }>;
9
+ updatedAt: string;
10
+ }
11
+ export declare function recordPolicyJudgeComparison(repoRoot: string, stateDir: string, mismatch: boolean): Promise<void>;
12
+ export declare function recordGateApprovalAsk(stateDir: string, reason: string, approved: boolean): Promise<void>;
13
+ export declare function evaluateShadowRatchetWarnings(state: GateShadowRatchetState): string[];
14
+ export declare function loadShadowRatchetWarnings(stateDir: string): Promise<string[]>;
@@ -0,0 +1,81 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ const DEFAULT_STATE = {
5
+ version: 1,
6
+ policyJudgeComparisons: 0,
7
+ policyJudgeMismatches: 0,
8
+ approvalByReason: {},
9
+ updatedAt: new Date(0).toISOString(),
10
+ };
11
+ const MISMATCH_RATE_THRESHOLD = 0.25;
12
+ const APPROVAL_RATE_THRESHOLD = 0.5;
13
+ const MIN_SAMPLES = 10;
14
+ function ratchetPath(stateDir) {
15
+ return path.join(stateDir, 'gate-shadow-ratchet.json');
16
+ }
17
+ async function loadState(stateDir) {
18
+ const filePath = ratchetPath(stateDir);
19
+ if (!existsSync(filePath)) {
20
+ return { ...DEFAULT_STATE, updatedAt: new Date().toISOString() };
21
+ }
22
+ try {
23
+ const raw = JSON.parse(await readFile(filePath, 'utf8'));
24
+ return {
25
+ ...DEFAULT_STATE,
26
+ ...raw,
27
+ approvalByReason: raw.approvalByReason ?? {},
28
+ };
29
+ }
30
+ catch {
31
+ return { ...DEFAULT_STATE, updatedAt: new Date().toISOString() };
32
+ }
33
+ }
34
+ async function saveState(stateDir, state) {
35
+ await mkdir(stateDir, { recursive: true, mode: 0o700 });
36
+ await writeFile(ratchetPath(stateDir), `${JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
37
+ }
38
+ export async function recordPolicyJudgeComparison(repoRoot, stateDir, mismatch) {
39
+ void repoRoot;
40
+ const state = await loadState(stateDir);
41
+ state.policyJudgeComparisons += 1;
42
+ if (mismatch) {
43
+ state.policyJudgeMismatches += 1;
44
+ }
45
+ await saveState(stateDir, state);
46
+ }
47
+ export async function recordGateApprovalAsk(stateDir, reason, approved) {
48
+ const state = await loadState(stateDir);
49
+ const entry = state.approvalByReason[reason] ?? { asks: 0, approved: 0 };
50
+ if (approved) {
51
+ entry.approved += 1;
52
+ }
53
+ else {
54
+ entry.asks += 1;
55
+ }
56
+ state.approvalByReason[reason] = entry;
57
+ await saveState(stateDir, state);
58
+ }
59
+ export function evaluateShadowRatchetWarnings(state) {
60
+ const warnings = [];
61
+ if (state.policyJudgeComparisons >= MIN_SAMPLES) {
62
+ const rate = state.policyJudgeMismatches / state.policyJudgeComparisons;
63
+ if (rate > MISMATCH_RATE_THRESHOLD) {
64
+ warnings.push(`Policy/judge shadow mismatch rate ${(rate * 100).toFixed(0)}% exceeds ${(MISMATCH_RATE_THRESHOLD * 100).toFixed(0)}% threshold (${state.policyJudgeMismatches}/${state.policyJudgeComparisons}).`);
65
+ }
66
+ }
67
+ for (const [reason, counts] of Object.entries(state.approvalByReason)) {
68
+ if (counts.asks < MIN_SAMPLES) {
69
+ continue;
70
+ }
71
+ const approvalRate = counts.approved / counts.asks;
72
+ if (approvalRate >= APPROVAL_RATE_THRESHOLD) {
73
+ warnings.push(`High approval rate for "${reason}": ${(approvalRate * 100).toFixed(0)}% (${counts.approved}/${counts.asks}). Consider tightening policy.`);
74
+ }
75
+ }
76
+ return warnings;
77
+ }
78
+ export async function loadShadowRatchetWarnings(stateDir) {
79
+ const state = await loadState(stateDir);
80
+ return evaluateShadowRatchetWarnings(state);
81
+ }
@@ -0,0 +1,9 @@
1
+ import type { ApprovalStateFile } from '../types.js';
2
+ import type { CapabilityGrantV1 } from './grant.js';
3
+ import type { CapabilityRequestV1 } from './request.js';
4
+ export declare function grantsFromApprovedState(state: ApprovalStateFile, repoRoot: string): CapabilityGrantV1[];
5
+ export declare function consumeGrantLease(state: ApprovalStateFile, grantId: string): {
6
+ state: ApprovalStateFile;
7
+ consumed: boolean;
8
+ };
9
+ export declare function findMatchingGrant(grants: CapabilityGrantV1[] | undefined, request: CapabilityRequestV1): CapabilityGrantV1 | undefined;
@@ -0,0 +1,46 @@
1
+ import { isGrantScopeTooBroad } from './grant.js';
2
+ import { grantMatchesRequest } from './grant-match.js';
3
+ export function grantsFromApprovedState(state, repoRoot) {
4
+ const now = Date.now();
5
+ return state.approvals
6
+ .filter((approval) => approval.repoRoot === repoRoot)
7
+ .map((approval) => approval.grant)
8
+ .filter((grant) => {
9
+ if (!grant) {
10
+ return false;
11
+ }
12
+ if (isGrantScopeTooBroad(grant)) {
13
+ return false;
14
+ }
15
+ const expires = Date.parse(grant.expiresAt);
16
+ return Number.isFinite(expires) && expires > now && grant.usesRemaining > 0;
17
+ });
18
+ }
19
+ export function consumeGrantLease(state, grantId) {
20
+ let consumed = false;
21
+ const approvals = state.approvals.map((approval) => {
22
+ const grant = approval.grant;
23
+ if (!grant || grant.grantId !== grantId || grant.usesRemaining <= 0) {
24
+ return approval;
25
+ }
26
+ consumed = true;
27
+ const usesRemaining = grant.usesRemaining - 1;
28
+ return {
29
+ ...approval,
30
+ grant: {
31
+ ...grant,
32
+ usesRemaining,
33
+ },
34
+ };
35
+ });
36
+ return {
37
+ state: { ...state, approvals },
38
+ consumed,
39
+ };
40
+ }
41
+ export function findMatchingGrant(grants, request) {
42
+ if (!grants?.length) {
43
+ return undefined;
44
+ }
45
+ return grants.find((grant) => grantMatchesRequest(grant, request));
46
+ }
@@ -0,0 +1,12 @@
1
+ import type { BelayConfigV4 } from '../config.js';
2
+ import type { ApprovalStateFile, ClassifierOptions } from '../types.js';
3
+ import type { BoundaryAttestation } from './attestation.js';
4
+ import type { CapabilityGrantV1 } from './grant.js';
5
+ export declare function capabilityGrantsEnabled(config: BelayConfigV4): boolean;
6
+ export declare function grantsFromApprovalState(state: ApprovalStateFile, repoRoot: string, config: BelayConfigV4): CapabilityGrantV1[] | undefined;
7
+ export declare function loadClassifierAuthorization(params: {
8
+ repoRoot: string;
9
+ config: BelayConfigV4;
10
+ approvedState: ApprovalStateFile;
11
+ }): Promise<Pick<ClassifierOptions, 'grants' | 'attestation' | 'boundaryProfile'>>;
12
+ export type { BoundaryAttestation, CapabilityGrantV1 };
@@ -0,0 +1,23 @@
1
+ import { configuredControlPlaneDir } from '../config.js';
2
+ import { resolveBoundaryProfile } from './boundary-profile.js';
3
+ import { boundaryAttestationPath, loadBoundaryAttestation } from './boundary-session.js';
4
+ import { grantsFromApprovedState } from './grant-lease.js';
5
+ export function capabilityGrantsEnabled(config) {
6
+ return config.capability?.grantsEnabled !== false;
7
+ }
8
+ export function grantsFromApprovalState(state, repoRoot, config) {
9
+ if (!capabilityGrantsEnabled(config)) {
10
+ return undefined;
11
+ }
12
+ const grants = grantsFromApprovedState(state, repoRoot);
13
+ return grants.length > 0 ? grants : undefined;
14
+ }
15
+ export async function loadClassifierAuthorization(params) {
16
+ const grants = grantsFromApprovalState(params.approvedState, params.repoRoot, params.config);
17
+ const attestation = await loadBoundaryAttestation(boundaryAttestationPath(params.repoRoot, params.config), params.repoRoot, configuredControlPlaneDir(params.config));
18
+ const boundaryProfile = resolveBoundaryProfile({
19
+ config: params.config,
20
+ attestation,
21
+ });
22
+ return { grants, attestation, boundaryProfile };
23
+ }
@@ -0,0 +1,6 @@
1
+ import type { CapabilityGrantV1 } from './grant.js';
2
+ import type { CapabilityRequestV1 } from './request.js';
3
+ export declare function grantTargetsRequest(grant: CapabilityGrantV1, request: CapabilityRequestV1): boolean;
4
+ /** Repo-scoped target match for forged broad-grant detection (principal may be incomplete). */
5
+ export declare function broadGrantTargetsRequest(grant: CapabilityGrantV1, request: CapabilityRequestV1): boolean;
6
+ export declare function grantMatchesRequest(grant: CapabilityGrantV1, request: CapabilityRequestV1): boolean;
@@ -0,0 +1,93 @@
1
+ import { canonicalPath } from '../path-utils.js';
2
+ import { isGrantScopeTooBroad } from './grant.js';
3
+ function principalMatches(grant, request) {
4
+ if (grant.repoRoot !== request.repoRoot) {
5
+ return false;
6
+ }
7
+ if (request.adapter !== undefined && grant.adapter !== request.adapter) {
8
+ return false;
9
+ }
10
+ if (request.sessionHash !== undefined && grant.sessionHash !== request.sessionHash) {
11
+ return false;
12
+ }
13
+ if (grant.adapter !== undefined && grant.adapter !== request.adapter) {
14
+ return false;
15
+ }
16
+ if (grant.sessionHash !== undefined && grant.sessionHash !== request.sessionHash) {
17
+ return false;
18
+ }
19
+ return true;
20
+ }
21
+ function networkResourceMatches(grant, request) {
22
+ if (grant.host !== request.host) {
23
+ return false;
24
+ }
25
+ if (grant.port !== undefined && grant.port !== request.port) {
26
+ return false;
27
+ }
28
+ if (grant.protocol !== undefined &&
29
+ grant.protocol !== 'unknown' &&
30
+ request.protocol !== undefined &&
31
+ request.protocol !== 'unknown' &&
32
+ grant.protocol !== request.protocol) {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ function grantTargetsResource(grant, request) {
38
+ if (grant.action !== request.action) {
39
+ if (grant.action === 'indeterminate') {
40
+ return request.action === 'indeterminate';
41
+ }
42
+ return false;
43
+ }
44
+ if (grant.inputFingerprint && grant.inputFingerprint !== request.context.inputFingerprint) {
45
+ return false;
46
+ }
47
+ if (grant.resource.kind === 'path' && request.resource.kind === 'path') {
48
+ return canonicalPath(grant.resource.path) === canonicalPath(request.resource.path);
49
+ }
50
+ if (grant.resource.kind === 'network' && request.resource.kind === 'network') {
51
+ return networkResourceMatches(grant.resource, request.resource);
52
+ }
53
+ if (grant.resource.kind === 'executable' && request.resource.kind === 'executable') {
54
+ return grant.resource.command === request.resource.command;
55
+ }
56
+ if (grant.resource.kind === 'git-ref' && request.resource.kind === 'git-ref') {
57
+ return grant.resource.ref === request.resource.ref;
58
+ }
59
+ if (grant.resource.kind === 'unknown') {
60
+ if (grant.action === 'network.connect' && request.resource.kind === 'network') {
61
+ return true;
62
+ }
63
+ if (grant.action === 'fs.write' && request.resource.kind === 'path') {
64
+ return true;
65
+ }
66
+ if (grant.action === 'indeterminate') {
67
+ return request.action === 'indeterminate';
68
+ }
69
+ }
70
+ return grant.resource.kind === request.resource.kind;
71
+ }
72
+ export function grantTargetsRequest(grant, request) {
73
+ if (grant.usesRemaining <= 0) {
74
+ return false;
75
+ }
76
+ if (!principalMatches(grant.principal, request.principal)) {
77
+ return false;
78
+ }
79
+ return grantTargetsResource(grant, request);
80
+ }
81
+ /** Repo-scoped target match for forged broad-grant detection (principal may be incomplete). */
82
+ export function broadGrantTargetsRequest(grant, request) {
83
+ if (grant.principal.repoRoot !== request.principal.repoRoot) {
84
+ return false;
85
+ }
86
+ return grantTargetsResource(grant, request);
87
+ }
88
+ export function grantMatchesRequest(grant, request) {
89
+ if (isGrantScopeTooBroad(grant)) {
90
+ return false;
91
+ }
92
+ return grantTargetsRequest(grant, request);
93
+ }
@@ -0,0 +1,16 @@
1
+ import type { CapabilityAction, CapabilityPrincipal, CapabilityResource } from './request.js';
2
+ export declare const CAPABILITY_GRANT_VERSION: 1;
3
+ export interface CapabilityGrantV1 {
4
+ version: typeof CAPABILITY_GRANT_VERSION;
5
+ grantId: string;
6
+ principal: CapabilityPrincipal;
7
+ action: CapabilityAction;
8
+ resource: CapabilityResource;
9
+ inputFingerprint?: string;
10
+ issuedAt: string;
11
+ expiresAt: string;
12
+ maxUses: number;
13
+ usesRemaining: number;
14
+ issuer: string;
15
+ }
16
+ export declare function isGrantScopeTooBroad(grant: CapabilityGrantV1): boolean;
@@ -0,0 +1,16 @@
1
+ export const CAPABILITY_GRANT_VERSION = 1;
2
+ export function isGrantScopeTooBroad(grant) {
3
+ if (grant.action === 'network.connect' && grant.resource.kind === 'unknown') {
4
+ return true;
5
+ }
6
+ if (grant.resource.kind === 'network' && grant.resource.host === '*') {
7
+ return true;
8
+ }
9
+ if (grant.action === 'fs.write' && grant.resource.kind === 'unknown') {
10
+ return true;
11
+ }
12
+ if (grant.action === 'indeterminate') {
13
+ return true;
14
+ }
15
+ return false;
16
+ }
@@ -1,6 +1,14 @@
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';
3
+ export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
2
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
+ export { CAPABILITY_GRANT_VERSION, type CapabilityGrantV1, isGrantScopeTooBroad, } from './grant.js';
6
+ export { checkGatedActionLimits, MAX_SHELL_COMMAND_BYTES, MAX_TOOL_PAYLOAD_BYTES, } from './limits.js';
3
7
  export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
8
+ export { type CapabilityAuthorizationMetadata, type PolicyLegacyReasonHints, policyDecisionToLegacyReason, policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
9
+ export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, type FileMutationCapabilityAnalysis, getDefaultPolicyEngine, policyDecisionRequiresAsk, type ShellCapabilityAnalysis, type SubagentCapabilityAnalysis, } from './policy-engine.js';
10
+ export type { AuthorizationContext, PolicyDecision, PolicyEngine, PolicyOutcome, } from './policy-types.js';
4
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';
5
13
  export { addTrustedWorkspaceRoot, isBroadTrustedWorkspaceRoot, isHighStakesTrustedWorkspaceRoot, isPathWithinTrustedWorkspaceRoots, loadTrustedWorkspaceRoots, loadTrustedWorkspaceRootsSync, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from './trusted-workspace-roots.js';
6
14
  export type { CapabilityApprovalScope, FsScopeAllowlistEntry, FsScopeAllowlistFile, TrustedWorkspaceRootEntry, TrustedWorkspaceRootsFile, } from './types.js';
@@ -1,5 +1,12 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
+ export { BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
3
+ export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
2
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
+ export { CAPABILITY_GRANT_VERSION, isGrantScopeTooBroad, } from './grant.js';
6
+ export { checkGatedActionLimits, MAX_SHELL_COMMAND_BYTES, MAX_TOOL_PAYLOAD_BYTES, } from './limits.js';
3
7
  export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
8
+ export { policyDecisionToLegacyReason, policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
9
+ export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, getDefaultPolicyEngine, policyDecisionRequiresAsk, } from './policy-engine.js';
4
10
  export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from './reasons.js';
11
+ export { CAPABILITY_REQUEST_VERSION, } from './request.js';
5
12
  export { addTrustedWorkspaceRoot, isBroadTrustedWorkspaceRoot, isHighStakesTrustedWorkspaceRoot, isPathWithinTrustedWorkspaceRoots, loadTrustedWorkspaceRoots, loadTrustedWorkspaceRootsSync, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from './trusted-workspace-roots.js';
@@ -0,0 +1,7 @@
1
+ import type { GatedAction } from '../gate-contract.js';
2
+ import type { ClassifyResult } from '../types.js';
3
+ import { BOUNDARY_PROFILE_L3_POLICY } from './boundary-profile.js';
4
+ export { BOUNDARY_PROFILE_L3_POLICY };
5
+ export declare const MAX_SHELL_COMMAND_BYTES: number;
6
+ export declare const MAX_TOOL_PAYLOAD_BYTES: number;
7
+ export declare function checkGatedActionLimits(action: GatedAction): ClassifyResult | null;
@@ -0,0 +1,55 @@
1
+ import { BOUNDARY_PROFILE_L3_POLICY } from './boundary-profile.js';
2
+ export { BOUNDARY_PROFILE_L3_POLICY };
3
+ export const MAX_SHELL_COMMAND_BYTES = 64 * 1024;
4
+ export const MAX_TOOL_PAYLOAD_BYTES = 1024 * 1024;
5
+ function limitExceededResult(reason, summary) {
6
+ return {
7
+ verdict: 'deny_pending_approval',
8
+ reason,
9
+ fingerprint: `limit:${reason}`,
10
+ summary,
11
+ assessment: {
12
+ reversibility: 'irreversible',
13
+ external: false,
14
+ blastRadius: 'gate input budget',
15
+ confidence: 1,
16
+ signals: [reason, 'analysis_budget_exceeded'],
17
+ },
18
+ boundaryProfile: BOUNDARY_PROFILE_L3_POLICY,
19
+ };
20
+ }
21
+ function shellCommandFromAction(action) {
22
+ if (action.command?.trim()) {
23
+ return action.command.trim();
24
+ }
25
+ const payload = action.payload;
26
+ if (!payload) {
27
+ return '';
28
+ }
29
+ const direct = payload.command;
30
+ if (typeof direct === 'string' && direct.trim()) {
31
+ return direct.trim();
32
+ }
33
+ const toolInput = payload.tool_input;
34
+ if (toolInput && typeof toolInput === 'object') {
35
+ const command = toolInput.command;
36
+ if (typeof command === 'string' && command.trim()) {
37
+ return command.trim();
38
+ }
39
+ }
40
+ return '';
41
+ }
42
+ export function checkGatedActionLimits(action) {
43
+ if (action.kind === 'shell') {
44
+ const command = shellCommandFromAction(action);
45
+ if (Buffer.byteLength(command, 'utf8') > MAX_SHELL_COMMAND_BYTES) {
46
+ return limitExceededResult('input_too_large', command.slice(0, 120));
47
+ }
48
+ return null;
49
+ }
50
+ const payloadBytes = Buffer.byteLength(JSON.stringify(action.payload ?? {}), 'utf8');
51
+ if (payloadBytes > MAX_TOOL_PAYLOAD_BYTES) {
52
+ return limitExceededResult('input_too_large', action.toolName ?? action.kind);
53
+ }
54
+ return null;
55
+ }
@@ -0,0 +1,18 @@
1
+ import type { PolicyDecision } from './policy-types.js';
2
+ import type { CapabilityRequestV1 } from './request.js';
3
+ export interface CapabilityAuthorizationMetadata {
4
+ capabilityRequests: CapabilityRequestV1[];
5
+ authorizationDecision: PolicyDecision;
6
+ }
7
+ export interface PolicyLegacyReasonHints {
8
+ outsideMutation?: boolean;
9
+ effect?: 'read_only' | 'local_mutation' | 'remote_mutation' | 'unknown';
10
+ }
11
+ export declare function singleCapabilityMetadata(request: CapabilityRequestV1, decision: PolicyDecision): CapabilityAuthorizationMetadata;
12
+ /**
13
+ * Maps a PolicyDecision to hook-facing legacy reason strings.
14
+ * Pass hints when the shell segment context is known (outside-repo mutation, etc.).
15
+ */
16
+ export declare function policyDecisionToLegacyReason(decision: PolicyDecision, hints?: PolicyLegacyReasonHints): string;
17
+ /** @deprecated Use policyDecisionToLegacyReason for shell segments that need outside-repo hints. */
18
+ export declare function policyReasonToLegacyReason(decision: PolicyDecision): string;
@@ -0,0 +1,51 @@
1
+ export function singleCapabilityMetadata(request, decision) {
2
+ return {
3
+ capabilityRequests: [request],
4
+ authorizationDecision: decision,
5
+ };
6
+ }
7
+ /**
8
+ * Maps a PolicyDecision to hook-facing legacy reason strings.
9
+ * Pass hints when the shell segment context is known (outside-repo mutation, etc.).
10
+ */
11
+ export function policyDecisionToLegacyReason(decision, hints = {}) {
12
+ if (decision.reason === 'outside_repo_mutation') {
13
+ return 'outside_repo_mutation';
14
+ }
15
+ if (decision.reason === 'external_effect' || decision.reason === 'network_connect') {
16
+ return 'external_effect';
17
+ }
18
+ if (decision.reason === 'high_stakes_path' || decision.reason === 'secret_path') {
19
+ return 'tier1_catastrophic';
20
+ }
21
+ if (decision.reason === 'indeterminate_effect') {
22
+ return 'unknown_local_effect';
23
+ }
24
+ if (decision.reason === 'opaque_execution') {
25
+ return 'opaque_execution';
26
+ }
27
+ if (decision.reason === 'control_plane_mutation') {
28
+ return 'control_plane_mutation';
29
+ }
30
+ if (decision.reason === 'policy_default') {
31
+ if (decision.signals.includes('outside_repo_path') ||
32
+ decision.signals.includes('outside_repo_mutation')) {
33
+ return 'outside_repo_mutation';
34
+ }
35
+ if (hints.outsideMutation && hints.effect !== 'read_only') {
36
+ return 'outside_repo_mutation';
37
+ }
38
+ return 'unknown_local_effect';
39
+ }
40
+ if (decision.reason === 'subagent_review') {
41
+ return 'subagent_review';
42
+ }
43
+ if (decision.outcome === 'deny') {
44
+ return decision.reason;
45
+ }
46
+ return decision.reason;
47
+ }
48
+ /** @deprecated Use policyDecisionToLegacyReason for shell segments that need outside-repo hints. */
49
+ export function policyReasonToLegacyReason(decision) {
50
+ return policyDecisionToLegacyReason(decision);
51
+ }