Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/minor-gvisor-runtime-support.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions docs/adr/44796-gvisor-runtime-for-agent-containers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ADR-44796: Use gVisor (runsc) as an Optional Container Runtime for Agent Containers

**Date**: 2026-07-10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smoke-test review: consider linking the referenced issue gh-aw-firewall#6093 for traceability once it lands.

**Status**: Draft
**Deciders**: @lpcox

---

### Context

Agent containers in gh-aw run under a standard Docker runtime backed by Linux cgroups and namespaces. This provides process-level isolation but leaves the host kernel's system call surface fully accessible to agent code. Any vulnerability in a system call handler can be exploited to escape the container. For higher-assurance workloads, operators need a runtime that interposes on all syscalls and runs them through a user-space kernel, substantially reducing the kernel attack surface exposed to untrusted agent code. The AWF daemon already supports `container.containerRuntime: "gvisor"` in its stdin config (gh-aw-firewall#6093), but the compiler had no way to emit that setting or install the runtime on the runner. This PR wires up the missing compiler side: a new `sandbox.agent.runtime` frontmatter field, an auto-generated pre-agent gVisor install step, and the AWF config JSON plumbing that passes `containerRuntime: "gvisor"` to AWF.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smoke-test review: the AWFContainerRuntimeMinVersion placeholder (v0.28.0) should be tracked so it is updated when the firewall release ships.


### Decision

