From 84c78a2d3745c3190ac05dcd02a0d72646fdeaa2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 15:04:07 -0400 Subject: [PATCH 01/15] feat(checks): add bounded candidate source seams TASK-6e4ff870-a386-4a11-a59c-444ebc23732e --- docs/TEST_INTEGRITY.md | 39 +++++++++++ src/checks/candidate-execution-fence-init.mjs | 3 + src/checks/candidate-execution-fence.mjs | 6 ++ src/checks/candidate-input-snapshot.mjs | 69 +++++++++++++++++++ src/checks/candidate-source-bootstrap.mjs | 16 +++++ src/checks/candidate-source-execution.mjs | 15 ++++ src/checks/service.mjs | 15 ++++ test/fleet/candidate-execution-fence.test.mjs | 5 ++ .../candidate-input-consumption.test.mjs | 10 +++ test/fleet/candidate-snapshot-policy.test.mjs | 7 ++ .../fleet/candidate-source-bootstrap.test.mjs | 12 ++++ .../fleet/candidate-source-execution.test.mjs | 9 +++ test/fleet/candidate-source-parent.test.mjs | 8 +++ 13 files changed, 214 insertions(+) create mode 100644 src/checks/candidate-execution-fence-init.mjs create mode 100644 src/checks/candidate-execution-fence.mjs create mode 100644 src/checks/candidate-input-snapshot.mjs create mode 100644 src/checks/candidate-source-bootstrap.mjs create mode 100644 src/checks/candidate-source-execution.mjs create mode 100644 test/fleet/candidate-execution-fence.test.mjs create mode 100644 test/fleet/candidate-input-consumption.test.mjs create mode 100644 test/fleet/candidate-snapshot-policy.test.mjs create mode 100644 test/fleet/candidate-source-bootstrap.test.mjs create mode 100644 test/fleet/candidate-source-execution.test.mjs create mode 100644 test/fleet/candidate-source-parent.test.mjs diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 44c659d..bed22bc 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2715,3 +2715,42 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +# Candidate source B2 frozen-input and fence boundaries (2026-10-01) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-candidate-bootstrap-one-shot + given: "An issued, nonce-bound native admission" + when: "It is opened, replayed, expired, foreign, or malformed" + then: "Only one bounded private bootstrap opens; every invalid case refuses before child start and exposes no authority handle" + - id: SCN-candidate-input-consumption + given: "A frozen project-local source snapshot" + when: "The original source changes before the fixed Node child reads its cwd" + then: "The child observes the captured bytes; snapshot drift refuses" + - id: SCN-candidate-execution-boundary + given: "A fixed command and explicit bounded environment" + when: "Ambient PATH/HOME/cache fallback or output overflow is attempted" + then: "Execution refuses or remains non-success" + - id: SCN-candidate-fence-closure + given: "A private PID1 namespace observation" + when: "Descendant closure is absent or uncertain" + then: "The outcome is UNKNOWN/INCOMPLETE, never terminal PASS" + observable_outcomes: + - "No DB, ControlPlane, writer, receipt adapter, or authority FD reaches the child" + - "Product input links and special nodes refuse; frozen product S stays under 64MiB/file and 512MiB total" + - "Artifact output remains M/artifactManifest; receipt-adapter A is absent and unclaimed" + determinism_controls: + - "Temporary source trees, fixed Node executable, explicit empty ambient environment, bounded streams, and injected observations" + - "No live policy registration, installation, network, cache, or provider process" + anti_cheat_rationale: + prevents: + - "Reading mutated live source instead of captured bytes" + - "Replaying a private admission or relabeling fixture output as a receipt" + - "Treating PGID emptiness, zero exit, or Chromium metadata as descendant/binary proof" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/checks/candidate-execution-fence-init.mjs b/src/checks/candidate-execution-fence-init.mjs new file mode 100644 index 0000000..bbadb03 --- /dev/null +++ b/src/checks/candidate-execution-fence-init.mjs @@ -0,0 +1,3 @@ +// Private init entrypoint. It is never a CLI surface: bootstrap is supplied only on inherited FD3 and acknowledgement only on FD4. +import { closeSync, readFileSync, writeFileSync } from 'node:fs'; +export function readPrivateFenceBootstrapV2() { const frame = readFileSync(3, 'utf8'); if (Buffer.byteLength(frame, 'utf8') > 65_536) throw new Error('CANDIDATE_FENCE_FRAME_TOO_LARGE'); const value = JSON.parse(frame); if (!value?.nonce || !value?.generation) throw new Error('CANDIDATE_FENCE_FRAME_INVALID'); writeFileSync(4, JSON.stringify({ nonce: value.nonce, generation: value.generation })); closeSync(3); closeSync(4); return Object.freeze({ nonce: value.nonce, generation: value.generation, admissionFdsClosed: true }); } diff --git a/src/checks/candidate-execution-fence.mjs b/src/checks/candidate-execution-fence.mjs new file mode 100644 index 0000000..4990a2f --- /dev/null +++ b/src/checks/candidate-execution-fence.mjs @@ -0,0 +1,6 @@ +import { TorchError } from '../kernel/errors.mjs'; +const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; +export function createCandidateExecutionFenceV2({ bootstrap, backend = 'private-pid1-namespace' } = {}) { if (!bootstrap?.transport?.closesBeforeCandidateSpawn || backend !== 'private-pid1-namespace') fail('Candidate fence has no supported private bootstrap', 'CANDIDATE_FENCE_UNAVAILABLE'); return Object.freeze({ schema: 'torch.dev/candidate-execution-fence/v2alpha1', backend, nonce: bootstrap.nonce, closed: false }); } +/** A direct exit or empty PGID is deliberately insufficient: missing observed PID1/reap proof is UNKNOWN. */ +export function observeCandidateExecutionFenceV2({ fence, observation = {} } = {}) { if (!fence || fence.closed) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE'); const closed = observation.privatePid1 === true && observation.namespaceObserved === true && observation.descendantsReaped === true && observation.admissionFdsClosed === true; return Object.freeze({ schema: 'torch.dev/candidate-terminal-proof/v2alpha1', outcome: closed ? 'terminal' : 'unknown', terminalProof: closed, reason: closed ? null : 'descendant-closure-unobserved', observation: Object.freeze({ privatePid1: observation.privatePid1 === true, namespaceObserved: observation.namespaceObserved === true, descendantsReaped: observation.descendantsReaped === true, admissionFdsClosed: observation.admissionFdsClosed === true }) }); } +export function closeCandidateExecutionFenceV2(fence) { if (!fence || fence.closed) fail('Candidate fence is already closed', 'CANDIDATE_FENCE_UNAVAILABLE'); return Object.freeze({ ...fence, closed: true }); } diff --git a/src/checks/candidate-input-snapshot.mjs b/src/checks/candidate-input-snapshot.mjs new file mode 100644 index 0000000..20f2fe8 --- /dev/null +++ b/src/checks/candidate-input-snapshot.mjs @@ -0,0 +1,69 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, writeSync } from 'node:fs'; +import { isAbsolute, join, relative, resolve } from 'node:path'; +import { TorchError } from '../kernel/errors.mjs'; + +export const CANDIDATE_PRODUCT_INPUT_LIMITS_V2 = Object.freeze({ maxRecords: 8192, maxPathUtf8Bytes: 4096, maxFileBytes: 67_108_864, maxTotalBytes: 536_870_912, maxFrameBytes: 1_048_576, chunkBytes: 65_536 }); +const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; +const order = (a, b) => Buffer.compare(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8')); +const bytes = (value) => Buffer.byteLength(value, 'utf8'); +function pathOf(value, limits) { + if (typeof value !== 'string' || !value || isAbsolute(value) || value.includes('\\') || value.includes('\0') || bytes(value) > limits.maxPathUtf8Bytes || value.split('/').some((part) => !part || part === '.' || part === '..' || part === '.git')) fail('Candidate snapshot path is invalid', 'CANDIDATE_SNAPSHOT_PATH_INVALID', { path: value }); + return value; +} +function normalizeLimits(value = {}) { + if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some((key) => !Object.hasOwn(CANDIDATE_PRODUCT_INPUT_LIMITS_V2, key))) fail('Candidate snapshot limits are invalid', 'CANDIDATE_SNAPSHOT_LIMIT_INVALID'); + const result = {}; + for (const [key, maximum] of Object.entries(CANDIDATE_PRODUCT_INPUT_LIMITS_V2)) { const v = value[key] ?? maximum; if (!Number.isInteger(v) || v < 1 || v > maximum) fail('Candidate snapshot limit is invalid', 'CANDIDATE_SNAPSHOT_LIMIT_INVALID', { key, value: v }); result[key] = v; } + return Object.freeze(result); +} +function regular(path) { const stat = lstatSync(path); if (stat.isSymbolicLink()) fail('Candidate snapshot rejects links', 'CANDIDATE_SNAPSHOT_LINK_REJECTED', { path }); if (!stat.isFile() && !stat.isDirectory()) fail('Candidate snapshot rejects special files', 'CANDIDATE_SNAPSHOT_SPECIAL_REJECTED', { path }); return stat; } +function files(root, paths, limits) { + const result = []; + const visit = (rel) => { const absolute = join(root, rel); const stat = regular(absolute); if (stat.isDirectory()) for (const name of readdirSync(absolute).sort(order)) visit(`${rel}/${name}`); else { if (stat.size > limits.maxFileBytes) fail('Candidate snapshot file cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED', { path: rel }); result.push({ path: rel, size: stat.size, executable: Boolean(stat.mode & 0o111) }); } }; + for (const path of [...new Set(paths.map((p) => pathOf(p, limits)))].sort(order)) visit(path); + if (!result.length || result.length > limits.maxRecords) fail('Candidate snapshot record cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); + return result.sort((a, b) => order(a.path, b.path)); +} +function copyAndHash(source, destination, expected, limits) { + let inFd; let outFd; + try { + inFd = openSync(source, constants.O_RDONLY | constants.O_NOFOLLOW); const before = fstatSync(inFd); + if (!before.isFile() || before.size !== expected.size || before.size > limits.maxFileBytes) fail('Candidate snapshot source drifted', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path }); + outFd = openSync(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, expected.executable ? 0o500 : 0o400); + const hash = createHash('sha256'); const buffer = Buffer.alloc(limits.chunkBytes); let total = 0; + for (;;) { const count = readSync(inFd, buffer, 0, buffer.length, null); if (!count) break; total += count; if (total > limits.maxFileBytes) fail('Candidate snapshot file cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); hash.update(buffer.subarray(0, count)); let offset = 0; while (offset < count) offset += writeSync(outFd, buffer, offset, count - offset); } + const after = fstatSync(inFd); if (total !== before.size || after.size !== before.size) fail('Candidate snapshot source changed while read', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path }); + return { path: expected.path, bytes: total, sha256: hash.digest('hex'), executable: expected.executable }; + } finally { if (inFd !== undefined) closeSync(inFd); if (outFd !== undefined) closeSync(outFd); } +} +function hashRegularFile(path, expected, limits) { + let fd; + try { + fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); const before = fstatSync(fd); + if (!before.isFile() || before.size !== expected.bytes || before.size > limits.maxFileBytes) fail('Candidate snapshot drifted', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path }); + const hash = createHash('sha256'); const buffer = Buffer.alloc(limits.chunkBytes); let total = 0; + for (;;) { const count = readSync(fd, buffer, 0, buffer.length, null); if (!count) break; total += count; if (total > limits.maxFileBytes) fail('Candidate snapshot cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); hash.update(buffer.subarray(0, count)); } + const after = fstatSync(fd); if (total !== before.size || after.size !== before.size) fail('Candidate snapshot drifted while read', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path }); + return { path: expected.path, bytes: total, sha256: hash.digest('hex'), executable: expected.executable }; + } finally { if (fd !== undefined) closeSync(fd); } +} +/** Private caller seam: callers obtain inputRoot from the returned opaque snapshot, never from a policy selector. */ +export function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, paths, limits: requestedLimits = {}, id = randomUUID() } = {}) { + const limits = normalizeLimits(requestedLimits); if (typeof sourceRoot !== 'string' || typeof destinationRoot !== 'string' || !Array.isArray(paths)) fail('Candidate snapshot request is invalid', 'CANDIDATE_SNAPSHOT_INPUT_INVALID'); + const source = resolve(sourceRoot); const destination = resolve(destinationRoot); if (source === destination || !relative(source, destination).startsWith('..')) fail('Candidate snapshot destination must be outside source', 'CANDIDATE_SNAPSHOT_INPUT_INVALID'); + const listed = files(source, paths, limits); mkdirSync(destination, { recursive: false, mode: 0o700 }); let totalBytes = 0; const records = []; + for (const item of listed) { const target = join(destination, item.path); mkdirSync(resolve(target, '..'), { recursive: true, mode: 0o700 }); const record = copyAndHash(join(source, item.path), target, item, limits); totalBytes += record.bytes; if (totalBytes > limits.maxTotalBytes) fail('Candidate snapshot total cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); records.push(record); } + const manifest = Object.freeze({ schema: 'torch.dev/candidate-input-manifest/v2alpha1', id, files: Object.freeze(records), totalBytes, digest: createHash('sha256').update(JSON.stringify(records)).digest('hex') }); + return Object.freeze({ schema: 'torch.dev/candidate-input-snapshot/v2alpha1', inputRoot: destination, manifest, limits }); +} +export function verifyCandidateInputSnapshotV2(snapshot = {}) { + if (snapshot?.schema !== 'torch.dev/candidate-input-snapshot/v2alpha1' || !snapshot.manifest?.files || typeof snapshot.inputRoot !== 'string') fail('Candidate snapshot is malformed', 'CANDIDATE_SNAPSHOT_INVALID'); + const listed = files(snapshot.inputRoot, snapshot.manifest.files.map((file) => file.path), snapshot.limits); + const observed = listed.map((file) => hashRegularFile(join(snapshot.inputRoot, file.path), { ...file, bytes: file.size }, snapshot.limits)); + if (JSON.stringify(observed) !== JSON.stringify(snapshot.manifest.files) + || createHash('sha256').update(JSON.stringify(observed)).digest('hex') !== snapshot.manifest.digest) { + fail('Candidate snapshot content drifted', 'CANDIDATE_SNAPSHOT_DRIFT'); + } + return Object.freeze({ verified: true, digest: snapshot.manifest.digest, files: observed.length, totalBytes: snapshot.manifest.totalBytes }); +} diff --git a/src/checks/candidate-source-bootstrap.mjs b/src/checks/candidate-source-bootstrap.mjs new file mode 100644 index 0000000..ccf0af2 --- /dev/null +++ b/src/checks/candidate-source-bootstrap.mjs @@ -0,0 +1,16 @@ +import { randomBytes } from 'node:crypto'; +import { TorchError } from '../kernel/errors.mjs'; + +const live = new WeakSet(); const used = new Set(); +const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; +/** CheckService-only issuer. The returned object is intentionally not serializable and carries no DB/adapter/writer capability. */ +export function issueCandidateNativeAdmissionV2({ projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt } = {}) { + for (const [key, value] of Object.entries({ projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt })) if (typeof value !== 'string' || !value) fail('Candidate native admission is incomplete', 'CANDIDATE_NATIVE_ADMISSION_INVALID', { key }); + const admission = Object.freeze({ schema: 'torch.dev/candidate-native-admission/v2alpha1', projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt, nonce: randomBytes(32).toString('base64url') }); live.add(admission); return admission; +} +export function openCandidateSourceBootstrapV2(admission, { now = () => new Date() } = {}) { + if (!live.has(admission) || used.has(admission.nonce)) fail('Candidate native admission is unavailable or replayed', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); + if (Number.isNaN(Date.parse(admission.expiresAt)) || Date.parse(admission.expiresAt) <= now().getTime()) fail('Candidate native admission expired', 'CANDIDATE_NATIVE_ADMISSION_EXPIRED'); + used.add(admission.nonce); live.delete(admission); + return Object.freeze({ schema: 'torch.dev/candidate-bootstrap/v2alpha1', nonce: admission.nonce, runId: admission.runId, generation: admission.generation, policyDigest: admission.policyDigest, engineDigest: admission.engineDigest, expiresAt: admission.expiresAt, transport: Object.freeze({ inputFd: 3, acknowledgementFd: 4, closesBeforeCandidateSpawn: true }) }); +} diff --git a/src/checks/candidate-source-execution.mjs b/src/checks/candidate-source-execution.mjs new file mode 100644 index 0000000..30151d8 --- /dev/null +++ b/src/checks/candidate-source-execution.mjs @@ -0,0 +1,15 @@ +import { spawnSync } from 'node:child_process'; +import { isAbsolute } from 'node:path'; +import { TorchError } from '../kernel/errors.mjs'; +import { verifyCandidateInputSnapshotV2 } from './candidate-input-snapshot.mjs'; +const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; +export const CANDIDATE_EXECUTION_LIMITS_V2 = Object.freeze({ timeoutMs: 300000, maxOutputBytes: 1048576, maxArgs: 64, maxArgBytes: 4096 }); +function bound(value, limit, name) { if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > limit) fail('Candidate execution value is invalid', 'CANDIDATE_EXECUTION_INVALID', { name }); return value; } +export function runCandidateSourceExecutionV2({ command, args = [], inputSnapshot, environment = {}, executor = spawnSync, limits = CANDIDATE_EXECUTION_LIMITS_V2 } = {}) { + if (!isAbsolute(command) || inputSnapshot?.schema !== 'torch.dev/candidate-input-snapshot/v2alpha1' || typeof executor !== 'function' || !Array.isArray(args) || args.length > limits.maxArgs || Object.keys(environment).some((key) => ['PATH', 'HOME', 'NODE_PATH', 'npm_config_cache'].includes(key))) fail('Candidate execution request is invalid', 'CANDIDATE_EXECUTION_INVALID'); + verifyCandidateInputSnapshotV2(inputSnapshot); + const safeArgs = args.map((arg) => bound(arg, limits.maxArgBytes, 'arg')); const env = Object.freeze(Object.fromEntries(Object.entries(environment).sort())); + let raw; try { raw = executor(command, safeArgs, { cwd: inputSnapshot.inputRoot, env, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], timeout: limits.timeoutMs, maxBuffer: limits.maxOutputBytes, killSignal: 'SIGTERM' }); } catch (error) { raw = { status: null, error }; } + const stdout = Buffer.from(raw?.stdout ?? ''); const stderr = Buffer.from(raw?.stderr ?? ''); const overflow = stdout.length > limits.maxOutputBytes || stderr.length > limits.maxOutputBytes || raw?.error?.code === 'ENOBUFS'; const timeout = raw?.error?.code === 'ETIMEDOUT' || raw?.timedOut === true; + return Object.freeze({ schema: 'torch.dev/candidate-source-execution/v2alpha1', terminal: Object.freeze({ kind: timeout ? 'timeout' : overflow ? 'overflow' : raw?.signal ? 'signal' : raw?.error ? 'error' : 'exit', exitStatus: Number.isInteger(raw?.status) ? raw.status : null, signal: raw?.signal ?? null, overflow, timeout, success: !timeout && !overflow && !raw?.signal && !raw?.error && raw?.status === 0 }), output: Object.freeze({ stdout: stdout.subarray(0, limits.maxOutputBytes), stderr: stderr.subarray(0, limits.maxOutputBytes) }) }); +} diff --git a/src/checks/service.mjs b/src/checks/service.mjs index fcc3458..422200f 100644 --- a/src/checks/service.mjs +++ b/src/checks/service.mjs @@ -84,6 +84,19 @@ function captureGeneratedArtifacts({ generatedDirectory, commit, checkId, defini }; } +// B2 admission boundary only. Kernel owns policy parsing/registration and the parent; +// this service must never synthesize either from cwd, environment, or caller JSON. +function assertCandidateNativeAdmissionAvailable(definition) { + if (!Object.hasOwn(definition ?? {}, 'candidate_execution_policy')) return; + if (!definition.candidate_execution_policy) { + throw new TorchError('Candidate execution policy is not registered', { code: 'CANDIDATE_SNAPSHOT_POLICY_UNDECLARED' }); + } + // A registered policy alone is not the genuine parent issuer or receipt adapter. + throw new TorchError('Candidate native parent bootstrap is unavailable', { + code: 'CANDIDATE_PARENT_NATIVE_BOOTSTRAP_UNAVAILABLE', + }); +} + export class CheckService { constructor({ repositoryRoot, controlPlane, resourceService = null, @@ -330,6 +343,8 @@ export class CheckService { if (missing.length) throw new TorchError('Prepared check requires resource leases', { code: 'CHECK_BLOCKED', details: missing.map((resourceId) => ({ code: 'RESOURCE_LEASE_REQUIRED', resourceId })), }); + // Deliberately before snapshot verification, state mutation, artifacts, or child spawn. + assertCandidateNativeAdmissionAvailable(currentDefinition); verifyCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot }); const claimed = this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'running', runner_json = ? WHERE id = ? AND project_id = ? AND area_id = ? AND state = 'prepared'`) diff --git a/test/fleet/candidate-execution-fence.test.mjs b/test/fleet/candidate-execution-fence.test.mjs new file mode 100644 index 0000000..7b7590c --- /dev/null +++ b/test/fleet/candidate-execution-fence.test.mjs @@ -0,0 +1,5 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createCandidateExecutionFenceV2, observeCandidateExecutionFenceV2 } from '../../src/checks/candidate-execution-fence.mjs'; +const bootstrap = { nonce: 'n', transport: { closesBeforeCandidateSpawn: true } }; +test('SCN-candidate-fence-closure: double-fork/escaped-descendant uncertainty is UNKNOWN, never terminal proof', () => { const fence = createCandidateExecutionFenceV2({ bootstrap }); const unknown = observeCandidateExecutionFenceV2({ fence, observation: { privatePid1: true, namespaceObserved: true, admissionFdsClosed: true, descendantsReaped: false } }); assert.equal(unknown.outcome, 'unknown'); assert.equal(unknown.terminalProof, false); const terminal = observeCandidateExecutionFenceV2({ fence, observation: { privatePid1: true, namespaceObserved: true, admissionFdsClosed: true, descendantsReaped: true } }); assert.equal(terminal.terminalProof, true); }); diff --git a/test/fleet/candidate-input-consumption.test.mjs b/test/fleet/candidate-input-consumption.test.mjs new file mode 100644 index 0000000..3aac9fb --- /dev/null +++ b/test/fleet/candidate-input-consumption.test.mjs @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { captureCandidateInputSnapshotV2, verifyCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; +import { runCandidateSourceExecutionV2 } from '../../src/checks/candidate-source-execution.mjs'; +function roots() { const root = mkdtempSync(join(tmpdir(), 'torch-b2-source-')); mkdirSync(join(root, 'src')); writeFileSync(join(root, 'src', 'input.txt'), 'captured'); return { root, snapshot: `${root}-snapshot` }; } +test('SCN-candidate-input-consumption: child execution seam receives frozen cwd bytes after original source mutation', () => { const { root, snapshot: destination } = roots(); const input = captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: destination, paths: ['src'] }); writeFileSync(join(root, 'src', 'input.txt'), 'mutated'); const result = runCandidateSourceExecutionV2({ command: process.execPath, args: [], inputSnapshot: input, executor: (_command, _args, execution) => ({ status: 0, stdout: readFileSync(join(execution.cwd, 'src', 'input.txt')) }) }); assert.equal(result.terminal.success, true); assert.equal(result.output.stdout.toString(), 'captured'); assert.equal(verifyCandidateInputSnapshotV2(input).verified, true); }); +test('SCN-candidate-input-drift: frozen-input mutation refuses rather than relabeling evidence', () => { const { root, snapshot: destination } = roots(); const input = captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: destination, paths: ['src'] }); const file = join(input.inputRoot, 'src', 'input.txt'); chmodSync(file, 0o600); writeFileSync(file, 'drift'); assert.throws(() => verifyCandidateInputSnapshotV2(input), { code: 'CANDIDATE_SNAPSHOT_DRIFT' }); }); diff --git a/test/fleet/candidate-snapshot-policy.test.mjs b/test/fleet/candidate-snapshot-policy.test.mjs new file mode 100644 index 0000000..61c2352 --- /dev/null +++ b/test/fleet/candidate-snapshot-policy.test.mjs @@ -0,0 +1,7 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, symlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { captureCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; +test('SCN-candidate-snapshot-policy: product links and S caps refuse before a snapshot is admitted', () => { const root = mkdtempSync(join(tmpdir(), 'torch-b2-policy-')); writeFileSync(join(root, 'file'), 'x'); symlinkSync('file', join(root, 'link')); assert.throws(() => captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['link'] }), { code: 'CANDIDATE_SNAPSHOT_LINK_REJECTED' }); }); diff --git a/test/fleet/candidate-source-bootstrap.test.mjs b/test/fleet/candidate-source-bootstrap.test.mjs new file mode 100644 index 0000000..95116dc --- /dev/null +++ b/test/fleet/candidate-source-bootstrap.test.mjs @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { issueCandidateNativeAdmissionV2, openCandidateSourceBootstrapV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; + +const issued = (expiresAt = '2030-01-01T00:00:00.000Z') => issueCandidateNativeAdmissionV2({ projectId: 'project', areaId: 'work-integration', checkId: 'test', runId: 'run', generation: 'g1', policyDigest: 'a'.repeat(64), engineDigest: 'b'.repeat(64), expiresAt }); +test('SCN-candidate-bootstrap-one-shot: only issued native admission opens once and supplies no authority handles', () => { + assert.throws(() => openCandidateSourceBootstrapV2({}), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); + const admission = issued(); const bootstrap = openCandidateSourceBootstrapV2(admission, { now: () => new Date('2029-01-01T00:00:00.000Z') }); + assert.equal(bootstrap.transport.closesBeforeCandidateSpawn, true); assert.deepEqual(Object.keys(bootstrap).sort(), ['engineDigest', 'expiresAt', 'generation', 'nonce', 'policyDigest', 'runId', 'schema', 'transport']); + assert.equal(Object.hasOwn(bootstrap, 'controlPlane'), false); assert.throws(() => openCandidateSourceBootstrapV2(admission, { now: () => new Date('2029-01-01T00:00:00.000Z') }), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); +}); +test('SCN-candidate-bootstrap-expiry: expired admission refuses before any child protocol exists', () => assert.throws(() => openCandidateSourceBootstrapV2(issued('2020-01-01T00:00:00.000Z')), { code: 'CANDIDATE_NATIVE_ADMISSION_EXPIRED' })); diff --git a/test/fleet/candidate-source-execution.test.mjs b/test/fleet/candidate-source-execution.test.mjs new file mode 100644 index 0000000..11b5f0e --- /dev/null +++ b/test/fleet/candidate-source-execution.test.mjs @@ -0,0 +1,9 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { captureCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; +import { runCandidateSourceExecutionV2 } from '../../src/checks/candidate-source-execution.mjs'; +function snapshot() { const source = mkdtempSync(join(tmpdir(), 'torch-b2-execution-')); writeFileSync(join(source, 'input'), 'x'); return captureCandidateInputSnapshotV2({ sourceRoot: source, destinationRoot: `${source}-snapshot`, paths: ['input'] }); } +test('SCN-candidate-execution-boundary: ambient PATH/HOME fallback and output overflow cannot become success', () => { const frozen = snapshot(); assert.throws(() => runCandidateSourceExecutionV2({ command: process.execPath, inputSnapshot: frozen, environment: { PATH: '/bin' } }), { code: 'CANDIDATE_EXECUTION_INVALID' }); const observed = runCandidateSourceExecutionV2({ command: process.execPath, inputSnapshot: frozen, limits: { timeoutMs: 1000, maxOutputBytes: 4, maxArgs: 64, maxArgBytes: 4096 }, executor: () => ({ status: 0, stdout: Buffer.from('xxxxxxxx') }) }); assert.equal(observed.terminal.overflow, true); assert.equal(observed.terminal.success, false); }); diff --git a/test/fleet/candidate-source-parent.test.mjs b/test/fleet/candidate-source-parent.test.mjs new file mode 100644 index 0000000..b1c426b --- /dev/null +++ b/test/fleet/candidate-source-parent.test.mjs @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { issueCandidateNativeAdmissionV2, openCandidateSourceBootstrapV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; +test('SCN-candidate-parent-capability: bootstrap frame carries only a bound tuple, never roots, env, writers, adapters, or receipt authority', () => { + const ticket = issueCandidateNativeAdmissionV2({ projectId: 'p', areaId: 'a', checkId: 'c', runId: 'r', generation: 'g', policyDigest: 'a'.repeat(64), engineDigest: 'b'.repeat(64), expiresAt: '2030-01-01T00:00:00.000Z' }); + const frame = openCandidateSourceBootstrapV2(ticket, { now: () => new Date('2029-01-01T00:00:00.000Z') }); + for (const forbidden of ['projectId', 'areaId', 'checkId', 'inputRoot', 'cwd', 'environment', 'writer', 'adapter', 'receipt']) assert.equal(Object.hasOwn(frame, forbidden), false); +}); From ea36a28d14ff332f24bc64b34ab94c79dac01514 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 15:07:14 -0400 Subject: [PATCH 02/15] TASK-6e4ff870-a386-4a11-a59c-444ebc23732e: bound fence bootstrap reads --- src/checks/candidate-execution-fence-init.mjs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/checks/candidate-execution-fence-init.mjs b/src/checks/candidate-execution-fence-init.mjs index bbadb03..cef9072 100644 --- a/src/checks/candidate-execution-fence-init.mjs +++ b/src/checks/candidate-execution-fence-init.mjs @@ -1,3 +1,28 @@ // Private init entrypoint. It is never a CLI surface: bootstrap is supplied only on inherited FD3 and acknowledgement only on FD4. -import { closeSync, readFileSync, writeFileSync } from 'node:fs'; -export function readPrivateFenceBootstrapV2() { const frame = readFileSync(3, 'utf8'); if (Buffer.byteLength(frame, 'utf8') > 65_536) throw new Error('CANDIDATE_FENCE_FRAME_TOO_LARGE'); const value = JSON.parse(frame); if (!value?.nonce || !value?.generation) throw new Error('CANDIDATE_FENCE_FRAME_INVALID'); writeFileSync(4, JSON.stringify({ nonce: value.nonce, generation: value.generation })); closeSync(3); closeSync(4); return Object.freeze({ nonce: value.nonce, generation: value.generation, admissionFdsClosed: true }); } +import { closeSync, readSync, writeFileSync } from 'node:fs'; + +const MAX_BOOTSTRAP_FRAME_BYTES = 65_536; + +function readBoundedFrame(fd) { + const chunks = []; + let total = 0; + const buffer = Buffer.allocUnsafe(8_192); + while (true) { + const bytesRead = readSync(fd, buffer, 0, buffer.length, null); + if (bytesRead === 0) break; + total += bytesRead; + if (total > MAX_BOOTSTRAP_FRAME_BYTES) throw new Error('CANDIDATE_FENCE_FRAME_TOO_LARGE'); + chunks.push(Buffer.from(buffer.subarray(0, bytesRead))); + } + return Buffer.concat(chunks, total).toString('utf8'); +} + +export function readPrivateFenceBootstrapV2() { + const frame = readBoundedFrame(3); + const value = JSON.parse(frame); + if (!value?.nonce || !value?.generation) throw new Error('CANDIDATE_FENCE_FRAME_INVALID'); + writeFileSync(4, JSON.stringify({ nonce: value.nonce, generation: value.generation })); + closeSync(3); + closeSync(4); + return Object.freeze({ nonce: value.nonce, generation: value.generation, admissionFdsClosed: true }); +} From 0665bb305b10efd9e7242eed2484ecf97646a0f3 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 15:32:38 -0400 Subject: [PATCH 03/15] TASK-6e4ff870-a386-4a11-a59c-444ebc23732e: correct candidate source boundaries --- docs/TEST_INTEGRITY.md | 41 +++++++++ src/checks/candidate-execution-fence.mjs | 37 ++++++-- src/checks/candidate-input-snapshot.mjs | 31 ++++++- src/checks/candidate-source-bootstrap.mjs | 89 ++++++++++++++++--- src/checks/candidate-source-execution.mjs | 68 +++++++++++--- test/fleet/candidate-execution-fence.test.mjs | 14 ++- .../candidate-input-consumption.test.mjs | 30 +++++-- test/fleet/candidate-snapshot-policy.test.mjs | 11 ++- .../fleet/candidate-source-bootstrap.test.mjs | 32 +++++-- .../fleet/candidate-source-execution.test.mjs | 17 ++-- test/fleet/candidate-source-parent.test.mjs | 16 ++-- 11 files changed, 327 insertions(+), 59 deletions(-) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index bed22bc..74b4b62 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2754,3 +2754,44 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +# Candidate source B2 authenticated-attempt spec correction (2026-10-01) + +```yaml +test_integrity_note: + change_type: spec_correction + scenarios: + - id: SCN-candidate-bootstrap-one-shot + given: "A CheckService-issued opaque registered-plan handle" + when: "A foreign, cloned, expired, replayed, or already-closed handle is used" + then: "No FD3 admission opens; the only accepted acknowledgement is the exact FD4 binding frame and it closes the original linear handle" + - id: SCN-candidate-input-consumption + given: "A frozen, bounded product snapshot and a fixed Node E binding" + when: "The original product bytes change before execution" + then: "An actual spawned child reads the frozen bytes; post-capture drift refuses before spawn" + - id: SCN-candidate-execution-boundary + given: "An opaque attempt with fixed command, arguments, explicit environment, and output cap" + when: "The child exits zero after exceeding the cap" + then: "The parent records overflow as non-success without accepting an injected executor or caller-selected limit" + - id: SCN-candidate-fence-closure + given: "A closed opaque attempt" + when: "The backend has not recorded namespace identity, FD closure, and descendant reaping" + then: "Terminal proof is UNKNOWN; fabricated observations and cloned handles cannot create proof" + observable_outcomes: + - "Only the exact FD4 acknowledgement closes a one-shot opaque admission" + - "Child stdout proves frozen-byte consumption, not same-process test reads" + - "Zero exit plus output overflow is never success" + - "Missing backend fence evidence is never TerminalProof" + determinism_controls: + - "Temporary source trees, fixed absolute Node runtime, explicit empty child environment, bounded streams" + - "No injected executor, caller-provided command, root, environment, limit, or terminal observation" + anti_cheat_rationale: + prevents: + - "Arbitrary tuple issuance or a fabricated bootstrap literal" + - "Boolean caller observations minting terminal proof" + - "Cloned or reused handles reopening an admission" + - "Same-process snapshot reads impersonating child consumption" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/checks/candidate-execution-fence.mjs b/src/checks/candidate-execution-fence.mjs index 4990a2f..6bd72ab 100644 --- a/src/checks/candidate-execution-fence.mjs +++ b/src/checks/candidate-execution-fence.mjs @@ -1,6 +1,33 @@ import { TorchError } from '../kernel/errors.mjs'; -const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; -export function createCandidateExecutionFenceV2({ bootstrap, backend = 'private-pid1-namespace' } = {}) { if (!bootstrap?.transport?.closesBeforeCandidateSpawn || backend !== 'private-pid1-namespace') fail('Candidate fence has no supported private bootstrap', 'CANDIDATE_FENCE_UNAVAILABLE'); return Object.freeze({ schema: 'torch.dev/candidate-execution-fence/v2alpha1', backend, nonce: bootstrap.nonce, closed: false }); } -/** A direct exit or empty PGID is deliberately insufficient: missing observed PID1/reap proof is UNKNOWN. */ -export function observeCandidateExecutionFenceV2({ fence, observation = {} } = {}) { if (!fence || fence.closed) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE'); const closed = observation.privatePid1 === true && observation.namespaceObserved === true && observation.descendantsReaped === true && observation.admissionFdsClosed === true; return Object.freeze({ schema: 'torch.dev/candidate-terminal-proof/v2alpha1', outcome: closed ? 'terminal' : 'unknown', terminalProof: closed, reason: closed ? null : 'descendant-closure-unobserved', observation: Object.freeze({ privatePid1: observation.privatePid1 === true, namespaceObserved: observation.namespaceObserved === true, descendantsReaped: observation.descendantsReaped === true, admissionFdsClosed: observation.admissionFdsClosed === true }) }); } -export function closeCandidateExecutionFenceV2(fence) { if (!fence || fence.closed) fail('Candidate fence is already closed', 'CANDIDATE_FENCE_UNAVAILABLE'); return Object.freeze({ ...fence, closed: true }); } + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); +} + +/** + * Fence observations are owned by the backend, never submitted by the caller. + * Until the backend has recorded a PID1 namespace identity, FD closure, and + * namespace-wide reaping, every attempt is UNKNOWN. A process-group exit never + * creates TerminalProof. + */ +export function createCandidateExecutionFenceRuntimeV2({ stateForAttempt, backendObservation } = {}) { + if (typeof stateForAttempt !== 'function' || typeof backendObservation !== 'function') throw new TypeError('candidate fence runtime is incomplete'); + const closed = new WeakSet(); + function close(attempt) { + if (!stateForAttempt(attempt) || closed.has(attempt)) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE'); + closed.add(attempt); + } + function terminal(attempt) { + if (!stateForAttempt(attempt) || !closed.has(attempt)) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE'); + const observed = backendObservation(attempt); + const qualified = observed?.backend === 'private-pid1-namespace' + && Number.isSafeInteger(observed.namespacePid) && observed.namespacePid > 0 + && typeof observed.namespaceIdentity === 'string' && observed.namespaceIdentity.length > 0 + && observed.admissionFdsClosed === true && observed.descendantsReaped === true && observed.escapedDescendants === false; + return Object.freeze({ + schema: 'torch.dev/candidate-terminal-proof/v2alpha1', outcome: qualified ? 'terminal' : 'unknown', terminalProof: qualified, + reason: qualified ? null : 'descendant-closure-unobserved', + }); + } + return Object.freeze({ close, terminal }); +} diff --git a/src/checks/candidate-input-snapshot.mjs b/src/checks/candidate-input-snapshot.mjs index 20f2fe8..e5dd562 100644 --- a/src/checks/candidate-input-snapshot.mjs +++ b/src/checks/candidate-input-snapshot.mjs @@ -49,7 +49,7 @@ function hashRegularFile(path, expected, limits) { } finally { if (fd !== undefined) closeSync(fd); } } /** Private caller seam: callers obtain inputRoot from the returned opaque snapshot, never from a policy selector. */ -export function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, paths, limits: requestedLimits = {}, id = randomUUID() } = {}) { +function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, paths, limits: requestedLimits = {}, id = randomUUID() } = {}) { const limits = normalizeLimits(requestedLimits); if (typeof sourceRoot !== 'string' || typeof destinationRoot !== 'string' || !Array.isArray(paths)) fail('Candidate snapshot request is invalid', 'CANDIDATE_SNAPSHOT_INPUT_INVALID'); const source = resolve(sourceRoot); const destination = resolve(destinationRoot); if (source === destination || !relative(source, destination).startsWith('..')) fail('Candidate snapshot destination must be outside source', 'CANDIDATE_SNAPSHOT_INPUT_INVALID'); const listed = files(source, paths, limits); mkdirSync(destination, { recursive: false, mode: 0o700 }); let totalBytes = 0; const records = []; @@ -57,7 +57,7 @@ export function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, p const manifest = Object.freeze({ schema: 'torch.dev/candidate-input-manifest/v2alpha1', id, files: Object.freeze(records), totalBytes, digest: createHash('sha256').update(JSON.stringify(records)).digest('hex') }); return Object.freeze({ schema: 'torch.dev/candidate-input-snapshot/v2alpha1', inputRoot: destination, manifest, limits }); } -export function verifyCandidateInputSnapshotV2(snapshot = {}) { +function verifyCandidateInputSnapshotV2(snapshot = {}) { if (snapshot?.schema !== 'torch.dev/candidate-input-snapshot/v2alpha1' || !snapshot.manifest?.files || typeof snapshot.inputRoot !== 'string') fail('Candidate snapshot is malformed', 'CANDIDATE_SNAPSHOT_INVALID'); const listed = files(snapshot.inputRoot, snapshot.manifest.files.map((file) => file.path), snapshot.limits); const observed = listed.map((file) => hashRegularFile(join(snapshot.inputRoot, file.path), { ...file, bytes: file.size }, snapshot.limits)); @@ -67,3 +67,30 @@ export function verifyCandidateInputSnapshotV2(snapshot = {}) { } return Object.freeze({ verified: true, digest: snapshot.manifest.digest, files: observed.length, totalBytes: snapshot.manifest.totalBytes }); } + +/** + * The public boundary accepts only a service-issued attempt. Source roots, + * paths, destinations, and S limits live in the service's private state. + */ +export function createCandidateInputSnapshotRuntimeV2({ stateForAttempt } = {}) { + if (typeof stateForAttempt !== 'function') throw new TypeError('stateForAttempt is required'); + const snapshots = new WeakMap(); + function capture(attempt) { + const state = stateForAttempt(attempt); + if (!state?.input) fail('Candidate attempt has no frozen input authority', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED'); + const snapshot = captureCandidateInputSnapshotV2(state.input); + snapshots.set(attempt, snapshot); + return Object.freeze({ digest: snapshot.manifest.digest, files: snapshot.manifest.files.length, totalBytes: snapshot.manifest.totalBytes }); + } + function verify(attempt) { + const snapshot = snapshots.get(attempt); + if (!snapshot) fail('Candidate snapshot is unavailable', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED'); + return verifyCandidateInputSnapshotV2(snapshot); + } + function cwd(attempt) { + const snapshot = snapshots.get(attempt); + if (!snapshot) fail('Candidate snapshot is unavailable', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED'); + return snapshot.inputRoot; + } + return Object.freeze({ capture, verify, cwd }); +} diff --git a/src/checks/candidate-source-bootstrap.mjs b/src/checks/candidate-source-bootstrap.mjs index ccf0af2..02c5c64 100644 --- a/src/checks/candidate-source-bootstrap.mjs +++ b/src/checks/candidate-source-bootstrap.mjs @@ -1,16 +1,81 @@ -import { randomBytes } from 'node:crypto'; +import { createHash, randomBytes } from 'node:crypto'; import { TorchError } from '../kernel/errors.mjs'; -const live = new WeakSet(); const used = new Set(); -const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; -/** CheckService-only issuer. The returned object is intentionally not serializable and carries no DB/adapter/writer capability. */ -export function issueCandidateNativeAdmissionV2({ projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt } = {}) { - for (const [key, value] of Object.entries({ projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt })) if (typeof value !== 'string' || !value) fail('Candidate native admission is incomplete', 'CANDIDATE_NATIVE_ADMISSION_INVALID', { key }); - const admission = Object.freeze({ schema: 'torch.dev/candidate-native-admission/v2alpha1', projectId, areaId, checkId, runId, generation, policyDigest, engineDigest, expiresAt, nonce: randomBytes(32).toString('base64url') }); live.add(admission); return admission; +const ADMISSION_SCHEMA = 'torch.dev/candidate-source-parent-admission/v2alpha1'; +const ACK_SCHEMA = 'torch.dev/candidate-source-parent-binding/v2alpha1'; +const SHA256 = /^[a-f0-9]{64}$/; +const MAX_FRAME_BYTES = 64 * 1024; + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); } -export function openCandidateSourceBootstrapV2(admission, { now = () => new Date() } = {}) { - if (!live.has(admission) || used.has(admission.nonce)) fail('Candidate native admission is unavailable or replayed', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); - if (Number.isNaN(Date.parse(admission.expiresAt)) || Date.parse(admission.expiresAt) <= now().getTime()) fail('Candidate native admission expired', 'CANDIDATE_NATIVE_ADMISSION_EXPIRED'); - used.add(admission.nonce); live.delete(admission); - return Object.freeze({ schema: 'torch.dev/candidate-bootstrap/v2alpha1', nonce: admission.nonce, runId: admission.runId, generation: admission.generation, policyDigest: admission.policyDigest, engineDigest: admission.engineDigest, expiresAt: admission.expiresAt, transport: Object.freeze({ inputFd: 3, acknowledgementFd: 4, closesBeforeCandidateSpawn: true }) }); + +function digest(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +function validString(value) { + return typeof value === 'string' && value.length > 0; +} + +/** + * Service-only closure factory. It has no public issuer that accepts caller + * project/check/run tuples: CheckService supplies an opaque registered-plan + * handle to stateForPlan and this module serializes only that captured state. + */ +export function createCandidateSourceBootstrapRuntimeV2({ stateForPlan, now = () => new Date() } = {}) { + if (typeof stateForPlan !== 'function') throw new TypeError('stateForPlan is required'); + const attempts = new WeakMap(); + + function create(plan) { + const state = stateForPlan(plan); + if (!state || typeof state !== 'object') fail('Registered candidate plan is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REQUIRED'); + const required = ['admissionId', 'leaseId', 'checkId', 'policyBytes', 'policyDigest', 'engine', 'subject', 'artifactManifest', 'expiresAt']; + for (const key of required) if (!Object.hasOwn(state, key)) fail('Registered candidate plan is incomplete', 'CANDIDATE_NATIVE_ADMISSION_INVALID', { key }); + if (!Buffer.isBuffer(state.policyBytes) || state.policyBytes.length === 0 || state.policyBytes.length > 1024 * 1024 + || !SHA256.test(state.policyDigest) || digest(state.policyBytes) !== state.policyDigest + || !validString(state.admissionId) || !validString(state.leaseId) || !validString(state.checkId) + || !Number.isSafeInteger(state.runGeneration) || !Number.isSafeInteger(state.guardGeneration) + || Number.isNaN(Date.parse(state.expiresAt))) fail('Registered candidate plan is invalid', 'CANDIDATE_NATIVE_ADMISSION_INVALID'); + const handle = Object.freeze({}); + attempts.set(handle, { state, used: false, closed: false, nonce: randomBytes(32).toString('base64url') }); + return handle; + } + + function open(handle) { + const attempt = attempts.get(handle); + if (!attempt || attempt.used || attempt.closed) fail('Candidate admission is unavailable or replayed', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); + if (Date.parse(attempt.state.expiresAt) <= now().getTime()) { + attempt.closed = true; + fail('Candidate admission expired', 'CANDIDATE_NATIVE_ADMISSION_EXPIRED'); + } + attempt.used = true; + const { state } = attempt; + const frame = Buffer.from(JSON.stringify({ + schema: ADMISSION_SCHEMA, version: 2, admissionId: state.admissionId, leaseId: state.leaseId, + checkId: state.checkId, runGeneration: state.runGeneration, guardGeneration: state.guardGeneration, + policyBytesBase64: state.policyBytes.toString('base64'), policyDigest: state.policyDigest, + engine: state.engine, subject: state.subject, artifactManifest: state.artifactManifest, + receiptAdapterAdmission: null, + }), 'utf8'); + if (frame.length > MAX_FRAME_BYTES) fail('Candidate parent admission frame exceeds its bound', 'CANDIDATE_NATIVE_ADMISSION_INVALID'); + return Object.freeze({ frame, acknowledgement: Buffer.from(`${ACK_SCHEMA}\n${state.admissionId}\n${state.policyDigest}\n`, 'utf8'), nonce: attempt.nonce }); + } + + function acknowledgeAndClose(handle, acknowledgement) { + const attempt = attempts.get(handle); + if (!attempt || !attempt.used || attempt.closed || !Buffer.isBuffer(acknowledgement)) fail('Candidate acknowledgement is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); + const expected = Buffer.from(`${ACK_SCHEMA}\n${attempt.state.admissionId}\n${attempt.state.policyDigest}\n`, 'utf8'); + if (!acknowledgement.equals(expected)) fail('Candidate parent acknowledgement mismatches admission', 'CANDIDATE_PARENT_ACK_MISMATCH'); + attempt.closed = true; + return Object.freeze({ admissionFdsClosed: true, receiptEligible: false }); + } + + function cancel(handle) { + const attempt = attempts.get(handle); + if (!attempt || attempt.used || attempt.closed) fail('Candidate admission is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REPLAY'); + attempt.closed = true; + } + + return Object.freeze({ create, open, acknowledgeAndClose, cancel }); } diff --git a/src/checks/candidate-source-execution.mjs b/src/checks/candidate-source-execution.mjs index 30151d8..5f52511 100644 --- a/src/checks/candidate-source-execution.mjs +++ b/src/checks/candidate-source-execution.mjs @@ -1,15 +1,61 @@ import { spawnSync } from 'node:child_process'; -import { isAbsolute } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; -import { verifyCandidateInputSnapshotV2 } from './candidate-input-snapshot.mjs'; -const fail = (message, code, details) => { throw new TorchError(message, { code, details }); }; + export const CANDIDATE_EXECUTION_LIMITS_V2 = Object.freeze({ timeoutMs: 300000, maxOutputBytes: 1048576, maxArgs: 64, maxArgBytes: 4096 }); -function bound(value, limit, name) { if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > limit) fail('Candidate execution value is invalid', 'CANDIDATE_EXECUTION_INVALID', { name }); return value; } -export function runCandidateSourceExecutionV2({ command, args = [], inputSnapshot, environment = {}, executor = spawnSync, limits = CANDIDATE_EXECUTION_LIMITS_V2 } = {}) { - if (!isAbsolute(command) || inputSnapshot?.schema !== 'torch.dev/candidate-input-snapshot/v2alpha1' || typeof executor !== 'function' || !Array.isArray(args) || args.length > limits.maxArgs || Object.keys(environment).some((key) => ['PATH', 'HOME', 'NODE_PATH', 'npm_config_cache'].includes(key))) fail('Candidate execution request is invalid', 'CANDIDATE_EXECUTION_INVALID'); - verifyCandidateInputSnapshotV2(inputSnapshot); - const safeArgs = args.map((arg) => bound(arg, limits.maxArgBytes, 'arg')); const env = Object.freeze(Object.fromEntries(Object.entries(environment).sort())); - let raw; try { raw = executor(command, safeArgs, { cwd: inputSnapshot.inputRoot, env, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], timeout: limits.timeoutMs, maxBuffer: limits.maxOutputBytes, killSignal: 'SIGTERM' }); } catch (error) { raw = { status: null, error }; } - const stdout = Buffer.from(raw?.stdout ?? ''); const stderr = Buffer.from(raw?.stderr ?? ''); const overflow = stdout.length > limits.maxOutputBytes || stderr.length > limits.maxOutputBytes || raw?.error?.code === 'ENOBUFS'; const timeout = raw?.error?.code === 'ETIMEDOUT' || raw?.timedOut === true; - return Object.freeze({ schema: 'torch.dev/candidate-source-execution/v2alpha1', terminal: Object.freeze({ kind: timeout ? 'timeout' : overflow ? 'overflow' : raw?.signal ? 'signal' : raw?.error ? 'error' : 'exit', exitStatus: Number.isInteger(raw?.status) ? raw.status : null, signal: raw?.signal ?? null, overflow, timeout, success: !timeout && !overflow && !raw?.signal && !raw?.error && raw?.status === 0 }), output: Object.freeze({ stdout: stdout.subarray(0, limits.maxOutputBytes), stderr: stderr.subarray(0, limits.maxOutputBytes) }) }); + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); +} + +function validRuntime(state) { + const runtime = state?.runtime; + if (!runtime || typeof runtime.command !== 'string' || !runtime.command.startsWith('/') || !Array.isArray(runtime.args) + || !runtime.args.every((arg) => typeof arg === 'string' && Buffer.byteLength(arg, 'utf8') <= CANDIDATE_EXECUTION_LIMITS_V2.maxArgBytes) + || runtime.args.length > CANDIDATE_EXECUTION_LIMITS_V2.maxArgs + || !runtime.environment || Object.keys(runtime.environment).some((key) => ['PATH', 'HOME', 'NODE_PATH', 'npm_config_cache'].includes(key))) { + fail('Candidate fixed runtime is invalid', 'CANDIDATE_EXECUTION_RUNTIME_INVALID'); + } + return runtime; +} + +/** + * Runs a fixed, CheckService-derived E binding. `run` takes only an opaque + * attempt; command, arguments, environment, executor, cwd and limits cannot + * be selected by callers. It is raw terminal evidence, never a receipt PASS. + */ +export function createCandidateSourceExecutionRuntimeV2({ stateForAttempt, snapshotRuntime } = {}) { + if (typeof stateForAttempt !== 'function' || !snapshotRuntime) throw new TypeError('candidate execution runtime is incomplete'); + const results = new WeakMap(); + function run(attempt) { + const state = stateForAttempt(attempt); + const runtime = validRuntime(state); + snapshotRuntime.verify(attempt); + const cwd = snapshotRuntime.cwd(attempt); + let raw; + try { + raw = spawnSync(runtime.command, runtime.args, { + cwd, env: Object.freeze({ ...runtime.environment }), encoding: 'buffer', + stdio: ['ignore', 'pipe', 'pipe'], timeout: CANDIDATE_EXECUTION_LIMITS_V2.timeoutMs, + maxBuffer: CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes, killSignal: 'SIGTERM', + }); + } catch (error) { raw = { status: null, error }; } + const stdout = Buffer.from(raw?.stdout ?? ''); + const stderr = Buffer.from(raw?.stderr ?? ''); + const overflow = stdout.length > CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes || stderr.length > CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes || raw?.error?.code === 'ENOBUFS'; + const timeout = raw?.error?.code === 'ETIMEDOUT' || raw?.timedOut === true; + const observation = Object.freeze({ + schema: 'torch.dev/candidate-source-execution/v2alpha1', + terminal: Object.freeze({ kind: timeout ? 'timeout' : overflow ? 'overflow' : raw?.signal ? 'signal' : raw?.error ? 'error' : 'exit', exitStatus: Number.isInteger(raw?.status) ? raw.status : null, signal: raw?.signal ?? null, overflow, timeout, success: !timeout && !overflow && !raw?.signal && !raw?.error && raw?.status === 0 }), + output: Object.freeze({ stdout: stdout.subarray(0, CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes), stderr: stderr.subarray(0, CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes) }), + }); + snapshotRuntime.verify(attempt); + results.set(attempt, observation); + return observation; + } + function observation(attempt) { + const value = results.get(attempt); + if (!value) fail('Candidate execution has not run', 'CANDIDATE_EXECUTION_UNAVAILABLE'); + return value; + } + return Object.freeze({ run, observation }); } diff --git a/test/fleet/candidate-execution-fence.test.mjs b/test/fleet/candidate-execution-fence.test.mjs index 7b7590c..00d6ad1 100644 --- a/test/fleet/candidate-execution-fence.test.mjs +++ b/test/fleet/candidate-execution-fence.test.mjs @@ -1,5 +1,13 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { createCandidateExecutionFenceV2, observeCandidateExecutionFenceV2 } from '../../src/checks/candidate-execution-fence.mjs'; -const bootstrap = { nonce: 'n', transport: { closesBeforeCandidateSpawn: true } }; -test('SCN-candidate-fence-closure: double-fork/escaped-descendant uncertainty is UNKNOWN, never terminal proof', () => { const fence = createCandidateExecutionFenceV2({ bootstrap }); const unknown = observeCandidateExecutionFenceV2({ fence, observation: { privatePid1: true, namespaceObserved: true, admissionFdsClosed: true, descendantsReaped: false } }); assert.equal(unknown.outcome, 'unknown'); assert.equal(unknown.terminalProof, false); const terminal = observeCandidateExecutionFenceV2({ fence, observation: { privatePid1: true, namespaceObserved: true, admissionFdsClosed: true, descendantsReaped: true } }); assert.equal(terminal.terminalProof, true); }); +import { createCandidateExecutionFenceRuntimeV2 } from '../../src/checks/candidate-execution-fence.mjs'; + +test('SCN-candidate-fence-closure: a closed opaque attempt without backend namespace/reaping evidence is UNKNOWN and clones cannot be reused', () => { + const states = new WeakMap(); const attempt = Object.freeze({}); states.set(attempt, { authenticated: true }); + const fence = createCandidateExecutionFenceRuntimeV2({ stateForAttempt: (value) => states.get(value), backendObservation: () => null }); + assert.throws(() => fence.close({}), { code: 'CANDIDATE_FENCE_UNAVAILABLE' }); + fence.close(attempt); const terminal = fence.terminal(attempt); + assert.equal(terminal.outcome, 'unknown'); assert.equal(terminal.terminalProof, false); + assert.throws(() => fence.terminal({ ...attempt }), { code: 'CANDIDATE_FENCE_UNAVAILABLE' }); + assert.throws(() => fence.close(attempt), { code: 'CANDIDATE_FENCE_UNAVAILABLE' }); +}); diff --git a/test/fleet/candidate-input-consumption.test.mjs b/test/fleet/candidate-input-consumption.test.mjs index 3aac9fb..6ab8ca0 100644 --- a/test/fleet/candidate-input-consumption.test.mjs +++ b/test/fleet/candidate-input-consumption.test.mjs @@ -1,10 +1,28 @@ import assert from 'node:assert/strict'; -import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { chmodSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; -import { captureCandidateInputSnapshotV2, verifyCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; -import { runCandidateSourceExecutionV2 } from '../../src/checks/candidate-source-execution.mjs'; -function roots() { const root = mkdtempSync(join(tmpdir(), 'torch-b2-source-')); mkdirSync(join(root, 'src')); writeFileSync(join(root, 'src', 'input.txt'), 'captured'); return { root, snapshot: `${root}-snapshot` }; } -test('SCN-candidate-input-consumption: child execution seam receives frozen cwd bytes after original source mutation', () => { const { root, snapshot: destination } = roots(); const input = captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: destination, paths: ['src'] }); writeFileSync(join(root, 'src', 'input.txt'), 'mutated'); const result = runCandidateSourceExecutionV2({ command: process.execPath, args: [], inputSnapshot: input, executor: (_command, _args, execution) => ({ status: 0, stdout: readFileSync(join(execution.cwd, 'src', 'input.txt')) }) }); assert.equal(result.terminal.success, true); assert.equal(result.output.stdout.toString(), 'captured'); assert.equal(verifyCandidateInputSnapshotV2(input).verified, true); }); -test('SCN-candidate-input-drift: frozen-input mutation refuses rather than relabeling evidence', () => { const { root, snapshot: destination } = roots(); const input = captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: destination, paths: ['src'] }); const file = join(input.inputRoot, 'src', 'input.txt'); chmodSync(file, 0o600); writeFileSync(file, 'drift'); assert.throws(() => verifyCandidateInputSnapshotV2(input), { code: 'CANDIDATE_SNAPSHOT_DRIFT' }); }); +import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs'; +import { createCandidateSourceExecutionRuntimeV2 } from '../../src/checks/candidate-source-execution.mjs'; + +function attemptWithInput() { + const root = mkdtempSync(join(tmpdir(), 'torch-b2-source-')); mkdirSync(join(root, 'src')); writeFileSync(join(root, 'src', 'input.txt'), 'captured'); + const states = new WeakMap(); const attempt = Object.freeze({}); + states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['src'] }, runtime: { command: realpathSync(process.execPath), args: ['-e', 'process.stdout.write(require("node:fs").readFileSync("src/input.txt"))'], environment: {} } }); + return { root, attempt, states }; +} + +test('SCN-candidate-input-consumption: an actual fixed Node child reads frozen bytes after the original source mutates', () => { + const { root, attempt, states } = attemptWithInput(); + const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); + snapshots.capture(attempt); writeFileSync(join(root, 'src', 'input.txt'), 'mutated'); + const execution = createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt); + assert.equal(execution.terminal.success, true); assert.equal(execution.output.stdout.toString(), 'captured'); +}); + +test('SCN-candidate-input-drift: a post-capture mutation refuses before a real child can be launched', () => { + const { attempt, states } = attemptWithInput(); const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); + snapshots.capture(attempt); const file = join(snapshots.cwd(attempt), 'src', 'input.txt'); chmodSync(file, 0o600); writeFileSync(file, 'drift'); + assert.throws(() => createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt), { code: 'CANDIDATE_SNAPSHOT_DRIFT' }); +}); diff --git a/test/fleet/candidate-snapshot-policy.test.mjs b/test/fleet/candidate-snapshot-policy.test.mjs index 61c2352..19a8887 100644 --- a/test/fleet/candidate-snapshot-policy.test.mjs +++ b/test/fleet/candidate-snapshot-policy.test.mjs @@ -3,5 +3,12 @@ import { mkdtempSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; -import { captureCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; -test('SCN-candidate-snapshot-policy: product links and S caps refuse before a snapshot is admitted', () => { const root = mkdtempSync(join(tmpdir(), 'torch-b2-policy-')); writeFileSync(join(root, 'file'), 'x'); symlinkSync('file', join(root, 'link')); assert.throws(() => captureCandidateInputSnapshotV2({ sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['link'] }), { code: 'CANDIDATE_SNAPSHOT_LINK_REJECTED' }); }); +import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs'; + +test('SCN-candidate-snapshot-policy: an opaque service attempt still refuses product links before capture', () => { + const root = mkdtempSync(join(tmpdir(), 'torch-b2-policy-')); writeFileSync(join(root, 'file'), 'x'); symlinkSync('file', join(root, 'link')); + const states = new WeakMap(); const attempt = Object.freeze({}); states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['link'] } }); + const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); + assert.throws(() => snapshots.capture(attempt), { code: 'CANDIDATE_SNAPSHOT_LINK_REJECTED' }); + assert.throws(() => snapshots.capture(Object.freeze({})), { code: 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED' }); +}); diff --git a/test/fleet/candidate-source-bootstrap.test.mjs b/test/fleet/candidate-source-bootstrap.test.mjs index 95116dc..f496762 100644 --- a/test/fleet/candidate-source-bootstrap.test.mjs +++ b/test/fleet/candidate-source-bootstrap.test.mjs @@ -1,12 +1,28 @@ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import test from 'node:test'; -import { issueCandidateNativeAdmissionV2, openCandidateSourceBootstrapV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; +import { createCandidateSourceBootstrapRuntimeV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; -const issued = (expiresAt = '2030-01-01T00:00:00.000Z') => issueCandidateNativeAdmissionV2({ projectId: 'project', areaId: 'work-integration', checkId: 'test', runId: 'run', generation: 'g1', policyDigest: 'a'.repeat(64), engineDigest: 'b'.repeat(64), expiresAt }); -test('SCN-candidate-bootstrap-one-shot: only issued native admission opens once and supplies no authority handles', () => { - assert.throws(() => openCandidateSourceBootstrapV2({}), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); - const admission = issued(); const bootstrap = openCandidateSourceBootstrapV2(admission, { now: () => new Date('2029-01-01T00:00:00.000Z') }); - assert.equal(bootstrap.transport.closesBeforeCandidateSpawn, true); assert.deepEqual(Object.keys(bootstrap).sort(), ['engineDigest', 'expiresAt', 'generation', 'nonce', 'policyDigest', 'runId', 'schema', 'transport']); - assert.equal(Object.hasOwn(bootstrap, 'controlPlane'), false); assert.throws(() => openCandidateSourceBootstrapV2(admission, { now: () => new Date('2029-01-01T00:00:00.000Z') }), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); +function planState(expiresAt = '2030-01-01T00:00:00.000Z') { + const policyBytes = Buffer.from('policy\n'); + return { admissionId: 'admission-1', leaseId: 'lease-1', checkId: 'test', runGeneration: 1, guardGeneration: 1, policyBytes, policyDigest: createHash('sha256').update(policyBytes).digest('hex'), engine: { pre: {}, post: {} }, subject: {}, artifactManifest: { schema: 'torch.dev/artifact-manifest/v1alpha1', manifestSha256: 'a'.repeat(64) }, expiresAt }; +} + +test('SCN-candidate-bootstrap-one-shot: only a service-issued opaque plan opens once and its observed FD acknowledgement closes it', () => { + const states = new WeakMap(); const plan = Object.freeze({}); states.set(plan, planState()); + const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value), now: () => new Date('2029-01-01T00:00:00.000Z') }); + assert.throws(() => runtime.create(Object.freeze({})), { code: 'CANDIDATE_NATIVE_ADMISSION_REQUIRED' }); + const handle = runtime.create(plan); const clone = { ...handle }; + assert.throws(() => runtime.open(clone), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); + const channel = runtime.open(handle); + assert.equal(JSON.parse(channel.frame).receiptAdapterAdmission, null); + runtime.acknowledgeAndClose(handle, channel.acknowledgement); + assert.throws(() => runtime.open(handle), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); + assert.throws(() => runtime.acknowledgeAndClose(handle, channel.acknowledgement), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' }); +}); + +test('SCN-candidate-bootstrap-expiry: an expired service plan refuses before a parent frame is made', () => { + const states = new WeakMap(); const plan = Object.freeze({}); states.set(plan, planState('2020-01-01T00:00:00.000Z')); + const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value) }); + assert.throws(() => runtime.open(runtime.create(plan)), { code: 'CANDIDATE_NATIVE_ADMISSION_EXPIRED' }); }); -test('SCN-candidate-bootstrap-expiry: expired admission refuses before any child protocol exists', () => assert.throws(() => openCandidateSourceBootstrapV2(issued('2020-01-01T00:00:00.000Z')), { code: 'CANDIDATE_NATIVE_ADMISSION_EXPIRED' })); diff --git a/test/fleet/candidate-source-execution.test.mjs b/test/fleet/candidate-source-execution.test.mjs index 11b5f0e..c3b0025 100644 --- a/test/fleet/candidate-source-execution.test.mjs +++ b/test/fleet/candidate-source-execution.test.mjs @@ -1,9 +1,16 @@ import assert from 'node:assert/strict'; -import { mkdtempSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; -import { captureCandidateInputSnapshotV2 } from '../../src/checks/candidate-input-snapshot.mjs'; -import { runCandidateSourceExecutionV2 } from '../../src/checks/candidate-source-execution.mjs'; -function snapshot() { const source = mkdtempSync(join(tmpdir(), 'torch-b2-execution-')); writeFileSync(join(source, 'input'), 'x'); return captureCandidateInputSnapshotV2({ sourceRoot: source, destinationRoot: `${source}-snapshot`, paths: ['input'] }); } -test('SCN-candidate-execution-boundary: ambient PATH/HOME fallback and output overflow cannot become success', () => { const frozen = snapshot(); assert.throws(() => runCandidateSourceExecutionV2({ command: process.execPath, inputSnapshot: frozen, environment: { PATH: '/bin' } }), { code: 'CANDIDATE_EXECUTION_INVALID' }); const observed = runCandidateSourceExecutionV2({ command: process.execPath, inputSnapshot: frozen, limits: { timeoutMs: 1000, maxOutputBytes: 4, maxArgs: 64, maxArgBytes: 4096 }, executor: () => ({ status: 0, stdout: Buffer.from('xxxxxxxx') }) }); assert.equal(observed.terminal.overflow, true); assert.equal(observed.terminal.success, false); }); +import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs'; +import { createCandidateSourceExecutionRuntimeV2 } from '../../src/checks/candidate-source-execution.mjs'; + +test('SCN-candidate-execution-boundary: only fixed E is run and actual zero-exit output overflow is not success', () => { + const root = mkdtempSync(join(tmpdir(), 'torch-b2-execution-')); writeFileSync(join(root, 'input'), 'x'); + const states = new WeakMap(); const attempt = Object.freeze({}); + states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['input'] }, runtime: { command: realpathSync(process.execPath), args: ['-e', 'process.stdout.write("x".repeat(1048577))'], environment: {} } }); + const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); snapshots.capture(attempt); + const result = createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt); + assert.equal(result.terminal.overflow, true); assert.equal(result.terminal.success, false); +}); diff --git a/test/fleet/candidate-source-parent.test.mjs b/test/fleet/candidate-source-parent.test.mjs index b1c426b..bf3c7d4 100644 --- a/test/fleet/candidate-source-parent.test.mjs +++ b/test/fleet/candidate-source-parent.test.mjs @@ -1,8 +1,14 @@ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import test from 'node:test'; -import { issueCandidateNativeAdmissionV2, openCandidateSourceBootstrapV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; -test('SCN-candidate-parent-capability: bootstrap frame carries only a bound tuple, never roots, env, writers, adapters, or receipt authority', () => { - const ticket = issueCandidateNativeAdmissionV2({ projectId: 'p', areaId: 'a', checkId: 'c', runId: 'r', generation: 'g', policyDigest: 'a'.repeat(64), engineDigest: 'b'.repeat(64), expiresAt: '2030-01-01T00:00:00.000Z' }); - const frame = openCandidateSourceBootstrapV2(ticket, { now: () => new Date('2029-01-01T00:00:00.000Z') }); - for (const forbidden of ['projectId', 'areaId', 'checkId', 'inputRoot', 'cwd', 'environment', 'writer', 'adapter', 'receipt']) assert.equal(Object.hasOwn(frame, forbidden), false); +import { createCandidateSourceBootstrapRuntimeV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; + +test('SCN-candidate-parent-capability: the private FD3 record carries M only and reserves A as null', () => { + const policyBytes = Buffer.from('policy\n'); const states = new WeakMap(); const plan = Object.freeze({}); + states.set(plan, { admissionId: 'admission-2', leaseId: 'lease-2', checkId: 'test', runGeneration: 1, guardGeneration: 2, policyBytes, policyDigest: createHash('sha256').update(policyBytes).digest('hex'), engine: { pre: {}, post: {} }, subject: {}, artifactManifest: { schema: 'torch.dev/artifact-manifest/v1alpha1', manifestSha256: 'a'.repeat(64) }, expiresAt: '2030-01-01T00:00:00.000Z' }); + const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value), now: () => new Date('2029-01-01T00:00:00.000Z') }); + const frame = JSON.parse(runtime.open(runtime.create(plan)).frame); + for (const forbidden of ['root', 'cwd', 'environment', 'writer', 'adapter', 'receipt', 'command', 'executor']) assert.equal(Object.hasOwn(frame, forbidden), false); + assert.equal(frame.receiptAdapterAdmission, null); + assert.deepEqual(Object.keys(frame.artifactManifest).sort(), ['manifestSha256', 'schema']); }); From 7e0734ae90314b6182046b1d813efd40b453743f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 01:47:13 -0400 Subject: [PATCH 04/15] TASK-pilot-brief-sandbox: fail closed read-only convergence --- src/convergence/service.mjs | 79 ++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/convergence/service.mjs b/src/convergence/service.mjs index a48ee13..2b1bf13 100644 --- a/src/convergence/service.mjs +++ b/src/convergence/service.mjs @@ -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 }; From 2c59634f81ad0d61514fff21e8c05d896a46b451 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 16:59:14 -0400 Subject: [PATCH 05/15] feat(integration): add source-only adoption preparation TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- src/integration/adoption.mjs | 226 +++++++++++++++++++++++++++++++++++ src/integration/service.mjs | 15 +++ 2 files changed, 241 insertions(+) create mode 100644 src/integration/adoption.mjs diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs new file mode 100644 index 0000000..495c6e1 --- /dev/null +++ b/src/integration/adoption.mjs @@ -0,0 +1,226 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { TorchError } from '../kernel/errors.mjs'; +import { observeRuntimeTurn } from '../runtime/turn-guard.mjs'; + +const CAPABILITY = 'integration.adoption.prepare'; +const ADOPTION_SCHEMA = 'torch.dev/integration-adoption/v1alpha1'; +const SHA = /^[0-9a-f]{40,64}$/; +const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/; + +function git(root, args, { allowFailure = false } = {}) { + const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); + if (result.status !== 0 && !allowFailure) { + throw new TorchError(`Git command failed: git ${args.join(' ')}`, { + code: 'GIT_COMMAND_FAILED', details: result.stderr?.trim() || null, + }); + } + return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' }; +} + +function tableExists(database, name) { + return Boolean(database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)); +} + +function text(value, name) { + if (typeof value !== 'string' || !value.trim()) { + throw new TorchError(`${name} is required`, { code: 'ADOPTION_INPUT_INVALID', details: { name } }); + } + return value.trim(); +} + +function sha(value, name) { + const normalized = text(value, name); + if (!SHA.test(normalized)) throw new TorchError(`${name} must be a full Git object id`, { + code: 'ADOPTION_INPUT_INVALID', details: { name }, + }); + return normalized; +} + +function durableRecord(event) { + if (!event?.details) return null; + try { + const record = JSON.parse(event.details); + return record?.schema === ADOPTION_SCHEMA ? record : null; + } catch { return null; } +} + +function listWorktreePaths(root) { + const records = []; + for (const line of git(root, ['worktree', 'list', '--porcelain']).stdout.split('\n')) { + if (line.startsWith('worktree ')) { + records.push(line.slice('worktree '.length)); + } + } + return records; +} + +function approvalAllows(approval, capability) { + if (!approval || approval.status !== 'approved' || approval.approver_id !== 'owner') return false; + const scope = [approval.title, approval.summary, approval.evidence, approval.decision_note] + .filter(Boolean).join('\n').toLowerCase(); + return capability === CAPABILITY && scope.includes('adoption') && scope.includes('source'); +} + +/** + * Source-only adoption boundary. It persists only immutable prepared intents in + * the existing audit journal. It deliberately has no ref-update, branch, CLI, + * manifest, configuration, schema, or installation side effect. + */ +export class AdoptionService { + constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy } = {}) { + this.repositoryRoot = repositoryRoot; + this.controlPlane = controlPlane; + this.clock = clock; + this.idFactory = idFactory; + this.policy = policy ?? {}; + } + + plan({ actorId, approvalId, approvalRevision, capability = CAPABILITY, adoptionId, + candidateSha, baseTargetSha, provenance, provenanceManifestDigest, contributorArea, + destinationArea, targetBranch, candidateRef, expectedOldRef } = {}) { + const blockers = []; + const id = adoptionId ? text(adoptionId, 'adoptionId') : null; + if (id && !SAFE_ID.test(id)) blockers.push({ code: 'ADOPTION_ID_INVALID' }); + if (actorId !== 'owner') blockers.push({ code: 'OWNER_ACTOR_REQUIRED' }); + if (capability !== CAPABILITY) blockers.push({ code: 'ADOPTION_CAPABILITY_INVALID' }); + if (!Number.isInteger(approvalRevision) || approvalRevision < 1) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_REQUIRED' }); + + let candidate; let base; let provenanceDigest; + try { + candidate = sha(candidateSha, 'candidateSha'); + base = sha(baseTargetSha, 'baseTargetSha'); + if (!Array.isArray(provenance) || !provenance.length || provenance.some((entry) => typeof entry !== 'string' || !entry)) { + throw new TorchError('provenance must be a non-empty ordered string list', { code: 'ADOPTION_INPUT_INVALID' }); + } + provenanceDigest = createHash('sha256').update(JSON.stringify(provenance)).digest('hex'); + if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) { + blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest }); + } + } catch (error) { + blockers.push({ code: error.code ?? 'ADOPTION_INPUT_INVALID', details: error.details ?? null }); + } + + const target = targetBranch ?? this.policy.target; + if (typeof target !== 'string' || !target.trim()) blockers.push({ code: 'ADOPTION_TARGET_REQUIRED' }); + const candidateReference = candidateRef ?? (id ? `refs/torch/integration-candidates/${id}` : null); + const expectedReference = expectedOldRef ?? (target ? `refs/heads/${target}` : null); + if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) { + blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' }); + } + if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) { + blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' }); + } + + let contributor; let destination; + try { contributor = this.controlPlane.assertIdentity(text(contributorArea, 'contributorArea')); } + catch (error) { blockers.push({ code: 'ADOPTION_CONTRIBUTOR_INVALID', details: error.code ?? null }); } + try { destination = this.controlPlane.assertIdentity(text(destinationArea, 'destinationArea')); } + catch (error) { blockers.push({ code: 'ADOPTION_DESTINATION_INVALID', details: error.code ?? null }); } + if (contributor && destination && contributor === destination) blockers.push({ code: 'ADOPTION_IDENTITIES_MUST_DIFFER' }); + + const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests + WHERE id = ? AND project_id = ?`).get(approvalId, this.controlPlane.projectId) : null; + if (!approval) blockers.push({ code: 'ADOPTION_APPROVAL_MISSING' }); + else if (approval.revision !== approvalRevision) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_STALE', actual: approval.revision }); + else if (!approvalAllows(approval, capability)) blockers.push({ code: 'ADOPTION_APPROVAL_SCOPE_INVALID' }); + + if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) { + blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' }); + } + if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) { + blockers.push({ code: 'ADOPTION_BASE_MISSING' }); + } + if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) { + blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' }); + } + if (expectedReference && base) { + const current = git(this.repositoryRoot, ['rev-parse', expectedReference], { allowFailure: true }); + if (current.status !== 0) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' }); + else if (current.stdout !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current.stdout }); + } + if (candidateReference && git(this.repositoryRoot, ['show-ref', '--verify', '--quiet', candidateReference], { allowFailure: true }).status === 0) { + blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' }); + } + + for (const worktree of listWorktreePaths(this.repositoryRoot)) { + const entries = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']).stdout.split('\n').filter(Boolean); + if (entries.length) blockers.push({ code: 'ADOPTION_WORKTREE_DIRTY', worktree, entries }); + } + if (tableExists(this.controlPlane.database, 'worktree_guards')) { + const guards = this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards + WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId); + for (const guard of guards) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard }); + } + if (tableExists(this.controlPlane.database, 'resource_leases')) { + const leases = this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases + WHERE released_at IS NULL`).all(); + for (const lease of leases) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease }); + } + if (tableExists(this.controlPlane.database, 'prepared_checks')) { + const prepared = this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks + WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId); + for (const check of prepared) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check }); + } + for (const area of [contributor, destination].filter(Boolean)) { + const executor = observeRuntimeTurn({ stateRoot: this.controlPlane.stateRoot, repositoryRoot: this.repositoryRoot, + projectId: this.controlPlane.projectId, areaId: area, now: this.clock }); + if (executor.state !== 'inactive') blockers.push({ code: 'ADOPTION_EXECUTOR_NOT_INACTIVE', areaId: area, executor }); + } + + return { + action: 'plan-adoption', adoptionId: id, capability, approval: approval ? { + id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id, + } : null, + candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null, + contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, + candidateRef: candidateReference, expectedOldRef: expectedReference, blockers, + canProceed: blockers.length === 0, mutationPerformed: false, + }; + } + + prepare(input = {}) { + const adoptionId = input.adoptionId ?? this.idFactory(); + const plan = this.plan({ ...input, adoptionId }); + if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { + code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers, + }); + const existing = this.controlPlane.database.prepare(`SELECT details FROM audit_events + WHERE project_id = ? AND entity_type = 'integration-adoption' AND entity_id = ? + ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, adoptionId); + if (existing) throw new TorchError('Adoption id is already journaled; reconcile its immutable record instead.', { + code: 'ADOPTION_ALREADY_EXISTS', details: { adoptionId }, + }); + const now = this.clock().toISOString(); + const record = { + schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'), + approvalRevision: input.approvalRevision, capability: plan.capability, candidateSha: plan.candidateSha, + baseTargetSha: plan.baseTargetSha, provenanceManifestDigest: plan.provenanceManifestDigest, + candidateRef: plan.candidateRef, expectedOldRef: plan.expectedOldRef, + contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch, + archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, + }; + this.controlPlane.database.exec('BEGIN IMMEDIATE'); + try { + this.controlPlane.database.prepare(`INSERT INTO audit_events + (id, project_id, actor_id, operation, entity_type, entity_id, details, created_at) + VALUES (?, ?, 'owner', 'integration.adoption.prepare', 'integration-adoption', ?, ?, ?)`) + .run(this.idFactory(), this.controlPlane.projectId, adoptionId, JSON.stringify(record), now); + this.controlPlane.database.exec('COMMIT'); + } catch (error) { + this.controlPlane.database.exec('ROLLBACK'); + throw error; + } + return { ...record, mutationPerformed: true }; + } + + getPrepared(adoptionId) { + const id = text(adoptionId, 'adoptionId'); + const row = this.controlPlane.database.prepare(`SELECT details FROM audit_events + WHERE project_id = ? AND entity_type = 'integration-adoption' AND entity_id = ? + ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, id); + const record = durableRecord(row); + if (!record) throw new TorchError('Unknown adoption journal record', { code: 'ADOPTION_NOT_FOUND', details: { adoptionId: id } }); + return record; + } +} diff --git a/src/integration/service.mjs b/src/integration/service.mjs index c996634..d144240 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -4,6 +4,7 @@ import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { loadFleetDefinition } from '../kernel/worktrees.mjs'; import { BacklogService } from '../backlog/service.mjs'; +import { AdoptionService } from './adoption.mjs'; function git(root, args, { allowFailure = false } = {}) { const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); @@ -42,6 +43,8 @@ export class IntegrationService { this.worktrees = new Map((manifest.external ?? []) .filter((entry) => entry.type === 'worktree') .map((entry) => [entry.area, entry])); + this.adoption = new AdoptionService({ repositoryRoot, controlPlane, clock, idFactory, + policy: { target: this.policy.target } }); this.controlPlane.database.exec(` CREATE TABLE IF NOT EXISTS integration_requests ( id TEXT PRIMARY KEY, @@ -110,6 +113,18 @@ export class IntegrationService { return this.evaluate(record.id); } + planAdoption(input = {}) { + return this.adoption.plan(input); + } + + prepareAdoption(input = {}) { + return this.adoption.prepare(input); + } + + getPreparedAdoption(adoptionId) { + return this.adoption.getPrepared(adoptionId); + } + get(requestId) { const row = this.controlPlane.database.prepare('SELECT * FROM integration_requests WHERE id = ?').get(requestId); if (!row) throw new TorchError(`Unknown integration request: ${requestId}`, { code: 'INTEGRATION_NOT_FOUND' }); From 8f894e9863e6be5bb11cb7e5548bf68ccac980fc Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:08:19 -0400 Subject: [PATCH 06/15] fix(integration): bind adoption actions to exact authority TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- src/integration/adoption.mjs | 219 +++++++++++++++++++++++------------ src/integration/service.mjs | 18 +++ 2 files changed, 160 insertions(+), 77 deletions(-) diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index 495c6e1..fc06c04 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -3,8 +3,10 @@ import { spawnSync } from 'node:child_process'; import { TorchError } from '../kernel/errors.mjs'; import { observeRuntimeTurn } from '../runtime/turn-guard.mjs'; -const CAPABILITY = 'integration.adoption.prepare'; const ADOPTION_SCHEMA = 'torch.dev/integration-adoption/v1alpha1'; +const AUTHORITY_SCHEMA = 'torch.dev/integration-adoption-authority/v1alpha1'; +const ACTIONS = new Set(['plan', 'prepare', 'finalize', 'reconcile', 'rollback', 'request']); +const MUTATING_ACTIONS = new Set(['prepare', 'finalize', 'rollback', 'request']); const SHA = /^[0-9a-f]{40,64}$/; const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/; @@ -48,42 +50,55 @@ function durableRecord(event) { function listWorktreePaths(root) { const records = []; for (const line of git(root, ['worktree', 'list', '--porcelain']).stdout.split('\n')) { - if (line.startsWith('worktree ')) { - records.push(line.slice('worktree '.length)); - } + if (line.startsWith('worktree ')) records.push(line.slice('worktree '.length)); } return records; } -function approvalAllows(approval, capability) { - if (!approval || approval.status !== 'approved' || approval.approver_id !== 'owner') return false; - const scope = [approval.title, approval.summary, approval.evidence, approval.decision_note] - .filter(Boolean).join('\n').toLowerCase(); - return capability === CAPABILITY && scope.includes('adoption') && scope.includes('source'); +function authorityFrom(approval) { + if (!approval || approval.status !== 'approved' || approval.approver_id !== 'owner' || approval.decided_by !== 'owner') return null; + try { + const authority = JSON.parse(approval.decision_note ?? ''); + return authority?.schema === AUTHORITY_SCHEMA ? authority : null; + } catch { return null; } +} + +function exactAuthority(authority, binding) { + if (!authority || authority.action !== binding.action) return false; + const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest', + 'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef']; + return fields.every((field) => authority[field] === binding[field]) + && authority.fixture?.id === binding.fixture?.id + && authority.fixture?.adapter === binding.fixture?.adapter; +} + +function adapterReceipt(value) { + return createHash('sha256').update(JSON.stringify(value)).digest('hex'); } /** - * Source-only adoption boundary. It persists only immutable prepared intents in - * the existing audit journal. It deliberately has no ref-update, branch, CLI, - * manifest, configuration, schema, or installation side effect. + * Adoption is source-only. Ref-changing methods can only call an injected + * hermetic fixture adapter; the normal service has none. Approval is an exact, + * revision-bound JSON decision recorded by the existing approval service. */ export class AdoptionService { - constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy } = {}) { + constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, fixtureAdapter = null } = {}) { this.repositoryRoot = repositoryRoot; this.controlPlane = controlPlane; this.clock = clock; this.idFactory = idFactory; this.policy = policy ?? {}; + this.fixtureAdapter = fixtureAdapter; } - plan({ actorId, approvalId, approvalRevision, capability = CAPABILITY, adoptionId, + plan({ action = 'plan', actorId, approvalId, approvalRevision, adoptionId, candidateSha, baseTargetSha, provenance, provenanceManifestDigest, contributorArea, - destinationArea, targetBranch, candidateRef, expectedOldRef } = {}) { + destinationArea, targetBranch, candidateRef, expectedOldRef, fixture } = {}) { const blockers = []; const id = adoptionId ? text(adoptionId, 'adoptionId') : null; if (id && !SAFE_ID.test(id)) blockers.push({ code: 'ADOPTION_ID_INVALID' }); + if (!ACTIONS.has(action)) blockers.push({ code: 'ADOPTION_ACTION_INVALID' }); if (actorId !== 'owner') blockers.push({ code: 'OWNER_ACTOR_REQUIRED' }); - if (capability !== CAPABILITY) blockers.push({ code: 'ADOPTION_CAPABILITY_INVALID' }); if (!Number.isInteger(approvalRevision) || approvalRevision < 1) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_REQUIRED' }); let candidate; let base; let provenanceDigest; @@ -111,6 +126,8 @@ export class AdoptionService { if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) { blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' }); } + const normalizedFixture = fixture?.id && fixture?.adapter ? { id: fixture.id, adapter: fixture.adapter } : null; + if (MUTATING_ACTIONS.has(action) && !normalizedFixture) blockers.push({ code: 'ADOPTION_HERMETIC_FIXTURE_REQUIRED' }); let contributor; let destination; try { contributor = this.controlPlane.assertIdentity(text(contributorArea, 'contributorArea')); } @@ -121,105 +138,153 @@ export class AdoptionService { const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests WHERE id = ? AND project_id = ?`).get(approvalId, this.controlPlane.projectId) : null; + const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base, + provenanceManifestDigest: provenanceDigest, contributorArea: contributor, destinationArea: destination, + targetBranch: target, candidateRef: candidateReference, expectedOldRef: expectedReference, fixture: normalizedFixture }; if (!approval) blockers.push({ code: 'ADOPTION_APPROVAL_MISSING' }); else if (approval.revision !== approvalRevision) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_STALE', actual: approval.revision }); - else if (!approvalAllows(approval, capability)) blockers.push({ code: 'ADOPTION_APPROVAL_SCOPE_INVALID' }); + else if (!exactAuthority(authorityFrom(approval), binding)) blockers.push({ code: 'ADOPTION_APPROVAL_BINDING_INVALID' }); - if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) { - blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' }); - } - if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) { - blockers.push({ code: 'ADOPTION_BASE_MISSING' }); - } - if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) { - blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' }); - } + if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' }); + if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_BASE_MISSING' }); + if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' }); + const fixtureRefs = normalizedFixture && this.fixtureAdapter + && this.fixtureAdapter.id === normalizedFixture.id && this.fixtureAdapter.name === normalizedFixture.adapter; if (expectedReference && base) { - const current = git(this.repositoryRoot, ['rev-parse', expectedReference], { allowFailure: true }); + const current = fixtureRefs + ? { status: 0, stdout: this.fixtureAdapter.readRefs([expectedReference])[expectedReference] ?? '' } + : git(this.repositoryRoot, ['rev-parse', expectedReference], { allowFailure: true }); if (current.status !== 0) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' }); else if (current.stdout !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current.stdout }); } - if (candidateReference && git(this.repositoryRoot, ['show-ref', '--verify', '--quiet', candidateReference], { allowFailure: true }).status === 0) { - blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' }); - } + const candidateExists = candidateReference && (fixtureRefs + ? this.fixtureAdapter.readRefs([candidateReference])[candidateReference] != null + : git(this.repositoryRoot, ['show-ref', '--verify', '--quiet', candidateReference], { allowFailure: true }).status === 0); + if (['plan', 'prepare', 'finalize'].includes(action) && candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' }); + if (['reconcile', 'rollback', 'request'].includes(action) && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' }); for (const worktree of listWorktreePaths(this.repositoryRoot)) { const entries = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']).stdout.split('\n').filter(Boolean); if (entries.length) blockers.push({ code: 'ADOPTION_WORKTREE_DIRTY', worktree, entries }); } if (tableExists(this.controlPlane.database, 'worktree_guards')) { - const guards = this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards - WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId); - for (const guard of guards) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard }); + for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards + WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard }); } if (tableExists(this.controlPlane.database, 'resource_leases')) { - const leases = this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases - WHERE released_at IS NULL`).all(); - for (const lease of leases) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease }); + for (const lease of this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases WHERE released_at IS NULL`).all()) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease }); } if (tableExists(this.controlPlane.database, 'prepared_checks')) { - const prepared = this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks - WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId); - for (const check of prepared) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check }); + for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks + WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check }); } for (const area of [contributor, destination].filter(Boolean)) { const executor = observeRuntimeTurn({ stateRoot: this.controlPlane.stateRoot, repositoryRoot: this.repositoryRoot, projectId: this.controlPlane.projectId, areaId: area, now: this.clock }); if (executor.state !== 'inactive') blockers.push({ code: 'ADOPTION_EXECUTOR_NOT_INACTIVE', areaId: area, executor }); } - - return { - action: 'plan-adoption', adoptionId: id, capability, approval: approval ? { - id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id, - } : null, - candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null, + return { action: `plan-adoption-${action}`, adoptionId: id, approval: approval ? { id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id } : null, + authority: binding, candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null, contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, - candidateRef: candidateReference, expectedOldRef: expectedReference, blockers, - canProceed: blockers.length === 0, mutationPerformed: false, - }; + candidateRef: candidateReference, expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false }; } - prepare(input = {}) { - const adoptionId = input.adoptionId ?? this.idFactory(); - const plan = this.plan({ ...input, adoptionId }); - if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { - code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers, - }); - const existing = this.controlPlane.database.prepare(`SELECT details FROM audit_events - WHERE project_id = ? AND entity_type = 'integration-adoption' AND entity_id = ? - ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, adoptionId); - if (existing) throw new TorchError('Adoption id is already journaled; reconcile its immutable record instead.', { - code: 'ADOPTION_ALREADY_EXISTS', details: { adoptionId }, - }); + #record(adoptionId, record, operation) { const now = this.clock().toISOString(); - const record = { - schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'), - approvalRevision: input.approvalRevision, capability: plan.capability, candidateSha: plan.candidateSha, - baseTargetSha: plan.baseTargetSha, provenanceManifestDigest: plan.provenanceManifestDigest, - candidateRef: plan.candidateRef, expectedOldRef: plan.expectedOldRef, - contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch, - archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, - }; this.controlPlane.database.exec('BEGIN IMMEDIATE'); try { this.controlPlane.database.prepare(`INSERT INTO audit_events (id, project_id, actor_id, operation, entity_type, entity_id, details, created_at) - VALUES (?, ?, 'owner', 'integration.adoption.prepare', 'integration-adoption', ?, ?, ?)`) - .run(this.idFactory(), this.controlPlane.projectId, adoptionId, JSON.stringify(record), now); + VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`) + .run(this.idFactory(), this.controlPlane.projectId, operation, adoptionId, JSON.stringify(record), now); this.controlPlane.database.exec('COMMIT'); - } catch (error) { - this.controlPlane.database.exec('ROLLBACK'); - throw error; + } catch (error) { this.controlPlane.database.exec('ROLLBACK'); throw error; } + } + + #fixture(input) { + if (!this.fixtureAdapter || input.fixture?.adapter !== this.fixtureAdapter.name || input.fixture?.id !== this.fixtureAdapter.id) { + throw new TorchError('Adoption mutation is available only through its named hermetic fixture adapter', { + code: 'ADOPTION_HERMETIC_ADAPTER_REQUIRED', details: { fixture: input.fixture ?? null }, + }); } + return this.fixtureAdapter; + } + + prepare(input = {}) { + const adoptionId = input.adoptionId ?? this.idFactory(); + const plan = this.plan({ ...input, action: 'prepare', adoptionId }); + if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers }); + this.#fixture(input); + if (this.#find(adoptionId)) throw new TorchError('Adoption id is already journaled; reconcile its immutable record instead.', { code: 'ADOPTION_ALREADY_EXISTS', details: { adoptionId } }); + const now = this.clock().toISOString(); + const record = { schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'), + approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha, baseTargetSha: plan.baseTargetSha, + provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef, expectedOldRef: plan.expectedOldRef, + contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch, + archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, rolledBackAt: null }; + this.#record(adoptionId, record, 'integration.adoption.prepare'); return { ...record, mutationPerformed: true }; } + finalize(input = {}) { + const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); + if (record.state !== 'prepared') throw new TorchError('Only a prepared adoption can be finalized', { code: 'ADOPTION_FINALIZE_STATE_INVALID', details: { state: record.state } }); + const plan = this.plan({ ...record, ...input, action: 'finalize' }); + if (!plan.canProceed) throw new TorchError('Adoption finalize is blocked', { code: 'ADOPTION_FINALIZE_BLOCKED', details: plan.blockers }); + const adapter = this.#fixture(input); + const receipt = adapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }], + writes: [{ ref: record.archiveRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: record.candidateSha }] }); + const finalized = { ...record, revision: record.revision + 1, state: 'finalized', finalizeApprovalId: text(input.approvalId, 'approvalId'), + finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), fixtureReceiptDigest: adapterReceipt(receipt) }; + this.#record(record.id, finalized, 'integration.adoption.finalize'); + return { ...finalized, mutationPerformed: true }; + } + + reconcile(input = {}) { + const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); + const plan = this.plan({ ...record, ...input, action: 'reconcile' }); + if (!plan.canProceed) return { record, plan, state: 'blocked', mutationPerformed: false }; + const adapter = this.#fixture(input); + const refs = adapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]); + const consistent = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha; + return { record, refs, state: consistent ? 'consistent' : 'diverged', mutationPerformed: false }; + } + + rollback(input = {}) { + const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); + if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can be rolled back', { code: 'ADOPTION_ROLLBACK_STATE_INVALID', details: { state: record.state } }); + const plan = this.plan({ ...record, ...input, action: 'rollback' }); + if (!plan.canProceed) throw new TorchError('Adoption rollback is blocked', { code: 'ADOPTION_ROLLBACK_BLOCKED', details: plan.blockers }); + const receipt = this.#fixture(input).compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] }); + const rolledBack = { ...record, revision: record.revision + 1, state: 'rolled-back', rollbackApprovalId: text(input.approvalId, 'approvalId'), + rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: adapterReceipt(receipt) }; + this.#record(record.id, rolledBack, 'integration.adoption.rollback'); + return { ...rolledBack, mutationPerformed: true }; + } + + requestAdoptedCandidate(input = {}, { requestIntegration } = {}) { + const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); + if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can request native qualification', { code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state } }); + const plan = this.plan({ ...record, ...input, action: 'request' }); + if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers }); + this.#fixture(input); + if (typeof requestIntegration !== 'function') throw new TorchError('Native integration request adapter is unavailable', { code: 'ADOPTION_NATIVE_REQUEST_UNAVAILABLE' }); + const request = requestIntegration({ areaId: record.contributorArea, commit: record.candidateSha }); + const requested = { ...record, revision: record.revision + 1, state: 'requested', requestApprovalId: text(input.approvalId, 'approvalId'), + requestApprovalRevision: input.approvalRevision, integrationRequestId: request.id, requestedAt: this.clock().toISOString() }; + this.#record(record.id, requested, 'integration.adoption.request'); + return { ...requested, integrationRequest: request, mutationPerformed: true }; + } + + #find(adoptionId) { + const row = this.controlPlane.database.prepare(`SELECT details FROM audit_events WHERE project_id = ? + AND entity_type = 'integration-adoption' AND entity_id = ? ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, adoptionId); + return durableRecord(row); + } + getPrepared(adoptionId) { const id = text(adoptionId, 'adoptionId'); - const row = this.controlPlane.database.prepare(`SELECT details FROM audit_events - WHERE project_id = ? AND entity_type = 'integration-adoption' AND entity_id = ? - ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, id); - const record = durableRecord(row); + const record = this.#find(id); if (!record) throw new TorchError('Unknown adoption journal record', { code: 'ADOPTION_NOT_FOUND', details: { adoptionId: id } }); return record; } diff --git a/src/integration/service.mjs b/src/integration/service.mjs index d144240..dcd9eb0 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -125,6 +125,24 @@ export class IntegrationService { return this.adoption.getPrepared(adoptionId); } + finalizeAdoption(input = {}) { + return this.adoption.finalize(input); + } + + reconcileAdoption(input = {}) { + return this.adoption.reconcile(input); + } + + rollbackAdoption(input = {}) { + return this.adoption.rollback(input); + } + + requestAdoptedCandidate(input = {}) { + return this.adoption.requestAdoptedCandidate(input, { + requestIntegration: ({ areaId, commit }) => this.request({ areaId, commit }), + }); + } + get(requestId) { const row = this.controlPlane.database.prepare('SELECT * FROM integration_requests WHERE id = ?').get(requestId); if (!row) throw new TorchError(`Unknown integration request: ${requestId}`, { code: 'INTEGRATION_NOT_FOUND' }); From 97973f0566c43c72de790ae55b6174a5f72abde5 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:18:20 -0400 Subject: [PATCH 07/15] fix(integration): recover private ref adoption TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- src/integration/adoption.mjs | 218 ++++++++++++++++++++++------------- src/integration/service.mjs | 58 +++++++++- 2 files changed, 194 insertions(+), 82 deletions(-) diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index fc06c04..2aeb1d5 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -1,14 +1,17 @@ import { createHash, randomUUID } from 'node:crypto'; import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; import { observeRuntimeTurn } from '../runtime/turn-guard.mjs'; const ADOPTION_SCHEMA = 'torch.dev/integration-adoption/v1alpha1'; const AUTHORITY_SCHEMA = 'torch.dev/integration-adoption-authority/v1alpha1'; +const ADAPTER_NAME = 'git-private-ref-cas-v1'; const ACTIONS = new Set(['plan', 'prepare', 'finalize', 'reconcile', 'rollback', 'request']); const MUTATING_ACTIONS = new Set(['prepare', 'finalize', 'rollback', 'request']); const SHA = /^[0-9a-f]{40,64}$/; const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/; +const ZERO_SHA = '0'.repeat(40); function git(root, args, { allowFailure = false } = {}) { const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); @@ -39,6 +42,10 @@ function sha(value, name) { return normalized; } +function digest(value) { + return createHash('sha256').update(JSON.stringify(value)).digest('hex'); +} + function durableRecord(event) { if (!event?.details) return null; try { @@ -68,38 +75,86 @@ function exactAuthority(authority, binding) { const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest', 'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef']; return fields.every((field) => authority[field] === binding[field]) - && authority.fixture?.id === binding.fixture?.id - && authority.fixture?.adapter === binding.fixture?.adapter; + && authority.adapter?.name === binding.adapter?.name + && authority.adapter?.id === binding.adapter?.id; } -function adapterReceipt(value) { - return createHash('sha256').update(JSON.stringify(value)).digest('hex'); +function exactInput(record, input) { + const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest', + 'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef']; + const mismatches = fields.filter((field) => input[field] !== undefined && input[field] !== record[field]); + if (input.provenance !== undefined && JSON.stringify(input.provenance) !== JSON.stringify(record.provenance)) mismatches.push('provenance'); + if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', { + code: 'ADOPTION_IMMUTABLE_BINDING_STALE', details: { adoptionId: record.id, fields: mismatches }, + }); } /** - * Adoption is source-only. Ref-changing methods can only call an injected - * hermetic fixture adapter; the normal service has none. Approval is an exact, - * revision-bound JSON decision recorded by the existing approval service. + * Real Git private-ref transaction adapter. It is source code only: callers + * decide when it is invoked, while the adapter itself gives temp-repository + * scenarios the same atomic update-ref semantics as a real repository. */ +export class GitPrivateRefAdapter { + constructor({ repositoryRoot } = {}) { + this.repositoryRoot = resolve(text(repositoryRoot, 'repositoryRoot')); + this.name = ADAPTER_NAME; + this.id = digest({ adapter: this.name, repositoryRoot: this.repositoryRoot }); + } + + readRefs(refs) { + const values = {}; + for (const ref of refs) { + const result = git(this.repositoryRoot, ['rev-parse', '--verify', '--quiet', ref], { allowFailure: true }); + values[ref] = result.status === 0 ? result.stdout : null; + } + return values; + } + + compareAndSwapRefs({ expected = [], writes = [] } = {}) { + const commands = ['start']; + for (const { ref, sha: expectedSha } of expected) { + commands.push(`verify ${text(ref, 'ref')} ${expectedSha ?? ZERO_SHA}`); + } + for (const { ref, sha: nextSha } of writes) { + const current = expected.find((entry) => entry.ref === ref)?.sha ?? null; + if (nextSha === null) commands.push(`delete ${text(ref, 'ref')} ${current ?? ZERO_SHA}`); + else commands.push(`update ${text(ref, 'ref')} ${sha(nextSha, 'next ref sha')} ${current ?? ZERO_SHA}`); + } + commands.push('prepare', 'commit', ''); + const result = spawnSync('git', ['-C', this.repositoryRoot, 'update-ref', '--stdin'], { + input: commands.join('\n'), encoding: 'utf8', + }); + if (result.status !== 0) throw new TorchError('Private ref compare-and-swap refused', { + code: 'ADOPTION_REF_CAS_FAILED', details: result.stderr?.trim() || null, + }); + return { adapter: { name: this.name, id: this.id }, expected, writes }; + } +} + export class AdoptionService { - constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, fixtureAdapter = null } = {}) { + constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, refAdapter } = {}) { this.repositoryRoot = repositoryRoot; this.controlPlane = controlPlane; this.clock = clock; this.idFactory = idFactory; this.policy = policy ?? {}; - this.fixtureAdapter = fixtureAdapter; + this.refAdapter = refAdapter ?? new GitPrivateRefAdapter({ repositoryRoot }); } plan({ action = 'plan', actorId, approvalId, approvalRevision, adoptionId, candidateSha, baseTargetSha, provenance, provenanceManifestDigest, contributorArea, - destinationArea, targetBranch, candidateRef, expectedOldRef, fixture } = {}) { + destinationArea, targetBranch, candidateRef, expectedOldRef, adapter } = {}) { const blockers = []; const id = adoptionId ? text(adoptionId, 'adoptionId') : null; if (id && !SAFE_ID.test(id)) blockers.push({ code: 'ADOPTION_ID_INVALID' }); if (!ACTIONS.has(action)) blockers.push({ code: 'ADOPTION_ACTION_INVALID' }); if (actorId !== 'owner') blockers.push({ code: 'OWNER_ACTOR_REQUIRED' }); if (!Number.isInteger(approvalRevision) || approvalRevision < 1) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_REQUIRED' }); + const adapterBinding = adapter?.name && adapter?.id ? { name: adapter.name, id: adapter.id } : null; + if (MUTATING_ACTIONS.has(action) && !adapterBinding) blockers.push({ code: 'ADOPTION_ADAPTER_BINDING_REQUIRED' }); + if (adapterBinding && (adapterBinding.name !== this.refAdapter.name || adapterBinding.id !== this.refAdapter.id)) { + blockers.push({ code: 'ADOPTION_ADAPTER_PROVENANCE_INVALID' }); + } let candidate; let base; let provenanceDigest; try { @@ -108,10 +163,8 @@ export class AdoptionService { if (!Array.isArray(provenance) || !provenance.length || provenance.some((entry) => typeof entry !== 'string' || !entry)) { throw new TorchError('provenance must be a non-empty ordered string list', { code: 'ADOPTION_INPUT_INVALID' }); } - provenanceDigest = createHash('sha256').update(JSON.stringify(provenance)).digest('hex'); - if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) { - blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest }); - } + provenanceDigest = digest(provenance); + if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest }); } catch (error) { blockers.push({ code: error.code ?? 'ADOPTION_INPUT_INVALID', details: error.details ?? null }); } @@ -120,14 +173,8 @@ export class AdoptionService { if (typeof target !== 'string' || !target.trim()) blockers.push({ code: 'ADOPTION_TARGET_REQUIRED' }); const candidateReference = candidateRef ?? (id ? `refs/torch/integration-candidates/${id}` : null); const expectedReference = expectedOldRef ?? (target ? `refs/heads/${target}` : null); - if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) { - blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' }); - } - if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) { - blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' }); - } - const normalizedFixture = fixture?.id && fixture?.adapter ? { id: fixture.id, adapter: fixture.adapter } : null; - if (MUTATING_ACTIONS.has(action) && !normalizedFixture) blockers.push({ code: 'ADOPTION_HERMETIC_FIXTURE_REQUIRED' }); + if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' }); + if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' }); let contributor; let destination; try { contributor = this.controlPlane.assertIdentity(text(contributorArea, 'contributorArea')); } @@ -136,11 +183,11 @@ export class AdoptionService { catch (error) { blockers.push({ code: 'ADOPTION_DESTINATION_INVALID', details: error.code ?? null }); } if (contributor && destination && contributor === destination) blockers.push({ code: 'ADOPTION_IDENTITIES_MUST_DIFFER' }); - const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests - WHERE id = ? AND project_id = ?`).get(approvalId, this.controlPlane.projectId) : null; - const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base, - provenanceManifestDigest: provenanceDigest, contributorArea: contributor, destinationArea: destination, - targetBranch: target, candidateRef: candidateReference, expectedOldRef: expectedReference, fixture: normalizedFixture }; + const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base, provenanceManifestDigest: provenanceDigest, + contributorArea: contributor, destinationArea: destination, targetBranch: target, candidateRef: candidateReference, + expectedOldRef: expectedReference, adapter: adapterBinding }; + const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests WHERE id = ? AND project_id = ?`) + .get(approvalId, this.controlPlane.projectId) : null; if (!approval) blockers.push({ code: 'ADOPTION_APPROVAL_MISSING' }); else if (approval.revision !== approvalRevision) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_STALE', actual: approval.revision }); else if (!exactAuthority(authorityFrom(approval), binding)) blockers.push({ code: 'ADOPTION_APPROVAL_BINDING_INVALID' }); @@ -148,35 +195,27 @@ export class AdoptionService { if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' }); if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_BASE_MISSING' }); if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' }); - const fixtureRefs = normalizedFixture && this.fixtureAdapter - && this.fixtureAdapter.id === normalizedFixture.id && this.fixtureAdapter.name === normalizedFixture.adapter; if (expectedReference && base) { - const current = fixtureRefs - ? { status: 0, stdout: this.fixtureAdapter.readRefs([expectedReference])[expectedReference] ?? '' } - : git(this.repositoryRoot, ['rev-parse', expectedReference], { allowFailure: true }); - if (current.status !== 0) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' }); - else if (current.stdout !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current.stdout }); + const current = this.refAdapter.readRefs([expectedReference])[expectedReference]; + if (!current) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' }); + else if (current !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current }); } - const candidateExists = candidateReference && (fixtureRefs - ? this.fixtureAdapter.readRefs([candidateReference])[candidateReference] != null - : git(this.repositoryRoot, ['show-ref', '--verify', '--quiet', candidateReference], { allowFailure: true }).status === 0); + const candidateExists = candidateReference && this.refAdapter.readRefs([candidateReference])[candidateReference] != null; if (['plan', 'prepare', 'finalize'].includes(action) && candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' }); - if (['reconcile', 'rollback', 'request'].includes(action) && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' }); + if (action === 'request' && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' }); for (const worktree of listWorktreePaths(this.repositoryRoot)) { const entries = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']).stdout.split('\n').filter(Boolean); if (entries.length) blockers.push({ code: 'ADOPTION_WORKTREE_DIRTY', worktree, entries }); } if (tableExists(this.controlPlane.database, 'worktree_guards')) { - for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards - WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard }); + for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard }); } if (tableExists(this.controlPlane.database, 'resource_leases')) { for (const lease of this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases WHERE released_at IS NULL`).all()) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease }); } if (tableExists(this.controlPlane.database, 'prepared_checks')) { - for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks - WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check }); + for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check }); } for (const area of [contributor, destination].filter(Boolean)) { const executor = observeRuntimeTurn({ stateRoot: this.controlPlane.stateRoot, repositoryRoot: this.repositoryRoot, @@ -185,14 +224,20 @@ export class AdoptionService { } return { action: `plan-adoption-${action}`, adoptionId: id, approval: approval ? { id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id } : null, authority: binding, candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null, - contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, - candidateRef: candidateReference, expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false }; + contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, candidateRef: candidateReference, + expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false }; } - #record(adoptionId, record, operation) { + #append(adoptionId, expectedRevision, record, operation) { const now = this.clock().toISOString(); this.controlPlane.database.exec('BEGIN IMMEDIATE'); try { + const current = this.#find(adoptionId); + if (expectedRevision === null ? current : !current || current.revision !== expectedRevision) { + throw new TorchError('Adoption journal changed before this transition could be recorded', { + code: 'ADOPTION_RECORD_REVISION_STALE', details: { adoptionId, expectedRevision, actual: current?.revision ?? null }, + }); + } this.controlPlane.database.prepare(`INSERT INTO audit_events (id, project_id, actor_id, operation, entity_type, entity_id, details, created_at) VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`) @@ -201,78 +246,91 @@ export class AdoptionService { } catch (error) { this.controlPlane.database.exec('ROLLBACK'); throw error; } } - #fixture(input) { - if (!this.fixtureAdapter || input.fixture?.adapter !== this.fixtureAdapter.name || input.fixture?.id !== this.fixtureAdapter.id) { - throw new TorchError('Adoption mutation is available only through its named hermetic fixture adapter', { - code: 'ADOPTION_HERMETIC_ADAPTER_REQUIRED', details: { fixture: input.fixture ?? null }, - }); - } - return this.fixtureAdapter; + #fromRecord(record, input, action) { + exactInput(record, input); + return this.plan({ action, actorId: input.actorId, approvalId: input.approvalId, approvalRevision: input.approvalRevision, + adoptionId: record.id, candidateSha: record.candidateSha, baseTargetSha: record.baseTargetSha, provenance: record.provenance, + provenanceManifestDigest: record.provenanceManifestDigest, contributorArea: record.contributorArea, destinationArea: record.destinationArea, + targetBranch: record.targetBranch, candidateRef: record.candidateRef, expectedOldRef: record.expectedOldRef, adapter: input.adapter }); } prepare(input = {}) { const adoptionId = input.adoptionId ?? this.idFactory(); const plan = this.plan({ ...input, action: 'prepare', adoptionId }); if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers }); - this.#fixture(input); - if (this.#find(adoptionId)) throw new TorchError('Adoption id is already journaled; reconcile its immutable record instead.', { code: 'ADOPTION_ALREADY_EXISTS', details: { adoptionId } }); const now = this.clock().toISOString(); const record = { schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'), - approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha, baseTargetSha: plan.baseTargetSha, - provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef, expectedOldRef: plan.expectedOldRef, - contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch, + approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha, + baseTargetSha: plan.baseTargetSha, provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef, + expectedOldRef: plan.expectedOldRef, contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch, archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, rolledBackAt: null }; - this.#record(adoptionId, record, 'integration.adoption.prepare'); + this.#append(adoptionId, null, record, 'integration.adoption.prepare'); return { ...record, mutationPerformed: true }; } finalize(input = {}) { const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); if (record.state !== 'prepared') throw new TorchError('Only a prepared adoption can be finalized', { code: 'ADOPTION_FINALIZE_STATE_INVALID', details: { state: record.state } }); - const plan = this.plan({ ...record, ...input, action: 'finalize' }); + const plan = this.#fromRecord(record, input, 'finalize'); if (!plan.canProceed) throw new TorchError('Adoption finalize is blocked', { code: 'ADOPTION_FINALIZE_BLOCKED', details: plan.blockers }); - const adapter = this.#fixture(input); - const receipt = adapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }], + const receipt = this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }], writes: [{ ref: record.archiveRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: record.candidateSha }] }); const finalized = { ...record, revision: record.revision + 1, state: 'finalized', finalizeApprovalId: text(input.approvalId, 'approvalId'), - finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), fixtureReceiptDigest: adapterReceipt(receipt) }; - this.#record(record.id, finalized, 'integration.adoption.finalize'); + finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), refReceiptDigest: digest(receipt) }; + this.#append(record.id, record.revision, finalized, 'integration.adoption.finalize'); return { ...finalized, mutationPerformed: true }; } reconcile(input = {}) { const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); - const plan = this.plan({ ...record, ...input, action: 'reconcile' }); + const plan = this.#fromRecord(record, input, 'reconcile'); if (!plan.canProceed) return { record, plan, state: 'blocked', mutationPerformed: false }; - const adapter = this.#fixture(input); - const refs = adapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]); - const consistent = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha; - return { record, refs, state: consistent ? 'consistent' : 'diverged', mutationPerformed: false }; + const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]); + const finalizedRefs = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha; + if (record.state === 'prepared' && finalizedRefs) { + const recovered = { ...record, revision: record.revision + 1, state: 'finalized', finalizedAt: this.clock().toISOString(), + recoveredAt: this.clock().toISOString(), recovery: 'private-ref-cas-completed-before-journal' }; + this.#append(record.id, record.revision, recovered, 'integration.adoption.reconcile-finalized'); + return { record: recovered, refs, state: 'recovered-finalized', mutationPerformed: true }; + } + if (record.state === 'finalized' && finalizedRefs) return { record, refs, state: 'consistent', mutationPerformed: false }; + if (record.state === 'prepared' && refs[record.candidateRef] === null && refs[record.archiveRef] === null) return { record, refs, state: 'prepared', mutationPerformed: false }; + return { record, refs, state: 'diverged', mutationPerformed: false }; } rollback(input = {}) { const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can be rolled back', { code: 'ADOPTION_ROLLBACK_STATE_INVALID', details: { state: record.state } }); - const plan = this.plan({ ...record, ...input, action: 'rollback' }); + const plan = this.#fromRecord(record, input, 'rollback'); if (!plan.canProceed) throw new TorchError('Adoption rollback is blocked', { code: 'ADOPTION_ROLLBACK_BLOCKED', details: plan.blockers }); - const receipt = this.#fixture(input).compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] }); + const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]); + const recovered = refs[record.expectedOldRef] === record.baseTargetSha + && refs[record.candidateRef] === null && refs[record.archiveRef] === record.baseTargetSha; + if (!recovered && refs[record.candidateRef] !== record.candidateSha) { + throw new TorchError('Candidate ref changed before rollback', { code: 'ADOPTION_ROLLBACK_REF_DIVERGED', details: refs }); + } + const receipt = recovered ? { adapter: { name: this.refAdapter.name, id: this.refAdapter.id }, recovered: true, refs } + : this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] }); const rolledBack = { ...record, revision: record.revision + 1, state: 'rolled-back', rollbackApprovalId: text(input.approvalId, 'approvalId'), - rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: adapterReceipt(receipt) }; - this.#record(record.id, rolledBack, 'integration.adoption.rollback'); + rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: digest(receipt), + ...(recovered ? { recoveredAt: this.clock().toISOString(), recovery: 'private-ref-rollback-completed-before-journal' } : {}) }; + this.#append(record.id, record.revision, rolledBack, 'integration.adoption.rollback'); return { ...rolledBack, mutationPerformed: true }; } - requestAdoptedCandidate(input = {}, { requestIntegration } = {}) { + requestAdoptedCandidate(input = {}, { requestIntegration, findIntegrationRequest } = {}) { const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); - if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can request native qualification', { code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state } }); - const plan = this.plan({ ...record, ...input, action: 'request' }); + if (!['finalized', 'requesting'].includes(record.state)) throw new TorchError('Only a finalized adoption can request native qualification', { code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state } }); + const plan = this.#fromRecord(record, input, 'request'); if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers }); - this.#fixture(input); if (typeof requestIntegration !== 'function') throw new TorchError('Native integration request adapter is unavailable', { code: 'ADOPTION_NATIVE_REQUEST_UNAVAILABLE' }); - const request = requestIntegration({ areaId: record.contributorArea, commit: record.candidateSha }); - const requested = { ...record, revision: record.revision + 1, state: 'requested', requestApprovalId: text(input.approvalId, 'approvalId'), - requestApprovalRevision: input.approvalRevision, integrationRequestId: request.id, requestedAt: this.clock().toISOString() }; - this.#record(record.id, requested, 'integration.adoption.request'); + const requesting = record.state === 'requesting' ? record : { ...record, revision: record.revision + 1, state: 'requesting', + requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() }; + if (record.state !== 'requesting') this.#append(record.id, record.revision, requesting, 'integration.adoption.request-started'); + const request = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha }) + ?? requestIntegration({ areaId: requesting.contributorArea, commit: requesting.candidateSha, candidateRef: requesting.candidateRef }); + const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() }; + this.#append(requesting.id, requesting.revision, requested, 'integration.adoption.request'); return { ...requested, integrationRequest: request, mutationPerformed: true }; } diff --git a/src/integration/service.mjs b/src/integration/service.mjs index dcd9eb0..89f65e4 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -16,6 +16,21 @@ function git(root, args, { allowFailure = false } = {}) { return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' }; } +function text(value, name) { + if (typeof value !== 'string' || !value.trim()) { + throw new TorchError(`${name} is required`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } }); + } + return value.trim(); +} + +function sha(value, name) { + const normalized = text(value, name); + if (!/^[0-9a-f]{40,64}$/.test(normalized)) { + throw new TorchError(`${name} must be a full Git object id`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } }); + } + return normalized; +} + function integrationRow(row) { if (!row) return null; return { @@ -87,12 +102,43 @@ export class IntegrationService { code: 'INTEGRATION_SOURCE_MOVED', details: { branchTip, sourceCommit }, }); } + return this.#registerRequest({ area, sourceBranch: worktree.branch, sourceCommit }); + } + + requestCandidate({ areaId, commit, candidateRef } = {}) { + const area = this.controlPlane.assertIdentity(areaId); + if (area === 'session-manager') { + throw new TorchError('Session Manager does not own a feature worktree by default', { + code: 'INTEGRATION_SOURCE_INVALID', + }); + } + const ref = text(candidateRef, 'candidateRef'); + if (!/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(ref)) { + throw new TorchError('Adopted candidates must use a private integration candidate ref', { + code: 'INTEGRATION_ADOPTED_REF_INVALID', details: { candidateRef: ref }, + }); + } + const sourceCommit = sha(commit, 'commit'); + const refTip = git(this.repositoryRoot, ['rev-parse', '--verify', ref], { allowFailure: true }); + if (refTip.status !== 0 || refTip.stdout !== sourceCommit) { + throw new TorchError('Adopted candidate ref no longer names the exact requested commit', { + code: 'INTEGRATION_ADOPTED_REF_STALE', details: { candidateRef: ref, sourceCommit, actual: refTip.stdout || null }, + }); + } + const existing = this.controlPlane.database.prepare(`SELECT id FROM integration_requests + WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`) + .get(this.controlPlane.projectId, ref, sourceCommit); + if (existing) return this.get(existing.id); + return this.#registerRequest({ area, sourceBranch: ref, sourceCommit }); + } + + #registerRequest({ area, sourceBranch, sourceCommit }) { const targetBranch = this.policy.target; const baseTargetCommit = git(this.repositoryRoot, ['rev-parse', targetBranch]).stdout; const now = this.clock().toISOString(); const record = { id: this.idFactory(), projectId: this.controlPlane.projectId, sourceArea: area, - sourceBranch: worktree.branch, sourceCommit, targetBranch, baseTargetCommit, + sourceBranch, sourceCommit, targetBranch, baseTargetCommit, state: 'testing', reason: null, requiredChecks: this.policy.required_checks ?? [], authorizedBy: null, authorizedAt: null, createdAt: now, updatedAt: now, landedAt: null, }; @@ -139,10 +185,18 @@ export class IntegrationService { requestAdoptedCandidate(input = {}) { return this.adoption.requestAdoptedCandidate(input, { - requestIntegration: ({ areaId, commit }) => this.request({ areaId, commit }), + requestIntegration: ({ areaId, commit, candidateRef }) => this.requestCandidate({ areaId, commit, candidateRef }), + findIntegrationRequest: ({ candidateRef, commit }) => this.findCandidateRequest({ candidateRef, commit }), }); } + findCandidateRequest({ candidateRef, commit } = {}) { + const row = this.controlPlane.database.prepare(`SELECT id FROM integration_requests + WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`) + .get(this.controlPlane.projectId, candidateRef, commit); + return row ? this.get(row.id) : null; + } + get(requestId) { const row = this.controlPlane.database.prepare('SELECT * FROM integration_requests WHERE id = ?').get(requestId); if (!row) throw new TorchError(`Unknown integration request: ${requestId}`, { code: 'INTEGRATION_NOT_FOUND' }); From 8ea3bef13e3c0353e8725de0bc4ae4c527a44722 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:20:48 -0400 Subject: [PATCH 08/15] fix(integration): make private ref cas atomic TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- src/integration/adoption.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index 2aeb1d5..cac0c5e 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -112,11 +112,17 @@ export class GitPrivateRefAdapter { compareAndSwapRefs({ expected = [], writes = [] } = {}) { const commands = ['start']; + const writtenRefs = new Set(writes.map((entry) => entry.ref)); for (const { ref, sha: expectedSha } of expected) { + if (writtenRefs.has(ref)) continue; commands.push(`verify ${text(ref, 'ref')} ${expectedSha ?? ZERO_SHA}`); } for (const { ref, sha: nextSha } of writes) { - const current = expected.find((entry) => entry.ref === ref)?.sha ?? null; + const expectedEntry = expected.find((entry) => entry.ref === ref); + if (!expectedEntry) throw new TorchError('Every private-ref write requires an exact expected value', { + code: 'ADOPTION_REF_CAS_EXPECTED_REQUIRED', details: { ref }, + }); + const current = expectedEntry.sha ?? null; if (nextSha === null) commands.push(`delete ${text(ref, 'ref')} ${current ?? ZERO_SHA}`); else commands.push(`update ${text(ref, 'ref')} ${sha(nextSha, 'next ref sha')} ${current ?? ZERO_SHA}`); } From 63dd032b7f78d483b1ff134adb27e611f64a3b06 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:30:03 -0400 Subject: [PATCH 09/15] test(integration): cover exact adoption recovery TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- docs/TEST_INTEGRITY.md | 41 +++++ src/integration/adoption.mjs | 3 +- test/fleet/integration-adoption.test.mjs | 199 +++++++++++++++++++++++ 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 test/fleet/integration-adoption.test.mjs diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 2e4dd8a..c332e14 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3143,3 +3143,44 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +# Exact-source adoption (2026-10-03) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-integration-adoption-private-ref-cas + given: "A clean disposable Git repository, a base commit, and an owner-bound private-ref adoption" + when: "The adoption finalizes and a stale compare-and-swap attempts to alter its candidate ref" + then: "Real Git private archive and candidate refs are created atomically, while the stale write refuses" + - id: SCN-integration-adoption-journal-and-binding + given: "A prepared adoption journal record" + when: "A second preparation or lifecycle request supplies a different candidate binding" + then: "The journal revision compare-and-swap and immutable binding checks refuse without changing the prepared record" + - id: SCN-integration-adoption-finalize-recovery + given: "A real private-ref transaction completed before its journal write" + when: "Reconciliation is run and then replayed" + then: "One durable finalized recovery is recorded and the replay is consistent without another mutation" + - id: SCN-integration-adoption-rollback-recovery + given: "A finalized candidate whose private ref was removed before its rollback journal write, or whose candidate ref diverged" + when: "Rollback is requested" + then: "The completed rollback is recovered once and divergent candidate bytes refuse" + - id: SCN-integration-adoption-private-ref-intake + given: "A finalized owner-bound private candidate ref" + when: "Native qualification is requested" + then: "The normal integration queue records the exact private ref and commit in ordinary testing state" + observable_outcomes: + - "Git ref values, structured refusal codes, durable journal state, and native integration-request fields" + determinism_controls: + - "Disposable local Git repositories, temporary SQLite-backed control-plane state, fixed fixture bytes, and no network or provider" + anti_cheat_rationale: + prevents: + - "Fixture-only CAS implementations that do not exercise Git update-ref" + - "Mutable lifecycle bindings or overwriteable adoption journal records" + - "Claiming crash recovery without preserving actual ref effects" + - "Treating a private candidate as an ordinary specialist branch" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index cac0c5e..7c0ed67 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -82,7 +82,8 @@ function exactAuthority(authority, binding) { function exactInput(record, input) { const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest', 'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef']; - const mismatches = fields.filter((field) => input[field] !== undefined && input[field] !== record[field]); + const mismatches = fields.filter((field) => input[field] !== undefined + && input[field] !== (field === 'adoptionId' ? record.id : record[field])); if (input.provenance !== undefined && JSON.stringify(input.provenance) !== JSON.stringify(record.provenance)) mismatches.push('provenance'); if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', { code: 'ADOPTION_IMMUTABLE_BINDING_STALE', details: { adoptionId: record.id, fields: mismatches }, diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs new file mode 100644 index 0000000..7363ef7 --- /dev/null +++ b/test/fleet/integration-adoption.test.mjs @@ -0,0 +1,199 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; +import test from 'node:test'; +import { openControlPlane } from '../../src/control-plane/service.mjs'; +import { analyzeRepository } from '../../src/kernel/analyze.mjs'; +import { proposeDomains } from '../../src/kernel/domains.mjs'; +import { inspectRepository } from '../../src/kernel/git.mjs'; +import { installProject } from '../../src/kernel/install.mjs'; +import { IntegrationService } from '../../src/integration/service.mjs'; + +const sha = value => execFileSync('git', ['-C', value, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); +const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex'); + +function fixture(name) { + const root = mkdtempSync(join(tmpdir(), `${name}-`)); + execFileSync('git', ['init', '-b', 'main', root]); + execFileSync('git', ['-C', root, 'config', 'user.email', 'torch-test@example.invalid']); + execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']); + writeFileSync(join(root, 'package.json'), `${JSON.stringify({ name, scripts: {} })}\n`); + writeFileSync(join(root, 'app.mjs'), 'export const value = 1;\n'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'fixture base']); + const repository = inspectRepository(root); + const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) }); + proposal.domains.push({ + id: 'review-area', title: 'Review Area', kind: 'development', + scope: ['review fixtures'], not_scope: ['application source'], owned_paths: ['review/**'], + shared_paths: [], neighbours: [], required_checks: [], resources: [], runtime: 'claude', evidence: ['app.mjs'], + }); + proposal.checks = []; + proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00.000Z', reviewedBy: 'fixture-owner', notes: [] }; + const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) }; + installProject({ repository, proposal, env, projectId: name }); + execFileSync('git', ['-C', root, 'add', '.torch']); + execFileSync('git', ['-C', root, 'commit', '-m', 'install fixture control plane']); + const base = sha(root); + execFileSync('git', ['-C', root, 'checkout', '-b', 'candidate']); + writeFileSync(join(root, 'candidate.mjs'), 'export const candidate = true;\n'); + execFileSync('git', ['-C', root, 'add', 'candidate.mjs']); + execFileSync('git', ['-C', root, 'commit', '-m', 'candidate contribution']); + const candidate = sha(root); + execFileSync('git', ['-C', root, 'checkout', 'main']); + const control = openControlPlane({ repositoryRoot: root, env }); + const service = new IntegrationService({ repositoryRoot: root, controlPlane: control, checkService: { exactPasses: () => false } }); + const contributor = proposal.domains[0].id; + return { root, control, service, contributor, destination: 'review-area', base, candidate }; +} + +function authority(context, { action, adoptionId }) { + const provenance = ['fixture:owner-approved', `candidate:${context.candidate}`]; + const binding = { + schema: 'torch.dev/integration-adoption-authority/v1alpha1', action, adoptionId, + candidateSha: context.candidate, baseTargetSha: context.base, + provenanceManifestDigest: digest(provenance), contributorArea: context.contributor, + destinationArea: context.destination, targetBranch: 'main', + candidateRef: `refs/torch/integration-candidates/${adoptionId}`, + expectedOldRef: 'refs/heads/main', adapter: { + name: context.service.adoption.refAdapter.name, id: context.service.adoption.refAdapter.id, + }, + }; + const pending = context.control.requestApproval({ + requester: context.contributor, approver: 'owner', task: 'TASK-f40126ee', + title: `Fixture ${action} authority`, summary: 'Hermetic exact-source adoption scenario.', + }); + const approved = context.control.decideApproval({ + approvalId: pending.id, decidedBy: 'owner', decision: 'approved', expectedRevision: pending.revision, + note: JSON.stringify(binding), + }); + return { + approvalId: approved.id, approvalRevision: approved.revision, provenance, + provenanceManifestDigest: binding.provenanceManifestDigest, adapter: binding.adapter, + }; +} + +function input(context, action, adoptionId, extra = {}) { + return { + actorId: 'owner', adoptionId, candidateSha: context.candidate, baseTargetSha: context.base, + contributorArea: context.contributor, destinationArea: context.destination, + targetBranch: 'main', candidateRef: `refs/torch/integration-candidates/${adoptionId}`, + expectedOldRef: 'refs/heads/main', ...authority(context, { action, adoptionId }), ...extra, + }; +} + +function prepare(context, adoptionId = 'fixture-adoption') { + const preparedInput = input(context, 'prepare', adoptionId); + const plan = context.service.planAdoption({ ...preparedInput, action: 'prepare' }); + assert.deepEqual(plan.blockers, [], `preparation must begin from an exact clean fixture: ${JSON.stringify(plan.blockers)}`); + return context.service.prepareAdoption(preparedInput); +} + +function lifecycleInput(context, action, adoptionId, extra = {}) { + const { approvalId, approvalRevision, adapter } = authority(context, { action, adoptionId }); + return { actorId: 'owner', adoptionId, approvalId, approvalRevision, adapter, ...extra }; +} + +test('SCN-integration-adoption-private-ref-cas: a real temporary Git transaction atomically creates private archive and candidate refs, and stale expected refs refuse', () => { + const context = fixture('torch-adoption-cas'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const refs = context.service.adoption.refAdapter.readRefs([ + 'refs/heads/main', finalized.candidateRef, finalized.archiveRef, + ]); + assert.deepEqual(refs, { + 'refs/heads/main': context.base, + [finalized.candidateRef]: context.candidate, + [finalized.archiveRef]: context.base, + }); + assert.throws(() => context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: finalized.candidateRef, sha: context.base }], + writes: [{ ref: finalized.candidateRef, sha: null }], + }), (error) => error.code === 'ADOPTION_REF_CAS_FAILED'); + assert.equal(context.service.adoption.getPrepared(adoptionId).state, 'finalized'); + context.control.close(); +}); + +test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cannot overwrite the journal and lifecycle input cannot alter its immutable candidate binding', () => { + const context = fixture('torch-adoption-journal'); + const adoptionId = 'fixture-adoption'; + const prepared = prepare(context, adoptionId); + assert.throws(() => context.service.prepareAdoption(input(context, 'prepare', adoptionId)), + (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); + assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { candidateSha: context.base })), + (error) => error.code === 'ADOPTION_IMMUTABLE_BINDING_STALE'); + const { mutationPerformed, ...durablePrepared } = prepared; + assert.equal(mutationPerformed, true); + assert.deepEqual(context.service.adoption.getPrepared(adoptionId), durablePrepared); + context.control.close(); +}); + +test('SCN-integration-adoption-finalize-recovery: a crash after real ref CAS but before the journal transition reconciles once and never duplicates refs', () => { + const context = fixture('torch-adoption-finalize-recovery'); + const adoptionId = 'fixture-adoption'; + const prepared = prepare(context, adoptionId); + context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [ + { ref: prepared.expectedOldRef, sha: prepared.baseTargetSha }, + { ref: prepared.candidateRef, sha: null }, + { ref: prepared.archiveRef, sha: null }, + ], + writes: [ + { ref: prepared.archiveRef, sha: prepared.baseTargetSha }, + { ref: prepared.candidateRef, sha: prepared.candidateSha }, + ], + }); + const recovered = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId)); + assert.equal(recovered.state, 'recovered-finalized'); + assert.equal(recovered.record.recovery, 'private-ref-cas-completed-before-journal'); + const replay = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId)); + assert.equal(replay.state, 'consistent'); + assert.equal(replay.mutationPerformed, false); + context.control.close(); +}); + +test('SCN-integration-adoption-rollback-recovery: a journal-missing rollback completes without a second deletion, while divergent candidate bytes refuse', () => { + const context = fixture('torch-adoption-rollback-recovery'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + assert.equal(finalized.state, 'finalized'); + context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: finalized.candidateRef, sha: finalized.candidateSha }], + writes: [{ ref: finalized.candidateRef, sha: null }], + }); + const recovered = context.service.rollbackAdoption(lifecycleInput(context, 'rollback', adoptionId)); + assert.equal(recovered.state, 'rolled-back'); + assert.equal(recovered.recovery, 'private-ref-rollback-completed-before-journal'); + + const divergent = fixture('torch-adoption-rollback-divergence'); + const divergentId = 'fixture-adoption'; + prepare(divergent, divergentId); + const divergentFinal = divergent.service.finalizeAdoption(lifecycleInput(divergent, 'finalize', divergentId)); + divergent.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: divergentFinal.candidateRef, sha: divergentFinal.candidateSha }], + writes: [{ ref: divergentFinal.candidateRef, sha: divergent.base }], + }); + assert.throws(() => divergent.service.rollbackAdoption(lifecycleInput(divergent, 'rollback', divergentId)), + (error) => error.code === 'ADOPTION_ROLLBACK_REF_DIVERGED'); + context.control.close(); + divergent.control.close(); +}); + +test('SCN-integration-adoption-private-ref-intake: finalized private refs enter the ordinary native integration queue, not a specialist branch shortcut', () => { + const context = fixture('torch-adoption-native-intake'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)); + assert.equal(requested.state, 'requested'); + assert.equal(requested.integrationRequest.sourceBranch, finalized.candidateRef); + assert.equal(requested.integrationRequest.sourceCommit, context.candidate); + assert.equal(requested.integrationRequest.state, 'testing'); + assert.equal(requested.integrationRequest.targetBranch, 'main'); + context.control.close(); +}); From 530afb3b6749d84dee1478544386ccb115eeeccd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:34:09 -0400 Subject: [PATCH 10/15] fix(integration): bind private intake to adoption TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- docs/TEST_INTEGRITY.md | 6 +++--- src/integration/adoption.mjs | 3 ++- src/integration/service.mjs | 15 ++++++++++++++- test/fleet/integration-adoption.test.mjs | 13 +++++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index c332e14..1beb8d3 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3167,9 +3167,9 @@ test_integrity_note: when: "Rollback is requested" then: "The completed rollback is recovered once and divergent candidate bytes refuse" - id: SCN-integration-adoption-private-ref-intake - given: "A finalized owner-bound private candidate ref" - when: "Native qualification is requested" - then: "The normal integration queue records the exact private ref and commit in ordinary testing state" + given: "An existing unfinalized private candidate ref and a separate finalized owner-bound private candidate ref" + when: "Direct private-ref intake and authenticated native qualification are requested" + then: "Direct intake refuses without a queue record, while the authenticated path records the exact private ref and commit in ordinary testing state" observable_outcomes: - "Git ref values, structured refusal codes, durable journal state, and native integration-request fields" determinism_controls: diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index 7c0ed67..3a6c263 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -335,7 +335,8 @@ export class AdoptionService { requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() }; if (record.state !== 'requesting') this.#append(record.id, record.revision, requesting, 'integration.adoption.request-started'); const request = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha }) - ?? requestIntegration({ areaId: requesting.contributorArea, commit: requesting.candidateSha, candidateRef: requesting.candidateRef }); + ?? requestIntegration({ adoptionId: requesting.id, areaId: requesting.contributorArea, + commit: requesting.candidateSha, candidateRef: requesting.candidateRef }); const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() }; this.#append(requesting.id, requesting.revision, requested, 'integration.adoption.request'); return { ...requested, integrationRequest: request, mutationPerformed: true }; diff --git a/src/integration/service.mjs b/src/integration/service.mjs index 89f65e4..e027dab 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -106,12 +106,25 @@ export class IntegrationService { } requestCandidate({ areaId, commit, candidateRef } = {}) { + throw new TorchError('Private candidate intake requires an authenticated finalized adoption', { + code: 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY', details: { areaId, commit, candidateRef }, + }); + } + + #requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef } = {}) { const area = this.controlPlane.assertIdentity(areaId); if (area === 'session-manager') { throw new TorchError('Session Manager does not own a feature worktree by default', { code: 'INTEGRATION_SOURCE_INVALID', }); } + const adoption = this.adoption.getPrepared(text(adoptionId, 'adoptionId')); + if (adoption.state !== 'requesting' || adoption.contributorArea !== area + || adoption.candidateSha !== commit || adoption.candidateRef !== candidateRef) { + throw new TorchError('Private candidate does not match an authenticated finalized adoption', { + code: 'INTEGRATION_ADOPTED_BINDING_INVALID', details: { adoptionId, state: adoption.state }, + }); + } const ref = text(candidateRef, 'candidateRef'); if (!/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(ref)) { throw new TorchError('Adopted candidates must use a private integration candidate ref', { @@ -185,7 +198,7 @@ export class IntegrationService { requestAdoptedCandidate(input = {}) { return this.adoption.requestAdoptedCandidate(input, { - requestIntegration: ({ areaId, commit, candidateRef }) => this.requestCandidate({ areaId, commit, candidateRef }), + requestIntegration: ({ adoptionId, areaId, commit, candidateRef }) => this.#requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef }), findIntegrationRequest: ({ candidateRef, commit }) => this.findCandidateRequest({ candidateRef, commit }), }); } diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs index 7363ef7..b485039 100644 --- a/test/fleet/integration-adoption.test.mjs +++ b/test/fleet/integration-adoption.test.mjs @@ -185,6 +185,19 @@ test('SCN-integration-adoption-rollback-recovery: a journal-missing rollback com }); test('SCN-integration-adoption-private-ref-intake: finalized private refs enter the ordinary native integration queue, not a specialist branch shortcut', () => { + const unfinalized = fixture('torch-adoption-unfinalized-intake'); + const unfinalizedId = 'fixture-adoption'; + const unfinalizedRef = `refs/torch/integration-candidates/${unfinalizedId}`; + unfinalized.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: unfinalizedRef, sha: null }], + writes: [{ ref: unfinalizedRef, sha: unfinalized.candidate }], + }); + assert.throws(() => unfinalized.service.requestCandidate({ + areaId: unfinalized.contributor, commit: unfinalized.candidate, candidateRef: unfinalizedRef, + }), (error) => error.code === 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY'); + assert.deepEqual(unfinalized.service.list(), []); + unfinalized.control.close(); + const context = fixture('torch-adoption-native-intake'); const adoptionId = 'fixture-adoption'; prepare(context, adoptionId); From beb99bcbdac5aa2c0a3fbae2a787f98acdb606ce Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:39:43 -0400 Subject: [PATCH 11/15] test(integration): enforce adoption authority boundaries TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- docs/TEST_INTEGRITY.md | 10 ++++ src/integration/service.mjs | 2 + test/fleet/integration-adoption.test.mjs | 75 ++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 1beb8d3..9bb24fd 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3170,6 +3170,14 @@ test_integrity_note: given: "An existing unfinalized private candidate ref and a separate finalized owner-bound private candidate ref" when: "Direct private-ref intake and authenticated native qualification are requested" then: "Direct intake refuses without a queue record, while the authenticated path records the exact private ref and commit in ordinary testing state" + - id: SCN-integration-adoption-approval-boundaries + given: "An ordinary source-only approval, or an adoption whose actor, action, approval revision, or provenance differs from the approved immutable binding" + when: "Preparation or a lifecycle transition is attempted" + then: "The transition refuses with its structured boundary code and no integration queue record" + - id: SCN-integration-adoption-request-replay + given: "A finalized adoption that has created its one ordinary private-ref integration request" + when: "A concurrent exact-row insert or later request replay is attempted" + then: "Real SQLite rejects the duplicate exact source row, the journal replay refuses, and exactly one queue row remains" observable_outcomes: - "Git ref values, structured refusal codes, durable journal state, and native integration-request fields" determinism_controls: @@ -3180,6 +3188,8 @@ test_integrity_note: - "Mutable lifecycle bindings or overwriteable adoption journal records" - "Claiming crash recovery without preserving actual ref effects" - "Treating a private candidate as an ordinary specialist branch" + - "Using an unrelated, source-only, stale, or mismatched approval as adoption authority" + - "Duplicating a private-ref queue record through a concurrent row or replay" relaxation: did_relax_any_assertion: false if_true_explain_spec_basis: "" diff --git a/src/integration/service.mjs b/src/integration/service.mjs index e027dab..a5e1f9d 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -80,6 +80,8 @@ export class IntegrationService { ); CREATE INDEX IF NOT EXISTS integration_state_created ON integration_requests(state, created_at, id); + CREATE UNIQUE INDEX IF NOT EXISTS integration_request_exact_source + ON integration_requests(project_id, source_branch, source_commit); `); } diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs index b485039..a692194 100644 --- a/test/fleet/integration-adoption.test.mjs +++ b/test/fleet/integration-adoption.test.mjs @@ -97,6 +97,11 @@ function lifecycleInput(context, action, adoptionId, extra = {}) { return { actorId: 'owner', adoptionId, approvalId, approvalRevision, adapter, ...extra }; } +function blockedCodes(error) { + assert.equal(error.code.endsWith('_BLOCKED'), true, `expected a blocked adoption transition, got ${error.code}`); + return error.details.map((blocker) => blocker.code); +} + test('SCN-integration-adoption-private-ref-cas: a real temporary Git transaction atomically creates private archive and candidate refs, and stale expected refs refuse', () => { const context = fixture('torch-adoption-cas'); const adoptionId = 'fixture-adoption'; @@ -210,3 +215,73 @@ test('SCN-integration-adoption-private-ref-intake: finalized private refs enter assert.equal(requested.integrationRequest.targetBranch, 'main'); context.control.close(); }); + +test('SCN-integration-adoption-approval-boundaries: unrelated or source-only approval records cannot authorize an owner adoption, and actor, action, revision, and provenance mismatches each refuse', () => { + const unrelated = fixture('torch-adoption-unrelated-approval'); + const unrelatedId = 'fixture-adoption'; + const unrelatedApproval = unrelated.control.requestApproval({ + requester: unrelated.contributor, approver: 'owner', task: 'TASK-unrelated', + title: 'Ordinary source branch review', summary: 'Not an integration-adoption authority.', + }); + const unrelatedApproved = unrelated.control.decideApproval({ + approvalId: unrelatedApproval.id, decidedBy: 'owner', decision: 'approved', expectedRevision: unrelatedApproval.revision, + note: JSON.stringify({ schema: 'torch.dev/ordinary-source-review/v1alpha1', sourceArea: unrelated.contributor }), + }); + const unrelatedInput = { + actorId: 'owner', adoptionId: unrelatedId, candidateSha: unrelated.candidate, baseTargetSha: unrelated.base, + contributorArea: unrelated.contributor, destinationArea: unrelated.destination, targetBranch: 'main', + candidateRef: `refs/torch/integration-candidates/${unrelatedId}`, expectedOldRef: 'refs/heads/main', + approvalId: unrelatedApproved.id, approvalRevision: unrelatedApproved.revision, + provenance: ['fixture:owner-approved', `candidate:${unrelated.candidate}`], + provenanceManifestDigest: digest(['fixture:owner-approved', `candidate:${unrelated.candidate}`]), + adapter: { name: unrelated.service.adoption.refAdapter.name, id: unrelated.service.adoption.refAdapter.id }, + }; + assert.throws(() => unrelated.service.prepareAdoption(unrelatedInput), (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID')); + assert.deepEqual(unrelated.service.list(), []); + unrelated.control.close(); + + const wrongActor = fixture('torch-adoption-wrong-actor'); + assert.throws(() => wrongActor.service.prepareAdoption({ + ...input(wrongActor, 'prepare', 'fixture-adoption'), actorId: wrongActor.contributor, + }), (error) => blockedCodes(error).includes('OWNER_ACTOR_REQUIRED')); + wrongActor.control.close(); + + const wrongAction = fixture('torch-adoption-wrong-action'); + prepare(wrongAction, 'fixture-adoption'); + assert.throws(() => wrongAction.service.finalizeAdoption(lifecycleInput(wrongAction, 'rollback', 'fixture-adoption')), + (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID')); + wrongAction.control.close(); + + const staleRevision = fixture('torch-adoption-stale-revision'); + prepare(staleRevision, 'fixture-adoption'); + const staleInput = lifecycleInput(staleRevision, 'finalize', 'fixture-adoption'); + assert.throws(() => staleRevision.service.finalizeAdoption({ ...staleInput, approvalRevision: staleInput.approvalRevision - 1 }), + (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_REVISION_STALE')); + staleRevision.control.close(); + + const wrongProvenance = fixture('torch-adoption-wrong-provenance'); + assert.throws(() => wrongProvenance.service.prepareAdoption({ + ...input(wrongProvenance, 'prepare', 'fixture-adoption'), provenance: ['fixture:owner-approved', 'unapproved:alternate-provenance'], + }), (error) => blockedCodes(error).includes('ADOPTION_PROVENANCE_DIGEST_MISMATCH')); + wrongProvenance.control.close(); +}); + +test('SCN-integration-adoption-request-replay: real SQLite rejects a duplicate exact private-ref queue row, and a completed adoption request cannot replay into a second row', () => { + const context = fixture('torch-adoption-request-replay'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)); + const duplicate = context.control.database.prepare(`INSERT INTO integration_requests ( + id, project_id, source_area, source_branch, source_commit, target_branch, + base_target_commit, state, reason, required_checks, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); + assert.throws(() => duplicate.run( + 'duplicate-race-row', context.control.projectId, context.contributor, finalized.candidateRef, context.candidate, + 'main', context.base, 'testing', null, '[]', requested.requestedAt, requested.requestedAt, + ), /UNIQUE constraint failed: integration_requests.project_id, integration_requests.source_branch, integration_requests.source_commit/); + assert.throws(() => context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)), + (error) => error.code === 'ADOPTION_REQUEST_STATE_INVALID'); + assert.deepEqual(context.service.list().map((record) => record.id), [requested.integrationRequest.id]); + context.control.close(); +}); From 004d9e76a9c0262c5e7c123b2c05935884a4b018 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:46:41 -0400 Subject: [PATCH 12/15] fix(integration): serialize adopted request replay TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- docs/TEST_INTEGRITY.md | 11 +++- src/integration/adoption.mjs | 66 +++++++++++++------- src/integration/service.mjs | 2 - test/fleet/integration-adoption.test.mjs | 77 ++++++++++++++++++++---- 4 files changed, 115 insertions(+), 41 deletions(-) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 9bb24fd..c17dc32 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3176,8 +3176,12 @@ test_integrity_note: then: "The transition refuses with its structured boundary code and no integration queue record" - id: SCN-integration-adoption-request-replay given: "A finalized adoption that has created its one ordinary private-ref integration request" - when: "A concurrent exact-row insert or later request replay is attempted" - then: "Real SQLite rejects the duplicate exact source row, the journal replay refuses, and exactly one queue row remains" + when: "Two independently opened authenticated API callers request it concurrently" + then: "The existing transactional adoption journal serializes them, one caller refuses after the completed transition, and exactly one queue row remains" + - id: SCN-integration-adoption-legacy-retries + given: "An existing project database containing two historical ordinary-request retry rows with the same source branch and commit" + when: "IntegrationService starts" + then: "Both records remain readable; startup adds no global uniqueness constraint and does not reject historical evidence" observable_outcomes: - "Git ref values, structured refusal codes, durable journal state, and native integration-request fields" determinism_controls: @@ -3189,7 +3193,8 @@ test_integrity_note: - "Claiming crash recovery without preserving actual ref effects" - "Treating a private candidate as an ordinary specialist branch" - "Using an unrelated, source-only, stale, or mismatched approval as adoption authority" - - "Duplicating a private-ref queue record through a concurrent row or replay" + - "Duplicating a private-ref queue record through concurrent authenticated API requests or replay" + - "Rejecting historical ordinary-request retry evidence with an unqualified schema constraint" relaxation: did_relax_any_assertion: false if_true_explain_spec_basis: "" diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index 3a6c263..9d5eabd 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -236,23 +236,26 @@ export class AdoptionService { } #append(adoptionId, expectedRevision, record, operation) { - const now = this.clock().toISOString(); this.controlPlane.database.exec('BEGIN IMMEDIATE'); try { - const current = this.#find(adoptionId); - if (expectedRevision === null ? current : !current || current.revision !== expectedRevision) { - throw new TorchError('Adoption journal changed before this transition could be recorded', { - code: 'ADOPTION_RECORD_REVISION_STALE', details: { adoptionId, expectedRevision, actual: current?.revision ?? null }, - }); - } - this.controlPlane.database.prepare(`INSERT INTO audit_events - (id, project_id, actor_id, operation, entity_type, entity_id, details, created_at) - VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`) - .run(this.idFactory(), this.controlPlane.projectId, operation, adoptionId, JSON.stringify(record), now); + this.#appendInTransaction(adoptionId, expectedRevision, record, operation); this.controlPlane.database.exec('COMMIT'); } catch (error) { this.controlPlane.database.exec('ROLLBACK'); throw error; } } + #appendInTransaction(adoptionId, expectedRevision, record, operation) { + const current = this.#find(adoptionId); + if (expectedRevision === null ? current : !current || current.revision !== expectedRevision) { + throw new TorchError('Adoption journal changed before this transition could be recorded', { + code: 'ADOPTION_RECORD_REVISION_STALE', details: { adoptionId, expectedRevision, actual: current?.revision ?? null }, + }); + } + this.controlPlane.database.prepare(`INSERT INTO audit_events + (id, project_id, actor_id, operation, entity_type, entity_id, details, created_at) + VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`) + .run(this.idFactory(), this.controlPlane.projectId, operation, adoptionId, JSON.stringify(record), this.clock().toISOString()); + } + #fromRecord(record, input, action) { exactInput(record, input); return this.plan({ action, actorId: input.actorId, approvalId: input.approvalId, approvalRevision: input.approvalRevision, @@ -326,20 +329,37 @@ export class AdoptionService { } requestAdoptedCandidate(input = {}, { requestIntegration, findIntegrationRequest } = {}) { - const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); - if (!['finalized', 'requesting'].includes(record.state)) throw new TorchError('Only a finalized adoption can request native qualification', { code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state } }); - const plan = this.#fromRecord(record, input, 'request'); - if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers }); if (typeof requestIntegration !== 'function') throw new TorchError('Native integration request adapter is unavailable', { code: 'ADOPTION_NATIVE_REQUEST_UNAVAILABLE' }); - const requesting = record.state === 'requesting' ? record : { ...record, revision: record.revision + 1, state: 'requesting', - requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() }; - if (record.state !== 'requesting') this.#append(record.id, record.revision, requesting, 'integration.adoption.request-started'); - const request = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha }) - ?? requestIntegration({ adoptionId: requesting.id, areaId: requesting.contributorArea, + const adoptionId = text(input.adoptionId, 'adoptionId'); + this.controlPlane.database.exec('BEGIN IMMEDIATE'); + try { + const record = this.getPrepared(adoptionId); + if (!['finalized', 'requesting'].includes(record.state)) { + throw new TorchError('Only a finalized adoption can request native qualification', { + code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state }, + }); + } + const plan = this.#fromRecord(record, input, 'request'); + if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers }); + const requesting = record.state === 'requesting' ? record : { ...record, revision: record.revision + 1, state: 'requesting', + requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() }; + if (record.state !== 'requesting') this.#appendInTransaction(record.id, record.revision, requesting, 'integration.adoption.request-started'); + const existing = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha }); + if (existing && (existing.sourceArea !== requesting.contributorArea || existing.targetBranch !== requesting.targetBranch)) { + throw new TorchError('Existing private-ref integration request does not match this adoption', { + code: 'ADOPTION_INTEGRATION_REQUEST_CONFLICT', details: { adoptionId: requesting.id, integrationRequestId: existing.id }, + }); + } + const request = existing ?? requestIntegration({ adoptionId: requesting.id, areaId: requesting.contributorArea, commit: requesting.candidateSha, candidateRef: requesting.candidateRef }); - const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() }; - this.#append(requesting.id, requesting.revision, requested, 'integration.adoption.request'); - return { ...requested, integrationRequest: request, mutationPerformed: true }; + const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() }; + this.#appendInTransaction(requesting.id, requesting.revision, requested, 'integration.adoption.request'); + this.controlPlane.database.exec('COMMIT'); + return { ...requested, integrationRequest: request, mutationPerformed: true }; + } catch (error) { + this.controlPlane.database.exec('ROLLBACK'); + throw error; + } } #find(adoptionId) { diff --git a/src/integration/service.mjs b/src/integration/service.mjs index a5e1f9d..e027dab 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -80,8 +80,6 @@ export class IntegrationService { ); CREATE INDEX IF NOT EXISTS integration_state_created ON integration_requests(state, created_at, id); - CREATE UNIQUE INDEX IF NOT EXISTS integration_request_exact_source - ON integration_requests(project_id, source_branch, source_commit); `); } diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs index a692194..741149e 100644 --- a/test/fleet/integration-adoption.test.mjs +++ b/test/fleet/integration-adoption.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; -import { execFileSync } from 'node:child_process'; +import { execFileSync, spawn } from 'node:child_process'; import { mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { basename, join } from 'node:path'; @@ -47,7 +47,43 @@ function fixture(name) { const control = openControlPlane({ repositoryRoot: root, env }); const service = new IntegrationService({ repositoryRoot: root, controlPlane: control, checkService: { exactPasses: () => false } }); const contributor = proposal.domains[0].id; - return { root, control, service, contributor, destination: 'review-area', base, candidate }; + return { root, env, control, service, contributor, destination: 'review-area', base, candidate }; +} + +function concurrentAdoptedRequest(context, requestInput) { + const childSource = ` + import { pathToFileURL } from 'node:url'; + import { resolve } from 'node:path'; + const { repositoryRoot, stateRoot, requestInput } = JSON.parse(process.argv[1]); + const sourceRoot = process.cwd(); + const { openControlPlane } = await import(pathToFileURL(resolve(sourceRoot, 'src/control-plane/service.mjs')).href); + const { IntegrationService } = await import(pathToFileURL(resolve(sourceRoot, 'src/integration/service.mjs')).href); + const control = openControlPlane({ repositoryRoot, env: { ...process.env, XDG_DATA_HOME: stateRoot } }); + const service = new IntegrationService({ repositoryRoot, controlPlane: control, checkService: { exactPasses: () => false } }); + try { + const result = service.requestAdoptedCandidate(requestInput); + console.log(JSON.stringify({ outcome: 'requested', integrationRequestId: result.integrationRequest.id })); + } catch (error) { + console.log(JSON.stringify({ outcome: 'refused', code: error.code ?? null })); + } finally { + control.close(); + } + `; + const encoded = JSON.stringify({ repositoryRoot: context.root, stateRoot: context.env.XDG_DATA_HOME, requestInput }); + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, ['--input-type=module', '-e', childSource, encoded], { + cwd: process.cwd(), env: process.env, stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; let stderr = ''; + child.stdout.on('data', chunk => { stdout += chunk; }); + child.stderr.on('data', chunk => { stderr += chunk; }); + child.on('error', reject); + child.on('close', (code) => { + if (code !== 0) return reject(new Error(`concurrent adopted request child failed: ${stderr}`)); + try { resolve(JSON.parse(stdout.trim().split('\n').at(-1))); } + catch (error) { reject(new Error(`invalid concurrent adopted request output: ${stdout}\n${stderr}\n${error.message}`)); } + }); + }); } function authority(context, { action, adoptionId }) { @@ -266,22 +302,37 @@ test('SCN-integration-adoption-approval-boundaries: unrelated or source-only app wrongProvenance.control.close(); }); -test('SCN-integration-adoption-request-replay: real SQLite rejects a duplicate exact private-ref queue row, and a completed adoption request cannot replay into a second row', () => { +test('SCN-integration-adoption-request-replay: concurrent authenticated API callers serialize on the adoption journal and create exactly one private-ref queue request', async () => { const context = fixture('torch-adoption-request-replay'); const adoptionId = 'fixture-adoption'; prepare(context, adoptionId); - const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); - const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)); - const duplicate = context.control.database.prepare(`INSERT INTO integration_requests ( + context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const requestInput = lifecycleInput(context, 'request', adoptionId); + const outcomes = await Promise.all([ + concurrentAdoptedRequest(context, requestInput), + concurrentAdoptedRequest(context, requestInput), + ]); + assert.deepEqual(outcomes.map((entry) => entry.outcome).sort(), ['refused', 'requested']); + assert.equal(outcomes.find((entry) => entry.outcome === 'refused').code, 'ADOPTION_REQUEST_STATE_INVALID'); + const requests = context.service.list(); + assert.equal(requests.length, 1); + assert.equal(context.service.getPreparedAdoption(adoptionId).state, 'requested'); + context.control.close(); +}); + +test('SCN-integration-adoption-legacy-retries: historical ordinary-request retries remain readable when integration service starts', () => { + const context = fixture('torch-adoption-legacy-retries'); + const insert = context.control.database.prepare(`INSERT INTO integration_requests ( id, project_id, source_area, source_branch, source_commit, target_branch, base_target_commit, state, reason, required_checks, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); - assert.throws(() => duplicate.run( - 'duplicate-race-row', context.control.projectId, context.contributor, finalized.candidateRef, context.candidate, - 'main', context.base, 'testing', null, '[]', requested.requestedAt, requested.requestedAt, - ), /UNIQUE constraint failed: integration_requests.project_id, integration_requests.source_branch, integration_requests.source_commit/); - assert.throws(() => context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)), - (error) => error.code === 'ADOPTION_REQUEST_STATE_INVALID'); - assert.deepEqual(context.service.list().map((record) => record.id), [requested.integrationRequest.id]); + for (const [id, state] of [['historical-failed', 'blocked'], ['historical-retry', 'superseded']]) { + insert.run(id, context.control.projectId, context.contributor, 'refs/heads/candidate', context.candidate, + 'main', context.base, state, 'historical-retry', '[]', '2026-10-03T00:00:00.000Z', '2026-10-03T00:00:00.000Z'); + } + assert.doesNotThrow(() => new IntegrationService({ + repositoryRoot: context.root, controlPlane: context.control, checkService: { exactPasses: () => false }, + })); + assert.equal(context.service.list().length, 2); context.control.close(); }); From 7533d86462c7f158f64c7458d385031b86a7f8eb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 18:03:16 -0400 Subject: [PATCH 13/15] TASK-f40126ee: bind adoption lifecycle revisions --- docs/TEST_INTEGRITY.md | 44 ++++++++++++++++++++++++ src/integration/adoption.mjs | 7 ++++ test/fleet/integration-adoption.test.mjs | 20 +++++++++-- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index c17dc32..7f9c360 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3199,3 +3199,47 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-integration-adoption-lifecycle-revision-cas + given: "A prepared or finalized owner-bound adoption record with a known journal revision" + when: "A caller supplies a stale expectedRecordRevision to finalize or request native qualification" + then: "The operation refuses before private-ref or queue effects; the original refs, journal state, and queue remain unchanged" + observable_outcomes: + - "ADOPTION_RECORD_REVISION_STALE is returned for stale lifecycle callers" + - "Private candidate/archive refs remain absent before stale finalize and no integration request is created by stale native request" + determinism_controls: + - "Disposable local Git repository and temporary SQLite-backed control-plane fixture; no network, provider, or live adoption" + anti_cheat_rationale: + prevents: + - "Issuing external ref or queue effects from a stale lifecycle read" + - "Treating journal revision as an internal-only implementation detail" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +```yaml +test_integrity_note: + change_type: spec_correction + scenarios: + - id: SCN-integration-adoption-request-replay + given: "Two authenticated callers carry the same finalized adoption revision into the serialized native-request transition" + when: "One caller completes the transition and the other resumes with its now-stale expectedRecordRevision" + then: "The second caller strictly receives ADOPTION_RECORD_REVISION_STALE while exactly one requested journal record and one queue row remain" + observable_outcomes: + - "The replay loser reports the stale journal-revision boundary, not a generic post-state refusal" + - "Exactly one private-ref integration request and requested audit transition exist" + determinism_controls: + - "Two independently opened local API processes over disposable Git and SQLite fixture state" + anti_cheat_rationale: + prevents: + - "Accepting a stale lifecycle read after another caller has committed" + - "Relaxing the exactly-one-request invariant after adding revision binding" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index 9d5eabd..4d2c7d5 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -88,6 +88,12 @@ function exactInput(record, input) { if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', { code: 'ADOPTION_IMMUTABLE_BINDING_STALE', details: { adoptionId: record.id, fields: mismatches }, }); + if (!Number.isInteger(input.expectedRecordRevision) || input.expectedRecordRevision !== record.revision) { + throw new TorchError('Lifecycle input must bind the current adoption record revision', { + code: 'ADOPTION_RECORD_REVISION_STALE', + details: { adoptionId: record.id, expectedRevision: input.expectedRecordRevision ?? null, actual: record.revision }, + }); + } } /** @@ -334,6 +340,7 @@ export class AdoptionService { this.controlPlane.database.exec('BEGIN IMMEDIATE'); try { const record = this.getPrepared(adoptionId); + exactInput(record, input); if (!['finalized', 'requesting'].includes(record.state)) { throw new TorchError('Only a finalized adoption can request native qualification', { code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state }, diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs index 741149e..666e6f6 100644 --- a/test/fleet/integration-adoption.test.mjs +++ b/test/fleet/integration-adoption.test.mjs @@ -130,7 +130,8 @@ function prepare(context, adoptionId = 'fixture-adoption') { function lifecycleInput(context, action, adoptionId, extra = {}) { const { approvalId, approvalRevision, adapter } = authority(context, { action, adoptionId }); - return { actorId: 'owner', adoptionId, approvalId, approvalRevision, adapter, ...extra }; + const record = context.service.getPreparedAdoption(adoptionId); + return { actorId: 'owner', adoptionId, expectedRecordRevision: record.revision, approvalId, approvalRevision, adapter, ...extra }; } function blockedCodes(error) { @@ -167,6 +168,17 @@ test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cann (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { candidateSha: context.base })), (error) => error.code === 'ADOPTION_IMMUTABLE_BINDING_STALE'); + assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { + expectedRecordRevision: prepared.revision + 1, + })), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); + const refs = context.service.adoption.refAdapter.readRefs([ + prepared.expectedOldRef, prepared.candidateRef, prepared.archiveRef, + ]); + assert.deepEqual(refs, { + [prepared.expectedOldRef]: context.base, + [prepared.candidateRef]: null, + [prepared.archiveRef]: null, + }); const { mutationPerformed, ...durablePrepared } = prepared; assert.equal(mutationPerformed, true); assert.deepEqual(context.service.adoption.getPrepared(adoptionId), durablePrepared); @@ -243,6 +255,10 @@ test('SCN-integration-adoption-private-ref-intake: finalized private refs enter const adoptionId = 'fixture-adoption'; prepare(context, adoptionId); const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + assert.throws(() => context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId, { + expectedRecordRevision: finalized.revision + 1, + })), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); + assert.deepEqual(context.service.list(), []); const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)); assert.equal(requested.state, 'requested'); assert.equal(requested.integrationRequest.sourceBranch, finalized.candidateRef); @@ -313,7 +329,7 @@ test('SCN-integration-adoption-request-replay: concurrent authenticated API call concurrentAdoptedRequest(context, requestInput), ]); assert.deepEqual(outcomes.map((entry) => entry.outcome).sort(), ['refused', 'requested']); - assert.equal(outcomes.find((entry) => entry.outcome === 'refused').code, 'ADOPTION_REQUEST_STATE_INVALID'); + assert.equal(outcomes.find((entry) => entry.outcome === 'refused').code, 'ADOPTION_RECORD_REVISION_STALE'); const requests = context.service.list(); assert.equal(requests.length, 1); assert.equal(context.service.getPreparedAdoption(adoptionId).state, 'requested'); From 4a5f3d0c94b5e06d3e171a73004d2412c21dc830 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 18:09:14 -0400 Subject: [PATCH 14/15] TASK-f40126ee: compose d179 adoption candidate Port the owner-authorized four-path adoption delta from the original Work lineage (2c59634, 8f894e9, 97973f0, 8ea3bef, 63dd032, 530afb3, beb99bc, 004d9e7) plus caller-revision correction 7533d864. Preserve unrelated B2/check/convergence history outside this isolated composition. --- docs/TEST_INTEGRITY.md | 180 ++++++++++ src/integration/adoption.mjs | 384 +++++++++++++++++++++ src/integration/service.mjs | 102 +++++- test/fleet/integration-adoption.test.mjs | 420 +++++++++++++++++++++++ 4 files changed, 1085 insertions(+), 1 deletion(-) create mode 100644 src/integration/adoption.mjs create mode 100644 test/fleet/integration-adoption.test.mjs diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 09afa7f..19deb55 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2883,6 +2883,45 @@ test_integrity_note: if_true_explain_spec_basis: "" ``` +# Candidate source B2 frozen-input and fence boundaries (2026-10-01) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-candidate-bootstrap-one-shot + given: "An issued, nonce-bound native admission" + when: "It is opened, replayed, expired, foreign, or malformed" + then: "Only one bounded private bootstrap opens; every invalid case refuses before child start and exposes no authority handle" + - id: SCN-candidate-input-consumption + given: "A frozen project-local source snapshot" + when: "The original source changes before the fixed Node child reads its cwd" + then: "The child observes the captured bytes; snapshot drift refuses" + - id: SCN-candidate-execution-boundary + given: "A fixed command and explicit bounded environment" + when: "Ambient PATH/HOME/cache fallback or output overflow is attempted" + then: "Execution refuses or remains non-success" + - id: SCN-candidate-fence-closure + given: "A private PID1 namespace observation" + when: "Descendant closure is absent or uncertain" + then: "The outcome is UNKNOWN/INCOMPLETE, never terminal PASS" + observable_outcomes: + - "No DB, ControlPlane, writer, receipt adapter, or authority FD reaches the child" + - "Product input links and special nodes refuse; frozen product S stays under 64MiB/file and 512MiB total" + - "Artifact output remains M/artifactManifest; receipt-adapter A is absent and unclaimed" + determinism_controls: + - "Temporary source trees, fixed Node executable, explicit empty ambient environment, bounded streams, and injected observations" + - "No live policy registration, installation, network, cache, or provider process" + anti_cheat_rationale: + prevents: + - "Reading mutated live source instead of captured bytes" + - "Replaying a private admission or relabeling fixture output as a receipt" + - "Treating PGID emptiness, zero exit, or Chromium metadata as descendant/binary proof" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + # Console attention advisory cohorts (2026-10-01) ```yaml @@ -2932,6 +2971,47 @@ test_integrity_note: if_true_explain_spec_basis: "" ``` +# Candidate source B2 authenticated-attempt spec correction (2026-10-01) + +```yaml +test_integrity_note: + change_type: spec_correction + scenarios: + - id: SCN-candidate-bootstrap-one-shot + given: "A CheckService-issued opaque registered-plan handle" + when: "A foreign, cloned, expired, replayed, or already-closed handle is used" + then: "No FD3 admission opens; the only accepted acknowledgement is the exact FD4 binding frame and it closes the original linear handle" + - id: SCN-candidate-input-consumption + given: "A frozen, bounded product snapshot and a fixed Node E binding" + when: "The original product bytes change before execution" + then: "An actual spawned child reads the frozen bytes; post-capture drift refuses before spawn" + - id: SCN-candidate-execution-boundary + given: "An opaque attempt with fixed command, arguments, explicit environment, and output cap" + when: "The child exits zero after exceeding the cap" + then: "The parent records overflow as non-success without accepting an injected executor or caller-selected limit" + - id: SCN-candidate-fence-closure + given: "A closed opaque attempt" + when: "The backend has not recorded namespace identity, FD closure, and descendant reaping" + then: "Terminal proof is UNKNOWN; fabricated observations and cloned handles cannot create proof" + observable_outcomes: + - "Only the exact FD4 acknowledgement closes a one-shot opaque admission" + - "Child stdout proves frozen-byte consumption, not same-process test reads" + - "Zero exit plus output overflow is never success" + - "Missing backend fence evidence is never TerminalProof" + determinism_controls: + - "Temporary source trees, fixed absolute Node runtime, explicit empty child environment, bounded streams" + - "No injected executor, caller-provided command, root, environment, limit, or terminal observation" + anti_cheat_rationale: + prevents: + - "Arbitrary tuple issuance or a fabricated bootstrap literal" + - "Boolean caller observations minting terminal proof" + - "Cloned or reused handles reopening an admission" + - "Same-process snapshot reads impersonating child consumption" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + # Compact Console Overview density (2026-10-01) ```yaml @@ -3063,3 +3143,103 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +# Exact-source adoption (2026-10-03) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-integration-adoption-private-ref-cas + given: "A clean disposable Git repository, a base commit, and an owner-bound private-ref adoption" + when: "The adoption finalizes and a stale compare-and-swap attempts to alter its candidate ref" + then: "Real Git private archive and candidate refs are created atomically, while the stale write refuses" + - id: SCN-integration-adoption-journal-and-binding + given: "A prepared adoption journal record" + when: "A second preparation or lifecycle request supplies a different candidate binding" + then: "The journal revision compare-and-swap and immutable binding checks refuse without changing the prepared record" + - id: SCN-integration-adoption-finalize-recovery + given: "A real private-ref transaction completed before its journal write" + when: "Reconciliation is run and then replayed" + then: "One durable finalized recovery is recorded and the replay is consistent without another mutation" + - id: SCN-integration-adoption-rollback-recovery + given: "A finalized candidate whose private ref was removed before its rollback journal write, or whose candidate ref diverged" + when: "Rollback is requested" + then: "The completed rollback is recovered once and divergent candidate bytes refuse" + - id: SCN-integration-adoption-private-ref-intake + given: "An existing unfinalized private candidate ref and a separate finalized owner-bound private candidate ref" + when: "Direct private-ref intake and authenticated native qualification are requested" + then: "Direct intake refuses without a queue record, while the authenticated path records the exact private ref and commit in ordinary testing state" + - id: SCN-integration-adoption-approval-boundaries + given: "An ordinary source-only approval, or an adoption whose actor, action, approval revision, or provenance differs from the approved immutable binding" + when: "Preparation or a lifecycle transition is attempted" + then: "The transition refuses with its structured boundary code and no integration queue record" + - id: SCN-integration-adoption-request-replay + given: "A finalized adoption that has created its one ordinary private-ref integration request" + when: "Two independently opened authenticated API callers request it concurrently" + then: "The existing transactional adoption journal serializes them, one caller refuses after the completed transition, and exactly one queue row remains" + - id: SCN-integration-adoption-legacy-retries + given: "An existing project database containing two historical ordinary-request retry rows with the same source branch and commit" + when: "IntegrationService starts" + then: "Both records remain readable; startup adds no global uniqueness constraint and does not reject historical evidence" + observable_outcomes: + - "Git ref values, structured refusal codes, durable journal state, and native integration-request fields" + determinism_controls: + - "Disposable local Git repositories, temporary SQLite-backed control-plane state, fixed fixture bytes, and no network or provider" + anti_cheat_rationale: + prevents: + - "Fixture-only CAS implementations that do not exercise Git update-ref" + - "Mutable lifecycle bindings or overwriteable adoption journal records" + - "Claiming crash recovery without preserving actual ref effects" + - "Treating a private candidate as an ordinary specialist branch" + - "Using an unrelated, source-only, stale, or mismatched approval as adoption authority" + - "Duplicating a private-ref queue record through concurrent authenticated API requests or replay" + - "Rejecting historical ordinary-request retry evidence with an unqualified schema constraint" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-integration-adoption-lifecycle-revision-cas + given: "A prepared or finalized owner-bound adoption record with a known journal revision" + when: "A caller supplies a stale or omitted expectedRecordRevision to finalize, reconcile, rollback, or request native qualification" + then: "The operation refuses before private-ref, journal, queue, or audit effects; the original record and refs remain unchanged" + observable_outcomes: + - "ADOPTION_RECORD_REVISION_STALE is returned for stale or omitted lifecycle callers" + - "Private candidate/archive refs, durable journal record, audit count, and integration queue remain unchanged for every rejected lifecycle call" + determinism_controls: + - "Disposable local Git repository and temporary SQLite-backed control-plane fixture; no network, provider, or live adoption" + anti_cheat_rationale: + prevents: + - "Issuing external ref or queue effects from a stale lifecycle read" + - "Treating journal revision as an internal-only implementation detail" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +```yaml +test_integrity_note: + change_type: spec_correction + scenarios: + - id: SCN-integration-adoption-request-replay + given: "Two authenticated callers carry the same finalized adoption revision into the serialized native-request transition" + when: "One caller completes the transition and the other resumes with its now-stale expectedRecordRevision" + then: "The second caller strictly receives ADOPTION_RECORD_REVISION_STALE while exactly one requested journal record and one queue row remain" + observable_outcomes: + - "The replay loser reports the stale journal-revision boundary, not a generic post-state refusal" + - "Exactly one private-ref integration request and requested audit transition exist" + determinism_controls: + - "Two independently opened local API processes over disposable Git and SQLite fixture state" + anti_cheat_rationale: + prevents: + - "Accepting a stale lifecycle read after another caller has committed" + - "Relaxing the exactly-one-request invariant after adding revision binding" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs new file mode 100644 index 0000000..4d2c7d5 --- /dev/null +++ b/src/integration/adoption.mjs @@ -0,0 +1,384 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { TorchError } from '../kernel/errors.mjs'; +import { observeRuntimeTurn } from '../runtime/turn-guard.mjs'; + +const ADOPTION_SCHEMA = 'torch.dev/integration-adoption/v1alpha1'; +const AUTHORITY_SCHEMA = 'torch.dev/integration-adoption-authority/v1alpha1'; +const ADAPTER_NAME = 'git-private-ref-cas-v1'; +const ACTIONS = new Set(['plan', 'prepare', 'finalize', 'reconcile', 'rollback', 'request']); +const MUTATING_ACTIONS = new Set(['prepare', 'finalize', 'rollback', 'request']); +const SHA = /^[0-9a-f]{40,64}$/; +const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/; +const ZERO_SHA = '0'.repeat(40); + +function git(root, args, { allowFailure = false } = {}) { + const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); + if (result.status !== 0 && !allowFailure) { + throw new TorchError(`Git command failed: git ${args.join(' ')}`, { + code: 'GIT_COMMAND_FAILED', details: result.stderr?.trim() || null, + }); + } + return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' }; +} + +function tableExists(database, name) { + return Boolean(database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)); +} + +function text(value, name) { + if (typeof value !== 'string' || !value.trim()) { + throw new TorchError(`${name} is required`, { code: 'ADOPTION_INPUT_INVALID', details: { name } }); + } + return value.trim(); +} + +function sha(value, name) { + const normalized = text(value, name); + if (!SHA.test(normalized)) throw new TorchError(`${name} must be a full Git object id`, { + code: 'ADOPTION_INPUT_INVALID', details: { name }, + }); + return normalized; +} + +function digest(value) { + return createHash('sha256').update(JSON.stringify(value)).digest('hex'); +} + +function durableRecord(event) { + if (!event?.details) return null; + try { + const record = JSON.parse(event.details); + return record?.schema === ADOPTION_SCHEMA ? record : null; + } catch { return null; } +} + +function listWorktreePaths(root) { + const records = []; + for (const line of git(root, ['worktree', 'list', '--porcelain']).stdout.split('\n')) { + if (line.startsWith('worktree ')) records.push(line.slice('worktree '.length)); + } + return records; +} + +function authorityFrom(approval) { + if (!approval || approval.status !== 'approved' || approval.approver_id !== 'owner' || approval.decided_by !== 'owner') return null; + try { + const authority = JSON.parse(approval.decision_note ?? ''); + return authority?.schema === AUTHORITY_SCHEMA ? authority : null; + } catch { return null; } +} + +function exactAuthority(authority, binding) { + if (!authority || authority.action !== binding.action) return false; + const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest', + 'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef']; + return fields.every((field) => authority[field] === binding[field]) + && authority.adapter?.name === binding.adapter?.name + && authority.adapter?.id === binding.adapter?.id; +} + +function exactInput(record, input) { + const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest', + 'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef']; + const mismatches = fields.filter((field) => input[field] !== undefined + && input[field] !== (field === 'adoptionId' ? record.id : record[field])); + if (input.provenance !== undefined && JSON.stringify(input.provenance) !== JSON.stringify(record.provenance)) mismatches.push('provenance'); + if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', { + code: 'ADOPTION_IMMUTABLE_BINDING_STALE', details: { adoptionId: record.id, fields: mismatches }, + }); + if (!Number.isInteger(input.expectedRecordRevision) || input.expectedRecordRevision !== record.revision) { + throw new TorchError('Lifecycle input must bind the current adoption record revision', { + code: 'ADOPTION_RECORD_REVISION_STALE', + details: { adoptionId: record.id, expectedRevision: input.expectedRecordRevision ?? null, actual: record.revision }, + }); + } +} + +/** + * Real Git private-ref transaction adapter. It is source code only: callers + * decide when it is invoked, while the adapter itself gives temp-repository + * scenarios the same atomic update-ref semantics as a real repository. + */ +export class GitPrivateRefAdapter { + constructor({ repositoryRoot } = {}) { + this.repositoryRoot = resolve(text(repositoryRoot, 'repositoryRoot')); + this.name = ADAPTER_NAME; + this.id = digest({ adapter: this.name, repositoryRoot: this.repositoryRoot }); + } + + readRefs(refs) { + const values = {}; + for (const ref of refs) { + const result = git(this.repositoryRoot, ['rev-parse', '--verify', '--quiet', ref], { allowFailure: true }); + values[ref] = result.status === 0 ? result.stdout : null; + } + return values; + } + + compareAndSwapRefs({ expected = [], writes = [] } = {}) { + const commands = ['start']; + const writtenRefs = new Set(writes.map((entry) => entry.ref)); + for (const { ref, sha: expectedSha } of expected) { + if (writtenRefs.has(ref)) continue; + commands.push(`verify ${text(ref, 'ref')} ${expectedSha ?? ZERO_SHA}`); + } + for (const { ref, sha: nextSha } of writes) { + const expectedEntry = expected.find((entry) => entry.ref === ref); + if (!expectedEntry) throw new TorchError('Every private-ref write requires an exact expected value', { + code: 'ADOPTION_REF_CAS_EXPECTED_REQUIRED', details: { ref }, + }); + const current = expectedEntry.sha ?? null; + if (nextSha === null) commands.push(`delete ${text(ref, 'ref')} ${current ?? ZERO_SHA}`); + else commands.push(`update ${text(ref, 'ref')} ${sha(nextSha, 'next ref sha')} ${current ?? ZERO_SHA}`); + } + commands.push('prepare', 'commit', ''); + const result = spawnSync('git', ['-C', this.repositoryRoot, 'update-ref', '--stdin'], { + input: commands.join('\n'), encoding: 'utf8', + }); + if (result.status !== 0) throw new TorchError('Private ref compare-and-swap refused', { + code: 'ADOPTION_REF_CAS_FAILED', details: result.stderr?.trim() || null, + }); + return { adapter: { name: this.name, id: this.id }, expected, writes }; + } +} + +export class AdoptionService { + constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, refAdapter } = {}) { + this.repositoryRoot = repositoryRoot; + this.controlPlane = controlPlane; + this.clock = clock; + this.idFactory = idFactory; + this.policy = policy ?? {}; + this.refAdapter = refAdapter ?? new GitPrivateRefAdapter({ repositoryRoot }); + } + + plan({ action = 'plan', actorId, approvalId, approvalRevision, adoptionId, + candidateSha, baseTargetSha, provenance, provenanceManifestDigest, contributorArea, + destinationArea, targetBranch, candidateRef, expectedOldRef, adapter } = {}) { + const blockers = []; + const id = adoptionId ? text(adoptionId, 'adoptionId') : null; + if (id && !SAFE_ID.test(id)) blockers.push({ code: 'ADOPTION_ID_INVALID' }); + if (!ACTIONS.has(action)) blockers.push({ code: 'ADOPTION_ACTION_INVALID' }); + if (actorId !== 'owner') blockers.push({ code: 'OWNER_ACTOR_REQUIRED' }); + if (!Number.isInteger(approvalRevision) || approvalRevision < 1) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_REQUIRED' }); + const adapterBinding = adapter?.name && adapter?.id ? { name: adapter.name, id: adapter.id } : null; + if (MUTATING_ACTIONS.has(action) && !adapterBinding) blockers.push({ code: 'ADOPTION_ADAPTER_BINDING_REQUIRED' }); + if (adapterBinding && (adapterBinding.name !== this.refAdapter.name || adapterBinding.id !== this.refAdapter.id)) { + blockers.push({ code: 'ADOPTION_ADAPTER_PROVENANCE_INVALID' }); + } + + let candidate; let base; let provenanceDigest; + try { + candidate = sha(candidateSha, 'candidateSha'); + base = sha(baseTargetSha, 'baseTargetSha'); + if (!Array.isArray(provenance) || !provenance.length || provenance.some((entry) => typeof entry !== 'string' || !entry)) { + throw new TorchError('provenance must be a non-empty ordered string list', { code: 'ADOPTION_INPUT_INVALID' }); + } + provenanceDigest = digest(provenance); + if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest }); + } catch (error) { + blockers.push({ code: error.code ?? 'ADOPTION_INPUT_INVALID', details: error.details ?? null }); + } + + const target = targetBranch ?? this.policy.target; + if (typeof target !== 'string' || !target.trim()) blockers.push({ code: 'ADOPTION_TARGET_REQUIRED' }); + const candidateReference = candidateRef ?? (id ? `refs/torch/integration-candidates/${id}` : null); + const expectedReference = expectedOldRef ?? (target ? `refs/heads/${target}` : null); + if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' }); + if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' }); + + let contributor; let destination; + try { contributor = this.controlPlane.assertIdentity(text(contributorArea, 'contributorArea')); } + catch (error) { blockers.push({ code: 'ADOPTION_CONTRIBUTOR_INVALID', details: error.code ?? null }); } + try { destination = this.controlPlane.assertIdentity(text(destinationArea, 'destinationArea')); } + catch (error) { blockers.push({ code: 'ADOPTION_DESTINATION_INVALID', details: error.code ?? null }); } + if (contributor && destination && contributor === destination) blockers.push({ code: 'ADOPTION_IDENTITIES_MUST_DIFFER' }); + + const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base, provenanceManifestDigest: provenanceDigest, + contributorArea: contributor, destinationArea: destination, targetBranch: target, candidateRef: candidateReference, + expectedOldRef: expectedReference, adapter: adapterBinding }; + const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests WHERE id = ? AND project_id = ?`) + .get(approvalId, this.controlPlane.projectId) : null; + if (!approval) blockers.push({ code: 'ADOPTION_APPROVAL_MISSING' }); + else if (approval.revision !== approvalRevision) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_STALE', actual: approval.revision }); + else if (!exactAuthority(authorityFrom(approval), binding)) blockers.push({ code: 'ADOPTION_APPROVAL_BINDING_INVALID' }); + + if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' }); + if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_BASE_MISSING' }); + if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' }); + if (expectedReference && base) { + const current = this.refAdapter.readRefs([expectedReference])[expectedReference]; + if (!current) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' }); + else if (current !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current }); + } + const candidateExists = candidateReference && this.refAdapter.readRefs([candidateReference])[candidateReference] != null; + if (['plan', 'prepare', 'finalize'].includes(action) && candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' }); + if (action === 'request' && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' }); + + for (const worktree of listWorktreePaths(this.repositoryRoot)) { + const entries = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']).stdout.split('\n').filter(Boolean); + if (entries.length) blockers.push({ code: 'ADOPTION_WORKTREE_DIRTY', worktree, entries }); + } + if (tableExists(this.controlPlane.database, 'worktree_guards')) { + for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard }); + } + if (tableExists(this.controlPlane.database, 'resource_leases')) { + for (const lease of this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases WHERE released_at IS NULL`).all()) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease }); + } + if (tableExists(this.controlPlane.database, 'prepared_checks')) { + for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check }); + } + for (const area of [contributor, destination].filter(Boolean)) { + const executor = observeRuntimeTurn({ stateRoot: this.controlPlane.stateRoot, repositoryRoot: this.repositoryRoot, + projectId: this.controlPlane.projectId, areaId: area, now: this.clock }); + if (executor.state !== 'inactive') blockers.push({ code: 'ADOPTION_EXECUTOR_NOT_INACTIVE', areaId: area, executor }); + } + return { action: `plan-adoption-${action}`, adoptionId: id, approval: approval ? { id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id } : null, + authority: binding, candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null, + contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, candidateRef: candidateReference, + expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false }; + } + + #append(adoptionId, expectedRevision, record, operation) { + this.controlPlane.database.exec('BEGIN IMMEDIATE'); + try { + this.#appendInTransaction(adoptionId, expectedRevision, record, operation); + this.controlPlane.database.exec('COMMIT'); + } catch (error) { this.controlPlane.database.exec('ROLLBACK'); throw error; } + } + + #appendInTransaction(adoptionId, expectedRevision, record, operation) { + const current = this.#find(adoptionId); + if (expectedRevision === null ? current : !current || current.revision !== expectedRevision) { + throw new TorchError('Adoption journal changed before this transition could be recorded', { + code: 'ADOPTION_RECORD_REVISION_STALE', details: { adoptionId, expectedRevision, actual: current?.revision ?? null }, + }); + } + this.controlPlane.database.prepare(`INSERT INTO audit_events + (id, project_id, actor_id, operation, entity_type, entity_id, details, created_at) + VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`) + .run(this.idFactory(), this.controlPlane.projectId, operation, adoptionId, JSON.stringify(record), this.clock().toISOString()); + } + + #fromRecord(record, input, action) { + exactInput(record, input); + return this.plan({ action, actorId: input.actorId, approvalId: input.approvalId, approvalRevision: input.approvalRevision, + adoptionId: record.id, candidateSha: record.candidateSha, baseTargetSha: record.baseTargetSha, provenance: record.provenance, + provenanceManifestDigest: record.provenanceManifestDigest, contributorArea: record.contributorArea, destinationArea: record.destinationArea, + targetBranch: record.targetBranch, candidateRef: record.candidateRef, expectedOldRef: record.expectedOldRef, adapter: input.adapter }); + } + + prepare(input = {}) { + const adoptionId = input.adoptionId ?? this.idFactory(); + const plan = this.plan({ ...input, action: 'prepare', adoptionId }); + if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers }); + const now = this.clock().toISOString(); + const record = { schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'), + approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha, + baseTargetSha: plan.baseTargetSha, provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef, + expectedOldRef: plan.expectedOldRef, contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch, + archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, rolledBackAt: null }; + this.#append(adoptionId, null, record, 'integration.adoption.prepare'); + return { ...record, mutationPerformed: true }; + } + + finalize(input = {}) { + const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); + if (record.state !== 'prepared') throw new TorchError('Only a prepared adoption can be finalized', { code: 'ADOPTION_FINALIZE_STATE_INVALID', details: { state: record.state } }); + const plan = this.#fromRecord(record, input, 'finalize'); + if (!plan.canProceed) throw new TorchError('Adoption finalize is blocked', { code: 'ADOPTION_FINALIZE_BLOCKED', details: plan.blockers }); + const receipt = this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }], + writes: [{ ref: record.archiveRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: record.candidateSha }] }); + const finalized = { ...record, revision: record.revision + 1, state: 'finalized', finalizeApprovalId: text(input.approvalId, 'approvalId'), + finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), refReceiptDigest: digest(receipt) }; + this.#append(record.id, record.revision, finalized, 'integration.adoption.finalize'); + return { ...finalized, mutationPerformed: true }; + } + + reconcile(input = {}) { + const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); + const plan = this.#fromRecord(record, input, 'reconcile'); + if (!plan.canProceed) return { record, plan, state: 'blocked', mutationPerformed: false }; + const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]); + const finalizedRefs = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha; + if (record.state === 'prepared' && finalizedRefs) { + const recovered = { ...record, revision: record.revision + 1, state: 'finalized', finalizedAt: this.clock().toISOString(), + recoveredAt: this.clock().toISOString(), recovery: 'private-ref-cas-completed-before-journal' }; + this.#append(record.id, record.revision, recovered, 'integration.adoption.reconcile-finalized'); + return { record: recovered, refs, state: 'recovered-finalized', mutationPerformed: true }; + } + if (record.state === 'finalized' && finalizedRefs) return { record, refs, state: 'consistent', mutationPerformed: false }; + if (record.state === 'prepared' && refs[record.candidateRef] === null && refs[record.archiveRef] === null) return { record, refs, state: 'prepared', mutationPerformed: false }; + return { record, refs, state: 'diverged', mutationPerformed: false }; + } + + rollback(input = {}) { + const record = this.getPrepared(text(input.adoptionId, 'adoptionId')); + if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can be rolled back', { code: 'ADOPTION_ROLLBACK_STATE_INVALID', details: { state: record.state } }); + const plan = this.#fromRecord(record, input, 'rollback'); + if (!plan.canProceed) throw new TorchError('Adoption rollback is blocked', { code: 'ADOPTION_ROLLBACK_BLOCKED', details: plan.blockers }); + const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]); + const recovered = refs[record.expectedOldRef] === record.baseTargetSha + && refs[record.candidateRef] === null && refs[record.archiveRef] === record.baseTargetSha; + if (!recovered && refs[record.candidateRef] !== record.candidateSha) { + throw new TorchError('Candidate ref changed before rollback', { code: 'ADOPTION_ROLLBACK_REF_DIVERGED', details: refs }); + } + const receipt = recovered ? { adapter: { name: this.refAdapter.name, id: this.refAdapter.id }, recovered: true, refs } + : this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] }); + const rolledBack = { ...record, revision: record.revision + 1, state: 'rolled-back', rollbackApprovalId: text(input.approvalId, 'approvalId'), + rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: digest(receipt), + ...(recovered ? { recoveredAt: this.clock().toISOString(), recovery: 'private-ref-rollback-completed-before-journal' } : {}) }; + this.#append(record.id, record.revision, rolledBack, 'integration.adoption.rollback'); + return { ...rolledBack, mutationPerformed: true }; + } + + requestAdoptedCandidate(input = {}, { requestIntegration, findIntegrationRequest } = {}) { + if (typeof requestIntegration !== 'function') throw new TorchError('Native integration request adapter is unavailable', { code: 'ADOPTION_NATIVE_REQUEST_UNAVAILABLE' }); + const adoptionId = text(input.adoptionId, 'adoptionId'); + this.controlPlane.database.exec('BEGIN IMMEDIATE'); + try { + const record = this.getPrepared(adoptionId); + exactInput(record, input); + if (!['finalized', 'requesting'].includes(record.state)) { + throw new TorchError('Only a finalized adoption can request native qualification', { + code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state }, + }); + } + const plan = this.#fromRecord(record, input, 'request'); + if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers }); + const requesting = record.state === 'requesting' ? record : { ...record, revision: record.revision + 1, state: 'requesting', + requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() }; + if (record.state !== 'requesting') this.#appendInTransaction(record.id, record.revision, requesting, 'integration.adoption.request-started'); + const existing = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha }); + if (existing && (existing.sourceArea !== requesting.contributorArea || existing.targetBranch !== requesting.targetBranch)) { + throw new TorchError('Existing private-ref integration request does not match this adoption', { + code: 'ADOPTION_INTEGRATION_REQUEST_CONFLICT', details: { adoptionId: requesting.id, integrationRequestId: existing.id }, + }); + } + const request = existing ?? requestIntegration({ adoptionId: requesting.id, areaId: requesting.contributorArea, + commit: requesting.candidateSha, candidateRef: requesting.candidateRef }); + const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() }; + this.#appendInTransaction(requesting.id, requesting.revision, requested, 'integration.adoption.request'); + this.controlPlane.database.exec('COMMIT'); + return { ...requested, integrationRequest: request, mutationPerformed: true }; + } catch (error) { + this.controlPlane.database.exec('ROLLBACK'); + throw error; + } + } + + #find(adoptionId) { + const row = this.controlPlane.database.prepare(`SELECT details FROM audit_events WHERE project_id = ? + AND entity_type = 'integration-adoption' AND entity_id = ? ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, adoptionId); + return durableRecord(row); + } + + getPrepared(adoptionId) { + const id = text(adoptionId, 'adoptionId'); + const record = this.#find(id); + if (!record) throw new TorchError('Unknown adoption journal record', { code: 'ADOPTION_NOT_FOUND', details: { adoptionId: id } }); + return record; + } +} diff --git a/src/integration/service.mjs b/src/integration/service.mjs index c996634..e027dab 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -4,6 +4,7 @@ import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { loadFleetDefinition } from '../kernel/worktrees.mjs'; import { BacklogService } from '../backlog/service.mjs'; +import { AdoptionService } from './adoption.mjs'; function git(root, args, { allowFailure = false } = {}) { const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); @@ -15,6 +16,21 @@ function git(root, args, { allowFailure = false } = {}) { return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' }; } +function text(value, name) { + if (typeof value !== 'string' || !value.trim()) { + throw new TorchError(`${name} is required`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } }); + } + return value.trim(); +} + +function sha(value, name) { + const normalized = text(value, name); + if (!/^[0-9a-f]{40,64}$/.test(normalized)) { + throw new TorchError(`${name} must be a full Git object id`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } }); + } + return normalized; +} + function integrationRow(row) { if (!row) return null; return { @@ -42,6 +58,8 @@ export class IntegrationService { this.worktrees = new Map((manifest.external ?? []) .filter((entry) => entry.type === 'worktree') .map((entry) => [entry.area, entry])); + this.adoption = new AdoptionService({ repositoryRoot, controlPlane, clock, idFactory, + policy: { target: this.policy.target } }); this.controlPlane.database.exec(` CREATE TABLE IF NOT EXISTS integration_requests ( id TEXT PRIMARY KEY, @@ -84,12 +102,56 @@ export class IntegrationService { code: 'INTEGRATION_SOURCE_MOVED', details: { branchTip, sourceCommit }, }); } + return this.#registerRequest({ area, sourceBranch: worktree.branch, sourceCommit }); + } + + requestCandidate({ areaId, commit, candidateRef } = {}) { + throw new TorchError('Private candidate intake requires an authenticated finalized adoption', { + code: 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY', details: { areaId, commit, candidateRef }, + }); + } + + #requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef } = {}) { + const area = this.controlPlane.assertIdentity(areaId); + if (area === 'session-manager') { + throw new TorchError('Session Manager does not own a feature worktree by default', { + code: 'INTEGRATION_SOURCE_INVALID', + }); + } + const adoption = this.adoption.getPrepared(text(adoptionId, 'adoptionId')); + if (adoption.state !== 'requesting' || adoption.contributorArea !== area + || adoption.candidateSha !== commit || adoption.candidateRef !== candidateRef) { + throw new TorchError('Private candidate does not match an authenticated finalized adoption', { + code: 'INTEGRATION_ADOPTED_BINDING_INVALID', details: { adoptionId, state: adoption.state }, + }); + } + const ref = text(candidateRef, 'candidateRef'); + if (!/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(ref)) { + throw new TorchError('Adopted candidates must use a private integration candidate ref', { + code: 'INTEGRATION_ADOPTED_REF_INVALID', details: { candidateRef: ref }, + }); + } + const sourceCommit = sha(commit, 'commit'); + const refTip = git(this.repositoryRoot, ['rev-parse', '--verify', ref], { allowFailure: true }); + if (refTip.status !== 0 || refTip.stdout !== sourceCommit) { + throw new TorchError('Adopted candidate ref no longer names the exact requested commit', { + code: 'INTEGRATION_ADOPTED_REF_STALE', details: { candidateRef: ref, sourceCommit, actual: refTip.stdout || null }, + }); + } + const existing = this.controlPlane.database.prepare(`SELECT id FROM integration_requests + WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`) + .get(this.controlPlane.projectId, ref, sourceCommit); + if (existing) return this.get(existing.id); + return this.#registerRequest({ area, sourceBranch: ref, sourceCommit }); + } + + #registerRequest({ area, sourceBranch, sourceCommit }) { const targetBranch = this.policy.target; const baseTargetCommit = git(this.repositoryRoot, ['rev-parse', targetBranch]).stdout; const now = this.clock().toISOString(); const record = { id: this.idFactory(), projectId: this.controlPlane.projectId, sourceArea: area, - sourceBranch: worktree.branch, sourceCommit, targetBranch, baseTargetCommit, + sourceBranch, sourceCommit, targetBranch, baseTargetCommit, state: 'testing', reason: null, requiredChecks: this.policy.required_checks ?? [], authorizedBy: null, authorizedAt: null, createdAt: now, updatedAt: now, landedAt: null, }; @@ -110,6 +172,44 @@ export class IntegrationService { return this.evaluate(record.id); } + planAdoption(input = {}) { + return this.adoption.plan(input); + } + + prepareAdoption(input = {}) { + return this.adoption.prepare(input); + } + + getPreparedAdoption(adoptionId) { + return this.adoption.getPrepared(adoptionId); + } + + finalizeAdoption(input = {}) { + return this.adoption.finalize(input); + } + + reconcileAdoption(input = {}) { + return this.adoption.reconcile(input); + } + + rollbackAdoption(input = {}) { + return this.adoption.rollback(input); + } + + requestAdoptedCandidate(input = {}) { + return this.adoption.requestAdoptedCandidate(input, { + requestIntegration: ({ adoptionId, areaId, commit, candidateRef }) => this.#requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef }), + findIntegrationRequest: ({ candidateRef, commit }) => this.findCandidateRequest({ candidateRef, commit }), + }); + } + + findCandidateRequest({ candidateRef, commit } = {}) { + const row = this.controlPlane.database.prepare(`SELECT id FROM integration_requests + WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`) + .get(this.controlPlane.projectId, candidateRef, commit); + return row ? this.get(row.id) : null; + } + get(requestId) { const row = this.controlPlane.database.prepare('SELECT * FROM integration_requests WHERE id = ?').get(requestId); if (!row) throw new TorchError(`Unknown integration request: ${requestId}`, { code: 'INTEGRATION_NOT_FOUND' }); diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs new file mode 100644 index 0000000..3ec6641 --- /dev/null +++ b/test/fleet/integration-adoption.test.mjs @@ -0,0 +1,420 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { execFileSync, spawn } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; +import test from 'node:test'; +import { openControlPlane } from '../../src/control-plane/service.mjs'; +import { analyzeRepository } from '../../src/kernel/analyze.mjs'; +import { proposeDomains } from '../../src/kernel/domains.mjs'; +import { inspectRepository } from '../../src/kernel/git.mjs'; +import { installProject } from '../../src/kernel/install.mjs'; +import { IntegrationService } from '../../src/integration/service.mjs'; + +const sha = value => execFileSync('git', ['-C', value, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); +const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex'); + +function fixture(name) { + const root = mkdtempSync(join(tmpdir(), `${name}-`)); + execFileSync('git', ['init', '-b', 'main', root]); + execFileSync('git', ['-C', root, 'config', 'user.email', 'torch-test@example.invalid']); + execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']); + writeFileSync(join(root, 'package.json'), `${JSON.stringify({ name, scripts: {} })}\n`); + writeFileSync(join(root, 'app.mjs'), 'export const value = 1;\n'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'fixture base']); + const repository = inspectRepository(root); + const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) }); + proposal.domains.push({ + id: 'review-area', title: 'Review Area', kind: 'development', + scope: ['review fixtures'], not_scope: ['application source'], owned_paths: ['review/**'], + shared_paths: [], neighbours: [], required_checks: [], resources: [], runtime: 'claude', evidence: ['app.mjs'], + }); + proposal.checks = []; + proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00.000Z', reviewedBy: 'fixture-owner', notes: [] }; + const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) }; + installProject({ repository, proposal, env, projectId: name }); + execFileSync('git', ['-C', root, 'add', '.torch']); + execFileSync('git', ['-C', root, 'commit', '-m', 'install fixture control plane']); + const base = sha(root); + execFileSync('git', ['-C', root, 'checkout', '-b', 'candidate']); + writeFileSync(join(root, 'candidate.mjs'), 'export const candidate = true;\n'); + execFileSync('git', ['-C', root, 'add', 'candidate.mjs']); + execFileSync('git', ['-C', root, 'commit', '-m', 'candidate contribution']); + const candidate = sha(root); + execFileSync('git', ['-C', root, 'checkout', 'main']); + const control = openControlPlane({ repositoryRoot: root, env }); + const service = new IntegrationService({ repositoryRoot: root, controlPlane: control, checkService: { exactPasses: () => false } }); + const contributor = proposal.domains[0].id; + return { root, env, control, service, contributor, destination: 'review-area', base, candidate }; +} + +function concurrentAdoptedRequest(context, requestInput) { + const childSource = ` + import { pathToFileURL } from 'node:url'; + import { resolve } from 'node:path'; + const { repositoryRoot, stateRoot, requestInput } = JSON.parse(process.argv[1]); + const sourceRoot = process.cwd(); + const { openControlPlane } = await import(pathToFileURL(resolve(sourceRoot, 'src/control-plane/service.mjs')).href); + const { IntegrationService } = await import(pathToFileURL(resolve(sourceRoot, 'src/integration/service.mjs')).href); + const control = openControlPlane({ repositoryRoot, env: { ...process.env, XDG_DATA_HOME: stateRoot } }); + const service = new IntegrationService({ repositoryRoot, controlPlane: control, checkService: { exactPasses: () => false } }); + try { + const result = service.requestAdoptedCandidate(requestInput); + console.log(JSON.stringify({ outcome: 'requested', integrationRequestId: result.integrationRequest.id })); + } catch (error) { + console.log(JSON.stringify({ outcome: 'refused', code: error.code ?? null })); + } finally { + control.close(); + } + `; + const encoded = JSON.stringify({ repositoryRoot: context.root, stateRoot: context.env.XDG_DATA_HOME, requestInput }); + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, ['--input-type=module', '-e', childSource, encoded], { + cwd: process.cwd(), env: process.env, stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; let stderr = ''; + child.stdout.on('data', chunk => { stdout += chunk; }); + child.stderr.on('data', chunk => { stderr += chunk; }); + child.on('error', reject); + child.on('close', (code) => { + if (code !== 0) return reject(new Error(`concurrent adopted request child failed: ${stderr}`)); + try { resolve(JSON.parse(stdout.trim().split('\n').at(-1))); } + catch (error) { reject(new Error(`invalid concurrent adopted request output: ${stdout}\n${stderr}\n${error.message}`)); } + }); + }); +} + +function authority(context, { action, adoptionId }) { + const provenance = ['fixture:owner-approved', `candidate:${context.candidate}`]; + const binding = { + schema: 'torch.dev/integration-adoption-authority/v1alpha1', action, adoptionId, + candidateSha: context.candidate, baseTargetSha: context.base, + provenanceManifestDigest: digest(provenance), contributorArea: context.contributor, + destinationArea: context.destination, targetBranch: 'main', + candidateRef: `refs/torch/integration-candidates/${adoptionId}`, + expectedOldRef: 'refs/heads/main', adapter: { + name: context.service.adoption.refAdapter.name, id: context.service.adoption.refAdapter.id, + }, + }; + const pending = context.control.requestApproval({ + requester: context.contributor, approver: 'owner', task: 'TASK-f40126ee', + title: `Fixture ${action} authority`, summary: 'Hermetic exact-source adoption scenario.', + }); + const approved = context.control.decideApproval({ + approvalId: pending.id, decidedBy: 'owner', decision: 'approved', expectedRevision: pending.revision, + note: JSON.stringify(binding), + }); + return { + approvalId: approved.id, approvalRevision: approved.revision, provenance, + provenanceManifestDigest: binding.provenanceManifestDigest, adapter: binding.adapter, + }; +} + +function input(context, action, adoptionId, extra = {}) { + return { + actorId: 'owner', adoptionId, candidateSha: context.candidate, baseTargetSha: context.base, + contributorArea: context.contributor, destinationArea: context.destination, + targetBranch: 'main', candidateRef: `refs/torch/integration-candidates/${adoptionId}`, + expectedOldRef: 'refs/heads/main', ...authority(context, { action, adoptionId }), ...extra, + }; +} + +function prepare(context, adoptionId = 'fixture-adoption') { + const preparedInput = input(context, 'prepare', adoptionId); + const plan = context.service.planAdoption({ ...preparedInput, action: 'prepare' }); + assert.deepEqual(plan.blockers, [], `preparation must begin from an exact clean fixture: ${JSON.stringify(plan.blockers)}`); + return context.service.prepareAdoption(preparedInput); +} + +function lifecycleInput(context, action, adoptionId, extra = {}) { + const { approvalId, approvalRevision, adapter } = authority(context, { action, adoptionId }); + const record = context.service.getPreparedAdoption(adoptionId); + return { actorId: 'owner', adoptionId, expectedRecordRevision: record.revision, approvalId, approvalRevision, adapter, ...extra }; +} + +function blockedCodes(error) { + assert.equal(error.code.endsWith('_BLOCKED'), true, `expected a blocked adoption transition, got ${error.code}`); + return error.details.map((blocker) => blocker.code); +} + +function lifecycleBoundary(context, adoptionId) { + const record = context.service.getPreparedAdoption(adoptionId); + return { + record, + refs: context.service.adoption.refAdapter.readRefs([ + record.expectedOldRef, record.candidateRef, record.archiveRef, + ]), + queue: context.service.list(), + auditCount: context.control.database.prepare(`SELECT COUNT(*) AS count FROM audit_events + WHERE project_id = ? AND entity_type = 'integration-adoption' AND entity_id = ?`) + .get(context.control.projectId, adoptionId).count, + }; +} + +function assertLifecycleBoundaryUnchanged(context, adoptionId, before) { + assert.deepEqual(lifecycleBoundary(context, adoptionId), before); +} + +test('SCN-integration-adoption-private-ref-cas: a real temporary Git transaction atomically creates private archive and candidate refs, and stale expected refs refuse', () => { + const context = fixture('torch-adoption-cas'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const refs = context.service.adoption.refAdapter.readRefs([ + 'refs/heads/main', finalized.candidateRef, finalized.archiveRef, + ]); + assert.deepEqual(refs, { + 'refs/heads/main': context.base, + [finalized.candidateRef]: context.candidate, + [finalized.archiveRef]: context.base, + }); + assert.throws(() => context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: finalized.candidateRef, sha: context.base }], + writes: [{ ref: finalized.candidateRef, sha: null }], + }), (error) => error.code === 'ADOPTION_REF_CAS_FAILED'); + assert.equal(context.service.adoption.getPrepared(adoptionId).state, 'finalized'); + context.control.close(); +}); + +test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cannot overwrite the journal and lifecycle input cannot alter its immutable candidate binding', () => { + const context = fixture('torch-adoption-journal'); + const adoptionId = 'fixture-adoption'; + const prepared = prepare(context, adoptionId); + assert.throws(() => context.service.prepareAdoption(input(context, 'prepare', adoptionId)), + (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); + assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { candidateSha: context.base })), + (error) => error.code === 'ADOPTION_IMMUTABLE_BINDING_STALE'); + assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { + expectedRecordRevision: prepared.revision + 1, + })), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); + const refs = context.service.adoption.refAdapter.readRefs([ + prepared.expectedOldRef, prepared.candidateRef, prepared.archiveRef, + ]); + assert.deepEqual(refs, { + [prepared.expectedOldRef]: context.base, + [prepared.candidateRef]: null, + [prepared.archiveRef]: null, + }); + const { mutationPerformed, ...durablePrepared } = prepared; + assert.equal(mutationPerformed, true); + assert.deepEqual(context.service.adoption.getPrepared(adoptionId), durablePrepared); + context.control.close(); +}); + +test('SCN-integration-adoption-lifecycle-revision-cas: stale or omitted revisions for every lifecycle action refuse before refs, journal, queue, or audit effects', () => { + const cases = [ + { + action: 'finalize', + setup: (context, adoptionId) => prepare(context, adoptionId), + invoke: (context, input) => context.service.finalizeAdoption(input), + }, + { + action: 'reconcile', + setup: (context, adoptionId) => prepare(context, adoptionId), + invoke: (context, input) => context.service.reconcileAdoption(input), + }, + { + action: 'rollback', + setup: (context, adoptionId) => { + prepare(context, adoptionId); + return context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + }, + invoke: (context, input) => context.service.rollbackAdoption(input), + }, + { + action: 'request', + setup: (context, adoptionId) => { + prepare(context, adoptionId); + return context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + }, + invoke: (context, input) => context.service.requestAdoptedCandidate(input), + }, + ]; + + for (const { action, setup, invoke } of cases) { + const context = fixture(`torch-adoption-${action}-revision-boundary`); + const adoptionId = 'fixture-adoption'; + setup(context, adoptionId); + for (const variant of ['omitted', 'stale']) { + const current = lifecycleInput(context, action, adoptionId); + const input = variant === 'omitted' + ? (({ expectedRecordRevision: _expectedRecordRevision, ...withoutRevision }) => withoutRevision)(current) + : { ...current, expectedRecordRevision: current.expectedRecordRevision + 1 }; + const before = lifecycleBoundary(context, adoptionId); + assert.throws(() => invoke(context, input), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE', + `${action} must reject a ${variant} lifecycle revision`); + assertLifecycleBoundaryUnchanged(context, adoptionId, before); + } + context.control.close(); + } +}); + +test('SCN-integration-adoption-finalize-recovery: a crash after real ref CAS but before the journal transition reconciles once and never duplicates refs', () => { + const context = fixture('torch-adoption-finalize-recovery'); + const adoptionId = 'fixture-adoption'; + const prepared = prepare(context, adoptionId); + context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [ + { ref: prepared.expectedOldRef, sha: prepared.baseTargetSha }, + { ref: prepared.candidateRef, sha: null }, + { ref: prepared.archiveRef, sha: null }, + ], + writes: [ + { ref: prepared.archiveRef, sha: prepared.baseTargetSha }, + { ref: prepared.candidateRef, sha: prepared.candidateSha }, + ], + }); + const recovered = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId)); + assert.equal(recovered.state, 'recovered-finalized'); + assert.equal(recovered.record.recovery, 'private-ref-cas-completed-before-journal'); + const replay = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId)); + assert.equal(replay.state, 'consistent'); + assert.equal(replay.mutationPerformed, false); + context.control.close(); +}); + +test('SCN-integration-adoption-rollback-recovery: a journal-missing rollback completes without a second deletion, while divergent candidate bytes refuse', () => { + const context = fixture('torch-adoption-rollback-recovery'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + assert.equal(finalized.state, 'finalized'); + context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: finalized.candidateRef, sha: finalized.candidateSha }], + writes: [{ ref: finalized.candidateRef, sha: null }], + }); + const recovered = context.service.rollbackAdoption(lifecycleInput(context, 'rollback', adoptionId)); + assert.equal(recovered.state, 'rolled-back'); + assert.equal(recovered.recovery, 'private-ref-rollback-completed-before-journal'); + + const divergent = fixture('torch-adoption-rollback-divergence'); + const divergentId = 'fixture-adoption'; + prepare(divergent, divergentId); + const divergentFinal = divergent.service.finalizeAdoption(lifecycleInput(divergent, 'finalize', divergentId)); + divergent.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: divergentFinal.candidateRef, sha: divergentFinal.candidateSha }], + writes: [{ ref: divergentFinal.candidateRef, sha: divergent.base }], + }); + assert.throws(() => divergent.service.rollbackAdoption(lifecycleInput(divergent, 'rollback', divergentId)), + (error) => error.code === 'ADOPTION_ROLLBACK_REF_DIVERGED'); + context.control.close(); + divergent.control.close(); +}); + +test('SCN-integration-adoption-private-ref-intake: finalized private refs enter the ordinary native integration queue, not a specialist branch shortcut', () => { + const unfinalized = fixture('torch-adoption-unfinalized-intake'); + const unfinalizedId = 'fixture-adoption'; + const unfinalizedRef = `refs/torch/integration-candidates/${unfinalizedId}`; + unfinalized.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: unfinalizedRef, sha: null }], + writes: [{ ref: unfinalizedRef, sha: unfinalized.candidate }], + }); + assert.throws(() => unfinalized.service.requestCandidate({ + areaId: unfinalized.contributor, commit: unfinalized.candidate, candidateRef: unfinalizedRef, + }), (error) => error.code === 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY'); + assert.deepEqual(unfinalized.service.list(), []); + unfinalized.control.close(); + + const context = fixture('torch-adoption-native-intake'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + assert.throws(() => context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId, { + expectedRecordRevision: finalized.revision + 1, + })), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); + assert.deepEqual(context.service.list(), []); + const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)); + assert.equal(requested.state, 'requested'); + assert.equal(requested.integrationRequest.sourceBranch, finalized.candidateRef); + assert.equal(requested.integrationRequest.sourceCommit, context.candidate); + assert.equal(requested.integrationRequest.state, 'testing'); + assert.equal(requested.integrationRequest.targetBranch, 'main'); + context.control.close(); +}); + +test('SCN-integration-adoption-approval-boundaries: unrelated or source-only approval records cannot authorize an owner adoption, and actor, action, revision, and provenance mismatches each refuse', () => { + const unrelated = fixture('torch-adoption-unrelated-approval'); + const unrelatedId = 'fixture-adoption'; + const unrelatedApproval = unrelated.control.requestApproval({ + requester: unrelated.contributor, approver: 'owner', task: 'TASK-unrelated', + title: 'Ordinary source branch review', summary: 'Not an integration-adoption authority.', + }); + const unrelatedApproved = unrelated.control.decideApproval({ + approvalId: unrelatedApproval.id, decidedBy: 'owner', decision: 'approved', expectedRevision: unrelatedApproval.revision, + note: JSON.stringify({ schema: 'torch.dev/ordinary-source-review/v1alpha1', sourceArea: unrelated.contributor }), + }); + const unrelatedInput = { + actorId: 'owner', adoptionId: unrelatedId, candidateSha: unrelated.candidate, baseTargetSha: unrelated.base, + contributorArea: unrelated.contributor, destinationArea: unrelated.destination, targetBranch: 'main', + candidateRef: `refs/torch/integration-candidates/${unrelatedId}`, expectedOldRef: 'refs/heads/main', + approvalId: unrelatedApproved.id, approvalRevision: unrelatedApproved.revision, + provenance: ['fixture:owner-approved', `candidate:${unrelated.candidate}`], + provenanceManifestDigest: digest(['fixture:owner-approved', `candidate:${unrelated.candidate}`]), + adapter: { name: unrelated.service.adoption.refAdapter.name, id: unrelated.service.adoption.refAdapter.id }, + }; + assert.throws(() => unrelated.service.prepareAdoption(unrelatedInput), (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID')); + assert.deepEqual(unrelated.service.list(), []); + unrelated.control.close(); + + const wrongActor = fixture('torch-adoption-wrong-actor'); + assert.throws(() => wrongActor.service.prepareAdoption({ + ...input(wrongActor, 'prepare', 'fixture-adoption'), actorId: wrongActor.contributor, + }), (error) => blockedCodes(error).includes('OWNER_ACTOR_REQUIRED')); + wrongActor.control.close(); + + const wrongAction = fixture('torch-adoption-wrong-action'); + prepare(wrongAction, 'fixture-adoption'); + assert.throws(() => wrongAction.service.finalizeAdoption(lifecycleInput(wrongAction, 'rollback', 'fixture-adoption')), + (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID')); + wrongAction.control.close(); + + const staleRevision = fixture('torch-adoption-stale-revision'); + prepare(staleRevision, 'fixture-adoption'); + const staleInput = lifecycleInput(staleRevision, 'finalize', 'fixture-adoption'); + assert.throws(() => staleRevision.service.finalizeAdoption({ ...staleInput, approvalRevision: staleInput.approvalRevision - 1 }), + (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_REVISION_STALE')); + staleRevision.control.close(); + + const wrongProvenance = fixture('torch-adoption-wrong-provenance'); + assert.throws(() => wrongProvenance.service.prepareAdoption({ + ...input(wrongProvenance, 'prepare', 'fixture-adoption'), provenance: ['fixture:owner-approved', 'unapproved:alternate-provenance'], + }), (error) => blockedCodes(error).includes('ADOPTION_PROVENANCE_DIGEST_MISMATCH')); + wrongProvenance.control.close(); +}); + +test('SCN-integration-adoption-request-replay: concurrent authenticated API callers serialize on the adoption journal and create exactly one private-ref queue request', async () => { + const context = fixture('torch-adoption-request-replay'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const requestInput = lifecycleInput(context, 'request', adoptionId); + const outcomes = await Promise.all([ + concurrentAdoptedRequest(context, requestInput), + concurrentAdoptedRequest(context, requestInput), + ]); + assert.deepEqual(outcomes.map((entry) => entry.outcome).sort(), ['refused', 'requested']); + assert.equal(outcomes.find((entry) => entry.outcome === 'refused').code, 'ADOPTION_RECORD_REVISION_STALE'); + const requests = context.service.list(); + assert.equal(requests.length, 1); + assert.equal(context.service.getPreparedAdoption(adoptionId).state, 'requested'); + context.control.close(); +}); + +test('SCN-integration-adoption-legacy-retries: historical ordinary-request retries remain readable when integration service starts', () => { + const context = fixture('torch-adoption-legacy-retries'); + const insert = context.control.database.prepare(`INSERT INTO integration_requests ( + id, project_id, source_area, source_branch, source_commit, target_branch, + base_target_commit, state, reason, required_checks, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); + for (const [id, state] of [['historical-failed', 'blocked'], ['historical-retry', 'superseded']]) { + insert.run(id, context.control.projectId, context.contributor, 'refs/heads/candidate', context.candidate, + 'main', context.base, state, 'historical-retry', '[]', '2026-10-03T00:00:00.000Z', '2026-10-03T00:00:00.000Z'); + } + assert.doesNotThrow(() => new IntegrationService({ + repositoryRoot: context.root, controlPlane: context.control, checkService: { exactPasses: () => false }, + })); + assert.equal(context.service.list().length, 2); + context.control.close(); +}); From 77122fed8e7bca1afdccd6ae42e1cf00cb45a1bb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 18:11:08 -0400 Subject: [PATCH 15/15] TASK-f40126ee: clarify replay spec correction Record why stale revision CAS, rather than a generic post-state refusal, is the first observable loser boundary in the controlling replay scenario. --- docs/TEST_INTEGRITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 19deb55..9051c12 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3233,6 +3233,8 @@ test_integrity_note: observable_outcomes: - "The replay loser reports the stale journal-revision boundary, not a generic post-state refusal" - "Exactly one private-ref integration request and requested audit transition exist" + spec_correction_basis: + - "The former ADOPTION_REQUEST_STATE_INVALID replay expectation was wrong: after the serialized winner commits, the loser carries an obsolete journal revision, so revision CAS is the first observable refusal boundary under SCN-integration-adoption-request-replay." determinism_controls: - "Two independently opened local API processes over disposable Git and SQLite fixture state" anti_cheat_rationale: