Repository navigation
feat: add protected command output capture - #10169
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved isolation bypasses and output-handling failures undermine the protected-capture contract.
12 open issues
Unbounded drain wait blocks cleanup and teardown · New Console backpressure stalls protected output capture · New Path overlap falsely flags valid names beginning with '..' · New Implicit workdir mounts bypass capture isolation checks · New Symlinked handoff paths allow forged capture files · New Docker socket exposure check ignores implicit mounts · New Execa buffering bypasses capture limits and drains · New Late destination errors become unhandled after source EOF · New Container diagnostics contaminate command output capture · New Cancelled restart attempts skip output collection · New Short writes followed by errors undercount captured bytes · New Sudo-created artifacts remain inaccessible to invoking user · New
What changed in this PR
Adds opt-in, host-only command output capture to AWF’s non-TTY Compose execution path.
Changes:
- Adds capture options and isolation validation.
- Records bounded streams with attempt and completeness metadata.
- Integrates lifecycle handling, tests, and documentation.
| File | Description |
|---|---|
| src/types/volume-options.ts | Defines capture configuration. |
| src/run-agent-command.test.ts | Tests raw streams and cancellation. |
| src/protected-command-output-capture.ts | Implements private capture and metadata. |
| src/protected-command-output-capture.test.ts | Tests isolation, limits, and outcomes. |
| src/enclave/mount-policy.ts | Extends isolation checks for capture. |
| src/container-lifecycle.ts | Connects streams and attempt tracking. |
| src/commands/validators/config-assembly.ts | Validates capture compatibility. |
| src/commands/main-action.ts | Coordinates capture setup and teardown. |
| src/commands/build-config.ts | Maps capture CLI options. |
| src/commands/build-config.test.ts | Tests option mapping. |
| src/cli-workflow.ts | Passes capture into execution. |
| src/cli-options.ts | Registers capture flags. |
| README.md | Introduces capture documentation. |
| docs/protected-command-output.md | Documents usage and limitations. |
Review effort: Max
| } | ||
| if (commandOutputCapture) { | ||
| if (agentCommandStarted) { | ||
| await commandOutputCapture.waitForCommandCompletion(); |
| if (!destination.write(chunk) && !destinationUnavailable) { | ||
| source.pause(); |
| env: NodeJS.ProcessEnv = process.env, | ||
| cwd = process.cwd(), | ||
| ): void { | ||
| assertPrivateRootIsolated(config, { root }, env, cwd, 'protected command output'); |
| env: NodeJS.ProcessEnv = process.env, | ||
| cwd = process.cwd(), | ||
| ): void { | ||
| assertPrivateRootIsolated(config, { root }, env, cwd, 'protected command output'); |
| assertPrivateCommandOutputRootIsolated(config, directory); | ||
| const maxAttempts = isGvisorRuntime(config.containerRuntime) ? 2 : 1; | ||
| return new ProtectedCommandOutputCaptureSession( | ||
| directory, |
| source.once('end', removeDestinationListeners); | ||
| source.once('close', removeDestinationListeners); |
| forwardOutput(logsProcess.stdout, process.stdout, 'stdout'); | ||
| forwardOutput(logsProcess.stderr, process.stderr, 'stderr'); |
| reject: false, | ||
| env: getLocalDockerEnv(), | ||
| }); | ||
| break; |
| writeAll(state.fd, chunk.subarray(0, length)); | ||
| state.capturedBytes += length; |
| close(): void { | ||
| if (this.closed) return; | ||
| for (const attempt of this.attempts) { | ||
| for (const stream of Object.values(attempt.streams)) { | ||
| try { | ||
| fs.closeSync(stream.fd); |
|
@copilot address review feedback |
Addressed the review feedback in |
|
@copilot address review feedback |
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
|
✅ Copilot review passed with no inline comments. @copilot Add the |
Documentation PreviewDocumentation has been built for this PR. To view locally:
Built from commit eb8bb38 |
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Capture-mode entrypoint diagnostics are now routed to the original stderr instead of being discarded ( |
|
📡 Smoke OTel Tracing completed. All tracing scenarios validated. ✅
|
|
🪪 Smoke Copilot BYOK AOAI (Entra) is testing Azure OpenAI BYOK (Entra / GitHub OIDC) mode on this pull request... |
|
💎 Smoke Gemini is crystallizing results on this pull request... |
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟 Smoke CodexOutcome: The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible. Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.
|
|
✅ Security Guard completed successfully! Security GuardOutcome: The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible. Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.
|
|
✅ Smoke Claude passed
|
|
Cloud Hypervisor + Copilot smoke test
|
Smoke Test: Claude Engine Validation
Overall result: ✅ PASS
|
|
Smoke: PRs "Share a release-pinned guest base across Cloud Hypervisor VM profiles"
|
Smoke Test: Copilot BYOK (Direct Mode) ✅Results:
Mode: Direct BYOK (COPILOT_PROVIDER_API_KEY) via api-proxy → api.githubcopilot.com Status: PASS
|
|
Smoke services: ✅ Redis PONG, ✅ pg_isready, ✅ SELECT 1. Overall PASS
|
|
OTEL smoke test
|
|
EGRESS_RESULT allow=pass deny=pass
|
Smoke Test
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — PASS Notes: the Bun and Deno curl installers were blocked by the sandbox, so I installed both from npm instead. .NET and Maven needed alternate cache and runtime dirs under
|
Chroot Version Comparison
Node.js differs between host and chroot (the chroot reports v2.98.0), so the
|


Agent-writable capture files cannot provide a trustworthy record of command output, and callers should not have to reproduce AWF’s mount-isolation and lifecycle handling. This adds opt-in host-only capture for non-TTY Compose runtimes, with explicit completeness and attempt metadata.
metadata.jsonrecords invocation and attempt IDs, observed exit or interruption, drain status, truncation or capture failures, presentation failures, and teardown. Raw output is not automatically published.awf --capture-command-output "$RUNNER_TEMP/awf-capture-$GITHUB_RUN_ID" \ --allow-domains github.com -- your-command