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
23 changes: 22 additions & 1 deletion docs/awf-config-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ following top-level properties. All are OPTIONAL:
| `apiProxy` | object | API proxy sidecar configuration |
| `security` | object | Security and isolation settings |
| `container` | object | Container and Docker settings |
| `firecracker` | object | Firecracker v1.16.1 control-plane preview settings |
| `chroot` | object | Chroot execution overrides for split-filesystem ARC/DinD runners |
| `dind` | object | Bootstrap helpers for ARC/DinD split runner/daemon filesystems |
| `runner` | object | Runner topology declaration (standard vs. ARC/DinD) |
Expand Down Expand Up @@ -188,7 +189,19 @@ 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. 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 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.)*
- `firecracker.previewEnabled` → `--firecracker-preview`
- `firecracker.firecrackerBinary` → `--firecracker-binary`
- `firecracker.jailerBinary` → `--firecracker-jailer-binary`
- `firecracker.kernelPath` → `--firecracker-kernel`
- `firecracker.rootfsPath` → `--firecracker-rootfs`
- `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`
- `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 @@ -248,6 +261,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.

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.

The following CLI flag has no config-file equivalent by design:
Expand Down
75 changes: 73 additions & 2 deletions docs/awf-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,75 @@
"type": "string",
"enum": [
"gvisor",
"sbx"
"sbx",
"firecracker"
],
"description": "Container runtime for the agent container. \"gvisor\" runs the agent under gVisor's runsc runtime (OCI runtime, compose-based). \"sbx\" runs the agent inside a Docker sbx microVM with hypervisor isolation; infrastructure containers (squid-proxy, api-proxy) stay in Docker Compose on the host and the sbx proxy chains upstream through AWF's Squid for domain filtering. Only the agent uses the custom runtime; 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 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."
}
}
},
"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.",
"additionalProperties": false,
"properties": {
"previewEnabled": {
"type": "boolean",
"default": false,
"description": "Explicitly acknowledge the incomplete Firecracker control-plane preview. This does not enable workload execution."
},
"firecrackerBinary": {
"type": "string",
"description": "Absolute path to the Firecracker v1.16.1 binary. Defaults to /usr/local/bin/firecracker."
},
"jailerBinary": {
"type": "string",
"description": "Absolute path to the matching v1.16.1 jailer binary. Defaults to /usr/local/bin/jailer."
},
"kernelPath": {
"type": "string",
"description": "Absolute path to the trusted guest Linux kernel image."
},
"rootfsPath": {
"type": "string",
"description": "Absolute path to the trusted guest root filesystem image."
},
"vcpuCount": {
"type": "integer",
"minimum": 1,
"default": 2,
"description": "Number of guest virtual CPUs."
},
"memoryMib": {
"type": "integer",
"minimum": 1,
"default": 512,
"description": "Guest memory in MiB."
},
"apiTimeoutMs": {
"type": "integer",
"minimum": 1,
"default": 5000,
"description": "Bounded timeout in milliseconds for Firecracker API socket readiness and requests."
},
"sha256": {
"type": "object",
"description": "Optional pinned SHA-256 digests for trusted Firecracker artifacts.",
"additionalProperties": false,
"properties": {
"firecracker": {
"$ref": "#/$defs/sha256Digest"
},
"jailer": {
"$ref": "#/$defs/sha256Digest"
},
"kernel": {
"$ref": "#/$defs/sha256Digest"
},
"rootfs": {
"$ref": "#/$defs/sha256Digest"
}
}
}
}
},
Expand Down Expand Up @@ -1139,6 +1205,11 @@
}
},
"$defs": {
"sha256Digest": {
"type": "string",
"pattern": "^[A-Fa-f0-9]{64}$",
"description": "A SHA-256 digest encoded as exactly 64 hexadecimal characters."
},
"providerTarget": {
"type": "object",
"description": "API provider target override.",
Expand Down
75 changes: 73 additions & 2 deletions src/awf-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,75 @@
"type": "string",
"enum": [
"gvisor",
"sbx"
"sbx",
"firecracker"
],
"description": "Container runtime for the agent container. \"gvisor\" runs the agent under gVisor's runsc runtime (OCI runtime, compose-based). \"sbx\" runs the agent inside a Docker sbx microVM with hypervisor isolation; infrastructure containers (squid-proxy, api-proxy) stay in Docker Compose on the host and the sbx proxy chains upstream through AWF's Squid for domain filtering. Only the agent uses the custom runtime; 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 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."
}
}
},
"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.",
"additionalProperties": false,
"properties": {
"previewEnabled": {
"type": "boolean",
"default": false,
"description": "Explicitly acknowledge the incomplete Firecracker control-plane preview. This does not enable workload execution."
},
"firecrackerBinary": {
"type": "string",
"description": "Absolute path to the Firecracker v1.16.1 binary. Defaults to /usr/local/bin/firecracker."
},
"jailerBinary": {
"type": "string",
"description": "Absolute path to the matching v1.16.1 jailer binary. Defaults to /usr/local/bin/jailer."
},
"kernelPath": {
"type": "string",
"description": "Absolute path to the trusted guest Linux kernel image."
},
"rootfsPath": {
"type": "string",
"description": "Absolute path to the trusted guest root filesystem image."
},
"vcpuCount": {
"type": "integer",
"minimum": 1,
"default": 2,
"description": "Number of guest virtual CPUs."
},
"memoryMib": {
"type": "integer",
"minimum": 1,
"default": 512,
"description": "Guest memory in MiB."
},
"apiTimeoutMs": {
"type": "integer",
"minimum": 1,
"default": 5000,
"description": "Bounded timeout in milliseconds for Firecracker API socket readiness and requests."
},
"sha256": {
"type": "object",
"description": "Optional pinned SHA-256 digests for trusted Firecracker artifacts.",
"additionalProperties": false,
"properties": {
"firecracker": {
"$ref": "#/$defs/sha256Digest"
},
"jailer": {
"$ref": "#/$defs/sha256Digest"
},
"kernel": {
"$ref": "#/$defs/sha256Digest"
},
"rootfs": {
"$ref": "#/$defs/sha256Digest"
}
}
}
}
},
Expand Down Expand Up @@ -1139,6 +1205,11 @@
}
},
"$defs": {
"sha256Digest": {
"type": "string",
"pattern": "^[A-Fa-f0-9]{64}$",
"description": "A SHA-256 digest encoded as exactly 64 hexadecimal characters."
},
"providerTarget": {
"type": "object",
"description": "API provider target override.",
Expand Down
19 changes: 19 additions & 0 deletions src/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const optionGroupHeaders: Record<string, string> = {
'config': 'Configuration:',
'allow-domains': 'Domain Filtering:',
'build-local': 'Image Management:',
'firecracker-preview': 'Firecracker Preview:',
'env': 'Container Configuration:',
'dns-servers': 'Network & Security:',
'upstream-proxy': 'Network & Security:',
Expand Down Expand Up @@ -174,8 +175,26 @@ 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' +
' 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.',
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-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.')

// -- Container Configuration --
.option(
Expand Down
79 changes: 79 additions & 0 deletions src/commands/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import type { AwfFileConfig } from '../config-file';
import { resolveApiCredentials } from './resolve-credentials';
import { normalizeEnclavesConfig } from '../parsers/enclave-parser';
import { logger } from '../logger';
import {
FIRECRACKER_DEFAULT_API_TIMEOUT_MS,
FIRECRACKER_DEFAULT_BINARY,
FIRECRACKER_DEFAULT_JAILER_BINARY,
FIRECRACKER_DEFAULT_MEMORY_MIB,
FIRECRACKER_DEFAULT_VCPU_COUNT,
} from '../types/runtime-options';

/**
* Resolves the effective `legacySecurity` value from CLI options.
Expand Down Expand Up @@ -117,6 +124,7 @@ export function buildConfig(inputs: BuildConfigInputs): WrapperConfig {

const chrootIdentity = buildChrootIdentity(options);
const dind = buildDindConfig(options);
const firecracker = buildFirecrackerConfig(options);
const apiCredentials = resolveApiCredentials(options, {
resolvedCopilotApiTarget,
resolvedCopilotApiBasePath,
Expand Down Expand Up @@ -218,12 +226,83 @@ export function buildConfig(inputs: BuildConfigInputs): WrapperConfig {
chrootBinariesSourcePath: options.chrootBinariesSourcePath as string | undefined,
chrootIdentity,
dind,
firecracker,
enclaves: normalizeEnclavesConfig(
options.enclaves as AwfFileConfig['enclaves'] | undefined,
),
};
}

function buildFirecrackerConfig(
options: Record<string, unknown>,
): WrapperConfig['firecracker'] {
const selected = options.containerRuntime === 'firecracker';
const configured = options.firecrackerPreview === true
|| [
'firecrackerBinary',
'firecrackerJailerBinary',
'firecrackerKernel',
'firecrackerRootfs',
'firecrackerVcpus',
'firecrackerMemoryMib',
'firecrackerApiTimeoutMs',
'firecrackerBinarySha256',
'firecrackerJailerSha256',
'firecrackerKernelSha256',
'firecrackerRootfsSha256',
].some((key) => options[key] !== undefined);
if (!selected && !configured) return undefined;

const sha256 = {
firecracker: options.firecrackerBinarySha256 as string | undefined,
jailer: options.firecrackerJailerSha256 as string | undefined,
kernel: options.firecrackerKernelSha256 as string | undefined,
rootfs: options.firecrackerRootfsSha256 as string | undefined,
};

return {
previewEnabled: options.firecrackerPreview === true,
firecrackerBinary:
(options.firecrackerBinary as string | undefined) ?? FIRECRACKER_DEFAULT_BINARY,
jailerBinary:
(options.firecrackerJailerBinary as string | undefined) ??
FIRECRACKER_DEFAULT_JAILER_BINARY,
kernelPath: options.firecrackerKernel as string | undefined,
rootfsPath: options.firecrackerRootfs as string | undefined,
vcpuCount: parseFirecrackerPositiveInteger(
options.firecrackerVcpus,
'--firecracker-vcpus',
FIRECRACKER_DEFAULT_VCPU_COUNT,
),
memoryMib: parseFirecrackerPositiveInteger(
options.firecrackerMemoryMib,
'--firecracker-memory-mib',
FIRECRACKER_DEFAULT_MEMORY_MIB,
),
apiTimeoutMs: parseFirecrackerPositiveInteger(
options.firecrackerApiTimeoutMs,
'--firecracker-api-timeout-ms',
FIRECRACKER_DEFAULT_API_TIMEOUT_MS,
),
sha256: Object.values(sha256).some((value) => value !== undefined)
? sha256
: undefined,
};
}

function parseFirecrackerPositiveInteger(
value: unknown,
optionName: string,
defaultValue: number,
): number {
if (value === undefined) return defaultValue;
const parsed = typeof value === 'number' ? value : Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error(`${optionName} must be a positive integer`);
}
return parsed;
}

function buildChrootIdentity(
options: Record<string, unknown>
): WrapperConfig['chrootIdentity'] {
Expand Down
22 changes: 22 additions & 0 deletions src/commands/main-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,28 @@ describe('createMainAction', () => {
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});

describe('when external runtime preflight fails', () => {
it('aborts before entering the main workflow', async () => {
mockedExternalRuntimeResolver.resolveExternalRuntimeBackend.mockImplementationOnce(() => ({
runtime: 'sbx',
preflight: jest.fn().mockRejectedValue(new Error('preflight failed')),
start: jest.fn(),
exec: jest.fn(),
collectDiagnostics: jest.fn(),
stop: jest.fn(),
}));

const action = createMainAction(getOptionValueSource);
await expect(action(['echo hi'], {})).rejects.toThrow('process.exit: 1');

expect(mockedCliWorkflow.runMainWorkflow).not.toHaveBeenCalled();
expect(mockedLogger.error).toHaveBeenCalledWith(
'Fatal error:',
expect.objectContaining({ message: 'preflight failed' }),
);
});
});
});

describe('performCleanup with keepContainers=true', () => {
Expand Down
Loading
Loading