Fix container execution and add debug tooling

Container fixes:
- Run as non-root 'node' user (required for --dangerously-skip-permissions)
- Add allowDangerouslySkipPermissions: true to SDK options
- Mount .env file to work around Apple Container -i env var bug
- Use --mount for readonly, -v for read-write (Apple Container quirk)
- Bump SDK to 0.2.29, zod to v4
- Install Claude Code CLI globally in container

Logging improvements:
- Write per-run logs to groups/{folder}/logs/container-*.log
- Add debug-level logging for mounts and container args

Documentation:
- Add /debug skill with comprehensive troubleshooting guide
- Update /setup skill with API key configuration step
- Update SPEC.md with container details, mount syntax, security notes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gavriel
2026-02-01 10:35:08 +02:00
parent 0ccdaaac48
commit 67e0295d82
7 changed files with 436 additions and 27 deletions

View File

@@ -109,6 +109,20 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
readonly: false
});
// Environment file directory (workaround for Apple Container -i env var bug)
const envDir = path.join(DATA_DIR, 'env');
fs.mkdirSync(envDir, { recursive: true });
const envFile = path.join(projectRoot, '.env');
if (fs.existsSync(envFile)) {
// Copy .env to the env directory as a plain file called 'env'
fs.copyFileSync(envFile, path.join(envDir, 'env'));
mounts.push({
hostPath: envDir,
containerPath: '/workspace/env-dir',
readonly: true
});
}
// Additional mounts from group config
if (group.containerConfig?.additionalMounts) {
for (const mount of group.containerConfig.additionalMounts) {
@@ -136,9 +150,13 @@ function buildContainerArgs(mounts: VolumeMount[]): string[] {
const args: string[] = ['run', '-i', '--rm'];
// Add volume mounts
// Apple Container: use --mount for readonly, -v for read-write
for (const mount of mounts) {
const mode = mount.readonly ? ':ro' : '';
args.push('-v', `${mount.hostPath}:${mount.containerPath}${mode}`);
if (mount.readonly) {
args.push('--mount', `type=bind,source=${mount.hostPath},target=${mount.containerPath},readonly`);
} else {
args.push('-v', `${mount.hostPath}:${mount.containerPath}`);
}
}
// Add the image name
@@ -161,12 +179,23 @@ export async function runContainerAgent(
const mounts = buildVolumeMounts(group, input.isMain);
const containerArgs = buildContainerArgs(mounts);
// Log detailed mount info at debug level
logger.debug({
group: group.name,
mounts: mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`),
containerArgs: containerArgs.join(' ')
}, 'Container mount configuration');
logger.info({
group: group.name,
mountCount: mounts.length,
isMain: input.isMain
}, 'Spawning container agent');
// Create logs directory for this group
const logsDir = path.join(GROUPS_DIR, group.folder, 'logs');
fs.mkdirSync(logsDir, { recursive: true });
return new Promise((resolve) => {
const container = spawn('container', containerArgs, {
stdio: ['pipe', 'pipe', 'pipe']
@@ -207,12 +236,42 @@ export async function runContainerAgent(
clearTimeout(timeout);
const duration = Date.now() - startTime;
// Always write stderr to log file for debugging
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const logFile = path.join(logsDir, `container-${timestamp}.log`);
const logContent = [
`=== Container Run Log ===`,
`Timestamp: ${new Date().toISOString()}`,
`Group: ${group.name}`,
`IsMain: ${input.isMain}`,
`Duration: ${duration}ms`,
`Exit Code: ${code}`,
``,
`=== Input ===`,
JSON.stringify(input, null, 2),
``,
`=== Container Args ===`,
containerArgs.join(' '),
``,
`=== Mounts ===`,
mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'),
``,
`=== Stderr ===`,
stderr,
``,
`=== Stdout ===`,
stdout
].join('\n');
fs.writeFileSync(logFile, logContent);
logger.debug({ logFile }, 'Container log written');
if (code !== 0) {
logger.error({
group: group.name,
code,
duration,
stderr: stderr.slice(-500)
stderr: stderr.slice(-500),
logFile
}, 'Container exited with error');
resolve({