fix: create jail-exec dir with correct permissions at provision time

The controlplane writes prompts to /var/tmp/jail-exec/ inside each
worker jail. Previously this directory didn't exist and the jail root
had no traverse permission for the agent user, causing EACCES.

Now ensureJailExecDir() is called during:
- provisionJail() in bastille-helpers.ts (all jails)
- agent-jails.ts after pi install (worker jails)
- verify-agent-jails.ts checks writability

The function creates the dir, sets o+rx on the path components,
and chowns to the agent user. Runs as root during setup.

For clawdie-iso: the ISO runs the Clawdie-AI installer, so this fix
covers both repos with no changes needed in clawdie-iso.

---
Build: pass | Tests: 1533 passed, 5 failed (unchanged)
This commit is contained in:
Mevy Assistant 2026-04-16 15:47:30 +00:00
parent 615d61db1d
commit f78acb9123
3 changed files with 50 additions and 0 deletions

View file

@ -23,6 +23,7 @@ import { commandExists, getPlatform, isRoot } from './platform.js';
import { emitStatus } from './status.js';
import {
bastille,
ensureJailExecDir,
jailExists,
detectFreeBSDRelease,
jailRoot,
@ -231,6 +232,9 @@ export async function run(args: string[]): Promise<void> {
ensurePiInstalled(jailName);
// Ensure jail-exec staging dir is writable by the agent user
ensureJailExecDir(jailName, AGENT_NAME);
const chsh = bastille(
'cmd',
jailName,

View file

@ -1,4 +1,5 @@
import { execSync, spawnSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
@ -88,6 +89,35 @@ export interface ProvisionResult {
created: boolean;
}
/**
* Ensure the jail-exec staging directory exists inside a jail and is writable
* by the agent user. Called during provisioning so the controlplane can write
* prompts into the jail at runtime.
*
* The directory is: /usr/local/bastille/jails/<name>/root/var/tmp/jail-exec
* On the host, this must be readable/writable by the agent user (e.g. mevy).
* Inside the jail, it appears at /var/tmp/jail-exec.
*/
export function ensureJailExecDir(jailName: string, agentUser: string): void {
const jailExecHostPath = `/usr/local/bastille/jails/${jailName}/root/var/tmp/jail-exec`;
fs.mkdirSync(jailExecHostPath, { recursive: true });
// Ensure the agent user can traverse into the jail root and write to jail-exec.
// This requires root (setup runs as root).
try {
execSync(`chmod o+rx /usr/local/bastille/jails/${jailName}/root`, { stdio: 'ignore' });
execSync(`chmod o+rx /usr/local/bastille/jails/${jailName}/root/var`, { stdio: 'ignore' });
execSync(`chmod o+rx /usr/local/bastille/jails/${jailName}/root/var/tmp`, { stdio: 'ignore' });
} catch {
// May fail if paths already have correct perms — non-fatal
}
try {
execSync(`chown -R ${agentUser}:${agentUser} ${jailExecHostPath}`, { stdio: 'ignore' });
} catch {
// Non-fatal — agent user may not exist yet in the jail
}
logger.info({ jailName, path: jailExecHostPath }, 'jail-exec dir ready');
}
export async function provisionJail(
jailDef: JailDefinition,
role: string,
@ -175,5 +205,9 @@ export async function provisionJail(
const runBastille = (args: string[]) => bastille(...args);
maybeEnableTailscaleInJail(runBastille, jailName, jailName);
// Ensure jail-exec staging dir is writable by the agent user
const agentUser = opts.agentName;
ensureJailExecDir(jailName, agentUser);
return { jailName, ip, created: !exists };
}

View file

@ -165,6 +165,18 @@ export function verifyJail(
}
result.envFileExists = true;
// 2b. jail-exec staging dir writable by agent user
const jailExecPath = path.join(jailRoot(jailName), 'root', 'var', 'tmp', 'jail-exec');
if (!fs.existsSync(jailExecPath)) {
result.notes.push(`jail-exec dir not found at ${jailExecPath} — run: sudo npx tsx setup/agent-jails.ts`);
} else {
try {
fs.accessSync(jailExecPath, fs.constants.W_OK);
} catch {
result.notes.push(`jail-exec dir not writable by current user: ${jailExecPath}`);
}
}
// 3. Parse keys
const content = fs.readFileSync(envPath, 'utf-8');
const presentKeys = parseEnvKeys(content);