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
19 changes: 11 additions & 8 deletions docs/awf-config-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,19 +189,21 @@ AWF settings MAY be supplied via config files, including stdin (`--config -`).
- `container.dockerHostPathPrefix` → `--docker-host-path-prefix`
- `container.runnerToolCachePath` → *(config-only; checked first for optional read-only runner tool cache mount, before `RUNNER_TOOL_CACHE` and `/home/runner/work/_tool` auto-detection)*
- `container.mounts[]` → `-v, --mount` *(repeatable; each array entry maps to one Docker volume mount in `/host_path:/container_path[:ro|rw]` format (both paths must be absolute; host path must exist); in chroot mode, container paths are automatically prefixed with `/host`)*
- `container.containerRuntime` → `--container-runtime` *(user-facing runtime name: `"gvisor"` for OCI runtime in compose, `"sbx"` for Docker sbx microVM, or `"firecracker"` for the fail-closed Firecracker v1.16.1 control-plane preview. For gvisor: translates to `"runsc"`, injects `extra_hosts` for DNS workaround. For sbx: agent runs in a hypervisor-isolated microVM, infra stays in compose, sbx proxy chains through AWF's Squid.)*
- `container.containerRuntime` → `--container-runtime` *(user-facing runtime name: `"gvisor"` for OCI runtime in compose, `"sbx"` for Docker sbx microVM, or `"firecracker"` for the explicit Firecracker v1.16.1 workload preview. For gvisor: translates to `"runsc"`, injects `extra_hosts` for DNS workaround. For sbx and Firecracker: infrastructure stays in Compose while the primary agent runs in a microVM.)*
- `firecracker.previewEnabled` → `--firecracker-preview`
- `firecracker.firecrackerBinary` → `--firecracker-binary`
- `firecracker.jailerBinary` → `--firecracker-jailer-binary`
- `firecracker.kernelPath` → `--firecracker-kernel`
- `firecracker.rootfsPath` → `--firecracker-rootfs`
- `firecracker.supervisorPath` → `--firecracker-supervisor`
- `firecracker.vcpuCount` → `--firecracker-vcpus`
- `firecracker.memoryMib` → `--firecracker-memory-mib`
- `firecracker.apiTimeoutMs` → `--firecracker-api-timeout-ms`
- `firecracker.sha256.firecracker` → `--firecracker-binary-sha256`
- `firecracker.sha256.jailer` → `--firecracker-jailer-sha256`
- `firecracker.sha256.kernel` → `--firecracker-kernel-sha256`
- `firecracker.sha256.rootfs` → `--firecracker-rootfs-sha256`
- `firecracker.sha256.supervisor` → `--firecracker-supervisor-sha256`
- `chroot.binariesSourcePath` → *(config-only; mounts a runner-side binaries directory at `/tmp/awf-runner-bin` inside chroot mode and prepends it to `PATH`)*
- `chroot.identity.home` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_HOME` and applied after chroot pivot)*
- `chroot.identity.user` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_USER` and applied to `USER`/`LOGNAME` after chroot pivot)*
Expand Down Expand Up @@ -261,13 +263,14 @@ AWF settings MAY be supplied via config files, including stdin (`--config -`).

When `container.dockerHostPathPrefix` points at a daemon-visible shared `/tmp` path, the implementation stages the invoking CLI binary together with `/etc/passwd`, `/etc/group`, and the generated chroot `/etc/hosts` under that shared path so chroot mode can bootstrap on split-filesystem ARC/DinD hosts.

The `firecracker` surface is a control-plane preview pinned to Firecracker
v1.16.1 on Linux/KVM (`x86_64` or `aarch64`). AWF MUST launch Firecracker
through the matching jailer, reject unsafe or mismatched artifacts, and MUST
NOT fall back to another runtime. Networking, workspace images, enclave
executors, and guest workload execution are intentionally unavailable in this
preview; selecting `firecracker` therefore fails closed before running the
requested command.
The `firecracker` surface is an explicit workload preview pinned to Firecracker
v1.16.1 on Linux/KVM (`x86_64` or `aarch64`). It requires strict network
isolation, a local Unix-socket Docker daemon, the matching jailer, and explicit
SHA-256 digests for Firecracker, jailer, kernel, rootfs, and the AWF guest
supervisor. AWF starts Compose infrastructure only, attaches the jailed
microVM to the proven internal bridge, and executes through vsock. Host access,
DinD, extra mounts, TTY, topology peers, and enclaves fail closed in this
preview. Selecting `firecracker` never falls back to another runtime.

When DinD is detected, AWF preserves the detected `DOCKER_HOST` value for the agent environment (including MCP servers) so DinD-aware tooling can reach the correct daemon without manual workflow env overrides.