We will add `sandbox.agent.runtime: gvisor` as an optional frontmatter field. When set, the compiler (1) emits a GitHub Actions step before the AWF install step that downloads `runsc` and `containerd-shim-runsc-v1` from `storage.googleapis.com/gvisor`, installs them to `/usr/local/bin`, registers them with Docker via `sudo runsc install`, restarts Docker with `systemctl restart`, and verifies the runtime with a test container, and (2) passes `containerRuntime: "gvisor"` in the AWF stdin config JSON so AWF starts the agent container under `runsc` — once the effective AWF version is at or above `AWFContainerRuntimeMinVersion` (currently a placeholder at `v0.28.0` pending the release of gh-aw-firewall#6093). One incompatible combination is rejected at compile time: `runtime: gvisor` + `runner.topology: arc-dind` (systemd unavailable on ARC DinD). The `runtime: gvisor` option is compatible with both `sudo: false` (default, network-isolation mode) and `sudo: true` — the gVisor install step uses shell-level sudo commands independently of the `sandbox.agent.sudo` setting. The existing `sudo: true` deprecation warning continues to apply regardless of `runtime`.

### Alternatives Considered

#### Alternative 1: Standard Docker runtime (status quo)

Continue using only Linux cgroups/namespaces for agent container isolation without adding a new runtime option. System calls from inside the container reach the host kernel directly. This approach requires zero compiler changes and no runner setup time, and it already satisfies many workloads. It was rejected because it does not meet the kernel-level isolation requirement: a single exploited syscall can escape the container, and operators running sensitive workloads need a stronger isolation boundary.

#### Alternative 2: Kata Containers

Kata Containers provide VM-level isolation — each container runs in a lightweight QEMU/Firecracker VM with a dedicated kernel. This gives stronger isolation guarantees than gVisor in some threat models. It was not chosen because: (a) Kata requires KVM hardware virtualization support, which is not available in all GitHub-hosted runner configurations; (b) the AWF daemon already has native support for `containerRuntime: "gvisor"` but not for Kata; and (c) gVisor's installation footprint (two binaries + `runsc install`) is far smaller than Kata's, reducing runner setup time and dependency surface.

#### Alternative 3: Seccomp / AppArmor profiles

Reduce the syscall surface by attaching a restrictive seccomp profile or AppArmor policy to the agent container, without changing the OCI runtime. This has near-zero overhead and no special hardware requirements. It was not chosen because it still routes all permitted syscalls through the real host kernel — a zero-day in a whitelisted syscall path remains exploitable. Profiles also require ongoing maintenance as agent behavior evolves, and they do not satisfy an isolation requirement that explicitly demands a user-space kernel intercept layer.

### Consequences

#### Positive
- Agent containers configured with `runtime: gvisor` run under gVisor's `runsc` OCI runtime, which interposes on all system calls via a user-space kernel, substantially reducing the host kernel attack surface.
- Compile-time validation catches the known incompatible combination (`arc-dind`) before a workflow is submitted, giving authors a clear error message rather than a runtime failure.
- The change is fully backward-compatible: existing workflows that do not set `sandbox.agent.runtime` are unaffected and continue to run under the default Docker runtime.
- `runtime: gvisor` is compatible with `sudo: false` (the default, network-isolation mode): the install step uses shell-level sudo commands independently of the `sandbox.agent.sudo` setting, so operators can have both gVisor kernel isolation and AWF network isolation.
- Removing the special case for `AgentRuntimeGVisor` in `validateStrictSandboxCustomization` simplifies the strict-mode validation path.

#### Negative
- `runtime: gvisor` is incompatible with `runner.topology: arc-dind`, restricting which runner configurations can use this feature.
- The gVisor install step adds observable setup time (binary downloads, Docker restart, verification container run) to every workflow execution that enables this option.
- The `containerRuntime` field in the AWF config is gated behind `AWFContainerRuntimeMinVersion` (placeholder `v0.28.0`) until gh-aw-firewall#6093 is released; until then, the install step runs but AWF uses its default runtime.

#### Neutral
- The `systemctl restart docker` (rather than `reload`) constraint documented in the implementation will need to be preserved in future modifications to the install step: Docker's SIGHUP reload does not call `setHostGatewayIP()`, which breaks `--add-host host.docker.internal:host-gateway`.
- AWF translates the `"gvisor"` string in its config to `"runsc"` internally; the compiler intentionally passes the human-readable label rather than the internal runtime name.
- Future container runtime options (e.g., Kata) can follow the same `AgentRuntime` type + `sandbox.agent.runtime` enum pattern established here.

---

*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*
2 changes: 2 additions & 0 deletions pkg/constants/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ constants.AWFAllowHostPortsMinVersion // "v0.25.24" — minimum AWF for --allo
constants.AWFDockerHostPathPrefixMinVersion // "v0.25.43" — minimum AWF for --docker-host-path-prefix
constants.AWFTokenSteeringMinVersion // "v0.25.44" — minimum AWF for token steering support
constants.AWFChrootConfigMinVersion // "v0.27.1" — minimum AWF for chroot.binariesSourcePath and identity.*
constants.AWFContainerRuntimeMinVersion // "v0.28.0" — minimum AWF for containerRuntime in container config (gh-aw-firewall#6093; TODO: update on release)
constants.DefaultGVisorVersion // "20250623.0" — pinned gVisor release for the generated install step; bump after reviewing release notes
constants.CopilotNoAskUserMinVersion // "1.0.19" — minimum Copilot CLI for --no-ask-user
constants.MCPGIntegrityReactionsMinVersion // "v0.2.18" — minimum MCPG for integrity-reactions policy
```
Expand Down
15 changes: 15 additions & 0 deletions pkg/constants/version_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ const AWFChrootConfigMinVersion Version = "v0.27.1"
// the agent container from starting in split-filesystem ARC/DinD environments.
const AWFArcDindMinVersion Version = "v0.27.20"

// AWFContainerRuntimeMinVersion is the minimum AWF version that supports the
// containerRuntime field in the container config (gh-aw-firewall#6093).
// TODO: update this constant to the actual release version once gh-aw-firewall#6093 lands.
// Until then, no released AWF binary accepts containerRuntime, so the compiler must not
// emit the field for workflows pinned to any currently-available version.
const AWFContainerRuntimeMinVersion Version = "v0.28.0"

// DefaultGVisorVersion is the pinned gVisor release used by the compiler-generated
// install step. A specific dated release name is used instead of "latest" to ensure
// reproducible, verifiable installs. Each release provides SHA-512 files for
// integrity verification before the binaries are installed with root privileges.
// Bump this constant after reviewing the release notes at
// https://github.com/google/gvisor/releases.
const DefaultGVisorVersion = "20250623.0"

// CopilotNoAskUserMinVersion is the minimum Copilot CLI version that supports the --no-ask-user
// flag, which enables fully autonomous agentic runs by suppressing interactive prompts.
// Workflows using an older Copilot CLI version must not emit --no-ask-user or the run will fail.
Expand Down
6 changes: 6 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3461,6 +3461,12 @@
"description": "Enable or disable model fallback for unresolved model selections. Set to false for BYOK Azure OpenAI deployments to prevent deployment-name rewriting. Supports literal boolean or GitHub Actions expression.",
"examples": [false, "${{ inputs.model-fallback }}"]
},
"runtime": {
"type": "string",
"description": "Container runtime for the agent container. Use 'gvisor' to run the agent under gVisor's runsc runtime for additional kernel-level isolation. Requires sandbox.agent.sudo: true (root access is needed to install and register runsc). Incompatible with runner.topology: arc-dind.",
"enum": ["gvisor"],
"examples": ["gvisor"]
},
"config": {
"type": "object",
"description": "Custom sandbox runtime configuration. Note: Network configuration is controlled by the top-level 'network' field, not here.",
Expand Down
22 changes: 20 additions & 2 deletions pkg/workflow/awf_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ type AWFContainerConfig struct {
// runner and daemon have separate filesystems.
// Maps to: --docker-host-path-prefix <value>
DockerHostPathPrefix string `json:"dockerHostPathPrefix,omitempty"`

// ContainerRuntime specifies the OCI runtime for the agent container.
// "gvisor" enables gVisor's runsc runtime for additional kernel-level isolation.
// AWF translates "gvisor" → "runsc" internally.
ContainerRuntime string `json:"containerRuntime,omitempty"`
}

// AWFLoggingConfig is the "logging" section of the AWF config file.
Expand Down Expand Up @@ -544,9 +549,19 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {

// ── Container section ─────────────────────────────────────────────────────
awfImageTag := buildAWFImageTagWithDigests(getAWFImageTag(firewallConfig), config.WorkflowData)
if awfImageTag != "" || isArcDindTopology(config.WorkflowData) {
agentRuntime := getAgentContainerRuntime(config.WorkflowData)
// containerRuntime is only emitted when the effective AWF version supports it.
// Gate here to avoid sending an unrecognised field to older AWF binaries.
if !awfSupportsContainerRuntime(firewallConfig) {
if agentRuntime != "" {
awfConfigLog.Printf("Skipping containerRuntime: AWF version %q requires at least %s (gh-aw-firewall#6093)", getAWFImageTag(firewallConfig), constants.AWFContainerRuntimeMinVersion)
}
agentRuntime = ""
}
if awfImageTag != "" || isArcDindTopology(config.WorkflowData) || agentRuntime != "" {
container := &AWFContainerConfig{
ImageTag: awfImageTag,
ImageTag: awfImageTag,
ContainerRuntime: agentRuntime,
Comment on lines +552 to +564
}
// NOTE: dockerHostPathPrefix is intentionally NOT set for arc-dind topology.
// With sysroot-stage active, the Docker daemon can access all needed paths:
Expand All @@ -560,6 +575,9 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
if awfImageTag != "" {
awfConfigLog.Printf("Container section: image_tag=%s", awfImageTag)
}
if agentRuntime != "" {
awfConfigLog.Printf("Container section: containerRuntime=%s", agentRuntime)
}
}

// ── Logging section ──────────────────────────────────────────────────────
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/awf_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,13 @@ func awfSupportsChrootConfig(firewallConfig *FirewallConfig) bool {
return awfVersionAtLeast(firewallConfig, constants.AWFChrootConfigMinVersion)
}

// awfSupportsContainerRuntime returns true when the effective AWF version supports the
// containerRuntime field in the container config (gh-aw-firewall#6093).
// The field must not be emitted for older versions that do not recognise it.
func awfSupportsContainerRuntime(firewallConfig *FirewallConfig) bool {
return awfVersionAtLeast(firewallConfig, constants.AWFContainerRuntimeMinVersion)
}

// buildArcDindChrootConfigPatchBody returns the Node.js command that patches the AWF
// config file with chroot.binariesSourcePath and chroot.identity.*. It is designed to be
// embedded inside a bash if-block that already guards on DOCKER_HOST=tcp://...
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ func (e *CodexEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubA
awfVersion = firewallConfig.Version
}

// gVisor must be installed and registered BEFORE AWF starts the agent container.
if isGVisorRuntime(workflowData) {
steps = append(steps, generateGVisorInstallStep())
}

// Install AWF binary (or skip if custom command is specified)
awfInstall := generateAWFInstallationStep(awfVersion, agentConfig)
if len(awfInstall) > 0 {
Expand Down
57 changes: 57 additions & 0 deletions pkg/workflow/copilot_engine_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,60 @@ func generateDockerComposeInstallStep() GitHubActionStep {
` docker compose version`,
})
}

// generateGVisorInstallStep creates a GitHub Actions step that downloads, installs,
// and verifies the gVisor (runsc) container runtime. This step must run BEFORE the
// AWF invocation step so that Docker can start the agent container under runsc.
//
// Key implementation notes:
// - Pins to constants.DefaultGVisorVersion rather than "latest" for reproducible,
// verifiable installs. Each binary is verified against its official SHA-512 file
// before being installed with root privileges, matching the pattern used by the
// adjacent AWF installer.
// - Uses uname -m directly (x86_64, aarch64) — gVisor download URLs use raw arch names.
// - Restarts Docker with `systemctl restart` (NOT reload): Docker's SIGHUP reload does
// not call setHostGatewayIP(), which breaks --add-host host.docker.internal:host-gateway.
// - Downloads both runsc and containerd-shim-runsc-v1; the shim is required for Docker's
// containerd integration.
func generateGVisorInstallStep() GitHubActionStep {
version := constants.DefaultGVisorVersion
return GitHubActionStep([]string{
" - name: Install gVisor (runsc)",
" run: |",
" set -euo pipefail",
"",
` echo "::group::Install gVisor (runsc)"`,
` ARCH=$(uname -m)`,
` URL="https://storage.googleapis.com/gvisor/releases/release/` + version + `/${ARCH}"`,
` echo "Downloading runsc ` + version + ` for ${ARCH}..."`,
` curl -fsSL "${URL}/runsc" -o /tmp/runsc`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No checksum verification for downloaded binaries — a compromised or MITM-replaced runsc would execute with sudo.

The AWF install step explicitly calls out SHA256 verification as a supply-chain protection. The gVisor curl downloads have no equivalent check.

💡 Suggested fix

gVisor publishes signed checksums at ${URL}/runsc.sha512. Add verification before the sudo install:

curl -fsSL "${URL}/runsc" -o /tmp/runsc
curl -fsSL "${URL}/runsc.sha512" -o /tmp/runsc.sha512
sha512sum -c /tmp/runsc.sha512
curl -fsSL "${URL}/containerd-shim-runsc-v1" -o /tmp/containerd-shim-runsc-v1
curl -fsSL "${URL}/containerd-shim-runsc-v1.sha512" -o /tmp/containerd-shim-runsc-v1.sha512
sha512sum -c /tmp/containerd-shim-runsc-v1.sha512

This mirrors the supply-chain protection described in generateAWFInstallationStep.

@copilot please address this.

` curl -fsSL "${URL}/runsc.sha512" -o /tmp/runsc.sha512`,
` echo "Verifying SHA-512 for runsc..."`,
` (cd /tmp && sha512sum -c runsc.sha512)`,
` curl -fsSL "${URL}/containerd-shim-runsc-v1" -o /tmp/containerd-shim-runsc-v1`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: No integrity verification of downloaded binaries

The runsc and containerd-shim-runsc-v1 binaries are downloaded from storage.googleapis.com and immediately installed as root (sudo install -m 755) without verifying their authenticity. The official gVisor release page publishes SHA-512 checksums (runsc.sha512, containerd-shim-runsc-v1.sha512) at the same URL prefix.

This is a supply-chain risk: a MITM or compromised GCS bucket could deliver a malicious binary that runs as root inside the agent container.

Suggested fix:

curl -fsSL "${URL}/runsc" -o /tmp/runsc
curl -fsSL "${URL}/runsc.sha512" -o /tmp/runsc.sha512
sha512sum -c /tmp/runsc.sha512
curl -fsSL "${URL}/containerd-shim-runsc-v1" -o /tmp/containerd-shim-runsc-v1
curl -fsSL "${URL}/containerd-shim-runsc-v1.sha512" -o /tmp/containerd-shim-runsc-v1.sha512
sha512sum -c /tmp/containerd-shim-runsc-v1.sha512

@copilot please address this.

` curl -fsSL "${URL}/containerd-shim-runsc-v1.sha512" -o /tmp/containerd-shim-runsc-v1.sha512`,
` echo "Verifying SHA-512 for containerd-shim-runsc-v1..."`,
` (cd /tmp && sha512sum -c containerd-shim-runsc-v1.sha512)`,
` sudo install -m 755 /tmp/runsc /usr/local/bin/runsc`,
` sudo install -m 755 /tmp/containerd-shim-runsc-v1 /usr/local/bin/containerd-shim-runsc-v1`,
` runsc --version`,
` echo "::endgroup::"`,
"",
` echo "::group::Register runsc as Docker runtime"`,
` sudo runsc install`,
` # IMPORTANT: Must use restart (not reload).`,
` # Docker's SIGHUP reload does NOT call setHostGatewayIP(), so`,
` # --add-host host.docker.internal:host-gateway breaks for any`,
` # container started after a reload-only config change.`,
` sudo systemctl restart docker`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemctl restart docker will fail on GitHub-hosted runners: these runners do not use systemd, so this command errors with System has not been booted with systemd — making this feature non-functional on its primary target environment.

💡 Suggested fix

Use a portable restart + readiness wait:

# Portable full restart (not SIGHUP/reload) on both systemd and non-systemd runners
sudo service docker restart 2>/dev/null || sudo systemctl restart docker

# Wait for daemon to be ready
timeout 30 bash -c 'until docker info &>/dev/null; do sleep 1; done'

service docker restart triggers a full daemon restart (not a SIGHUP reload), so the setHostGatewayIP() concern from the PR description still applies correctly here. The readiness loop also addresses the race condition where docker run immediately follows a restart.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] systemctl restart docker will fail in workflows that don't run on systemd-based GitHub-hosted runners (e.g., container jobs, self-hosted runners without systemd). There's no test or validation guard for this runner requirement.

