Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
748d299
feat: add Docker sbx microVM runtime support
lpcox Jul 10, 2026
d0bd85f
ci: add sbx CLI install step to smoke-docker-sbx workflow
lpcox Jul 10, 2026
1d34db3
fix: use 'sbx version' instead of 'sbx --version'
lpcox Jul 10, 2026
7858f16
ci: add sbx login step with Docker PAT authentication
lpcox Jul 10, 2026
3982181
fix: strip secret env vars from sbx CLI calls
lpcox Jul 10, 2026
ccc8d49
fix: address sbx runtime review feedback
Copilot Jul 10, 2026
5e9b35e
fix: pass full env to sbx create, sanitize only sbx exec
lpcox Jul 10, 2026
4c56494
fix: add sbx connectivity diagnostics before agent launch
lpcox Jul 10, 2026
497cbaf
fix: add timeouts and verbose logging to sbx daemon/create/exec
lpcox Jul 10, 2026
14bb291
fix: remove sbx daemon restart, pass proxy via env to sbx create
lpcox Jul 10, 2026
9e16e2f
fix: add auth/daemon diagnostics before sbx create
lpcox Jul 10, 2026
a022753
fix: background sbx daemon with nohup+disown for cross-step persistence
lpcox Jul 10, 2026
7e52f24
fix: replace nonexistent 'sbx auth status' with 'sbx ls' probe
lpcox Jul 10, 2026
aa1e146
fix: initialize sbx network policy before sandbox creation
lpcox Jul 10, 2026
33ea618
fix: use correct 'sbx policy init' (no --global flag)
lpcox Jul 10, 2026
e287ba3
fix: add sbx help output for policy/create syntax discovery
lpcox Jul 10, 2026
bd3aadd
fix: use 'sbx policy init allow-all' for network policy
lpcox Jul 10, 2026
156ab5f
fix: pipe 'y' to sbx create stdin for interactive confirmation
lpcox Jul 10, 2026
3732b7d
fix: use 'yes | sbx create' to bypass interactive TTY check
lpcox Jul 10, 2026
112ffa1
fix: add docker login for template image pulls in sbx create
lpcox Jul 10, 2026
1060418
fix: validate Docker Hub credentials before sbx auth
lpcox Jul 10, 2026
6bf9af6
fix: remove DOCKER_SANDBOXES_PROXY from sbx create env
lpcox Jul 10, 2026
ce1a74c
fix: use printf for sbx login, add sbx diagnose
lpcox Jul 10, 2026
3930afb
debug: sbx create --debug logging + pre-agent create test
lpcox Jul 10, 2026
26109bf
debug: use workspace dir for sbx create test, dump policy help
lpcox Jul 10, 2026
a301156
fix: convert Docker mount format to sbx positional format
lpcox Jul 10, 2026
e279034
fix: add fs mount policies and pre-pull template image
lpcox Jul 10, 2026
b75cb91
debug: dump policy profile/inspect/init help for mount policy
lpcox Jul 10, 2026
f387bcb
debug: inspect policy details, try --clone and bare create
lpcox Jul 10, 2026
7139e5d
debug: check KVM, try --clone and direct mount with --debug
lpcox Jul 10, 2026
4073c0f
debug: early exit after auth step to speed up iteration
lpcox Jul 10, 2026
0db1ab2
debug: explore mount policy config, daemon logs, sbx setup
lpcox Jul 10, 2026
bc214ce
debug: structured sbx mount policy diagnostics
lpcox Jul 10, 2026
b9d4964
fix: chmod /dev/kvm for sbx microVM access
lpcox Jul 11, 2026
9f0c636
fix: sbx create works! Remove early exit, add exec test
lpcox Jul 11, 2026
28586d5
debug: add credential state diagnostics + fix broken pipe exit code
lpcox Jul 11, 2026
59985be
fix: refresh sbx credentials immediately before AWF execution
lpcox Jul 11, 2026
a44a2fe
fix: remove XDG_CONFIG_HOME from sbx create env
lpcox Jul 11, 2026
6fc5bbc
debug: unset XDG_CONFIG_HOME in bash + find credential files
lpcox Jul 11, 2026
e3035fe
fix: mount /tmp and /usr/local/bin into sbx, deduplicate mounts, clea…
lpcox Jul 11, 2026
22487ed
fix: mount $HOME into sbx for agent writable dirs (.cache, etc.)
lpcox Jul 11, 2026
6e3e5ef
diag: probe sbx network routes and gateway to find host access
lpcox Jul 11, 2026
06c386b
diag: test DNS resolution and raw IP connectivity from sbx
lpcox Jul 11, 2026
4563e12
fix(sbx): route proxy through sbx gateway IP + publish api-proxy ports
lpcox Jul 11, 2026
8672c49
diag: log sbx env vars and test api-proxy port reachability
lpcox Jul 11, 2026
a627e70
fix(sbx): merge credential isolation env vars for api-proxy routing
lpcox Jul 11, 2026
c4abd5f
fix(sbx): wait for api-proxy health + dump logs on failure
lpcox Jul 11, 2026
8ade07d
fix(sbx): attach api-proxy to awf-ext network for port publishing
lpcox Jul 11, 2026
63bf1cf
debug(sbx): add port reachability diagnostics from sbx VM
lpcox Jul 11, 2026
514be0c
fix(sbx): use host.docker.internal for api-proxy from microVM
lpcox Jul 11, 2026
05616ca
fix(sbx): route MCP gateway through host.docker.internal
lpcox Jul 11, 2026
820605e
fix(sbx): use host.docker.internal for MCP CLI wrappers
lpcox Jul 11, 2026
708f6aa
Merge branch 'main' into feat/docker-sbx-runtime
lpcox Jul 11, 2026
86e32a1
fix: avoid shell-based sbx create command injection path
Copilot Jul 11, 2026
004e260
fix: avoid clear-text logging of sensitive env vars (CodeQL)
lpcox Jul 11, 2026
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
98 changes: 89 additions & 9 deletions .github/workflows/smoke-docker-sbx.lock.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/awf-config-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ 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, e.g. `"gvisor"`; AWF translates to the Docker OCI runtime identifier, e.g. `"runsc"`. Only the agent container uses the custom runtime; infrastructure containers always use `runc`. When set, AWF injects `extra_hosts` entries for compose-internal services to work around DNS issues with non-default runtimes. Requires the runtime to be installed and registered with Docker on the 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.)*
- `chroot.binariesSourcePath` → *(config-only; overlays a runner-side binaries directory at `/usr/local/bin` inside chroot mode)*
- `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
4 changes: 2 additions & 2 deletions docs/awf-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,8 @@
},
"containerRuntime": {
"type": "string",
"enum": ["gvisor"],
"description": "Container runtime for the agent container. Set to \"gvisor\" to run the agent under gVisor's runsc runtime for additional sandboxing. Only the agent container uses the custom runtime; infrastructure containers (squid-proxy, api-proxy) always use the default runc runtime. When set, AWF automatically injects extra_hosts entries for compose-internal services to work around DNS resolution issues with non-default runtimes. Requires gVisor (runsc) to be installed and registered with Docker on the host."
"enum": ["gvisor", "sbx"],
"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."
}
}
},
Expand Down
28 changes: 22 additions & 6 deletions scripts/ci/postprocess-smoke-workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ for (const workflowPath of codexWorkflowPaths) {
}
}

// ── gVisor workflow: inject --container-runtime gvisor into the AWF command ───
// ── Runtime workflow patching: inject --container-runtime into AWF commands ───
// The compiler doesn't support sandbox.agent.containerRuntime yet, so we inject it here.
const runtimeCmdPattern = /awf --config /g;

const gvisorLockPath = path.join(workflowsDir, 'smoke-gvisor.lock.yml');
try {
let gvisorContent = fs.readFileSync(gvisorLockPath, 'utf-8');
// Insert --container-runtime gvisor before --config on the awf command line.
// The compiler doesn't support sandbox.agent.containerRuntime yet, so we inject it here.
const awfCmdPattern = /awf --config /g;
const replacedContent = gvisorContent.replace(awfCmdPattern, 'awf --container-runtime gvisor --config ');
const gvisorContent = fs.readFileSync(gvisorLockPath, 'utf-8');
const replacedContent = gvisorContent.replace(runtimeCmdPattern, 'awf --container-runtime gvisor --config ');
if (replacedContent !== gvisorContent) {
fs.writeFileSync(gvisorLockPath, replacedContent);
console.log(` Injected --container-runtime gvisor into AWF command`);
Expand All @@ -93,3 +93,19 @@ try {
} catch {
console.log(`Skipping ${gvisorLockPath}: file not found.`);
}

const sbxLockPath = path.join(workflowsDir, 'smoke-docker-sbx.lock.yml');
try {
const sbxContent = fs.readFileSync(sbxLockPath, 'utf-8');
runtimeCmdPattern.lastIndex = 0;
const sbxReplacedContent = sbxContent.replace(runtimeCmdPattern, 'awf --container-runtime sbx --config ');
if (sbxReplacedContent !== sbxContent) {
fs.writeFileSync(sbxLockPath, sbxReplacedContent);
console.log(` Injected --container-runtime sbx into AWF command`);
console.log(`Updated ${sbxLockPath}`);
} else {
console.log(`Skipping ${sbxLockPath}: no AWF command found to patch.`);
}
} catch {
console.log(`Skipping ${sbxLockPath}: file not found.`);
}
4 changes: 2 additions & 2 deletions src/awf-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,8 @@
},
"containerRuntime": {
"type": "string",
"enum": ["gvisor"],
"description": "Container runtime for the agent container. Set to \"gvisor\" to run the agent under gVisor's runsc runtime for additional sandboxing. Only the agent container uses the custom runtime; infrastructure containers (squid-proxy, api-proxy) always use the default runc runtime. When set, AWF automatically injects extra_hosts entries for compose-internal services to work around DNS resolution issues with non-default runtimes. Requires gVisor (runsc) to be installed and registered with Docker on the host."
"enum": ["gvisor", "sbx"],
"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."
}
}
},
Expand Down
7 changes: 4 additions & 3 deletions src/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,10 @@ program
)
.option(
'--container-runtime <runtime>',
'Container runtime for the agent container (e.g. "gvisor" for gVisor sandboxing).\n' +
' AWF translates friendly names to Docker runtime identifiers\n' +
' (gvisor → runsc). Unknown values are passed through as-is.'
'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' +
' Unknown values are passed through as raw Docker runtime names.'
)

// -- Container Configuration --
Expand Down
46 changes: 46 additions & 0 deletions src/commands/main-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jest.mock('../dind-bootstrap');
jest.mock('./preflight');
jest.mock('./signal-handler');
jest.mock('./validate-options');
jest.mock('../sbx-manager');

import { logger } from '../logger';
import * as dockerManager from '../docker-manager';
Expand All @@ -45,6 +46,7 @@ import * as dindBootstrap from '../dind-bootstrap';
import * as preflight from './preflight';
import * as signalHandler from './signal-handler';
import * as validateOptions from './validate-options';
import * as sbxManager from '../sbx-manager';

const mockedLogger = logger as jest.Mocked<typeof logger>;
const mockedDockerManager = dockerManager as jest.Mocked<typeof dockerManager>;
Expand All @@ -57,6 +59,7 @@ const mockedDindBootstrap = dindBootstrap as jest.Mocked<typeof dindBootstrap>;
const mockedPreflight = preflight as jest.Mocked<typeof preflight>;
const mockedSignalHandler = signalHandler as jest.Mocked<typeof signalHandler>;
const mockedValidateOptions = validateOptions as jest.Mocked<typeof validateOptions>;
const mockedSbxManager = sbxManager as jest.Mocked<typeof sbxManager>;

/** Minimal WrapperConfig returned by the validateOptions mock. */
const STUB_CONFIG = {
Expand Down Expand Up @@ -108,6 +111,10 @@ describe('createMainAction', () => {
mockedDindBootstrap.runDindBootstrap.mockResolvedValue(undefined);
mockedSignalHandler.registerSignalHandlers.mockImplementation(() => {});
mockedCliWorkflow.runMainWorkflow.mockResolvedValue(0);
mockedSbxManager.isSbxAvailable.mockResolvedValue(true);
mockedSbxManager.createSandbox.mockResolvedValue('awf-agent-test');
mockedSbxManager.execInSandbox.mockResolvedValue({ exitCode: 0 });
mockedSbxManager.removeSandbox.mockResolvedValue(undefined);
});

afterEach(() => {
Expand Down Expand Up @@ -292,6 +299,45 @@ describe('createMainAction', () => {
await action(['curl https://example.com'], {});
expect(processExitSpy).toHaveBeenCalledWith(42);
});

describe('sbx runtime wiring', () => {
it('passes configured mounts/workdir/environment into sbx create/exec', async () => {
const sbxConfig = {
...STUB_CONFIG,
containerRuntime: 'sbx',
containerWorkDir: '/home/runner/work/repo/repo',
volumeMounts: ['/tmp/tooling:/tmp/tooling:ro'],
enableApiProxy: true,
tty: true,
} as unknown as import('../types').WrapperConfig;
mockedValidateOptions.validateOptions.mockReturnValue(sbxConfig);
mockedCliWorkflow.runMainWorkflow.mockImplementation(async (_config, deps, _callbacks) => {
await deps.startContainers('/tmp/awf-test', ['github.com']);
const result = await deps.runAgentCommand('/tmp/awf-test', ['github.com'], undefined, 10);
return result.exitCode;
});

const action = createMainAction(getOptionValueSource);
await action(['echo hi'], {});

expect(mockedSbxManager.createSandbox).toHaveBeenCalledWith(expect.objectContaining({
extraMounts: ['/tmp/tooling:/tmp/tooling:ro'],
}));
expect(mockedSbxManager.execInSandbox).toHaveBeenCalledWith(
'awf-agent-test',
'echo hi',
expect.objectContaining({
timeoutMinutes: 10,
workDir: '/home/runner/work/repo/repo',
tty: true,
environment: expect.objectContaining({
HTTPS_PROXY: expect.any(String),
SQUID_PROXY_HOST: expect.any(String),
}),
}),
);
});
});
});

describe('when runMainWorkflow throws', () => {
Expand Down
165 changes: 163 additions & 2 deletions src/commands/main-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,19 @@ import { validateOptions } from './validate-options';
import { probeSplitFilesystem } from '../dind-probe';
import { assertTopologySupported, connectTopologyContainers } from '../topology';
import { runDindBootstrap } from '../dind-bootstrap';
import { runtimeUsesComposeAgent } from '../container-runtime';
import { createSandbox, execInSandbox, removeSandbox, isSbxAvailable, SBX_DEFAULT_NAME } from '../sbx-manager';
import type { WrapperConfig } from '../types';
import { buildAgentEnvironment } from '../services/agent-service';
import { buildAgentCredentialEnv } from '../services/api-proxy-credential-env';
import { DEFAULT_DNS_SERVERS } from '../dns-resolver';
import { AGENT_IP, CLI_PROXY_IP, DOH_PROXY_IP, SQUID_IP } from '../host-iptables-shared';

/** Report whether a secret is set (and its length) without exposing the value. */
function redactSecret(value: string | undefined): string {
if (!value) return '(unset)';
return `(set, len=${value.length})`;
}

const SENSITIVE_CONFIG_KEYS = new Set([
'openaiApiKey',
Expand Down Expand Up @@ -89,6 +101,15 @@ function buildCleanupFn(
logger.info(`Received ${signal}, cleaning up...`);
}

// Clean up sbx sandbox if using microVM runtime
if (!runtimeUsesComposeAgent(config.containerRuntime) && !config.keepContainers) {
try {
await removeSandbox(SBX_DEFAULT_NAME);
} catch {
// Sandbox may not exist yet — that's fine
}
}

// Copy iptables audit BEFORE stopping containers (volumes are destroyed by `docker compose down -v`)
if (getContainersStarted()) {
preserveIptablesAudit(config.workDir, config.auditDir);
Expand Down Expand Up @@ -231,14 +252,154 @@ export function createMainAction(getOptionValueSource: OptionSourceResolver) {
});

try {
// For sbx (microVM) runtime, wrap startContainers and runAgentCommand
// to launch the agent in a sandbox instead of Docker Compose.
const useSbx = !runtimeUsesComposeAgent(config.containerRuntime);
let sbxName: string | undefined;
let sbxEnvironment: Record<string, string> | undefined;

const sbxStartContainers = useSbx
? async (workDir: string, allowedDomains: string[], proxyLogsDir?: string, skipPull?: boolean, onNetworkReady?: () => Promise<void>) => {
// Start infra-only compose (squid, api-proxy — no agent service)
await startContainers(workDir, allowedDomains, proxyLogsDir, skipPull, onNetworkReady);

// Verify sbx is available
if (!await isSbxAvailable()) {
throw new Error('Docker sbx CLI not found. Install sbx to use --container-runtime sbx.');
}

// For sbx, the microVM can't reach Docker internal IPs (172.30.0.x).
// Published Squid port (3128) is accessible via the sbx gateway IP.
// The api-proxy is on the awf-ext bridge network and reachable from
// inside the sbx via `host.docker.internal` (resolves to the docker0
// bridge IP, typically 172.17.0.1).
const SBX_GATEWAY_IP = '172.17.0.0';
const SBX_HOST_DOCKER_INTERNAL = 'host.docker.internal';

sbxEnvironment = buildAgentEnvironment({
config,
networkConfig: {
subnet: '172.30.0.0/24',
squidIp: SBX_GATEWAY_IP,
agentIp: AGENT_IP,
proxyIp: config.enableApiProxy ? SBX_HOST_DOCKER_INTERNAL : undefined,
dohProxyIp: config.dnsOverHttps ? DOH_PROXY_IP : undefined,
cliProxyIp: config.difcProxyHost ? CLI_PROXY_IP : undefined,
},
dnsServers: config.dnsServers || DEFAULT_DNS_SERVERS,
});

// Merge credential isolation env vars (COPILOT_API_URL, COPILOT_PROVIDER_BASE_URL, etc.)
// In Docker mode these are merged by assembleOptionalServices during compose generation.
// For sbx, we call buildAgentCredentialEnv directly with host.docker.internal
// as the proxy target (the api-proxy is on the awf-ext bridge network).
if (config.enableApiProxy) {
const credentialEnv = buildAgentCredentialEnv({
config,
networkConfig: {
subnet: '172.30.0.0/24',
squidIp: SBX_GATEWAY_IP,
agentIp: AGENT_IP,
proxyIp: SBX_HOST_DOCKER_INTERNAL,
},
});
Object.assign(sbxEnvironment, credentialEnv);
}

// Log critical env vars for debugging auth flow (redact secret values)
logger.info(`[sbx-env] COPILOT_API_URL=${sbxEnvironment.COPILOT_API_URL || '(unset)'}`);
logger.info(`[sbx-env] COPILOT_PROVIDER_BASE_URL=${sbxEnvironment.COPILOT_PROVIDER_BASE_URL || '(unset)'}`);
logger.info(`[sbx-env] COPILOT_GITHUB_TOKEN=${redactSecret(sbxEnvironment.COPILOT_GITHUB_TOKEN)}`);
logger.info(`[sbx-env] COPILOT_API_KEY=${redactSecret(sbxEnvironment.COPILOT_API_KEY)}`);
logger.info(`[sbx-env] HTTPS_PROXY=${sbxEnvironment.HTTPS_PROXY || '(unset)'}`);
logger.info(`[sbx-env] COPILOT_PROVIDER_API_KEY=${redactSecret(sbxEnvironment.COPILOT_PROVIDER_API_KEY)}`);

// Create the sandbox with configured mounts, proxy chaining through Squid
const workspaceDir = process.env.GITHUB_WORKSPACE || process.cwd();
sbxName = await createSandbox({
workspaceDir,
squidIp: SQUID_IP,
extraMounts: config.volumeMounts,
});

// Wait for api-proxy to be healthy before launching agent.
// In Docker mode, depends_on: service_healthy gates this; for sbx we poll
// via host.docker.internal which resolves to the docker0 bridge from the VM.
if (config.enableApiProxy) {
logger.info('[sbx] Polling api-proxy health via host.docker.internal...');
const healthCmd = [
'for i in $(seq 1 30); do',
` if curl -sf --max-time 2 http://${SBX_HOST_DOCKER_INTERNAL}:10000/health >/dev/null 2>&1; then`,
' echo "api-proxy healthy after ${i}s"; exit 0;',
' fi;',
' sleep 1;',
'done;',
'echo "api-proxy health timeout"; exit 1',
].join(' ');

