mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-12 18:11:51 +08:00
Merge pull request #1523 from qwibitai/apple-container-fixes
fix: Apple Container networking and .env mount
This commit is contained in:
+2
-10
@@ -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({
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user