Merge pull request #1523 from qwibitai/apple-container-fixes

fix: Apple Container networking and .env mount
This commit is contained in:
gavrielc
2026-03-29 00:08:46 +03:00
committed by GitHub
3 changed files with 60 additions and 21 deletions
+2 -10
View File
@@ -77,16 +77,8 @@ function buildVolumeMounts(
readonly: true,
});
// Shadow .env so the agent cannot read secrets from the mounted project root.
// Credentials are injected by the OneCLI gateway, never exposed to containers.
const envFile = path.join(projectRoot, '.env');
if (fs.existsSync(envFile)) {
mounts.push({
hostPath: '/dev/null',
containerPath: '/workspace/project/.env',
readonly: true,
});
}
// .env shadowing is handled inside the container entrypoint via mount --bind
// (Apple Container only supports directory mounts, not file mounts like /dev/null)
// Main also gets its group folder as the working directory
mounts.push({
+9 -3
View File
@@ -51,8 +51,12 @@ describe('stopContainer', () => {
});
it('rejects names with shell metacharacters', () => {
expect(() => stopContainer('foo; rm -rf /')).toThrow('Invalid container name');
expect(() => stopContainer('foo$(whoami)')).toThrow('Invalid container name');
expect(() => stopContainer('foo; rm -rf /')).toThrow(
'Invalid container name',
);
expect(() => stopContainer('foo$(whoami)')).toThrow(
'Invalid container name',
);
expect(() => stopContainer('foo`id`')).toThrow('Invalid container name');
expect(mockExecSync).not.toHaveBeenCalled();
});
@@ -71,7 +75,9 @@ describe('ensureContainerRuntimeRunning', () => {
`${CONTAINER_RUNTIME_BIN} system status`,
{ stdio: 'pipe' },
);
expect(logger.debug).toHaveBeenCalledWith('Container runtime already running');
expect(logger.debug).toHaveBeenCalledWith(
'Container runtime already running',
);
});
it('auto-starts when system status fails', () => {
+49 -8
View File
@@ -3,6 +3,7 @@
* All runtime-specific logic lives here so swapping runtimes means changing one file.
*/
import { execSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import { logger } from './logger.js';
@@ -10,8 +11,32 @@ import { logger } from './logger.js';
/** The container runtime binary name. */
export const CONTAINER_RUNTIME_BIN = 'container';
/** Hostname containers use to reach the host machine. */
export const CONTAINER_HOST_GATEWAY = 'host.docker.internal';
/**
* IP address containers use to reach the host machine.
* Apple Container VMs use a bridge network (192.168.64.x); the host is at the gateway.
* Detected from the bridge0 interface, falling back to 192.168.64.1.
*/
export const CONTAINER_HOST_GATEWAY = detectHostGateway();
function detectHostGateway(): string {
// Apple Container on macOS: containers reach the host via the bridge network gateway
const ifaces = os.networkInterfaces();
const bridge = ifaces['bridge100'] || ifaces['bridge0'];
if (bridge) {
const ipv4 = bridge.find((a) => a.family === 'IPv4');
if (ipv4) return ipv4.address;
}
// Fallback: Apple Container's default gateway
return '192.168.64.1';
}
/**
* Address the credential proxy binds to.
* Binds to the bridge interface IP so only Apple Container VMs can reach it.
* Never 0.0.0.0 — that would expose credentials to the local network.
*/
export const PROXY_BIND_HOST =
process.env.CREDENTIAL_PROXY_HOST || CONTAINER_HOST_GATEWAY;
/** CLI args needed for the container to resolve the host gateway. */
export function hostGatewayArgs(): string[] {
@@ -23,8 +48,14 @@ export function hostGatewayArgs(): string[] {
}
/** Returns CLI args for a readonly bind mount. */
export function readonlyMountArgs(hostPath: string, containerPath: string): string[] {
return ['--mount', `type=bind,source=${hostPath},target=${containerPath},readonly`];
export function readonlyMountArgs(
hostPath: string,
containerPath: string,
): string[] {
return [
'--mount',
`type=bind,source=${hostPath},target=${containerPath},readonly`,
];
}
/** Stop a container by name. Uses execFileSync to avoid shell injection. */
@@ -43,7 +74,10 @@ export function ensureContainerRuntimeRunning(): void {
} catch {
logger.info('Starting container runtime...');
try {
execSync(`${CONTAINER_RUNTIME_BIN} system start`, { stdio: 'pipe', timeout: 30000 });
execSync(`${CONTAINER_RUNTIME_BIN} system start`, {
stdio: 'pipe',
timeout: 30000,
});
logger.info('Container runtime started');
} catch (err) {
logger.error({ err }, 'Failed to start container runtime');
@@ -83,9 +117,13 @@ export function cleanupOrphans(): void {
stdio: ['pipe', 'pipe', 'pipe'],
encoding: 'utf-8',
});
const containers: { status: string; configuration: { id: string } }[] = JSON.parse(output || '[]');
const containers: { status: string; configuration: { id: string } }[] =
JSON.parse(output || '[]');
const orphans = containers
.filter((c) => c.status === 'running' && c.configuration.id.startsWith('nanoclaw-'))
.filter(
(c) =>
c.status === 'running' && c.configuration.id.startsWith('nanoclaw-'),
)
.map((c) => c.configuration.id);
for (const name of orphans) {
try {
@@ -95,7 +133,10 @@ export function cleanupOrphans(): void {
}
}
if (orphans.length > 0) {
logger.info({ count: orphans.length, names: orphans }, 'Stopped orphaned containers');
logger.info(
{ count: orphans.length, names: orphans },
'Stopped orphaned containers',
);
}
} catch (err) {
logger.warn({ err }, 'Failed to clean up orphaned containers');