A failed Docker restart leaves the runner in a broken state mid-job rather than surfacing a clear compile-time or early-runtime error.

💡 Options
  1. Compile-time check: Add a runner.os: linux (or similar) guard in sandbox_validation.go, parallel to the arc-dind check.
  2. Runtime fallback: Wrap the restart in a check:
    if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null; then
      sudo systemctl restart docker
    else
      echo "::error::gVisor requires systemd to restart Docker. Use a GitHub-hosted Linux runner."
      exit 1
    fi

Option 1 is preferred — fail fast at compile time with a clear message.

@copilot please address this.

` echo "Docker runtimes:"`,
` docker info --format '{{.Runtimes}}' || docker info | grep -i runtime`,
` echo "::endgroup::"`,
"",
` echo "::group::Verify gVisor works"`,
` docker pull hello-world`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reliability: Verification step pulls from Docker Hub, which may be blocked by sandbox network policy

When sandbox.agent.runtime: gvisor is used with a restricted network (network.allowed not including registry-1.docker.io), docker pull hello-world will fail during the install step — before AWF even starts, so the AWF network isolation hasn't taken effect yet. But if the runner is already restricted at the host firewall level, this pulls from an unconfigured external endpoint.

Consider using docker run --rm --runtime=runsc busybox true (or any image guaranteed to be already present on GitHub-hosted runners) instead of pulling hello-world at verification time.