Expand Down
15 changes: 11 additions & 4 deletions docs/awf-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -652,19 +652,19 @@
"sbx",
"firecracker"
],
"description": "Runtime for the primary agent. \"gvisor\" uses runsc in Docker Compose. \"sbx\" uses a Docker sbx microVM. \"firecracker\" selects the fail-closed Firecracker v1.16.1 control-plane preview; networking and guest workload execution are not implemented. Infrastructure containers always use the default runc runtime."
"description": "Runtime for the primary agent. \"gvisor\" uses runsc in Docker Compose. \"sbx\" uses a Docker sbx microVM. \"firecracker\" selects the explicit Linux/KVM Firecracker v1.16.1 workload preview. Infrastructure containers always use the default runc runtime."
}
}
},
"firecracker": {
"type": "object",
"description": "Firecracker v1.16.1 control-plane preview configuration. Selecting this runtime never falls back to Docker and cannot execute workloads until networking and guest execution are implemented.",
"description": "Firecracker v1.16.1 workload preview configuration. Requires strict network isolation, a local Docker daemon, KVM, jailer, and explicitly checksummed artifacts. Selecting this runtime never falls back to Docker.",
"additionalProperties": false,
"properties": {
"previewEnabled": {
"type": "boolean",
"default": false,
"description": "Explicitly acknowledge the incomplete Firecracker control-plane preview. This does not enable workload execution."
"description": "Explicitly enable Firecracker preview workload execution."
},
"firecrackerBinary": {
"type": "string",
Expand All @@ -682,6 +682,10 @@
"type": "string",
"description": "Absolute path to the trusted guest root filesystem image."
},
"supervisorPath": {
"type": "string",
"description": "Absolute path to the built AWF Firecracker guest supervisor."
},
"vcpuCount": {
"type": "integer",
"minimum": 1,
Expand All @@ -702,7 +706,7 @@
},
"sha256": {
"type": "object",
"description": "Optional pinned SHA-256 digests for trusted Firecracker artifacts.",
"description": "Pinned SHA-256 digests for trusted Firecracker artifacts. All entries are required for preview workload execution.",
"additionalProperties": false,
"properties": {
"firecracker": {
Expand All @@ -716,6 +720,9 @@
},
"rootfs": {
"$ref": "#/$defs/sha256Digest"
},
"supervisor": {
"$ref": "#/$defs/sha256Digest"
}
}
}
Expand Down
15 changes: 11 additions & 4 deletions src/awf-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -652,19 +652,19 @@
"sbx",
"firecracker"
],
"description": "Runtime for the primary agent. \"gvisor\" uses runsc in Docker Compose. \"sbx\" uses a Docker sbx microVM. \"firecracker\" selects the fail-closed Firecracker v1.16.1 control-plane preview; networking and guest workload execution are not implemented. Infrastructure containers always use the default runc runtime."
"description": "Runtime for the primary agent. \"gvisor\" uses runsc in Docker Compose. \"sbx\" uses a Docker sbx microVM. \"firecracker\" selects the explicit Linux/KVM Firecracker v1.16.1 workload preview. Infrastructure containers always use the default runc runtime."
}
}
},
"firecracker": {
"type": "object",
"description": "Firecracker v1.16.1 control-plane preview configuration. Selecting this runtime never falls back to Docker and cannot execute workloads until networking and guest execution are implemented.",
"description": "Firecracker v1.16.1 workload preview configuration. Requires strict network isolation, a local Docker daemon, KVM, jailer, and explicitly checksummed artifacts. Selecting this runtime never falls back to Docker.",
"additionalProperties": false,
"properties": {
"previewEnabled": {
"type": "boolean",
"default": false,
"description": "Explicitly acknowledge the incomplete Firecracker control-plane preview. This does not enable workload execution."
"description": "Explicitly enable Firecracker preview workload execution."
},
"firecrackerBinary": {
"type": "string",
Expand All @@ -682,6 +682,10 @@
"type": "string",
"description": "Absolute path to the trusted guest root filesystem image."
},
"supervisorPath": {
"type": "string",
"description": "Absolute path to the built AWF Firecracker guest supervisor."
},
"vcpuCount": {
"type": "integer",
"minimum": 1,
Expand All @@ -702,7 +706,7 @@
},
"sha256": {
"type": "object",
"description": "Optional pinned SHA-256 digests for trusted Firecracker artifacts.",
"description": "Pinned SHA-256 digests for trusted Firecracker artifacts. All entries are required for preview workload execution.",
"additionalProperties": false,
"properties": {
"firecracker": {
Expand All @@ -716,6 +720,9 @@
},
"rootfs": {
"$ref": "#/$defs/sha256Digest"
},
"supervisor": {
"$ref": "#/$defs/sha256Digest"
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,26 +175,28 @@ program
'Container runtime for the agent container.\n' +
' "gvisor" — OCI runtime via Docker Compose (translates to runsc).\n' +
' "sbx" — Docker sbx microVM with hypervisor isolation.\n' +
' "firecracker" — preview Firecracker v1.16.1 control plane (workload execution unavailable).\n' +
' "firecracker" — explicit Linux/KVM Firecracker v1.16.1 preview.\n' +
' Unknown values are passed through as raw Docker runtime names.'
)
.option(
'--firecracker-preview',
'Acknowledge the incomplete Firecracker v1.16.1 preview control plane.\n' +
' Networking and guest workload execution remain unavailable.',
'Enable the Firecracker v1.16.1 workload-execution preview.\n' +
' Requires Linux/KVM, local Docker, jailer, and pinned guest artifacts.',
false
)
.option('--firecracker-binary <path>', 'Path to the Firecracker v1.16.1 binary.')
.option('--firecracker-jailer-binary <path>', 'Path to the matching Firecracker v1.16.1 jailer binary.')
.option('--firecracker-kernel <path>', 'Path to the guest Linux kernel image.')
.option('--firecracker-rootfs <path>', 'Path to the guest root filesystem image.')
.option('--firecracker-supervisor <path>', 'Path to the built AWF Firecracker guest supervisor.')
.option('--firecracker-vcpus <count>', 'Guest virtual CPU count (default: 2).')
.option('--firecracker-memory-mib <mib>', 'Guest memory in MiB (default: 512).')
.option('--firecracker-api-timeout-ms <ms>', 'Bounded API socket readiness timeout in milliseconds (default: 5000).')
.option('--firecracker-binary-sha256 <digest>', 'Expected SHA-256 digest of the Firecracker binary.')
.option('--firecracker-jailer-sha256 <digest>', 'Expected SHA-256 digest of the jailer binary.')
.option('--firecracker-kernel-sha256 <digest>', 'Expected SHA-256 digest of the guest kernel.')
.option('--firecracker-rootfs-sha256 <digest>', 'Expected SHA-256 digest of the guest rootfs.')
.option('--firecracker-supervisor-sha256 <digest>', 'Expected SHA-256 digest of the AWF guest supervisor.')

// -- Container Configuration --
.option(
Expand Down
4 changes: 4 additions & 0 deletions src/commands/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,15 @@ function buildFirecrackerConfig(
'firecrackerJailerBinary',
'firecrackerKernel',
'firecrackerRootfs',
'firecrackerSupervisor',
'firecrackerVcpus',
'firecrackerMemoryMib',
'firecrackerApiTimeoutMs',
'firecrackerBinarySha256',
'firecrackerJailerSha256',
'firecrackerKernelSha256',
'firecrackerRootfsSha256',
'firecrackerSupervisorSha256',
].some((key) => options[key] !== undefined);
if (!selected && !configured) return undefined;

Expand All @@ -258,6 +260,7 @@ function buildFirecrackerConfig(
jailer: options.firecrackerJailerSha256 as string | undefined,
kernel: options.firecrackerKernelSha256 as string | undefined,
rootfs: options.firecrackerRootfsSha256 as string | undefined,
supervisor: options.firecrackerSupervisorSha256 as string | undefined,
};

return {
Expand All @@ -269,6 +272,7 @@ function buildFirecrackerConfig(
FIRECRACKER_DEFAULT_JAILER_BINARY,
kernelPath: options.firecrackerKernel as string | undefined,
rootfsPath: options.firecrackerRootfs as string | undefined,
supervisorPath: options.firecrackerSupervisor as string | undefined,
vcpuCount: parseFirecrackerPositiveInteger(
options.firecrackerVcpus,
'--firecracker-vcpus',
Expand Down
51 changes: 51 additions & 0 deletions src/commands/main-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,57 @@ describe('createMainAction', () => {
expect.stringContaining('Configuration files preserved')
);
});

it('quiesces an external runtime through its preserve hook', async () => {
const preserve = jest.fn().mockResolvedValue(undefined);
const backend = {
runtime: 'firecracker',
preflight: jest.fn(),
start: jest.fn(),
exec: jest.fn(),
collectDiagnostics: jest.fn(),
stop: jest.fn(),
preserve,
};
const cleanup = testHelpers.buildCleanupFn(
{ ...MAIN_ACTION_STUB_CONFIG, keepContainers: true },
() => false,
() => false,
backend,
);

await cleanup();

expect(preserve).toHaveBeenCalledTimes(1);
expect(backend.stop).not.toHaveBeenCalled();
});
});

describe('external runtime cleanup failures', () => {
it('continues generic cleanup and then rethrows the runtime failure', async () => {
const runtimeError = new Error('Firecracker teardown failed');
const backend = {
runtime: 'firecracker',
preflight: jest.fn(),
start: jest.fn(),
exec: jest.fn(),
collectDiagnostics: jest.fn(),
stop: jest.fn().mockRejectedValue(runtimeError),
};
const cleanup = testHelpers.buildCleanupFn(
{ ...MAIN_ACTION_STUB_CONFIG, keepContainers: false },
() => false,
() => false,
backend,
);

await expect(cleanup()).rejects.toBe(runtimeError);
expect(mockedDockerManager.cleanup).toHaveBeenCalled();
expect(mockedLogger.warn).toHaveBeenCalledWith(
'External runtime cleanup failed; continuing with infrastructure teardown.',
runtimeError,
);
});
});

describe('performCleanup with containers started', () => {
Expand Down
18 changes: 16 additions & 2 deletions src/commands/main-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,25 @@ function buildCleanupFn(
externalRuntimeBackend?: ExternalAgentRuntimeBackend,
) {
return async (signal?: string) => {
let externalRuntimeCleanupError: unknown;
if (signal) {
logger.info(`Received ${signal}, cleaning up...`);
}

if (externalRuntimeBackend && !config.keepContainers) {
await externalRuntimeBackend.stop();
if (externalRuntimeBackend) {
try {
if (config.keepContainers && externalRuntimeBackend.preserve) {
await externalRuntimeBackend.preserve();
} else if (!config.keepContainers) {
await externalRuntimeBackend.stop();
}
} catch (error) {
externalRuntimeCleanupError = error;
logger.warn(
'External runtime cleanup failed; continuing with infrastructure teardown.',
error,
);
}
}

// Let the enclave server emit final cleanup telemetry before preserving
Expand Down Expand Up @@ -169,6 +182,7 @@ function buildCleanupFn(
logger.info(`Squid logs available at: ${config.workDir}/squid-logs/`);
logger.info(`Host iptables rules preserved (--keep-containers enabled)`);
}
if (externalRuntimeCleanupError) throw externalRuntimeCleanupError;
};
}

Expand Down
69 changes: 69 additions & 0 deletions src/commands/validate-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,75 @@ describe('validateOptions', () => {
// Post-config validations (docker host, rate limits, feature flags, ports)
// ---------------------------------------------------------------------------

describe('Firecracker runtime validation', () => {
const digest = 'a'.repeat(64);
const firecracker = {
previewEnabled: true,
firecrackerBinary: '/opt/firecracker',
jailerBinary: '/opt/jailer',
kernelPath: '/opt/kernel',
rootfsPath: '/opt/rootfs',
supervisorPath: '/opt/supervisor',
vcpuCount: 2,
memoryMib: 512,
apiTimeoutMs: 5000,
sha256: {
firecracker: digest,
jailer: digest,
kernel: digest,
rootfs: digest,
supervisor: digest,
},
};

function firecrackerConfig(overrides: Record<string, unknown> = {}) {
return {
...STUB_CONFIG,
containerRuntime: 'firecracker',
legacySecurity: false,
networkIsolation: undefined,
enableApiProxy: undefined,
firecracker,
...overrides,
};
}

it('accepts a complete strict preview configuration', () => {
mockedBuildConfig.buildConfig.mockReturnValue(firecrackerConfig());
expect(() => validateOptions(validOptions(), 'echo hi')).not.toThrow();
});

it('rejects Firecracker options for another runtime', () => {
mockedBuildConfig.buildConfig.mockReturnValue(firecrackerConfig({
containerRuntime: 'gvisor',
}));
expect(() => validateOptions(validOptions(), 'echo hi')).toThrow('process.exit called');
expect(mockedLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Firecracker options require'),
);
});

it('rejects unsupported Firecracker policy before strict-mode coercion', () => {
mockedBuildConfig.buildConfig.mockReturnValue(firecrackerConfig({
enableDind: true,
}));
expect(() => validateOptions(validOptions(), 'echo hi')).toThrow('process.exit called');
expect(mockedLogger.error).toHaveBeenCalledWith(
expect.stringContaining('does not support Docker-in-Docker'),
);
});

it('rejects an incomplete Firecracker runtime after security defaults', () => {
mockedBuildConfig.buildConfig.mockReturnValue(firecrackerConfig({
firecracker: { ...firecracker, previewEnabled: false },
}));
expect(() => validateOptions(validOptions(), 'echo hi')).toThrow('process.exit called');
expect(mockedLogger.error).toHaveBeenCalledWith(
expect.stringContaining('requires explicit --firecracker-preview'),
);
});
});

describe('--docker-host validation', () => {
it('exits when --docker-host is not a unix:// URI', () => {
mockedBuildConfig.buildConfig.mockReturnValue({
Expand Down
Loading
Loading