TASK-pilot-brief-sandbox: fail closed read-only convergence

This commit is contained in:
thePR0M3TH3AN committed 2026-10-03 01:47:13 -04:00
1 parent 0665bb305b
commit 7e0734ae90
1 file changed
+78 -1
+78 -1
View File
@@ -6,6 +6,10 @@ import { readInstallManifest } from '../kernel/install.mjs';
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
const GUARD_TYPES = Object.freeze(['check', 'measurement', 'pin']);
const READ_ONLY_GUARD_COLUMNS = Object.freeze([
'id', 'project_id', 'area_id', 'guard_type', 'reason', 'created_at', 'released_at',
]);
const READ_ONLY_GUARD_INDEX = 'one_open_guard_per_type';
function git(root, args, { allowFailure = false } = {}) {
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
@@ -33,6 +37,61 @@ export function initializeWorktreeGuards(database) {
`);
}
function unavailableGuardSchema(details = null) {
throw new TorchError('Convergence guard coverage cannot be established', {
code: 'CONVERGENCE_GUARDS_UNAVAILABLE', details,
});
}
function verifyReadableGuardSchema(database) {
let columns;
let indexes;
let indexColumns;
let indexSql;
try {
columns = database.prepare('PRAGMA table_info(worktree_guards)').all();
indexes = database.prepare('PRAGMA index_list(worktree_guards)').all();
indexColumns = database.prepare(`PRAGMA index_info(${READ_ONLY_GUARD_INDEX})`).all();
indexSql = database.prepare(`
SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?
`).get(READ_ONLY_GUARD_INDEX)?.sql ?? null;
} catch (error) {
unavailableGuardSchema({ cause: error.message });
}
const actualColumns = columns.map((column) => column.name);
const expectedColumns = [...READ_ONLY_GUARD_COLUMNS];
const index = indexes.find((candidate) => candidate.name === READ_ONLY_GUARD_INDEX);
const actualIndexColumns = indexColumns.map((column) => column.name);
const normalizedIndexSql = typeof indexSql === 'string'
? indexSql.replaceAll(/\s+/g, ' ').trim().toLowerCase()
: null;
const requiredIndexSql = 'create unique index one_open_guard_per_type on worktree_guards(area_id, guard_type) where released_at is null';
if (
actualColumns.length !== expectedColumns.length
|| actualColumns.some((column, index) => column !== expectedColumns[index])
|| !index
|| Number(index.unique) !== 1
|| Number(index.partial) !== 1
|| actualIndexColumns.length !== 2
|| actualIndexColumns[0] !== 'area_id'
|| actualIndexColumns[1] !== 'guard_type'
|| normalizedIndexSql !== requiredIndexSql
) {
unavailableGuardSchema({
expectedColumns,
actualColumns,
requiredIndex: READ_ONLY_GUARD_INDEX,
actualIndexes: indexes.map((candidate) => ({
name: candidate.name, unique: candidate.unique, partial: candidate.partial,
})),
actualIndexColumns,
indexSql,
});
}
}
export function beginWorktreeGuard(controlPlane, {
areaId, type, reason, clock = () => new Date(), idFactory = randomUUID,
} = {}) {
@@ -89,6 +148,7 @@ export class ConvergenceService {
this.controlPlane = controlPlane;
this.clock = clock;
this.idFactory = idFactory;
this.readOnly = controlPlane?.readOnly === true;
const { config } = loadFleetDefinition(repositoryRoot);
this.targetBranch = config.project.main_branch;
this.policy = config.synchronization ?? {};
@@ -96,11 +156,24 @@ export class ConvergenceService {
this.worktrees = new Map((manifest.external ?? [])
.filter((entry) => entry.type === 'worktree')
.map((entry) => [entry.area, entry]));
initializeWorktreeGuards(controlPlane.database);
if (!this.readOnly) initializeWorktreeGuards(controlPlane.database);
}
assertWritable() {
if (this.readOnly) {
throw new TorchError('Read-only convergence instances cannot mutate guards or worktrees', {
code: 'CONVERGENCE_READ_ONLY',
});
}
}
verifyGuardSchema() {
verifyReadableGuardSchema(this.controlPlane.database);
}
guards(areaId) {
const area = this.controlPlane.assertIdentity(areaId);
this.verifyGuardSchema();
return this.controlPlane.database.prepare(`
SELECT id, area_id AS areaId, guard_type AS type, reason, created_at AS createdAt
FROM worktree_guards WHERE area_id = ? AND released_at IS NULL ORDER BY created_at, id
@@ -108,17 +181,20 @@ export class ConvergenceService {
}
hold({ areaId, type, reason } = {}) {
this.assertWritable();
return beginWorktreeGuard(this.controlPlane, {
areaId, type, reason, clock: this.clock, idFactory: this.idFactory,
});
}
release({ areaId, type } = {}) {
this.assertWritable();
return endWorktreeGuard(this.controlPlane, { areaId, type, clock: this.clock });
}
plan({ areaId } = {}) {
const area = this.controlPlane.assertIdentity(areaId);
this.verifyGuardSchema();
const worktree = this.worktrees.get(area);
if (!worktree) throw new TorchError(`No managed worktree for ${area}`, { code: 'WORKTREE_MISSING' });
const blockers = [];
@@ -145,6 +221,7 @@ export class ConvergenceService {
}
converge({ areaId } = {}) {
this.assertWritable();
const plan = this.plan({ areaId });
if (!plan.canProceed) throw new TorchError('Worktree convergence is blocked', { code: 'CONVERGENCE_BLOCKED', details: plan.blockers });
if (plan.upToDate) return { ...plan, changed: false };