@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.
Files changed (127) hide show
  1. package/README.md +67 -11
  2. package/dist/adapters/claude/runtime-entry.js +2 -0
  3. package/dist/adapters/codex/runtime-entry.js +2 -0
  4. package/dist/adapters/cursor/hooks.d.ts +5 -1
  5. package/dist/adapters/cursor/hooks.js +37 -2
  6. package/dist/adapters/cursor/runtime-entry.js +5 -0
  7. package/dist/adapters/shared/gate-runtime.d.ts +6 -0
  8. package/dist/adapters/shared/gate-runtime.js +260 -59
  9. package/dist/bundle/claude-runtime.mjs +7068 -3966
  10. package/dist/bundle/codex-runtime.mjs +7001 -3899
  11. package/dist/bundle/cursor-runtime.mjs +7004 -3899
  12. package/dist/commands/classify-for-report.js +5 -0
  13. package/dist/commands/doctor.js +88 -9
  14. package/dist/commands/explain.js +3 -2
  15. package/dist/commands/metrics.js +42 -0
  16. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  17. package/dist/commands/recovery-checkpoints.js +44 -11
  18. package/dist/commands/session.d.ts +5 -0
  19. package/dist/commands/session.js +8 -1
  20. package/dist/config-io.js +2 -2
  21. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  22. package/dist/conformance/contained-execution-guarantee.js +121 -0
  23. package/dist/conformance/guarantee-table.js +7 -0
  24. package/dist/core/audit-analysis.js +7 -4
  25. package/dist/core/audit-io.d.ts +1 -0
  26. package/dist/core/audit-io.js +16 -6
  27. package/dist/core/audit-legacy-archive.d.ts +6 -0
  28. package/dist/core/audit-legacy-archive.js +51 -0
  29. package/dist/core/audit-metrics.d.ts +15 -0
  30. package/dist/core/audit-metrics.js +16 -2
  31. package/dist/core/audit-query.d.ts +2 -0
  32. package/dist/core/audit-query.js +33 -9
  33. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  34. package/dist/core/audit-recovery-metrics.js +146 -0
  35. package/dist/core/audit-serialize.d.ts +9 -0
  36. package/dist/core/audit-serialize.js +179 -0
  37. package/dist/core/audit-types.d.ts +8 -1
  38. package/dist/core/audit-types.js +1 -1
  39. package/dist/core/bounded-output.d.ts +11 -0
  40. package/dist/core/bounded-output.js +26 -0
  41. package/dist/core/capability/attestation.d.ts +56 -0
  42. package/dist/core/capability/attestation.js +110 -3
  43. package/dist/core/capability/boundary-session.d.ts +5 -0
  44. package/dist/core/capability/boundary-session.js +38 -1
  45. package/dist/core/capability/index.d.ts +1 -1
  46. package/dist/core/capability/index.js +1 -1
  47. package/dist/core/capability/policy-engine.js +32 -13
  48. package/dist/core/config.d.ts +18 -0
  49. package/dist/core/config.js +89 -10
  50. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  51. package/dist/core/contained-execution/docker-policy.js +97 -0
  52. package/dist/core/contained-execution/docker.d.ts +223 -0
  53. package/dist/core/contained-execution/docker.js +846 -0
  54. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  55. package/dist/core/contained-execution/eligibility.js +103 -0
  56. package/dist/core/contained-execution/failure.d.ts +18 -0
  57. package/dist/core/contained-execution/failure.js +105 -0
  58. package/dist/core/contained-execution/mirror.d.ts +54 -0
  59. package/dist/core/contained-execution/mirror.js +587 -0
  60. package/dist/core/contained-execution/policy.d.ts +10 -0
  61. package/dist/core/contained-execution/policy.js +20 -0
  62. package/dist/core/decision-config-fingerprint.d.ts +3 -0
  63. package/dist/core/decision-config-fingerprint.js +6 -0
  64. package/dist/core/effect-ir/index.d.ts +1 -1
  65. package/dist/core/effect-ir/index.js +1 -1
  66. package/dist/core/effect-ir/normalize.d.ts +5 -0
  67. package/dist/core/effect-ir/normalize.js +17 -7
  68. package/dist/core/effect-ir/shell-build.js +2 -14
  69. package/dist/core/effect-ir/shell-lower.js +103 -25
  70. package/dist/core/gate-contract.d.ts +4 -1
  71. package/dist/core/gate-contract.js +2 -0
  72. package/dist/core/process-runner.d.ts +10 -3
  73. package/dist/core/process-runner.js +66 -10
  74. package/dist/core/recovery/checkpoint.d.ts +3 -0
  75. package/dist/core/recovery/checkpoint.js +33 -4
  76. package/dist/core/recovery/types.d.ts +1 -0
  77. package/dist/core/scrub.d.ts +17 -0
  78. package/dist/core/scrub.js +499 -5
  79. package/dist/core/standing-allow.d.ts +0 -20
  80. package/dist/core/standing-allow.js +0 -71
  81. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  82. package/dist/core/transactional/apply-observed-changes.js +11 -0
  83. package/dist/core/transactional/backend-selector.d.ts +2 -1
  84. package/dist/core/transactional/backend-selector.js +32 -29
  85. package/dist/core/transactional/backend.d.ts +3 -0
  86. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  87. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  88. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  89. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  90. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  91. package/dist/core/transactional/git-worktree-backend.js +3 -0
  92. package/dist/core/transactional/git-worktree.d.ts +1 -0
  93. package/dist/core/transactional/git-worktree.js +1 -0
  94. package/dist/core/transactional/runner.js +59 -13
  95. package/dist/core/types.d.ts +13 -0
  96. package/dist/core/verdict/egress-classify.js +1 -0
  97. package/dist/core/verdict/parser.d.ts +10 -0
  98. package/dist/core/verdict/parser.js +271 -40
  99. package/dist/core/verdict/shell-semantics.js +21 -0
  100. package/dist/corpus/adversarial-probe.d.ts +1 -1
  101. package/dist/corpus/adversarial-probe.js +3 -3
  102. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  103. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  104. package/dist/corpus/coverage-compare.d.ts +36 -0
  105. package/dist/corpus/coverage-compare.js +197 -0
  106. package/dist/corpus/coverage-contexts.d.ts +17 -0
  107. package/dist/corpus/coverage-contexts.js +88 -0
  108. package/dist/corpus/coverage-matrix.d.ts +37 -0
  109. package/dist/corpus/coverage-matrix.js +159 -0
  110. package/dist/corpus/coverage-probe.d.ts +91 -0
  111. package/dist/corpus/coverage-probe.js +341 -0
  112. package/dist/corpus/evaluate.d.ts +1 -1
  113. package/dist/corpus/evaluate.js +1 -1
  114. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  115. package/dist/corpus/structural-fixture-root.js +5 -0
  116. package/dist/defaults.js +0 -8
  117. package/dist/egress-daemon.js +20 -4
  118. package/dist/installer.js +3 -0
  119. package/dist/runtime-provenance.d.ts +10 -0
  120. package/dist/runtime-provenance.js +63 -0
  121. package/dist/templates.js +12 -7
  122. package/dist/version.d.ts +1 -1
  123. package/dist/version.js +1 -1
  124. package/package.json +4 -1
  125. package/skills/belay/SKILL.md +19 -0
  126. package/skills/belay/belay-status.md +5 -0
  127. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -1,4 +1,4 @@
1
- import { createHash, randomUUID } from 'node:crypto';
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
- const runtimeVersion = typeof runtime?.runtimeVersion === 'string' ? runtime.runtimeVersion : PACKAGE_VERSION;
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 scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
102
- Object.assign(scrubbed, provenance);
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: gateAuditEventName(params.kind),
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: gateAuditEventName(kind),
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) &&