@copilot please address this.

` docker run --rm --runtime=runsc hello-world`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docker pull hello-world / docker run hello-world in the install step will hit DockerHub rate limits or fail in air-gapped environments, breaking every gVisor-enabled workflow on shared runners that exhaust the anonymous pull quota (100 pulls/6 h per IP).

💡 Suggested fix

Verify gVisor using only local tools — no image pull needed:

# Confirm runsc binary is functional — no network required
runsc --version
runsc --help &>/dev/null && echo "runsc OK"

If a container smoke-test is truly needed, use the Docker daemon's built-in hello-world-equivalent with a scratch image that can be baked into the runner, or skip it and rely on runsc --version + docker info --format '{{.Runtimes}}' to confirm registration.

` echo "✅ gVisor runtime verified"`,
` echo "::endgroup::"`,
})
}
19 changes: 19 additions & 0 deletions pkg/workflow/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ func getAgentConfig(workflowData *WorkflowData) *AgentSandboxConfig {
return workflowData.SandboxConfig.Agent
}

// getAgentContainerRuntime returns the container runtime string for the AWF config,
// or an empty string if no custom runtime is configured.
func getAgentContainerRuntime(workflowData *WorkflowData) string {
agentConfig := getAgentConfig(workflowData)
if agentConfig == nil || agentConfig.Disabled {
return ""
}
return string(agentConfig.Runtime)
}

