mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-30 09:21:52 +00:00
Verify runtime ownership before stop
This commit is contained in:
1 parent
6e79960721
commit
325ed613be
5 files changed
+66
-5
No files matched your search
@@ -933,6 +933,14 @@ domain implementation. A domain session works only in its assigned worktree.
|
||||
TORCH MUST inspect process ownership and working directory before stopping a
|
||||
process. Parentage alone does not establish ownership.
|
||||
|
||||
Implementation checkpoint (2026-09-27): the Claude stop adapter resolves the
|
||||
durable runtime-session ID through the provider's live session inventory and
|
||||
requires its reported working directory to equal the managed identity's
|
||||
worktree before issuing a stop. Missing ownership evidence or a mismatched
|
||||
directory fails closed. A provider-reported offline session is treated as
|
||||
already stopped. Codex declares stop unsupported and is never silently killed
|
||||
through an operating-system process heuristic.
|
||||
|
||||
### 16.2 Safe convergence
|
||||
|
||||
TORCH MUST distinguish scheduled observation from scheduled mutation:
|
||||
|
||||
@@ -93,7 +93,7 @@ test_integrity_note:
|
||||
- id: SCN-claude-adapter
|
||||
given: a Claude runtime adapter with a virtualized command runner
|
||||
when: sessions are planned and runtime state is queried
|
||||
then: command construction is deterministic, capabilities are explicit, and planning launches no process
|
||||
then: command construction is deterministic, capabilities are explicit, planning launches no process, and stop requires matching live session and worktree ownership evidence
|
||||
- id: SCN-codex-adapter
|
||||
given: a Codex runtime adapter with a virtualized command runner and an identity-bound TORCH control plane
|
||||
when: create, resume, MCP configuration, runtime-ID capture, and steering are planned or attempted
|
||||
@@ -257,6 +257,7 @@ test_integrity_note:
|
||||
- SQLite state after process restart
|
||||
- CLI and MCP output over real process boundaries
|
||||
- runtime command plans and execution order
|
||||
- runtime stop session identity, reported working directory, and fail-closed ownership mismatch
|
||||
- local resume snapshot, tracked resume brief, final worker message, preserved task identity, and presence transitions
|
||||
- immutable check receipt commit and result
|
||||
- resource holder, FIFO position, expiry, and cancellation state
|
||||
@@ -299,6 +300,7 @@ test_integrity_note:
|
||||
- repository content redirecting machine-local database writes
|
||||
- MCP and CLI drifting onto separate business logic
|
||||
- launch plans that execute provider commands during preview
|
||||
- stopping a runtime by PID, parentage, or session label without matching managed-worktree evidence
|
||||
- a mixed-runtime claim implemented as renamed Claude-only commands
|
||||
- runtime sessions launching without their identity-bound TORCH tool surface
|
||||
- lost Codex thread IDs or native steering that bypasses durable messaging
|
||||
|
||||
+24
-3
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { accessSync, constants, existsSync } from 'node:fs';
|
||||
import { delimiter, join } from 'node:path';
|
||||
import { delimiter, join, resolve } from 'node:path';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
import { unsupportedCapability, validateRuntimeAdapter } from './runtime.mjs';
|
||||
|
||||
@@ -161,10 +161,31 @@ export class ClaudeRuntimeAdapter {
|
||||
return session ?? { sessionId: id, status: 'offline' };
|
||||
}
|
||||
|
||||
stopSession({ runtimeSessionId } = {}) {
|
||||
stopSession({ runtimeSessionId, worktree } = {}) {
|
||||
const id = text(runtimeSessionId, 'runtimeSessionId');
|
||||
const expectedWorktree = resolve(text(worktree, 'worktree'));
|
||||
const session = this.getStatus({ runtimeSessionId: id });
|
||||
if (session.status === 'offline') {
|
||||
return { runtimeSessionId: id, stopped: true, alreadyOffline: true };
|
||||
}
|
||||
const reportedWorktree = session.cwd ?? session.worktree
|
||||
?? session.workingDirectory ?? session.working_directory;
|
||||
if (typeof reportedWorktree !== 'string' || !reportedWorktree.trim()) {
|
||||
throw new TorchError('Claude session stop refused because its working directory is unavailable', {
|
||||
code: 'RUNTIME_OWNERSHIP_UNVERIFIED', details: { runtimeSessionId: id, expectedWorktree },
|
||||
});
|
||||
}
|
||||
if (resolve(reportedWorktree) !== expectedWorktree) {
|
||||
throw new TorchError('Claude session stop refused because its working directory does not match the managed identity', {
|
||||
code: 'RUNTIME_OWNERSHIP_MISMATCH',
|
||||
details: { runtimeSessionId: id, expectedWorktree, reportedWorktree: resolve(reportedWorktree) },
|
||||
});
|
||||
}
|
||||
const result = this.run(['stop', id]);
|
||||
return { runtimeSessionId: id, stopped: result.status === undefined || result.status === 0 };
|
||||
return {
|
||||
runtimeSessionId: id, stopped: result.status === undefined || result.status === 0,
|
||||
verifiedWorktree: expectedWorktree, alreadyOffline: false,
|
||||
};
|
||||
}
|
||||
|
||||
captureRuntimeId({ areaId, runtimeSessionId } = {}) {
|
||||
|
||||
+3
-1
@@ -221,7 +221,9 @@ function stopThrough(adapters, action) {
|
||||
if (!adapter) throw new TorchError(`No runtime stopper for ${action.runtime}`, {
|
||||
code: 'RUNTIME_ADAPTER_MISSING', details: { runtime: action.runtime },
|
||||
});
|
||||
return adapter.stopSession({ runtimeSessionId: action.runtimeSessionId });
|
||||
return adapter.stopSession({
|
||||
runtimeSessionId: action.runtimeSessionId, worktree: action.worktree?.path,
|
||||
});
|
||||
}
|
||||
|
||||
function runCandidateAcceptance(candidateRoot, spawn) {
|
||||
|
||||
@@ -100,6 +100,34 @@ test('SCN-claude-adapter: planning is deterministic, capabilities are honest, an
|
||||
|
||||
const noRunner = createClaudeAdapter({ idFactory: () => 'never-run' });
|
||||
assert.throws(() => noRunner.listSessions(), (error) => error.code === 'RUNTIME_EXECUTION_NOT_AUTHORIZED');
|
||||
|
||||
const stoppedCalls = [];
|
||||
const stoppable = createClaudeAdapter({
|
||||
runner: (command, args) => {
|
||||
stoppedCalls.push({ command, args });
|
||||
if (args[0] === 'agents') {
|
||||
return { status: 0, stdout: '[{"sessionId":"owned-session","status":"idle","cwd":"/tmp/owned-worktree"}]' };
|
||||
}
|
||||
return { status: 0, stdout: '' };
|
||||
},
|
||||
});
|
||||
const stopped = stoppable.stopSession({
|
||||
runtimeSessionId: 'owned-session', worktree: '/tmp/owned-worktree',
|
||||
});
|
||||
assert.equal(stopped.stopped, true);
|
||||
assert.equal(stopped.verifiedWorktree, '/tmp/owned-worktree');
|
||||
assert.deepEqual(stoppedCalls.map((call) => call.args[0]), ['agents', 'stop']);
|
||||
assert.throws(
|
||||
() => stoppable.stopSession({ runtimeSessionId: 'owned-session', worktree: '/tmp/not-owned' }),
|
||||
(error) => error.code === 'RUNTIME_OWNERSHIP_MISMATCH',
|
||||
);
|
||||
const unverifiable = createClaudeAdapter({
|
||||
runner: () => ({ status: 0, stdout: '[{"sessionId":"opaque-session","status":"idle"}]' }),
|
||||
});
|
||||
assert.throws(
|
||||
() => unverifiable.stopSession({ runtimeSessionId: 'opaque-session', worktree: '/tmp/owned-worktree' }),
|
||||
(error) => error.code === 'RUNTIME_OWNERSHIP_UNVERIFIED',
|
||||
);
|
||||
});
|
||||
|
||||
test('SCN-codex-adapter: turns are resumable, identity-bound, durable, and capability-honest', () => {
|
||||
|
||||
Reference in new issue
Block a user