Harden safe fleet purge

This commit is contained in:
thePR0M3TH3AN committed 2026-09-27 19:28:43 -04:00
1 parent f2c4e478c2
commit 35056507c8
10 files changed
+151 -34

No files matched your search

+3 -1
View File
@@ -29,7 +29,9 @@ and the first Claude reference-fleet path:
- read-only health diagnostics covering installation integrity, runtime
availability, worktree drift count and age, Git operations, and honest
recoverability grade;
- conservative, reversible purge;
- conservative, explicitly confirmed purge that validates the exact local
state path and distinguishes owned history from live sessions, locks,
leases, guards, landing, persistent launchers, and unknown files;
- reviewed worktree plans with branch/path collision protection;
- isolated branches and worktrees with locally ignored task markers;
- purge protection for dirty and uniquely committed domain work.
+10
View File
@@ -411,6 +411,16 @@ No uninstall mode may delete or remove a worktree containing:
Uninstall MUST stop before destructive cleanup and report exact remediation.
Implementation checkpoint (2026-09-27): CLI purge requires both `--purge` and
explicit `--yes` after a dry-run review. Reversal validates the manifest's
machine-local state path against the project ID and current XDG environment,
allows TORCH-owned historical database and evidence files, and fails closed on
unknown state entries, active runtime identities, local locks, resource leases,
worktree guards, in-flight landing, unsafe worktrees, unique canonical history,
or installed persistent launchers. Historical messages and receipts therefore
do not make reversal permanently impossible, while active or unowned state
still blocks deletion.
## 10. Project analysis and bootstrap
### 10.1 Repository reconnaissance
@@ -9,7 +9,7 @@ test_integrity_note:
- id: SCN-install-doctor-purge
given: a reviewed fresh installation into a Git repository
when: TORCH installs, diagnoses, and purges its unchanged state
then: doctor reports healthy, all owned TORCH state is removed, and project source remains unchanged
then: active runtime state blocks purge, historical durable state remains safely reversible after shutdown, all owned TORCH state is removed, and project source remains unchanged
- id: SCN-existing-repository-preservation
given: an existing repository with history, a long-lived branch, owner configuration, and an untracked file
when: TORCH is installed and safely purged
@@ -254,6 +254,7 @@ test_integrity_note:
- measured and estimated context groups, cache-read ratio, and cost per verified item
- schedule plan blockers, subprocess shell flag, retry count, and durable failure message
- preservation of project source and modified owner configuration
- explicit purge approval, exact XDG project-state path, and active versus historical local-state classification
- SQLite state after process restart
- CLI and MCP output over real process boundaries
- runtime command plans and execution order
@@ -290,6 +291,8 @@ test_integrity_note:
- doctor that reports healthy without required state
- doctor hiding convergence age, an active Git operation, a missing runtime, or one-disk-only recovery
- uninstall that deletes owner-modified configuration
- purge without explicit confirmation or through a redirected local-state path
- historical messages making safe reversal permanently impossible
- CLI that mutates without explicit approval
- in-memory-only messaging that disappears on restart
- runtime session IDs masquerading as durable Fleet identities
+7 -1
View File
@@ -141,7 +141,7 @@ Usage:
torch fleet activate --change <id> --by <owner> --yes [--json]
torch fleet start --change <id> [--fresh] [--dry-run] --yes [--json]
torch doctor [--json]
torch uninstall [--dry-run] [--purge] [--json]
torch uninstall [--dry-run] [--purge --yes] [--json]
`;
function print(value, { json = false } = {}) {
@@ -1149,10 +1149,16 @@ export async function runCli(argv = process.argv.slice(2), {
}
}
if (command === 'uninstall') {
if (argv.includes('--purge') && !argv.includes('--dry-run') && !argv.includes('--yes')) {
throw new TorchError('Purging TORCH removes tracked configuration, managed worktrees, and local state. Review --dry-run, then re-run with --purge --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
const result = uninstallProject({
repository,
purge: argv.includes('--purge'),
dryRun: argv.includes('--dry-run'),
env,
});
print(result, { json });
return result.canProceed === false ? 1 : 0;
+82 -19
View File
@@ -1,7 +1,8 @@
import { randomUUID } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { DatabaseSync } from 'node:sqlite';
import {
existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync,
existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync,
} from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { TorchError } from './errors.mjs';
@@ -268,15 +269,74 @@ export function installProject({
}
}
function nonEmptyEntries(path, ignoredRoots = []) {
if (!existsSync(path)) return [];
return readdirSync(path, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => resolve(entry.parentPath, entry.name))
.filter((entry) => entry !== join(resolve(path), 'project.json'))
.filter((entry) => !ignoredRoots.some((root) =>
entry === resolve(root) || entry.startsWith(`${resolve(root)}${sep}`)))
.map((entry) => relative(path, entry));
function inspectLocalState(statePath, manifest, env, problems) {
const expected = projectStatePath(manifest.projectId, env);
if (resolve(statePath) !== resolve(expected)) {
problems.push({ type: 'unsafe-local-state-path', recorded: statePath, expected });
return;
}
try {
if (!lstatSync(statePath).isDirectory() || realpathSync(statePath) !== resolve(statePath)) {
problems.push({ type: 'unsafe-local-state-node', path: statePath });
return;
}
} catch (error) {
problems.push({ type: 'local-state-unreadable', path: statePath, message: error.message });
return;
}
const allowedFiles = new Set(['project.json', 'state.db', 'state.db-shm', 'state.db-wal']);
const allowedDirectories = new Set([
'sessions', 'messages', 'locks', 'leases', 'checks', 'integration', 'logs', 'remote.git',
]);
const unexpected = readdirSync(statePath, { withFileTypes: true })
.filter((entry) => entry.isDirectory()
? !allowedDirectories.has(entry.name) : !allowedFiles.has(entry.name))
.map((entry) => entry.name);
if (unexpected.length) problems.push({ type: 'unknown-local-state', path: statePath, entries: unexpected });
const locksPath = join(statePath, 'locks');
const locks = existsSync(locksPath) ? readdirSync(locksPath) : [];
if (locks.length) problems.push({ type: 'active-local-locks', path: locksPath, entries: locks });
const databasePath = join(statePath, 'state.db');
if (!existsSync(databasePath)) return;
let database;
try {
database = new DatabaseSync(databasePath, { readOnly: true });
const hasTable = (name) => Boolean(database.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
).get(name));
if (hasTable('identities')) {
const active = database.prepare(`
SELECT area_id AS areaId, state, runtime_session_id AS runtimeSessionId
FROM identities WHERE state != 'offline' ORDER BY area_id
`).all();
if (active.length) problems.push({ type: 'active-runtime-sessions', sessions: active });
}
if (hasTable('resource_leases')) {
const leases = database.prepare(`
SELECT resource_id AS resourceId, area_id AS areaId, expires_at AS expiresAt
FROM resource_leases WHERE released_at IS NULL ORDER BY resource_id, acquired_at
`).all();
if (leases.length) problems.push({ type: 'active-resource-leases', leases });
}
if (hasTable('worktree_guards')) {
const guards = database.prepare(`
SELECT area_id AS areaId, guard_type AS guardType, reason
FROM worktree_guards WHERE released_at IS NULL ORDER BY created_at, id
`).all();
if (guards.length) problems.push({ type: 'active-worktree-guards', guards });
}
if (hasTable('integration_requests')) {
const landing = database.prepare(`
SELECT id, source_area AS sourceArea, source_commit AS sourceCommit
FROM integration_requests WHERE state = 'landing' ORDER BY created_at, id
`).all();
if (landing.length) problems.push({ type: 'integration-landing-active', requests: landing });
}
} catch (error) {
problems.push({ type: 'local-state-database-invalid', path: databasePath, message: error.message });
} finally {
database?.close();
}
}
function gitOptional(root, args) {
@@ -310,7 +370,7 @@ function inspectCanonicalRemoval(repositoryRoot, entry, localStatePath) {
return problems;
}
export function planUninstall({ repository, purge = false }) {
export function planUninstall({ repository, purge = false, env = process.env }) {
const manifest = readInstallManifest(repository.root);
const problems = [];
let mainBranch = repository.branch || 'main';
@@ -319,12 +379,19 @@ export function planUninstall({ repository, purge = false }) {
mainBranch = config.project?.main_branch ?? mainBranch;
} catch { /* created-file validation below reports the invalid config */ }
const localState = (manifest.external ?? []).find((entry) => entry.type === 'local-state');
if (localState && existsSync(join(localState.path, 'project.json'))) {
if (localState && !existsSync(join(localState.path, 'project.json'))) {
problems.push({ type: 'local-state-metadata-missing', path: join(localState.path, 'project.json') });
} else if (localState) {
try {
const metadata = JSON.parse(readFileSync(join(localState.path, 'project.json'), 'utf8'));
if (resolve(metadata.root) !== resolve(repository.root)) {
problems.push({ type: 'not-installation-root', expected: metadata.root, actual: repository.root });
}
if (metadata.projectId !== manifest.projectId) {
problems.push({
type: 'local-state-project-mismatch', expected: manifest.projectId, actual: metadata.projectId ?? null,
});
}
} catch {
problems.push({ type: 'local-state-metadata-invalid', path: localState.path });
}
@@ -336,11 +403,7 @@ export function planUninstall({ repository, purge = false }) {
}
for (const entry of manifest.external ?? []) {
if (entry.type === 'local-state') {
const canonicalRoots = (manifest.external ?? [])
.filter((candidate) => candidate.type === 'canonical-remote')
.map((candidate) => candidate.path);
const extras = nonEmptyEntries(entry.path, canonicalRoots);
if (extras.length) problems.push({ type: 'local-state-not-empty', path: entry.path, entries: extras });
if (existsSync(entry.path)) inspectLocalState(entry.path, manifest, env, problems);
}
if (entry.type === 'canonical-remote') {
if (!localState?.path) problems.push({ type: 'local-state-missing-for-canonical', path: entry.path });
@@ -361,8 +424,8 @@ export function planUninstall({ repository, purge = false }) {
};
}
export function uninstallProject({ repository, purge = false, dryRun = false }) {
const plan = planUninstall({ repository, purge });
export function uninstallProject({ repository, purge = false, dryRun = false, env = process.env }) {
const plan = planUninstall({ repository, purge, env });
if (dryRun || !purge) return plan;
if (!plan.canProceed) {
throw new TorchError('TORCH purge stopped because managed state changed', {
+1 -1
View File
@@ -149,7 +149,7 @@ test('SCN-bootstrap-acceptance: one installed fixture proves the complete organi
assert.deepEqual(resume.actions.map((action) => action.runtimeSessionId), started.started.map((entry) => entry.runtimeSessionId));
control.close();
const reversal = planUninstall({ repository: inspectRepository(context.root), purge: true });
const reversal = planUninstall({ repository: inspectRepository(context.root), purge: true, env: context.env });
assert.equal(reversal.mutationPerformed, false);
assert.equal(reversal.manifest.external.filter((entry) => entry.type === 'worktree').length, roster.agents.length);
assert.equal(reversal.problems.some((problem) => problem.type === 'unsafe-worktree'), true,
+3
View File
@@ -155,6 +155,9 @@ test('SCN-cli-lifecycle-surface: runtime selection, selective startup, capture,
const doctor = run(root, ['doctor', '--json']);
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
assert.equal(JSON.parse(doctor.stdout).findings.some((finding) => finding.code === 'FLEET_DETACHED'), true);
const unapprovedPurge = run(root, ['uninstall', '--purge', '--json']);
assert.equal(unapprovedPurge.status, 2);
assert.equal(JSON.parse(unapprovedPurge.stdout).error, 'APPROVAL_REQUIRED');
});
test('SCN-cli-control-plane: CLI messages, acknowledgements, ownership, and status share durable state', () => {
+36 -6
View File
@@ -15,6 +15,7 @@ import {
} from '../../src/kernel/install.mjs';
import { proposeDomains, validateApprovedProposal } from '../../src/kernel/domains.mjs';
import { createLocalCanonical } from '../../src/canonical/local.mjs';
import { openControlPlane } from '../../src/control-plane/service.mjs';
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-kernel-'));
@@ -72,7 +73,16 @@ test('SCN-install-doctor-purge: a fresh install is healthy and exactly reversibl
assert.equal(diagnosis.healthy, true);
assert.equal(diagnosis.status, 'ok');
const result = uninstallProject({ repository: inspectRepository(root), purge: true });
const control = openControlPlane({ repositoryRoot: root, env });
control.sendMessage({ sender: 'session-manager', recipient: proposal.domains[0].id, body: 'Historical state remains reversible.' });
control.reportStatus({ areaId: proposal.domains[0].id, state: 'working', summary: 'Still active.' });
control.close();
const activePlan = planUninstall({ repository: inspectRepository(root), purge: true, env });
assert.equal(activePlan.problems.some((problem) => problem.type === 'active-runtime-sessions'), true);
const reopened = openControlPlane({ repositoryRoot: root, env });
reopened.reportStatus({ areaId: proposal.domains[0].id, state: 'offline', summary: 'Stopped.' });
reopened.close();
const result = uninstallProject({ repository: inspectRepository(root), purge: true, env });
assert.equal(result.mutationPerformed, true);
assert.equal(existsSync(join(root, '.torch')), false);
assert.equal(existsSync(installed.stateRoot), false);
@@ -93,7 +103,7 @@ test('SCN-existing-repository-preservation: install and purge preserve history,
installProject({
repository, proposal: approvedProposal(repository), env: externalEnv, projectId: 'existing-repository',
});
const purged = uninstallProject({ repository: inspectRepository(root), purge: true });
const purged = uninstallProject({ repository: inspectRepository(root), purge: true, env: externalEnv });
assert.equal(purged.mutationPerformed, true);
assert.equal(readFileSync(join(root, '.projectrc'), 'utf8'), 'owner-setting=true\n');
assert.equal(readFileSync(join(root, 'owner-notes.txt'), 'utf8'), 'untracked owner data\n');
@@ -109,13 +119,33 @@ test('SCN-purge-protects-user-change: uninstall refuses to delete modified manag
writeFileSync(configPath, `${readFileSync(configPath, 'utf8')}\n# owner change\n`);
assert.throws(
() => uninstallProject({ repository: inspectRepository(root), purge: true }),
() => uninstallProject({ repository: inspectRepository(root), purge: true, env }),
(error) => error.code === 'UNSAFE_TO_PURGE'
&& error.details.some((problem) => problem.type === 'modified' && problem.path === '.torch/torch.yaml'),
);
assert.equal(existsSync(configPath), true);
});
test('SCN-purge-protects-local-identity: uninstall refuses mismatched machine-local metadata', () => {
const { root, env } = fixture();
const repository = inspectRepository(root);
const installed = installProject({
repository, proposal: approvedProposal(repository), env, projectId: 'expected-project',
});
const metadataPath = join(installed.stateRoot, 'project.json');
const metadata = JSON.parse(readFileSync(metadataPath, 'utf8'));
writeFileSync(metadataPath, `${JSON.stringify({ ...metadata, projectId: 'different-project' }, null, 2)}\n`);
const plan = planUninstall({ repository: inspectRepository(root), purge: true, env });
assert.equal(plan.canProceed, false);
assert.equal(plan.problems.some((problem) => problem.type === 'local-state-project-mismatch'), true);
assert.throws(
() => uninstallProject({ repository: inspectRepository(root), purge: true, env }),
(error) => error.code === 'UNSAFE_TO_PURGE',
);
assert.equal(existsSync(installed.stateRoot), true);
});
test('SCN-domain-approval: installation rejects stale or unapproved organizational proposals', () => {
const { root, env } = fixture();
const repository = inspectRepository(root);
@@ -180,15 +210,15 @@ test('SCN-canonical-reversal: owned local canonical state reverses only after un
], { encoding: 'utf8' }).trim();
execFileSync('git', ['--git-dir', canonical.path, 'update-ref', 'refs/heads/remote-only', uniqueCommit]);
repository = inspectRepository(root);
assert.equal(planUninstall({ repository, purge: true }).problems.some((problem) =>
assert.equal(planUninstall({ repository, purge: true, env: externalEnv }).problems.some((problem) =>
problem.type === 'canonical-unique-commit' && problem.commit === uniqueCommit), true);
assert.throws(
() => uninstallProject({ repository, purge: true }),
() => uninstallProject({ repository, purge: true, env: externalEnv }),
(error) => error.code === 'UNSAFE_TO_PURGE',
);
execFileSync('git', ['--git-dir', canonical.path, 'update-ref', '-d', 'refs/heads/remote-only']);
const purged = uninstallProject({ repository: inspectRepository(root), purge: true });
const purged = uninstallProject({ repository: inspectRepository(root), purge: true, env: externalEnv });
assert.equal(purged.mutationPerformed, true);
assert.equal(existsSync(installed.stateRoot), false);
assert.equal(execFileSync('git', ['-C', root, 'remote'], { encoding: 'utf8' }).includes('torch-canonical'), false);
+1 -1
View File
@@ -133,7 +133,7 @@ test('SCN-system-schedule-launcher: exact-config user units install, dispatch, a
['--user', 'daemon-reload'], ['--user', 'enable'],
]);
assert.equal(launcher.status().stale, false);
assert.equal(planUninstall({ repository: inspectRepository(context.root), purge: true }).problems
assert.equal(planUninstall({ repository: inspectRepository(context.root), purge: true, env: context.env }).problems
.some((problem) => problem.type === 'persistent-integration-installed'), true);
const originalConfig = readFileSync(join(context.root, '.torch', 'torch.yaml'), 'utf8');
+4 -4
View File
@@ -75,7 +75,7 @@ test('SCN-worktree-bootstrap: approved identities receive isolated branches and
finding.code === 'RECOVERABILITY' && finding.level === 'ONE-DISK'), true);
assert.equal(diagnosis.findings.some((finding) => finding.code.startsWith('RUNTIME_')), true);
unlinkSync(mergeHead);
uninstallProject({ repository: inspectRepository(fixture.root), purge: true });
uninstallProject({ repository: inspectRepository(fixture.root), purge: true, env: fixture.env });
assert.equal(created.created.some((action) => existsSync(action.path)), false);
assert.equal(readFileSync(join(fixture.root, '.git', 'info', 'exclude'), 'utf8').includes('.task'), false);
});
@@ -86,19 +86,19 @@ test('SCN-worktree-purge-safety: modified or uniquely committed domain work bloc
const core = created.created.find((entry) => entry.area === 'core');
writeFileSync(join(core.path, 'domain-work.txt'), 'not safe to delete\n');
const plan = planUninstall({ repository: inspectRepository(fixture.root), purge: true });
const plan = planUninstall({ repository: inspectRepository(fixture.root), purge: true, env: fixture.env });
assert.equal(plan.canProceed, false);
assert.equal(plan.problems.some((problem) =>
problem.type === 'unsafe-worktree' && problem.area === 'core'
&& problem.problems.includes('worktree-dirty')), true);
execFileSync('git', ['-C', core.path, 'add', 'domain-work.txt']);
execFileSync('git', ['-C', core.path, 'commit', '-m', 'domain work']);
const committedPlan = planUninstall({ repository: inspectRepository(fixture.root), purge: true });
const committedPlan = planUninstall({ repository: inspectRepository(fixture.root), purge: true, env: fixture.env });
assert.equal(committedPlan.problems.some((problem) =>
problem.type === 'unsafe-worktree' && problem.area === 'core'
&& problem.problems.includes('unique-commits:1')), true);
assert.throws(
() => uninstallProject({ repository: inspectRepository(fixture.root), purge: true }),
() => uninstallProject({ repository: inspectRepository(fixture.root), purge: true, env: fixture.env }),
(error) => error.code === 'UNSAFE_TO_PURGE',
);
assert.equal(existsSync(join(core.path, 'domain-work.txt')), true);