Drop stale jail and agent migration paths (Codex)

Remove completed controlplane agent-id migration, simplify jail-name resolution to current canonical names, and drop SUDO_UID ownership fallback from service setup.

---
Build: pass | Tests: pass — 2370 passed (704 files)
This commit is contained in:
Operator & Codex 2026-05-10 21:24:47 +02:00
parent c136418c5a
commit f1dc7ea6df
16 changed files with 37 additions and 236 deletions

View file

@ -198,11 +198,7 @@ export async function run(args: string[]): Promise<void> {
}
const { role, def } = entry;
const jailName = resolveJailName({
agentName: TENANT_ID,
role,
legacyNames: [role],
});
const jailName = resolveJailName({ role });
const ip = resolveJailIp(registry, role);
const hostname = `${role}.${internalDomain}`;

View file

@ -147,7 +147,6 @@ describe('resolveJailName()', () => {
it('returns envOverride immediately without calling bastille', () => {
const result = resolveJailName({
agentName: 'clawdie',
role: 'db',
envOverride: 'custom-db-jail',
});
@ -155,39 +154,23 @@ describe('resolveJailName()', () => {
expect(spawnSyncMock).not.toHaveBeenCalled();
});
it('returns preferred name when it exists', () => {
// preferred = agentName (strip [-_]) + '_' + role (hyphens→_) = "clawdie_db"
const list = BASTILLE_LIST_RUNNING + '\n 3 clawdie_db on 99 Up thick 10.0.0.100 - 15.0-RELEASE';
it('returns the role name when it exists', () => {
const list = BASTILLE_LIST_RUNNING + '\n 3 db on 99 Up thick 10.0.0.100 - 15.0-RELEASE';
spawnSyncMock.mockReturnValue(makeSpawnResult(list, 0));
const result = resolveJailName({ agentName: 'clawdie', role: 'db' });
expect(result).toBe('clawdie_db');
const result = resolveJailName({ role: 'db' });
expect(result).toBe('db');
});
it('falls back to legacy hyphen name when preferred does not exist', () => {
// preferred = "clawdie_db" not found, legacyHyphen = "clawdie-db" exists
const list = BASTILLE_LIST_RUNNING + '\n 3 clawdie-db on 99 Up thick 10.0.0.100 - 15.0-RELEASE';
spawnSyncMock.mockReturnValue(makeSpawnResult(list, 0));
const result = resolveJailName({ agentName: 'clawdie', role: 'db' });
expect(result).toBe('clawdie-db');
});
it('returns preferred name when nothing is found (default)', () => {
it('returns the role name when nothing is found', () => {
spawnSyncMock.mockReturnValue(makeSpawnResult(BASTILLE_LIST_RUNNING, 0));
const result = resolveJailName({ agentName: 'clawdie', role: 'newrole' });
expect(result).toBe('clawdie_newrole');
const result = resolveJailName({ role: 'newrole' });
expect(result).toBe('newrole');
});
it('strips hyphens and underscores from agentName for preferred', () => {
spawnSyncMock.mockReturnValue(makeSpawnResult('', 0));
const result = resolveJailName({ agentName: 'my-agent_bot', role: 'db' });
// safeAgentName = 'myagentbot', separated by _
expect(result).toBe('myagentbot_db');
});
it('converts hyphens in role to underscores for preferred', () => {
spawnSyncMock.mockReturnValue(makeSpawnResult('', 0));
const result = resolveJailName({ agentName: 'clawdie', role: 'db-worker' });
// safeRole = 'db_worker' (hyphens converted to _ for readability)
expect(result).toBe('clawdie_db_worker');
it('uses explicit candidate names when supplied', () => {
const list = BASTILLE_LIST_RUNNING + '\n 3 cms on 99 Up thick 10.0.0.100 - 15.0-RELEASE';
spawnSyncMock.mockReturnValue(makeSpawnResult(list, 0));
const result = resolveJailName({ role: 'web', names: ['web', 'cms'] });
expect(result).toBe('cms');
});
});

View file

@ -74,29 +74,19 @@ export function jailRoot(jailName: string): string {
}
export interface ResolveJailNameOptions {
agentName: string;
role: string;
legacyNames?: string[];
names?: string[];
envOverride?: string;
}
export function resolveJailName(opts: ResolveJailNameOptions): string {
if (opts.envOverride) return opts.envOverride;
const safeAgentName = opts.agentName.replace(/[-_]/g, '');
const safeRole = opts.role.replace(/-/g, '_');
const preferred = `${safeAgentName}_${safeRole}`;
const legacyHyphen = `${opts.agentName}-${opts.role}`;
const candidates = [
preferred,
legacyHyphen,
...(opts.legacyNames || [opts.role]),
];
const candidates = opts.names && opts.names.length > 0 ? opts.names : [opts.role];
for (const candidate of candidates) {
if (jailExists(candidate)) return candidate;
}
return preferred;
return candidates[0];
}
export interface ProvisionOpts {
@ -201,11 +191,7 @@ export async function provisionJail(
opts: ProvisionOpts,
): Promise<ProvisionResult> {
const safeAgentName = opts.agentName.replace(/[-_]/g, '');
const jailName = resolveJailName({
agentName: opts.agentName,
role,
legacyNames: [role],
});
const jailName = resolveJailName({ role });
const registry = loadJailRegistry();
const ip = resolveJailIp(registry, role);

View file

@ -916,25 +916,14 @@ export async function run(_args: string[]): Promise<void> {
process.exit(1);
}
const safeAgentName = TENANT_ID.replace(/[-_]/g, '');
const defaultJailName = 'cms';
const preferredJailName = `${safeAgentName}cms`;
const legacyHyphenName = `${TENANT_ID}-cms`;
const astroSitePathReal = jailPathNoHomeSymlink(CMS_DOCS_SITE_PATH);
const landingSitePathReal = jailPathNoHomeSymlink(PLATFORM_LANDING_SITE_PATH);
const landingPublicDomain = publicRootDomain();
const landingEnabled = landingPublicDomain.length > 0;
let jailName = explicitJailName;
if (!jailName) {
if (jailExists(defaultJailName)) {
jailName = defaultJailName;
} else if (jailExists(preferredJailName)) {
jailName = preferredJailName;
} else if (jailExists(legacyHyphenName)) {
jailName = legacyHyphenName;
} else {
jailName = defaultJailName;
}
jailName = defaultJailName;
}
const runBastille = (args: string[]) => bastille(...args);

View file

@ -4,8 +4,8 @@
* Supports two modes:
* - `DB_RUNTIME=host` (default): provision PostgreSQL directly on the host;
* jails connect via warden0 at `${AGENT_SUBNET_BASE}.1:5432`
* - `DB_RUNTIME=jail`: provision a Bastille db jail (legacy/optional
* pulls a fat jail and duplicates pkg upgrade paths)
* - `DB_RUNTIME=jail`: provision PostgreSQL in a Bastille db jail when an
* operator intentionally wants database isolation from the host service)
*/
import { execSync, spawnSync } from 'child_process';
import fs from 'fs';
@ -563,7 +563,6 @@ export async function run(_args: string[]): Promise<void> {
''
).trim();
const jailName = resolveJailName({
agentName: TENANT_ID,
role: 'db',
envOverride: explicitJailName || undefined,
});

View file

@ -7,7 +7,7 @@
*/
import { execSync, spawnSync } from 'child_process';
import { AGENT_INTERNAL_DOMAIN, SUBNET_BASE, TENANT_ID } from '../src/config.js';
import { AGENT_INTERNAL_DOMAIN, SUBNET_BASE } from '../src/config.js';
import { logger } from '../src/logger.js';
import { loadPackageList, mountPkgCacheInJail } from './packages.js';
import { commandExists, getPlatform, isRoot } from './platform.js';
@ -59,16 +59,7 @@ export async function run(args: string[]): Promise<void> {
return;
}
const safeAgentName = TENANT_ID.replace(/[-_]/g, '');
const preferredJailName = `${safeAgentName}worker`;
const legacyHyphenName = `${TENANT_ID}-worker`;
const legacyPlainName = 'worker';
let workerJail = preferredJailName;
if (jailExists(legacyHyphenName) && !jailExists(preferredJailName)) {
workerJail = legacyHyphenName;
} else if (jailExists(legacyPlainName) && !jailExists(preferredJailName)) {
workerJail = legacyPlainName;
}
const workerJail = 'worker';
const workerIp =
process.env.WORKER_JAIL_IP_START ||
process.env.WORKER_JAIL_IP ||

View file

@ -55,14 +55,7 @@ export async function run(_args: string[]): Promise<void> {
}
try {
const preferredJailName = `${RUNTIME_ID}-llamacpp`;
const legacyJailName = 'llamacpp';
let jailName = explicitJailName || preferredJailName;
if (!explicitJailName) {
if (jailExists(legacyJailName) && !jailExists(preferredJailName)) {
jailName = legacyJailName;
}
}
const jailName = explicitJailName || `${RUNTIME_ID}-llamacpp`;
const exists = jailExists(jailName);

View file

@ -55,14 +55,7 @@ export async function run(_args: string[]): Promise<void> {
}
try {
const preferredJailName = `${RUNTIME_ID}-ollama`;
const legacyJailName = 'ollama';
let jailName = explicitJailName || preferredJailName;
if (!explicitJailName) {
if (jailExists(legacyJailName) && !jailExists(preferredJailName)) {
jailName = legacyJailName;
}
}
const jailName = explicitJailName || `${RUNTIME_ID}-ollama`;
const exists = jailExists(jailName);

View file

@ -471,11 +471,6 @@ function resolveCmsJailName(projectRoot: string): string {
.map((line) => line.trim())
.filter(Boolean);
if (names.includes('cms')) return 'cms';
const safeAgentName = TENANT_ID.replace(/[-_]/g, '');
const preferred = `${safeAgentName}cms`;
const legacyHyphen = `${TENANT_ID}-cms`;
if (names.includes(preferred)) return preferred;
if (names.includes(legacyHyphen)) return legacyHyphen;
} catch {
// Default to cms when jail discovery is unavailable.
}

View file

@ -71,16 +71,6 @@ function buildProject(projectRoot: string): void {
});
}
function chownIfSudoContext(filePath: string): void {
const sudo = getSudoContext();
if (!isRoot() || sudo.uid === null || sudo.gid === null) return;
try {
fs.chownSync(filePath, sudo.uid, sudo.gid);
} catch {
// best-effort only
}
}
// Recursively chown a directory to the agent user.
// Used after root creates runtime dirs so the agent process (which runs as the
// named user via daemon -u) can write to them without EACCES on first startup.
@ -397,8 +387,6 @@ export async function run(_args: string[]): Promise<void> {
fs.mkdirSync(dirPath, { recursive: true });
chownRuntimeDir(dirPath, runtime.runtimeUser);
}
// Legacy: also chown via SUDO_UID/SUDO_GID for the logs file itself (best-effort)
chownIfSudoContext(path.join(projectRoot, 'logs'));
const pidFile = path.join(projectRoot, `${runtime.serviceName}.pid`);
const logPath = path.join(projectRoot, 'logs', `${runtime.serviceName}.log`);
@ -415,7 +403,6 @@ export async function run(_args: string[]): Promise<void> {
runScriptPath,
generateRunScript(runtime, nodePath, projectRoot),
);
chownIfSudoContext(runScriptPath);
logger.info({ runScriptPath }, 'Wrote run wrapper');
writeWrapper(rcdPath, generateRcdService(runtime, projectRoot, logPath));

View file

@ -14,7 +14,6 @@ import {
SKILLS_DB_NAME,
SKILLS_DB_URL,
SKILLS_DB_USER,
TENANT_ID,
} from '../src/config.js';
import {
BUILTIN_KNOWLEDGE_ARTIFACT_SQL as ARTIFACT_SQL,
@ -175,15 +174,7 @@ function detectDbJailName(): string {
).trim();
if (explicit) return explicit;
const safeAgentName = TENANT_ID.replace(/[-_]/g, '');
const preferred = `${safeAgentName}db`;
const legacyHyphen = `${TENANT_ID}-db`;
const legacy = 'db';
if (jailExists(preferred)) return preferred;
if (jailExists(legacyHyphen)) return legacyHyphen;
if (jailExists(legacy)) return legacy;
return preferred;
return 'db';
}
function importArtifactViaBastille(jailName: string, dbName: string): void {

View file

@ -322,11 +322,7 @@ export async function run(_args: string[]): Promise<void> {
continue;
}
const jailName = resolveJailName({
agentName: TENANT_ID,
role: entry.role,
legacyNames: [entry.role],
});
const jailName = resolveJailName({ role: entry.role });
logger.info({ specialist, jailName }, 'Verifying agent jail');
const result = verifyJail(specialist, jailName);

View file

@ -357,11 +357,7 @@ export async function run(_args: string[]): Promise<void> {
// 7. Check host/jail package baseline and CMS runtime
const rsyncTool = commandExists('rsync') ? 'available' : 'missing';
const safeAgentName = TENANT_ID.replace(/[-_]/g, '');
const preferredWorkerJailName = `${safeAgentName}worker`;
const legacyWorkerJailName = `${TENANT_ID}-worker`;
const legacyPlainWorkerJailName = 'worker';
let workerJailName = preferredWorkerJailName;
let workerJailName = 'worker';
let workerJailPackages = 'unknown';
if (platform === 'freebsd') {
try {
@ -371,26 +367,7 @@ export async function run(_args: string[]): Promise<void> {
});
const jailList = jails.split('\n').map((line) => line.trim());
if (/^(YES|yes|true|TRUE|1)$/u.test(cmsEnabled)) {
const safeAgentName = TENANT_ID.replace(/[-_]/g, '');
const preferred = `${safeAgentName}cms`;
const legacyHyphen = `${TENANT_ID}-cms`;
const legacy = 'cms';
if (!jailList.some((line) => line === cmsJailName)) {
if (jailList.some((line) => line === preferred)) {
cmsJailName = preferred;
} else if (jailList.some((line) => line === legacyHyphen)) {
cmsJailName = legacyHyphen;
} else if (jailList.some((line) => line === legacy)) {
cmsJailName = legacy;
}
}
}
if (jailList.some((line) => line === preferredWorkerJailName)) {
workerJailName = preferredWorkerJailName;
} else if (jailList.some((line) => line === legacyWorkerJailName)) {
workerJailName = legacyWorkerJailName;
} else if (jailList.some((line) => line === legacyPlainWorkerJailName)) {
workerJailName = legacyPlainWorkerJailName;
cmsJailName = 'cms';
}
if (jailList.some((line) => line === workerJailName)) {

View file

@ -22,10 +22,10 @@ describe('checkAgentTaskCapability', () => {
expect(result.ok).toBe(true);
});
it('normalizes prefixed jail names', () => {
expect(__TEST_ONLY.normalizeJailName('alpha_git_worker')).toBe('git-worker');
expect(__TEST_ONLY.normalizeJailName('alpha_db_worker')).toBe('db-worker');
expect(__TEST_ONLY.normalizeJailName('alpha_ctrl_worker')).toBe('ctrl-worker');
it('maps tenant worker jail names to capability keys', () => {
expect(__TEST_ONLY.capabilityKeyForJail('alpha_git_worker')).toBe('git-worker');
expect(__TEST_ONLY.capabilityKeyForJail('alpha_db_worker')).toBe('db-worker');
expect(__TEST_ONLY.capabilityKeyForJail('alpha_ctrl_worker')).toBe('ctrl-worker');
});
it('refuses git-worker for git-push-upstream', () => {

View file

@ -18,9 +18,7 @@ export interface CapabilityCheck {
const OK: CapabilityCheck = { ok: true, missing: [], errorCode: null };
// Runtime jail names are prefixed (for example "clawdie_git_worker"). Normalize
// them to stable role keys before capability lookup.
function normalizeJailName(jailName: string): string {
function capabilityKeyForJail(jailName: string): string {
if (jailName.endsWith('_git_worker')) return 'git-worker';
if (jailName.endsWith('_db_worker')) return 'db-worker';
if (jailName.endsWith('_ctrl_worker')) return 'ctrl-worker';
@ -46,19 +44,19 @@ export function checkAgentTaskCapability(
if (!jailName || !skillName) return OK;
const required = SKILL_REQUIRES[skillName];
if (!required || required.length === 0) return OK;
const normalizedJail = normalizeJailName(jailName);
const granted = JAIL_CAPABILITIES[normalizedJail] ?? [];
const capabilityKey = capabilityKeyForJail(jailName);
const granted = JAIL_CAPABILITIES[capabilityKey] ?? [];
const missing = required.filter((capability) => !granted.includes(capability));
if (missing.length === 0) return OK;
return {
ok: false,
missing: [...missing],
errorCode: `task_requires_${[...missing].sort().join('_')}_jail_${normalizedJail}_lacks`,
errorCode: `task_requires_${[...missing].sort().join('_')}_jail_${capabilityKey}_lacks`,
};
}
export const __TEST_ONLY = {
JAIL_CAPABILITIES,
SKILL_REQUIRES,
normalizeJailName,
capabilityKeyForJail,
};

View file

@ -231,7 +231,6 @@ CREATE TABLE IF NOT EXISTS chat_spend (
export async function runSchemaMigration(pool: pg.Pool): Promise<void> {
await pool.query(CONTROLPLANE_SCHEMA_SQL);
await migrateLegacyAgentIds(pool);
await ensureRoleConstraint(pool);
await ensureApprovalDecisionColumn(pool);
await ensureParentTaskId(pool);
@ -256,78 +255,6 @@ export async function runSchemaMigration(pool: pg.Pool): Promise<void> {
}
}
async function migrateLegacyAgentIds(pool: pg.Pool): Promise<void> {
// Aggressive dev policy: canonical agent IDs are:
// sysadmin, db-admin, git-admin, coordinator, plus orchestrator = TENANT_ID.
// Migrate old *_agent IDs in-place once (idempotent).
const legacyToCanonical: Array<[string, string]> = [
['sysadmin_agent', 'sysadmin'],
['db_admin_agent', 'db-admin'],
['git_admin_agent', 'git-admin'],
];
const legacyIds = legacyToCanonical.map(([legacy]) => legacy);
const { rows } = await pool.query(
`SELECT id FROM agents WHERE id = ANY($1)`,
[legacyIds],
);
if (rows.length === 0) return;
await pool.query('BEGIN');
try {
await pool.query(
'ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_role_check',
);
for (const table of [
'tasks',
'agent_activity',
'agent_budgets',
'approvals',
]) {
await pool.query(
`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_${table === 'tasks' ? 'assigned_to' : 'agent_id'}_fkey`,
);
}
for (const [legacy, canonical] of legacyToCanonical) {
await pool.query(
'UPDATE tasks SET assigned_to = $1 WHERE assigned_to = $2',
[canonical, legacy],
);
await pool.query(
'UPDATE agent_activity SET agent_id = $1 WHERE agent_id = $2',
[canonical, legacy],
);
await pool.query(
'UPDATE agent_budgets SET agent_id = $1 WHERE agent_id = $2',
[canonical, legacy],
);
await pool.query(
'UPDATE approvals SET agent_id = $1 WHERE agent_id = $2',
[canonical, legacy],
);
const canonicalExists = await pool.query(
'SELECT 1 FROM agents WHERE id = $1 LIMIT 1',
[canonical],
);
if (canonicalExists.rows.length > 0) {
await pool.query('DELETE FROM agents WHERE id = $1', [legacy]);
} else {
await pool.query('UPDATE agents SET id = $1, role = $1 WHERE id = $2', [
canonical,
legacy,
]);
}
}
await pool.query('COMMIT');
} catch (err) {
await pool.query('ROLLBACK');
throw err;
}
}
async function ensureRoleConstraint(pool: pg.Pool): Promise<void> {
await pool.query(
'ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_role_check',