@guilz-dev/belay 0.8.1 → 0.9.1
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 +67 -11
- package/dist/adapters/claude/runtime-entry.js +2 -0
- package/dist/adapters/codex/runtime-entry.js +2 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -1
- package/dist/adapters/cursor/hooks.js +37 -2
- package/dist/adapters/cursor/runtime-entry.js +5 -0
- package/dist/adapters/shared/gate-runtime.d.ts +6 -0
- package/dist/adapters/shared/gate-runtime.js +260 -59
- package/dist/bundle/claude-runtime.mjs +7068 -3966
- package/dist/bundle/codex-runtime.mjs +7001 -3899
- package/dist/bundle/cursor-runtime.mjs +7004 -3899
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +88 -9
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +42 -0
- package/dist/commands/recovery-checkpoints.d.ts +8 -1
- package/dist/commands/recovery-checkpoints.js +44 -11
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +8 -1
- package/dist/config-io.js +2 -2
- package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
- package/dist/conformance/contained-execution-guarantee.js +121 -0
- package/dist/conformance/guarantee-table.js +7 -0
- package/dist/core/audit-analysis.js +7 -4
- package/dist/core/audit-io.d.ts +1 -0
- package/dist/core/audit-io.js +16 -6
- package/dist/core/audit-legacy-archive.d.ts +6 -0
- package/dist/core/audit-legacy-archive.js +51 -0
- package/dist/core/audit-metrics.d.ts +15 -0
- package/dist/core/audit-metrics.js +16 -2
- package/dist/core/audit-query.d.ts +2 -0
- package/dist/core/audit-query.js +33 -9
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-serialize.d.ts +9 -0
- package/dist/core/audit-serialize.js +179 -0
- package/dist/core/audit-types.d.ts +8 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/bounded-output.d.ts +11 -0
- package/dist/core/bounded-output.js +26 -0
- package/dist/core/capability/attestation.d.ts +56 -0
- package/dist/core/capability/attestation.js +110 -3
- package/dist/core/capability/boundary-session.d.ts +5 -0
- package/dist/core/capability/boundary-session.js +38 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.js +32 -13
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +89 -10
- package/dist/core/contained-execution/docker-policy.d.ts +18 -0
- package/dist/core/contained-execution/docker-policy.js +97 -0
- package/dist/core/contained-execution/docker.d.ts +223 -0
- package/dist/core/contained-execution/docker.js +846 -0
- package/dist/core/contained-execution/eligibility.d.ts +9 -0
- package/dist/core/contained-execution/eligibility.js +103 -0
- package/dist/core/contained-execution/failure.d.ts +18 -0
- package/dist/core/contained-execution/failure.js +105 -0
- package/dist/core/contained-execution/mirror.d.ts +54 -0
- package/dist/core/contained-execution/mirror.js +587 -0
- package/dist/core/contained-execution/policy.d.ts +10 -0
- package/dist/core/contained-execution/policy.js +20 -0
- package/dist/core/decision-config-fingerprint.d.ts +3 -0
- package/dist/core/decision-config-fingerprint.js +6 -0
- package/dist/core/effect-ir/index.d.ts +1 -1
- package/dist/core/effect-ir/index.js +1 -1
- package/dist/core/effect-ir/normalize.d.ts +5 -0
- package/dist/core/effect-ir/normalize.js +17 -7
- package/dist/core/effect-ir/shell-build.js +2 -14
- package/dist/core/effect-ir/shell-lower.js +103 -25
- package/dist/core/gate-contract.d.ts +4 -1
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/process-runner.d.ts +10 -3
- package/dist/core/process-runner.js +66 -10
- package/dist/core/recovery/checkpoint.d.ts +3 -0
- package/dist/core/recovery/checkpoint.js +33 -4
- package/dist/core/recovery/types.d.ts +1 -0
- package/dist/core/scrub.d.ts +17 -0
- package/dist/core/scrub.js +499 -5
- package/dist/core/standing-allow.d.ts +0 -20
- package/dist/core/standing-allow.js +0 -71
- package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
- package/dist/core/transactional/apply-observed-changes.js +11 -0
- package/dist/core/transactional/backend-selector.d.ts +2 -1
- package/dist/core/transactional/backend-selector.js +32 -29
- package/dist/core/transactional/backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +242 -54
- package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-git.js +53 -0
- package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
- package/dist/core/transactional/git-worktree-backend.js +3 -0
- package/dist/core/transactional/git-worktree.d.ts +1 -0
- package/dist/core/transactional/git-worktree.js +1 -0
- package/dist/core/transactional/runner.js +59 -13
- package/dist/core/types.d.ts +13 -0
- package/dist/core/verdict/egress-classify.js +1 -0
- package/dist/core/verdict/parser.d.ts +10 -0
- package/dist/core/verdict/parser.js +271 -40
- package/dist/core/verdict/shell-semantics.js +21 -0
- package/dist/corpus/adversarial-probe.d.ts +1 -1
- package/dist/corpus/adversarial-probe.js +3 -3
- package/dist/corpus/benign-probe-cores.d.ts +6 -0
- package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
- package/dist/corpus/coverage-compare.d.ts +36 -0
- package/dist/corpus/coverage-compare.js +197 -0
- package/dist/corpus/coverage-contexts.d.ts +17 -0
- package/dist/corpus/coverage-contexts.js +88 -0
- package/dist/corpus/coverage-matrix.d.ts +37 -0
- package/dist/corpus/coverage-matrix.js +159 -0
- package/dist/corpus/coverage-probe.d.ts +91 -0
- package/dist/corpus/coverage-probe.js +341 -0
- package/dist/corpus/evaluate.d.ts +1 -1
- package/dist/corpus/evaluate.js +1 -1
- package/dist/corpus/structural-fixture-root.d.ts +2 -0
- package/dist/corpus/structural-fixture-root.js +5 -0
- package/dist/defaults.js +0 -8
- package/dist/egress-daemon.js +20 -4
- package/dist/installer.js +3 -0
- package/dist/runtime-provenance.d.ts +10 -0
- package/dist/runtime-provenance.js +63 -0
- package/dist/templates.js +12 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/skills/belay/SKILL.md +19 -0
- package/skills/belay/belay-status.md +5 -0
- package/dist/corpus/must-allow-commands.d.ts +0 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -7,11 +7,14 @@ import { buildReplayHint, buildRetryInstructionForConfig, canAutoReplay, getExec
|
|
|
7
7
|
import { claimApprovedForReplay, 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 { approvalCorrelationId, serializeAuditRecordV3 } from '../../core/audit-serialize.js';
|
|
11
|
+
import { boundedUtf8Tail } from '../../core/bounded-output.js';
|
|
10
12
|
import { mutateApprovalStateWithRetry } from '../../core/capability/approval-state-mutation.js';
|
|
11
13
|
import { APPROVAL_STATE_VERSION_V3 } from '../../core/capability/approval-v3.js';
|
|
14
|
+
import { readSignedAttestationFile } from '../../core/capability/boundary-attestation-sign.js';
|
|
12
15
|
import { isEgressProxyActive } from '../../core/capability/boundary-egress.js';
|
|
13
16
|
import { isBoundaryCleanupError, safeBoundaryCleanupResourceId, } from '../../core/capability/boundary-run.js';
|
|
14
|
-
import { resolveBoundaryDriverContext, runBoundaryAgentCommand, } from '../../core/capability/boundary-session.js';
|
|
17
|
+
import { boundaryAttestationPath, resolveBoundaryDriverContext, runBoundaryAgentCommand, } from '../../core/capability/boundary-session.js';
|
|
15
18
|
import { hashCapabilityRequests } from '../../core/capability/capability-request-hash.js';
|
|
16
19
|
import { recordGateApprovalAsk, scheduleGateShadowAudit, } from '../../core/capability/gate-shadow-audit.js';
|
|
17
20
|
import { canConsumeCapabilityGrantLease } from '../../core/capability/grant-consumption.js';
|
|
@@ -19,20 +22,24 @@ import { approvalGrantBundleExhausted, consumeApprovedRecordGrantBundle, consume
|
|
|
19
22
|
import { loadClassifierAuthorization } from '../../core/capability/grant-loader.js';
|
|
20
23
|
import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, loadTrustedWorkspaceRootsSync, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from '../../core/capability/index.js';
|
|
21
24
|
import { resolveLayeredConfig, teamConfigPath } from '../../core/config-layers.js';
|
|
25
|
+
import { ContainedDockerBoundaryUnavailableError, executeContainedDocker, } from '../../core/contained-execution/docker.js';
|
|
26
|
+
import { isContainedUnknownExecutionEligible } from '../../core/contained-execution/eligibility.js';
|
|
27
|
+
import { ContainedExecutionFailureError } from '../../core/contained-execution/failure.js';
|
|
28
|
+
import { withContainedExecutionMirror, } from '../../core/contained-execution/mirror.js';
|
|
29
|
+
import { CONTAINED_EXECUTION_OUTPUT_SCRUB_OPTIONS, isContainedExecutionApprovalFallbackReason, } from '../../core/contained-execution/policy.js';
|
|
22
30
|
import { effectPlanAuditFields, hashEffectPlan } from '../../core/effect-ir/audit.js';
|
|
23
31
|
import { buildCapabilityEffectPlan } from '../../core/effect-ir/build.js';
|
|
24
32
|
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
25
33
|
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
26
|
-
import { approvalCommandMatch, approvedApprovalsFile, belayStateDir, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, hashValue, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
34
|
+
import { approvalCommandMatch, approvedApprovalsFile, belayStateDir, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, hashValue, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubString, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
27
35
|
import { extractJudgeFallbackReason, formatJudgeInfrastructureDenyMessage, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../../core/judge-fallback-hints.js';
|
|
28
36
|
import { notifyDeny } from '../../core/notify.js';
|
|
29
37
|
import { canonicalPath } from '../../core/path-utils.js';
|
|
30
38
|
import { recoveryFailClosedResult, recoveryFailReasonFromSkip, } from '../../core/recovery/fail-closed.js';
|
|
31
39
|
import { fingerprintReplayPayload } from '../../core/replay-scrub.js';
|
|
32
|
-
import { loadStandingAllow, resolveStandingAllowMatch, standingAllowFile, } from '../../core/standing-allow.js';
|
|
33
40
|
import { FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
41
|
+
import { buildAuditProvenanceFields } from '../../runtime-provenance.js';
|
|
34
42
|
import { egressStatus } from '../../services/egress-service.js';
|
|
35
|
-
import { PACKAGE_VERSION } from '../../version.js';
|
|
36
43
|
import { protectedArtifactRoots } from '../layouts/protected-paths.js';
|
|
37
44
|
const EMPTY_APPROVALS = {
|
|
38
45
|
version: 1,
|
|
@@ -41,15 +48,7 @@ const EMPTY_APPROVALS = {
|
|
|
41
48
|
const RUNTIME_PROVENANCE_KEY = Symbol.for('agent-belay.runtime-provenance');
|
|
42
49
|
function auditProvenance(config) {
|
|
43
50
|
const runtime = globalThis[RUNTIME_PROVENANCE_KEY];
|
|
44
|
-
|
|
45
|
-
const runtimeBuildStamp = typeof runtime?.runtimeBuildStamp === 'string'
|
|
46
|
-
? runtime.runtimeBuildStamp
|
|
47
|
-
: `${PACKAGE_VERSION}@source`;
|
|
48
|
-
return {
|
|
49
|
-
runtimeVersion,
|
|
50
|
-
runtimeBuildStamp,
|
|
51
|
-
configFingerprint: hashValue(canonicalStringify(config)),
|
|
52
|
-
};
|
|
51
|
+
return buildAuditProvenanceFields(config, runtime);
|
|
53
52
|
}
|
|
54
53
|
function adapterIdFromContext(ctx) {
|
|
55
54
|
if (ctx.config.adapter === 'cursor' ||
|
|
@@ -62,6 +61,14 @@ function adapterIdFromContext(ctx) {
|
|
|
62
61
|
}
|
|
63
62
|
return undefined;
|
|
64
63
|
}
|
|
64
|
+
function extractShellCommandFromPayload(payload) {
|
|
65
|
+
const toolInput = payload.tool_input;
|
|
66
|
+
if (!toolInput || typeof toolInput !== 'object') {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
const input = toolInput;
|
|
70
|
+
return typeof input.command === 'string' ? input.command : '';
|
|
71
|
+
}
|
|
65
72
|
const REPLAY_AUDIT_FAILURE_NOTE = ' Audit recording failed; inspect audit storage before the next approval.';
|
|
66
73
|
async function appendReplayAuditSafely(ctx, deps, event) {
|
|
67
74
|
try {
|
|
@@ -92,15 +99,15 @@ export function createDefaultGateRuntimeDeps() {
|
|
|
92
99
|
const provenance = auditProvenance(ctx.config);
|
|
93
100
|
const record = {
|
|
94
101
|
timestamp: new Date().toISOString(),
|
|
102
|
+
mode: ctx.config.mode,
|
|
95
103
|
...event,
|
|
96
104
|
...provenance,
|
|
97
105
|
};
|
|
98
106
|
if (!ctx.config.audit.includeAssessment) {
|
|
99
107
|
delete record.assessment;
|
|
100
108
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
await writeFile(auditPath, `${JSON.stringify(scrubbed)}\n`, {
|
|
109
|
+
const serialized = serializeAuditRecordV3(record, scrubOptionsFromConfig(ctx.config));
|
|
110
|
+
await writeFile(auditPath, `${JSON.stringify(serialized)}\n`, {
|
|
104
111
|
encoding: 'utf8',
|
|
105
112
|
flag: 'a',
|
|
106
113
|
});
|
|
@@ -135,6 +142,9 @@ export function createDefaultGateRuntimeDeps() {
|
|
|
135
142
|
runOptions: { mountReadOnly: false },
|
|
136
143
|
});
|
|
137
144
|
},
|
|
145
|
+
readSignedAttestation: readSignedAttestationFile,
|
|
146
|
+
withContainedExecutionMirror,
|
|
147
|
+
executeContainedDocker,
|
|
138
148
|
};
|
|
139
149
|
}
|
|
140
150
|
export async function resolveGateConfig(ctx, deps) {
|
|
@@ -182,6 +192,22 @@ function gateAuditEventName(kind) {
|
|
|
182
192
|
}
|
|
183
193
|
return 'subagentGate';
|
|
184
194
|
}
|
|
195
|
+
function resolveGateAuditEvent(sourceEvent, kind) {
|
|
196
|
+
if (!sourceEvent) {
|
|
197
|
+
return gateAuditEventName(kind);
|
|
198
|
+
}
|
|
199
|
+
switch (sourceEvent.toLowerCase()) {
|
|
200
|
+
case 'beforeshellexecution':
|
|
201
|
+
return 'beforeShellExecution';
|
|
202
|
+
case 'pretooluse':
|
|
203
|
+
return 'preToolUse';
|
|
204
|
+
case 'subagentstart':
|
|
205
|
+
case 'subagentgate':
|
|
206
|
+
return 'subagentGate';
|
|
207
|
+
default:
|
|
208
|
+
return gateAuditEventName(kind);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
185
211
|
async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, scopeHint) {
|
|
186
212
|
const candidate = createApprovalRecordWithEnvelope({
|
|
187
213
|
kind,
|
|
@@ -217,17 +243,6 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, sco
|
|
|
217
243
|
}
|
|
218
244
|
return outcome;
|
|
219
245
|
}
|
|
220
|
-
function approvalCorrelationId(approvalId) {
|
|
221
|
-
return createHash('sha256').update(approvalId).digest('hex').slice(0, 16);
|
|
222
|
-
}
|
|
223
|
-
function extractShellCommandFromPayload(payload) {
|
|
224
|
-
const toolInput = payload.tool_input;
|
|
225
|
-
if (!toolInput || typeof toolInput !== 'object') {
|
|
226
|
-
return '';
|
|
227
|
-
}
|
|
228
|
-
const input = toolInput;
|
|
229
|
-
return typeof input.command === 'string' ? input.command : '';
|
|
230
|
-
}
|
|
231
246
|
function deriveWorkspaceRootScopeHint(params) {
|
|
232
247
|
if (params.result.reason !== 'outside_repo_mutation' &&
|
|
233
248
|
params.result.reason !== 'outside_repo_redirect') {
|
|
@@ -364,6 +379,200 @@ async function discardApprovedApproval(ctx, deps, approvalId) {
|
|
|
364
379
|
throw new Error(`Failed to discard rejected approval ${approvalId}`);
|
|
365
380
|
}
|
|
366
381
|
}
|
|
382
|
+
function scrubMediatedOutput(value) {
|
|
383
|
+
const source = boundedUtf8Tail(value);
|
|
384
|
+
const scrubbed = boundedUtf8Tail(scrubString(source.value, CONTAINED_EXECUTION_OUTPUT_SCRUB_OPTIONS));
|
|
385
|
+
return { value: scrubbed.value, truncated: source.truncated || scrubbed.truncated };
|
|
386
|
+
}
|
|
387
|
+
function normalizeContainedFailure(error) {
|
|
388
|
+
if (error instanceof ContainedExecutionFailureError) {
|
|
389
|
+
return error;
|
|
390
|
+
}
|
|
391
|
+
if (error?.code === 'ENOENT') {
|
|
392
|
+
return new ContainedExecutionFailureError('contained_execution_capability_invalid', {
|
|
393
|
+
cause: error,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return new ContainedExecutionFailureError('contained_execution_boundary_failed', { cause: error });
|
|
397
|
+
}
|
|
398
|
+
function containedAuditBase(kind, mode, result, sourceEvent) {
|
|
399
|
+
const planFields = effectPlanAuditFields(result.effectPlan);
|
|
400
|
+
const rawSourceEvent = sourceEvent ?? gateAuditEventName(kind);
|
|
401
|
+
const auditEvent = resolveGateAuditEvent(rawSourceEvent, kind);
|
|
402
|
+
return {
|
|
403
|
+
event: auditEvent,
|
|
404
|
+
sourceEvent: rawSourceEvent,
|
|
405
|
+
kind,
|
|
406
|
+
fingerprint: result.fingerprint,
|
|
407
|
+
mode,
|
|
408
|
+
schemaVersion: result.axes ? 2 : 1,
|
|
409
|
+
effectPlanVersion: planFields.effectPlanVersion,
|
|
410
|
+
effectIRHash: planFields.effectIRHash,
|
|
411
|
+
effectPlanSignals: planFields.effectPlanSignals,
|
|
412
|
+
effectPlanOpacity: planFields.effectPlanOpacity,
|
|
413
|
+
effectPlanDisposition: planFields.effectPlanDisposition,
|
|
414
|
+
effectPlanCompleteness: planFields.effectPlanCompleteness,
|
|
415
|
+
wouldMediate: true,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function mediateContainedUnknownExecution(params) {
|
|
419
|
+
const { ctx, deps, action, command, sourceEvent } = params;
|
|
420
|
+
const result = { ...params.result, wouldMediate: true };
|
|
421
|
+
if (ctx.config.mode === 'audit') {
|
|
422
|
+
await deps.appendAudit(ctx, {
|
|
423
|
+
...containedAuditBase(action.kind, ctx.config.mode, result, sourceEvent),
|
|
424
|
+
verdict: result.verdict,
|
|
425
|
+
reason: result.reason,
|
|
426
|
+
wouldBlock: true,
|
|
427
|
+
permission: 'allow',
|
|
428
|
+
});
|
|
429
|
+
return classifyResultToGateVerdict({
|
|
430
|
+
result,
|
|
431
|
+
mode: ctx.config.mode,
|
|
432
|
+
permission: 'allow',
|
|
433
|
+
wouldBlock: true,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const controlPlaneDir = configuredControlPlaneDir(ctx.config);
|
|
437
|
+
const attestationPath = boundaryAttestationPath(ctx.repoRoot, ctx.config);
|
|
438
|
+
const protectedRoots = [
|
|
439
|
+
...new Set([
|
|
440
|
+
...protectedArtifactRoots(ctx.layout, ctx.repoRoot, controlPlaneDir),
|
|
441
|
+
attestationPath,
|
|
442
|
+
]),
|
|
443
|
+
];
|
|
444
|
+
const checkpoint = ctx.config.policy.transactional.fileCheckpoint;
|
|
445
|
+
const containedConfig = ctx.config.sandbox.containedExecution;
|
|
446
|
+
if (!containedConfig?.enabled) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
let execution;
|
|
450
|
+
let mirrorBackend;
|
|
451
|
+
try {
|
|
452
|
+
let signedAttestation;
|
|
453
|
+
try {
|
|
454
|
+
signedAttestation = await deps.readSignedAttestation(attestationPath);
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
throw new ContainedExecutionFailureError('contained_execution_capability_invalid', {
|
|
458
|
+
cause: error,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
execution = await deps.withContainedExecutionMirror({
|
|
462
|
+
sourceRoot: ctx.repoRoot,
|
|
463
|
+
controlPlaneRoots: protectedRoots,
|
|
464
|
+
limits: {
|
|
465
|
+
maxFiles: checkpoint.maxFiles,
|
|
466
|
+
maxSourceBytes: checkpoint.maxSourceBytes,
|
|
467
|
+
maxWorkspaceBytes: checkpoint.maxWorkspaceBytes,
|
|
468
|
+
prepareTimeoutMs: checkpoint.prepareTimeoutMs,
|
|
469
|
+
},
|
|
470
|
+
}, async (mirror) => {
|
|
471
|
+
mirrorBackend = mirror.backend;
|
|
472
|
+
return deps.executeContainedDocker({
|
|
473
|
+
repoRoot: ctx.repoRoot,
|
|
474
|
+
controlPlaneDir,
|
|
475
|
+
protectedRoots,
|
|
476
|
+
config: containedConfig,
|
|
477
|
+
mirror,
|
|
478
|
+
guestCwd: action.cwd,
|
|
479
|
+
command,
|
|
480
|
+
inputFingerprint: result.fingerprint,
|
|
481
|
+
signedAttestation,
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (error instanceof ContainedDockerBoundaryUnavailableError &&
|
|
487
|
+
isContainedExecutionApprovalFallbackReason(error.reason)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
const failure = normalizeContainedFailure(error);
|
|
491
|
+
const reason = failure.code;
|
|
492
|
+
const failedResult = {
|
|
493
|
+
...result,
|
|
494
|
+
verdict: 'deny_pending_approval',
|
|
495
|
+
reason,
|
|
496
|
+
};
|
|
497
|
+
await deps.appendAudit(ctx, {
|
|
498
|
+
...containedAuditBase(action.kind, ctx.config.mode, failedResult, sourceEvent),
|
|
499
|
+
verdict: failedResult.verdict,
|
|
500
|
+
reason,
|
|
501
|
+
wouldBlock: true,
|
|
502
|
+
permission: 'deny',
|
|
503
|
+
...(mirrorBackend ? { mirrorBackend } : {}),
|
|
504
|
+
});
|
|
505
|
+
const sessionHint = failure.remediation === 'session-start' ? ' Run `belay session start`, then retry.' : '';
|
|
506
|
+
return classifyResultToGateVerdict({
|
|
507
|
+
result: failedResult,
|
|
508
|
+
mode: ctx.config.mode,
|
|
509
|
+
permission: 'deny',
|
|
510
|
+
wouldBlock: true,
|
|
511
|
+
user_message: `Belay could not confirm safe contained execution (${reason}).${sessionHint}`,
|
|
512
|
+
agent_message: `Belay denied the original host command because contained execution failed closed (${reason}).` +
|
|
513
|
+
sessionHint,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const stdout = scrubMediatedOutput(execution.stdout);
|
|
517
|
+
const stderr = scrubMediatedOutput(execution.stderr);
|
|
518
|
+
const mediatedExecution = {
|
|
519
|
+
exitCode: execution.exitCode,
|
|
520
|
+
signal: execution.signal,
|
|
521
|
+
timedOut: execution.timedOut,
|
|
522
|
+
stdout: stdout.value,
|
|
523
|
+
stderr: stderr.value,
|
|
524
|
+
stdoutTruncated: execution.stdoutTruncated || stdout.truncated,
|
|
525
|
+
stderrTruncated: execution.stderrTruncated || stderr.truncated,
|
|
526
|
+
receiptHash: execution.receiptHash,
|
|
527
|
+
workspaceChangesDiscarded: true,
|
|
528
|
+
};
|
|
529
|
+
const reason = execution.exitCode === 0 && !execution.timedOut
|
|
530
|
+
? 'contained_execution_complete'
|
|
531
|
+
: 'contained_execution_failed';
|
|
532
|
+
const mediatedResult = {
|
|
533
|
+
...result,
|
|
534
|
+
verdict: 'allow',
|
|
535
|
+
reason,
|
|
536
|
+
mediatedExecution,
|
|
537
|
+
};
|
|
538
|
+
await deps.appendAudit(ctx, {
|
|
539
|
+
...containedAuditBase(action.kind, ctx.config.mode, mediatedResult, sourceEvent),
|
|
540
|
+
verdict: mediatedResult.verdict,
|
|
541
|
+
reason,
|
|
542
|
+
wouldBlock: false,
|
|
543
|
+
permission: 'deny',
|
|
544
|
+
receiptHash: execution.receiptHash,
|
|
545
|
+
imageId: execution.receipt.imageId,
|
|
546
|
+
mirrorBackend,
|
|
547
|
+
exitCode: execution.exitCode,
|
|
548
|
+
timedOut: execution.timedOut,
|
|
549
|
+
});
|
|
550
|
+
const status = execution.timedOut
|
|
551
|
+
? 'timed out'
|
|
552
|
+
: execution.exitCode === 0
|
|
553
|
+
? 'completed successfully'
|
|
554
|
+
: `failed with exit ${execution.exitCode}`;
|
|
555
|
+
const output = [
|
|
556
|
+
mediatedExecution.stdout
|
|
557
|
+
? `stdout tail${mediatedExecution.stdoutTruncated ? ' (truncated)' : ''}:\n${mediatedExecution.stdout}`
|
|
558
|
+
: '',
|
|
559
|
+
mediatedExecution.stderr
|
|
560
|
+
? `stderr tail${mediatedExecution.stderrTruncated ? ' (truncated)' : ''}:\n${mediatedExecution.stderr}`
|
|
561
|
+
: '',
|
|
562
|
+
]
|
|
563
|
+
.filter(Boolean)
|
|
564
|
+
.join('\n');
|
|
565
|
+
return classifyResultToGateVerdict({
|
|
566
|
+
result: mediatedResult,
|
|
567
|
+
mode: ctx.config.mode,
|
|
568
|
+
permission: 'deny',
|
|
569
|
+
wouldBlock: false,
|
|
570
|
+
user_message: `Belay contained execution ${status}; the original host command was not run and workspace changes were discarded. Receipt: ${execution.receiptHash}.` +
|
|
571
|
+
(output ? `\n${output}` : ''),
|
|
572
|
+
agent_message: `Belay already ran this command once inside the contained boundary (${status}). Do not retry it on the host. Receipt: ${execution.receiptHash}.` +
|
|
573
|
+
(output ? `\n${output}` : ''),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
367
576
|
export async function evaluateGatedAction(ctx, deps, params) {
|
|
368
577
|
let action;
|
|
369
578
|
try {
|
|
@@ -402,8 +611,10 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
402
611
|
effectFree: false,
|
|
403
612
|
}),
|
|
404
613
|
};
|
|
614
|
+
const sourceEvent = params.sourceEvent ?? gateAuditEventName(params.kind);
|
|
405
615
|
await deps.appendAudit(ctx, {
|
|
406
|
-
event:
|
|
616
|
+
event: resolveGateAuditEvent(sourceEvent, params.kind),
|
|
617
|
+
sourceEvent,
|
|
407
618
|
kind: params.kind,
|
|
408
619
|
fingerprint: verdict.fingerprint,
|
|
409
620
|
verdict: verdict.verdict,
|
|
@@ -462,6 +673,21 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
462
673
|
egressProxyActive,
|
|
463
674
|
};
|
|
464
675
|
const predicted = await classifyGatedActionAsync(action, ctx.config, enrichedClassifierOptions);
|
|
676
|
+
if (action.kind === 'shell' &&
|
|
677
|
+
action.command &&
|
|
678
|
+
isContainedUnknownExecutionEligible(ctx.config, action, predicted)) {
|
|
679
|
+
const mediated = await mediateContainedUnknownExecution({
|
|
680
|
+
ctx,
|
|
681
|
+
deps,
|
|
682
|
+
action,
|
|
683
|
+
command: action.command,
|
|
684
|
+
result: predicted,
|
|
685
|
+
sourceEvent: params.sourceEvent,
|
|
686
|
+
});
|
|
687
|
+
if (mediated) {
|
|
688
|
+
return mediated;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
465
691
|
let result = predicted;
|
|
466
692
|
let predictedAssessment;
|
|
467
693
|
let observedAssessment;
|
|
@@ -552,6 +778,7 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
552
778
|
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
553
779
|
const scrubbedPayload = fingerprintReplayPayload(params.kind, params.payload, scrubOpts);
|
|
554
780
|
return gateDecisionToVerdict(ctx, deps, params.kind, result, {
|
|
781
|
+
sourceEvent: params.sourceEvent,
|
|
555
782
|
predictedAssessment,
|
|
556
783
|
observedAssessment,
|
|
557
784
|
transactionalLayer,
|
|
@@ -671,8 +898,11 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
671
898
|
(kind === 'shell' ? auditExtras.approvalInput?.input : undefined),
|
|
672
899
|
stateDir,
|
|
673
900
|
});
|
|
901
|
+
const sourceEvent = auditExtras.sourceEvent ?? gateAuditEventName(kind);
|
|
902
|
+
const auditEvent = resolveGateAuditEvent(sourceEvent, kind);
|
|
674
903
|
const gateBase = {
|
|
675
|
-
event:
|
|
904
|
+
event: auditEvent,
|
|
905
|
+
sourceEvent,
|
|
676
906
|
kind,
|
|
677
907
|
fingerprint: result.fingerprint,
|
|
678
908
|
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
@@ -787,35 +1017,6 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
787
1017
|
agent_message: agentMessage,
|
|
788
1018
|
});
|
|
789
1019
|
}
|
|
790
|
-
if (kind !== 'shell' &&
|
|
791
|
-
result.verdict === 'deny_pending_approval' &&
|
|
792
|
-
ctx.config.mode === 'enforce' &&
|
|
793
|
-
!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason)) {
|
|
794
|
-
const standingAllowPath = standingAllowFile(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot));
|
|
795
|
-
const standingState = await loadStandingAllow(standingAllowPath);
|
|
796
|
-
const standingMatch = resolveStandingAllowMatch({
|
|
797
|
-
kind,
|
|
798
|
-
result,
|
|
799
|
-
repoRoot: ctx.repoRoot,
|
|
800
|
-
state: standingState,
|
|
801
|
-
});
|
|
802
|
-
if (standingMatch) {
|
|
803
|
-
await deps.appendAudit(ctx, {
|
|
804
|
-
...gateBase,
|
|
805
|
-
verdict: 'allow',
|
|
806
|
-
reason: 'standing_allow',
|
|
807
|
-
standingAllowSource: standingMatch.source,
|
|
808
|
-
wouldBlock: false,
|
|
809
|
-
permission: 'allow',
|
|
810
|
-
});
|
|
811
|
-
return classifyResultToGateVerdict({
|
|
812
|
-
result: { ...result, verdict: 'allow', reason: 'standing_allow' },
|
|
813
|
-
mode: ctx.config.mode,
|
|
814
|
-
permission: 'allow',
|
|
815
|
-
wouldBlock: false,
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
1020
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
820
1021
|
let approved = null;
|
|
821
1022
|
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) &&
|