const healthResult = await execInSandbox(sbxName, healthCmd, {
timeoutMinutes: 1,
workDir: config.containerWorkDir,
environment: sbxEnvironment,
});
if (healthResult.exitCode !== 0) {
logger.warn('[sbx] api-proxy health check failed — proceeding anyway');
}
}

// Verify squid proxy is reachable from sandbox
logger.info('[sbx-diag] Verifying squid proxy connectivity...');
const diagCmd = [
`echo -n "squid ${SBX_GATEWAY_IP}:3128 → "`,
`curl -sS --max-time 5 --proxy "http://${SBX_GATEWAY_IP}:3128" -o /dev/null -w "%{http_code}" https://api.github.com/ 2>&1`,
'echo ""',
].join(' && ');

const diagResult = await execInSandbox(sbxName, diagCmd, {
timeoutMinutes: 1,
workDir: config.containerWorkDir,
environment: sbxEnvironment,
});
logger.info(`[sbx-diag] Connectivity check exited with code ${diagResult.exitCode}`);
}
: startContainers;

const sbxRunAgentCommand = useSbx
? async (_workDir: string, _allowedDomains: string[], _proxyLogsDir?: string, agentTimeoutMinutes?: number) => {
if (!sbxName) throw new Error('Sandbox not created');
logger.info(`[sbx] Launching agent command in sandbox "${sbxName}" (timeout: ${agentTimeoutMinutes ?? 'none'} min)`);
logger.debug(`[sbx] Agent command: ${config.agentCommand.substring(0, 200)}...`);
const result = await execInSandbox(sbxName, config.agentCommand, {
timeoutMinutes: agentTimeoutMinutes,
workDir: config.containerWorkDir,
environment: sbxEnvironment,
tty: config.tty,
});
logger.info(`[sbx] Agent command exited with code ${result.exitCode}`);

// Dump api-proxy logs for debugging connection issues
if (config.enableApiProxy && result.exitCode !== 0) {
try {
const { execSync } = await import('child_process');
const proxyLogs = execSync('docker logs --tail 80 awf-api-proxy 2>&1', { encoding: 'utf-8', timeout: 10000 });
logger.info(`[sbx-diag] api-proxy logs:\n${proxyLogs}`);
const healthStatus = execSync('docker inspect --format={{.State.Health.Status}} awf-api-proxy 2>&1', { encoding: 'utf-8', timeout: 5000 });
logger.info(`[sbx-diag] api-proxy health status: ${healthStatus.trim()}`);
} catch { /* ignore diagnostic failures */ }
}

return { exitCode: result.exitCode, blockedDomains: [] as string[] };
}
: runAgentCommand;

exitCode = await runMainWorkflow(
config,
{
ensureFirewallNetwork,
setupHostIptables,
writeConfigs,
startContainers,
runAgentCommand,
startContainers: sbxStartContainers,
runAgentCommand: sbxRunAgentCommand,
collectDiagnosticLogs,
assertTopologySupported,
connectTopologyContainers,
Expand Down
Loading
Loading