@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.
- package/README.md +7 -1
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +1 -2
- package/dist/adapters/shared/gate-runtime.js +226 -28
- package/dist/bundle/claude-runtime.mjs +6967 -4078
- package/dist/bundle/codex-runtime.mjs +5951 -3064
- package/dist/bundle/cursor-runtime.mjs +5951 -3064
- package/dist/cli.js +43 -1
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +134 -11
- package/dist/commands/doctor.js +36 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/config-io.js +49 -12
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +14 -9
- package/dist/core/approval-service.js +6 -2
- package/dist/core/approval.d.ts +2 -0
- package/dist/core/approval.js +14 -3
- package/dist/core/capability/approval-state-mutation.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.js +92 -0
- package/dist/core/capability/approval-v3.d.ts +15 -0
- package/dist/core/capability/approval-v3.js +74 -0
- package/dist/core/capability/attestation.d.ts +13 -0
- package/dist/core/capability/attestation.js +55 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +7 -0
- package/dist/core/capability/boundary-driver-container.js +122 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +43 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
- package/dist/core/capability/boundary-grant-materialize.js +78 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +23 -0
- package/dist/core/capability/boundary-run.js +23 -0
- package/dist/core/capability/boundary-session.d.ts +57 -0
- package/dist/core/capability/boundary-session.js +129 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +108 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-lease.d.ts +9 -0
- package/dist/core/capability/grant-lease.js +46 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +93 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +8 -0
- package/dist/core/capability/index.js +7 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +70 -0
- package/dist/core/capability/policy-engine.js +476 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +45 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +38 -2
- package/dist/core/classify-tool.js +126 -62
- package/dist/core/config.d.ts +14 -3
- package/dist/core/config.js +56 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +6 -1
- package/dist/core/judge-doctor.js +39 -14
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +3 -0
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/types.d.ts +20 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/standing-allow.js +3 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/git-worktree.d.ts +10 -3
- package/dist/core/transactional/git-worktree.js +81 -11
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +4 -0
- package/dist/core/transactional/runner.js +22 -6
- package/dist/core/transactional/types.d.ts +4 -0
- package/dist/core/types.d.ts +21 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +16 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.js +1 -0
- package/dist/core/verdict/judge-baseline.d.ts +1 -1
- package/dist/core/verdict/judge-baseline.js +1 -0
- package/dist/core/verdict/judge-broker-service.js +12 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
- package/dist/core/verdict/judge-cli.js +5 -3
- package/dist/core/verdict/judge-provider-matrix.js +1 -1
- package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
- package/dist/core/verdict/judge-runtime-config.js +10 -6
- package/dist/core/verdict/judge-session-broker.d.ts +1 -1
- package/dist/core/verdict/judge-session-broker.js +10 -5
- package/dist/core/verdict/judge-transport.d.ts +5 -1
- package/dist/core/verdict/judge-transport.js +24 -14
- package/dist/core/verdict/judge.d.ts +2 -15
- package/dist/core/verdict/judge.js +5 -68
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +16 -0
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +25 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +13 -1
- package/dist/core/verdict/verdict.js +296 -178
- package/dist/corpus/evaluate.js +3 -7
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/mutators.d.ts +1 -1
- package/dist/corpus/mutators.js +11 -0
- package/dist/corpus/runtime-match.js +1 -3
- package/dist/corpus/standing-allow-catalog.generated.js +29 -9
- package/dist/judge-broker-daemon.js +15 -1
- package/dist/services/sandbox-service.d.ts +4 -0
- package/dist/services/sandbox-service.js +28 -2
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,7 +244,13 @@ Notable settings:
|
|
|
244
244
|
(`judge.providerId`); **driver** is the API compatibility layer (`judge.provider`:
|
|
245
245
|
`ollama`, `openai-compatible`, or `anthropic`); **host** is where hooks install
|
|
246
246
|
(`config.adapter`: `cursor`, `claude`, `codex`). Set `judge.endpoint` when needed;
|
|
247
|
-
credentials via `belay config credential mode project|apiKey` or env vars.
|
|
247
|
+
credentials via `belay config credential mode project|apiKey` or env vars. **`project`**
|
|
248
|
+
reads API keys from the shell environment at runtime (`BELAY_JUDGE_API_KEY`, then provider
|
|
249
|
+
vars such as `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `CURSOR_API_KEY`); Belay does **not**
|
|
250
|
+
load `.env` files for Judge. When the native CLI is available, `project` can also use the
|
|
251
|
+
host login session without a stored key. **`apiKey`** stores a key in Belay's credential store
|
|
252
|
+
(`credentials.json`, mode `0600`; under `~/.config/belay/` when control plane is enabled).
|
|
253
|
+
Record egress
|
|
248
254
|
consent during `belay config` or via `belay judge consent` → `belay approve` →
|
|
249
255
|
`belay judge use … --cloud-consent-approval-id`. Cloud providers can use native CLI
|
|
250
256
|
transport without `judge.endpoint` when the host CLI is available (`codex-cli`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { isPathOutsideRoot } from '../../core/path-utils.js';
|
|
3
4
|
function agentHomeDir(adapter) {
|
|
4
5
|
const home = os.homedir();
|
|
5
6
|
if (adapter === 'cursor') {
|
|
@@ -23,13 +24,13 @@ export function isPathInside(child, parent) {
|
|
|
23
24
|
const resolvedChild = path.resolve(child);
|
|
24
25
|
const resolvedParent = path.resolve(parent);
|
|
25
26
|
const relative = path.relative(resolvedParent, resolvedChild);
|
|
26
|
-
return relative === '' || (!relative
|
|
27
|
+
return relative === '' || (!isPathOutsideRoot(relative) && !path.isAbsolute(relative));
|
|
27
28
|
}
|
|
28
29
|
export function buildRunnerInvocation(platform, hooksDir, repoRoot, hookScript, ...args) {
|
|
29
30
|
const runnerFile = platform === 'win32' ? 'belay-runner.cmd' : 'belay-runner';
|
|
30
31
|
const runnerAbs = path.resolve(hooksDir, runnerFile);
|
|
31
32
|
const relative = path.relative(path.resolve(repoRoot), runnerAbs);
|
|
32
|
-
const useRelative = relative.length > 0 && !relative
|
|
33
|
+
const useRelative = relative.length > 0 && !isPathOutsideRoot(relative) && !path.isAbsolute(relative);
|
|
33
34
|
const runnerRef = useRelative
|
|
34
35
|
? platform === 'win32'
|
|
35
36
|
? `.\\${relative.split(path.sep).join('\\')}`
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ApprovalReplayHint } from '../../core/approval-replay.js';
|
|
2
|
-
import type
|
|
3
|
-
import { type GatePermissionResponse, type GateVerdict } from '../../core/gate-contract.js';
|
|
2
|
+
import { type GatedActionKind, type GatePermissionResponse, type GateVerdict } from '../../core/gate-contract.js';
|
|
4
3
|
import { GateNormalizationError } from '../../core/gate-engine.js';
|
|
5
4
|
import { type BelayConfigV3 } from '../../core/index.js';
|
|
6
5
|
import type { ApprovalStateFile, ClassifierOptions } from '../../core/types.js';
|
|
@@ -7,16 +7,27 @@ import { buildReplayHint, buildRetryInstructionForConfig, getExecutionLeaseMs, r
|
|
|
7
7
|
import { consumeApprovedAfterCliReplay, gateApprovalStoreFromDeps, recordApproval, } from '../../core/approval-service.js';
|
|
8
8
|
import { issueApprovalToken } from '../../core/approval-token.js';
|
|
9
9
|
import { buildAuditActionSnapshot, buildAuditReplayContext, } from '../../core/audit-replay-context.js';
|
|
10
|
+
import { mutateApprovalStateWithRetry } from '../../core/capability/approval-state-mutation.js';
|
|
11
|
+
import { APPROVAL_STATE_VERSION_V3 } from '../../core/capability/approval-v3.js';
|
|
12
|
+
import { isEgressProxyActive } from '../../core/capability/boundary-egress.js';
|
|
13
|
+
import { resolveBoundaryDriverContext } from '../../core/capability/boundary-session.js';
|
|
14
|
+
import { hashCapabilityRequests } from '../../core/capability/capability-request-hash.js';
|
|
15
|
+
import { recordGateApprovalAsk, scheduleGateShadowAudit, } from '../../core/capability/gate-shadow-audit.js';
|
|
16
|
+
import { consumeGrantLease, findMatchingGrant, grantsFromApprovedState, } from '../../core/capability/grant-lease.js';
|
|
17
|
+
import { loadClassifierAuthorization } from '../../core/capability/grant-loader.js';
|
|
10
18
|
import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, loadTrustedWorkspaceRootsSync, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from '../../core/capability/index.js';
|
|
11
19
|
import { resolveLayeredConfig, teamConfigPath } from '../../core/config-layers.js';
|
|
12
20
|
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
13
21
|
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
14
|
-
import { approvalCommandMatch, approvedApprovalsFile, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
22
|
+
import { approvalCommandMatch, approvedApprovalsFile, belayStateDir, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
23
|
+
import { extractJudgeFallbackReason, formatJudgeInfrastructureDenyMessage, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../../core/judge-fallback-hints.js';
|
|
15
24
|
import { notifyDeny } from '../../core/notify.js';
|
|
16
25
|
import { canonicalPath } from '../../core/path-utils.js';
|
|
26
|
+
import { recoveryFailClosedResult, recoveryFailReasonFromSkip, } from '../../core/recovery/fail-closed.js';
|
|
17
27
|
import { fingerprintReplayPayload } from '../../core/replay-scrub.js';
|
|
18
28
|
import { loadStandingAllow, resolveStandingAllowMatch, standingAllowFile, } from '../../core/standing-allow.js';
|
|
19
29
|
import { isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
30
|
+
import { egressStatus } from '../../services/egress-service.js';
|
|
20
31
|
import { protectedArtifactRoots } from '../layouts/protected-paths.js';
|
|
21
32
|
const EMPTY_APPROVALS = {
|
|
22
33
|
version: 1,
|
|
@@ -66,10 +77,12 @@ export function createDefaultGateRuntimeDeps() {
|
|
|
66
77
|
? pendingApprovalsFile(ctx.config, repoLocalStateDir)
|
|
67
78
|
: approvedApprovalsFile(ctx.config, repoLocalStateDir);
|
|
68
79
|
const loaded = await loadJsonFile(filePath, EMPTY_APPROVALS);
|
|
80
|
+
const version = loaded.version === 3 ? 3 : loaded.version === 2 ? 2 : 1;
|
|
69
81
|
return {
|
|
70
82
|
filePath,
|
|
71
83
|
state: {
|
|
72
|
-
version
|
|
84
|
+
version,
|
|
85
|
+
...(loaded.revision !== undefined ? { revision: loaded.revision } : {}),
|
|
73
86
|
approvals: Array.isArray(loaded.approvals) ? loaded.approvals : [],
|
|
74
87
|
},
|
|
75
88
|
};
|
|
@@ -133,7 +146,7 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, sco
|
|
|
133
146
|
approval.repoRoot === ctx.repoRoot);
|
|
134
147
|
if (existing) {
|
|
135
148
|
await deps.writeApprovals(pending.filePath, pending.state);
|
|
136
|
-
return existing;
|
|
149
|
+
return { approval: existing, created: false };
|
|
137
150
|
}
|
|
138
151
|
const approval = createApprovalRecordWithEnvelope({
|
|
139
152
|
kind,
|
|
@@ -145,11 +158,13 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, sco
|
|
|
145
158
|
approvalId: `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`,
|
|
146
159
|
approvalInput,
|
|
147
160
|
scopeHint,
|
|
161
|
+
capabilityRequests: result.capabilityRequests,
|
|
148
162
|
});
|
|
149
|
-
pending.state.version =
|
|
163
|
+
pending.state.version = APPROVAL_STATE_VERSION_V3;
|
|
164
|
+
pending.state.revision = (pending.state.revision ?? 0) + 1;
|
|
150
165
|
pending.state.approvals.push(approval);
|
|
151
166
|
await deps.writeApprovals(pending.filePath, pending.state);
|
|
152
|
-
return approval;
|
|
167
|
+
return { approval, created: true };
|
|
153
168
|
}
|
|
154
169
|
function extractShellCommandFromPayload(payload) {
|
|
155
170
|
const toolInput = payload.tool_input;
|
|
@@ -208,26 +223,53 @@ function deriveWorkspaceRootScopeHint(params) {
|
|
|
208
223
|
return { scope: 'workspace-root', path: validation.normalizedPath };
|
|
209
224
|
}
|
|
210
225
|
async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
const index =
|
|
226
|
+
const loaded = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
227
|
+
loaded.state = compactApprovals(loaded.state);
|
|
228
|
+
const index = loaded.state.approvals.findIndex((approval) => approval.kind === kind &&
|
|
214
229
|
approval.fingerprint === fingerprint &&
|
|
215
230
|
approval.repoRoot === ctx.repoRoot);
|
|
216
231
|
if (index === -1) {
|
|
217
|
-
await deps.writeApprovals(
|
|
232
|
+
await deps.writeApprovals(loaded.filePath, loaded.state);
|
|
218
233
|
return null;
|
|
219
234
|
}
|
|
220
|
-
const approval =
|
|
235
|
+
const approval = loaded.state.approvals[index];
|
|
221
236
|
if (approval.executionLeaseExpiresAt) {
|
|
222
|
-
await deps.writeApprovals(
|
|
223
|
-
return approval;
|
|
237
|
+
await deps.writeApprovals(loaded.filePath, loaded.state);
|
|
238
|
+
return { approval, firstExecution: false };
|
|
239
|
+
}
|
|
240
|
+
const consumed = await mutateApprovalStateWithRetry({
|
|
241
|
+
load: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
242
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
243
|
+
mutate: (state) => {
|
|
244
|
+
const compacted = compactApprovals(state);
|
|
245
|
+
const matchIndex = compacted.approvals.findIndex((approval) => approval.kind === kind &&
|
|
246
|
+
approval.fingerprint === fingerprint &&
|
|
247
|
+
approval.repoRoot === ctx.repoRoot);
|
|
248
|
+
if (matchIndex === -1) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const approval = compacted.approvals[matchIndex];
|
|
252
|
+
if (approval.grant && approval.grant.usesRemaining <= 0) {
|
|
253
|
+
compacted.approvals.splice(matchIndex, 1);
|
|
254
|
+
return { state: compacted, result: null };
|
|
255
|
+
}
|
|
256
|
+
compacted.approvals[matchIndex] = {
|
|
257
|
+
...approval,
|
|
258
|
+
executionLeaseExpiresAt: new Date(Date.now() + getExecutionLeaseMs(ctx.config)).toISOString(),
|
|
259
|
+
grant: approval.grant
|
|
260
|
+
? {
|
|
261
|
+
...approval.grant,
|
|
262
|
+
usesRemaining: Math.max(0, approval.grant.usesRemaining - 1),
|
|
263
|
+
}
|
|
264
|
+
: undefined,
|
|
265
|
+
};
|
|
266
|
+
return { state: compacted, result: approval };
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
if (!consumed) {
|
|
270
|
+
return null;
|
|
224
271
|
}
|
|
225
|
-
|
|
226
|
-
...approval,
|
|
227
|
-
executionLeaseExpiresAt: new Date(Date.now() + getExecutionLeaseMs(ctx.config)).toISOString(),
|
|
228
|
-
};
|
|
229
|
-
await deps.writeApprovals(approved.filePath, approved.state);
|
|
230
|
-
return approval;
|
|
272
|
+
return { approval: consumed, firstExecution: true };
|
|
231
273
|
}
|
|
232
274
|
export async function evaluateGatedAction(ctx, deps, params) {
|
|
233
275
|
let action;
|
|
@@ -280,15 +322,42 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
280
322
|
});
|
|
281
323
|
}
|
|
282
324
|
const classifierOptions = runtimeClassifierOptions(ctx, ctx.config);
|
|
283
|
-
const
|
|
325
|
+
const approvedForAuth = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
326
|
+
const authorization = await loadClassifierAuthorization({
|
|
327
|
+
repoRoot: ctx.repoRoot,
|
|
328
|
+
config: ctx.config,
|
|
329
|
+
approvedState: compactApprovals(approvedForAuth.state),
|
|
330
|
+
});
|
|
331
|
+
const egress = await egressStatus({ targetDir: ctx.repoRoot });
|
|
332
|
+
const egressProxyActive = isEgressProxyActive({
|
|
333
|
+
config: ctx.config,
|
|
334
|
+
running: egress.running,
|
|
335
|
+
foreignProxy: egress.foreignProxy,
|
|
336
|
+
repoRootMismatch: egress.repoRootMismatch,
|
|
337
|
+
});
|
|
338
|
+
const enrichedClassifierOptions = {
|
|
339
|
+
...classifierOptions,
|
|
340
|
+
...authorization,
|
|
341
|
+
egressProxyActive,
|
|
342
|
+
};
|
|
343
|
+
const predicted = await classifyGatedActionAsync(action, ctx.config, enrichedClassifierOptions);
|
|
284
344
|
let result = predicted;
|
|
285
345
|
let predictedAssessment;
|
|
286
346
|
let observedAssessment;
|
|
287
347
|
let transactionalLayer;
|
|
288
|
-
|
|
348
|
+
const recoveryCandidate = isTransactionalEligible(ctx.config, params.kind, predicted) &&
|
|
289
349
|
params.kind === 'shell' &&
|
|
290
|
-
params.command)
|
|
350
|
+
Boolean(params.command);
|
|
351
|
+
if (recoveryCandidate && params.command) {
|
|
291
352
|
const transactional = ctx.config.policy.transactional;
|
|
353
|
+
const boundaryDriverId = ctx.config.capability?.boundaryDriver ??
|
|
354
|
+
(ctx.config.sandbox.runtime === 'container' ? 'container' : 'host-integration');
|
|
355
|
+
const boundaryContext = await resolveBoundaryDriverContext({
|
|
356
|
+
repoRoot: ctx.repoRoot,
|
|
357
|
+
config: ctx.config,
|
|
358
|
+
driverId: boundaryDriverId,
|
|
359
|
+
egressProxyRunning: egressProxyActive,
|
|
360
|
+
});
|
|
292
361
|
const txResult = await runTransactionalExecution({
|
|
293
362
|
command: params.command,
|
|
294
363
|
cwd: params.cwd,
|
|
@@ -299,9 +368,11 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
299
368
|
diffContext: {
|
|
300
369
|
repoRoot: ctx.repoRoot,
|
|
301
370
|
sensitivePaths: ctx.config.classifier.sensitivePaths,
|
|
302
|
-
protectedRoots:
|
|
371
|
+
protectedRoots: enrichedClassifierOptions.protectedArtifactRoots ?? [],
|
|
303
372
|
maxDeletionCount: transactional.maxDeletionCount,
|
|
304
373
|
},
|
|
374
|
+
boundaryContext,
|
|
375
|
+
dirtyIgnoreRoots: protectedArtifactRoots(ctx.layout, ctx.repoRoot, ctx.config.controlPlane.enabled ? configuredControlPlaneDir(ctx.config) : null),
|
|
305
376
|
});
|
|
306
377
|
if (!txResult.skipped && txResult.observed) {
|
|
307
378
|
result = txResult.result;
|
|
@@ -315,10 +386,15 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
315
386
|
transactionalTimedOut: txResult.timedOut === true,
|
|
316
387
|
};
|
|
317
388
|
}
|
|
318
|
-
else
|
|
389
|
+
else {
|
|
390
|
+
const skipReason = txResult.skipReason ?? 'recovery_observation_failed';
|
|
391
|
+
result = recoveryFailClosedResult(predicted, recoveryFailReasonFromSkip(skipReason), [
|
|
392
|
+
skipReason,
|
|
393
|
+
]);
|
|
319
394
|
transactionalLayer = {
|
|
320
395
|
transactional: false,
|
|
321
|
-
transactionalSkipReason:
|
|
396
|
+
transactionalSkipReason: skipReason,
|
|
397
|
+
recoveryFailClosed: true,
|
|
322
398
|
};
|
|
323
399
|
}
|
|
324
400
|
}
|
|
@@ -353,7 +429,7 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
353
429
|
fingerprint: result.fingerprint,
|
|
354
430
|
repoRoot: ctx.repoRoot,
|
|
355
431
|
},
|
|
356
|
-
classifierOptions,
|
|
432
|
+
classifierOptions: enrichedClassifierOptions,
|
|
357
433
|
scopeHintPayload: params.payload,
|
|
358
434
|
});
|
|
359
435
|
}
|
|
@@ -392,9 +468,47 @@ export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
|
392
468
|
},
|
|
393
469
|
});
|
|
394
470
|
}
|
|
471
|
+
async function consumeCapabilityGrantIfUsed(ctx, deps, result) {
|
|
472
|
+
if (result.reason !== 'capability_grant' &&
|
|
473
|
+
result.authorizationDecision?.matchedRule !== 'grant.exact') {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
const request = result.capabilityRequests?.[0];
|
|
477
|
+
if (!request) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const consumed = await mutateApprovalStateWithRetry({
|
|
481
|
+
load: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
482
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
483
|
+
mutate: (state) => {
|
|
484
|
+
const compacted = compactApprovals(state);
|
|
485
|
+
const grant = findMatchingGrant(grantsFromApprovedState(compacted, ctx.repoRoot), request);
|
|
486
|
+
if (!grant) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const lease = consumeGrantLease(compacted, grant.grantId);
|
|
490
|
+
if (!lease.consumed) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
return { state: lease.state, result: true };
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
return consumed === true;
|
|
497
|
+
}
|
|
395
498
|
async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
|
|
396
499
|
const replayContext = buildAuditReplayContext(kind, result, auditExtras.replayAction);
|
|
397
500
|
const actionSnapshot = buildAuditActionSnapshot(kind, result, auditExtras.replayAction);
|
|
501
|
+
const providerId = String(ctx.config.judge?.providerId ?? 'cursor');
|
|
502
|
+
const stateDir = belayStateDir(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot));
|
|
503
|
+
const capabilityAudit = scheduleGateShadowAudit({
|
|
504
|
+
repoRoot: ctx.repoRoot,
|
|
505
|
+
config: ctx.config,
|
|
506
|
+
providerId,
|
|
507
|
+
result,
|
|
508
|
+
command: auditExtras.replayAction?.command ??
|
|
509
|
+
(kind === 'shell' ? auditExtras.approvalInput?.input : undefined),
|
|
510
|
+
stateDir,
|
|
511
|
+
});
|
|
398
512
|
const gateBase = {
|
|
399
513
|
event: gateAuditEventName(kind),
|
|
400
514
|
kind,
|
|
@@ -407,6 +521,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
407
521
|
schemaVersion: result.axes ? 2 : 1,
|
|
408
522
|
...(result.axes ?? {}),
|
|
409
523
|
...auditExtras.transactionalLayer,
|
|
524
|
+
...capabilityAudit,
|
|
410
525
|
...(replayContext ? { replayContext } : {}),
|
|
411
526
|
...(actionSnapshot ? { actionSnapshot } : {}),
|
|
412
527
|
};
|
|
@@ -470,6 +585,32 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
470
585
|
entry.fingerprint === result.fingerprint &&
|
|
471
586
|
entry.repoRoot === ctx.repoRoot);
|
|
472
587
|
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
588
|
+
if (matchedApproval &&
|
|
589
|
+
matchedApproval.capabilityRequestHash &&
|
|
590
|
+
result.capabilityRequests?.length &&
|
|
591
|
+
matchedApproval.capabilityRequestHash !== hashCapabilityRequests(result.capabilityRequests)) {
|
|
592
|
+
await deps.appendAudit(ctx, {
|
|
593
|
+
...gateBase,
|
|
594
|
+
verdict: 'deny_pending_approval',
|
|
595
|
+
reason: 'approval_replay_mismatch',
|
|
596
|
+
approvalId: matchedApproval.approvalId,
|
|
597
|
+
wouldBlock: true,
|
|
598
|
+
permission: 'deny',
|
|
599
|
+
});
|
|
600
|
+
return classifyResultToGateVerdict({
|
|
601
|
+
result: {
|
|
602
|
+
...result,
|
|
603
|
+
verdict: 'deny_pending_approval',
|
|
604
|
+
reason: 'approval_replay_mismatch',
|
|
605
|
+
},
|
|
606
|
+
mode: ctx.config.mode,
|
|
607
|
+
permission: 'deny',
|
|
608
|
+
wouldBlock: true,
|
|
609
|
+
approvalId: matchedApproval.approvalId,
|
|
610
|
+
user_message: 'Belay denied this action because capability requests changed after approval. Re-approve the exact action or run belay explain.',
|
|
611
|
+
agent_message: 'Belay denied this action because capability requests changed after approval.',
|
|
612
|
+
});
|
|
613
|
+
}
|
|
473
614
|
if (matchedApproval &&
|
|
474
615
|
auditExtras.replayAction &&
|
|
475
616
|
!validateReplayEnvelope(matchedApproval, auditExtras.replayAction)) {
|
|
@@ -499,11 +640,14 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
499
640
|
}
|
|
500
641
|
}
|
|
501
642
|
if (approved) {
|
|
643
|
+
if (approved.firstExecution) {
|
|
644
|
+
await recordGateApprovalAsk(stateDir, result.reason, true);
|
|
645
|
+
}
|
|
502
646
|
await deps.appendAudit(ctx, {
|
|
503
647
|
...gateBase,
|
|
504
648
|
verdict: 'allow',
|
|
505
649
|
reason: 'approved_once',
|
|
506
|
-
approvalId: approved.approvalId,
|
|
650
|
+
approvalId: approved.approval.approvalId,
|
|
507
651
|
wouldBlock: false,
|
|
508
652
|
permission: 'allow',
|
|
509
653
|
});
|
|
@@ -512,10 +656,32 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
512
656
|
mode: ctx.config.mode,
|
|
513
657
|
permission: 'allow',
|
|
514
658
|
wouldBlock: false,
|
|
515
|
-
approvalId: approved.approvalId,
|
|
659
|
+
approvalId: approved.approval.approvalId,
|
|
516
660
|
});
|
|
517
661
|
}
|
|
518
662
|
if (result.verdict === 'allow' || result.verdict === 'allow_flagged') {
|
|
663
|
+
const grantConsumed = await consumeCapabilityGrantIfUsed(ctx, deps, result);
|
|
664
|
+
if (!grantConsumed) {
|
|
665
|
+
await deps.appendAudit(ctx, {
|
|
666
|
+
...gateBase,
|
|
667
|
+
verdict: 'deny_pending_approval',
|
|
668
|
+
reason: 'capability_grant_unavailable',
|
|
669
|
+
wouldBlock: true,
|
|
670
|
+
permission: 'deny',
|
|
671
|
+
});
|
|
672
|
+
return classifyResultToGateVerdict({
|
|
673
|
+
result: {
|
|
674
|
+
...result,
|
|
675
|
+
verdict: 'deny_pending_approval',
|
|
676
|
+
reason: 'capability_grant_unavailable',
|
|
677
|
+
},
|
|
678
|
+
mode: ctx.config.mode,
|
|
679
|
+
permission: 'deny',
|
|
680
|
+
wouldBlock: true,
|
|
681
|
+
user_message: 'Belay denied this action because the capability grant could not be consumed. Re-approve or run belay explain.',
|
|
682
|
+
agent_message: 'Belay denied this action because the capability grant was missing, expired, or already used.',
|
|
683
|
+
});
|
|
684
|
+
}
|
|
519
685
|
await deps.appendAudit(ctx, {
|
|
520
686
|
...gateBase,
|
|
521
687
|
verdict: result.verdict,
|
|
@@ -545,12 +711,44 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
545
711
|
wouldBlock: true,
|
|
546
712
|
});
|
|
547
713
|
}
|
|
548
|
-
|
|
714
|
+
if (isJudgeInfrastructureFailure(result)) {
|
|
715
|
+
const fallbackReason = extractJudgeFallbackReason(result);
|
|
716
|
+
const providerId = inferProviderIdFromFallbackReason(fallbackReason, String(ctx.config.judge.providerId ?? 'cursor'));
|
|
717
|
+
const denyMessages = formatJudgeInfrastructureDenyMessage({
|
|
718
|
+
providerId,
|
|
719
|
+
fallbackReason,
|
|
720
|
+
command: result.normalizedCommand ?? result.summary,
|
|
721
|
+
});
|
|
722
|
+
await deps.appendAudit(ctx, {
|
|
723
|
+
...gateBase,
|
|
724
|
+
verdict: result.verdict,
|
|
725
|
+
reason: 'judge_transport_unavailable',
|
|
726
|
+
judgeFallbackReason: fallbackReason,
|
|
727
|
+
wouldBlock: true,
|
|
728
|
+
permission: 'deny',
|
|
729
|
+
});
|
|
730
|
+
return classifyResultToGateVerdict({
|
|
731
|
+
result: {
|
|
732
|
+
...result,
|
|
733
|
+
verdict: 'deny_pending_approval',
|
|
734
|
+
reason: 'judge_transport_unavailable',
|
|
735
|
+
},
|
|
736
|
+
mode: ctx.config.mode,
|
|
737
|
+
permission: 'deny',
|
|
738
|
+
wouldBlock: true,
|
|
739
|
+
user_message: denyMessages.user_message,
|
|
740
|
+
agent_message: denyMessages.agent_message,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const { approval, created } = await ensurePendingApproval(ctx, deps, kind, result, auditExtras.approvalInput, deriveWorkspaceRootScopeHint({
|
|
549
744
|
result,
|
|
550
745
|
replayAction: auditExtras.replayAction,
|
|
551
746
|
payloadForScopeHint: auditExtras.scopeHintPayload,
|
|
552
747
|
options: auditExtras.classifierOptions ?? {},
|
|
553
748
|
}));
|
|
749
|
+
if (created) {
|
|
750
|
+
await recordGateApprovalAsk(stateDir, result.reason, false);
|
|
751
|
+
}
|
|
554
752
|
let approvalToken;
|
|
555
753
|
try {
|
|
556
754
|
approvalToken = await issueApprovalToken({
|