// isGVisorRuntime returns true when the agent container should use gVisor (runsc).
func isGVisorRuntime(workflowData *WorkflowData) bool {
agentConfig := getAgentConfig(workflowData)
if agentConfig == nil || agentConfig.Disabled {
return false
}
return agentConfig.Runtime == AgentRuntimeGVisor
}

func isAWFNetworkIsolationEnabled(workflowData *WorkflowData) bool {
agentConfig := getAgentConfig(workflowData)
if agentConfig == nil || agentConfig.Disabled {
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/frontmatter_extraction_security.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ func (c *Compiler) extractAgentSandboxConfig(agentVal any) *AgentSandboxConfig {
}
}

// Extract runtime (container runtime for the agent container)
if runtimeVal, hasRuntime := agentObj["runtime"]; hasRuntime {
if runtimeStr, ok := runtimeVal.(string); ok {
agentConfig.Runtime = AgentRuntime(runtimeStr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unvalidated runtime string is passed downstream without allowlist check: any arbitrary string from the workflow YAML is cast directly to AgentRuntime and forwarded as containerRuntime in the AWF config JSON. If AWF passes this value to Docker as --runtime=<value>, this is an injection vector.

💡 Suggested fix

Validate against the allowlist of known AgentRuntime constants before accepting the value:

if runtimeStr, ok := runtimeVal.(string); ok {
    rt := AgentRuntime(runtimeStr)
    switch rt {
    case AgentRuntimeGVisor:
        agentConfig.Runtime = rt
        frontmatterExtractionSecurityLog.Printf("Extracted sandbox.agent.runtime: %s", runtimeStr)
    default:
        // Return a parse/validation error — unknown runtime
        return nil, fmt.Errorf("sandbox.agent.runtime: unsupported value %q (supported: gvisor)", runtimeStr)
    }
}

This is particularly important in a file named frontmatter_extraction_security.go.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Unknown runtime values are silently accepted — an invalid runtime: sometypo will produce no error and silently have no effect at runtime.

The JSON schema rejects unknown values via the enum, but the extraction layer performs no parallel check, so workflows bypassing schema validation (e.g., tests) could pass an invalid runtime undetected.

💡 Suggested fix

Add a check after extracting the value:

switch agentConfig.Runtime {
case AgentRuntimeGVisor, "":
    // valid
default:
    return nil, fmt.Errorf("sandbox.agent.runtime: unknown value %q; valid values: gvisor", runtimeStr)
}

This makes the Go layer the authoritative validator rather than relying solely on the JSON schema.

@copilot please address this.

frontmatterExtractionSecurityLog.Printf("Extracted sandbox.agent.runtime: %s", runtimeStr)
}
}

// Extract model-fallback (AWF API proxy model fallback enable/disable flag)
if mfVal, hasMF := agentObj["model-fallback"]; hasMF {
switch v := mfVal.(type) {
Expand Down
Loading
Loading