diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 7f9c360..9051c12 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3206,11 +3206,11 @@ test_integrity_note: 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" + 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 lifecycle callers" - - "Private candidate/archive refs remain absent before stale finalize and no integration request is created by stale native request" + - "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: @@ -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: diff --git a/src/checks/candidate-execution-fence-init.mjs b/src/checks/candidate-execution-fence-init.mjs deleted file mode 100644 index cef9072..0000000 --- a/src/checks/candidate-execution-fence-init.mjs +++ /dev/null @@ -1,28 +0,0 @@ -// Private init entrypoint. It is never a CLI surface: bootstrap is supplied only on inherited FD3 and acknowledgement only on FD4. -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 }); -} diff --git a/src/checks/candidate-execution-fence.mjs b/src/checks/candidate-execution-fence.mjs deleted file mode 100644 index 6bd72ab..0000000 --- a/src/checks/candidate-execution-fence.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { TorchError } from '../kernel/errors.mjs'; - -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 deleted file mode 100644 index e5dd562..0000000 --- a/src/checks/candidate-input-snapshot.mjs +++ /dev/null @@ -1,96 +0,0 @@ -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. */ -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 }); -} -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 }); -} - -/** - * 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 deleted file mode 100644 index 02c5c64..0000000 --- a/src/checks/candidate-source-bootstrap.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import { createHash, randomBytes } from 'node:crypto'; -import { TorchError } from '../kernel/errors.mjs'; - -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 }); -} - -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 deleted file mode 100644 index 5f52511..0000000 --- a/src/checks/candidate-source-execution.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import { spawnSync } from 'node:child_process'; -import { TorchError } from '../kernel/errors.mjs'; - -export const CANDIDATE_EXECUTION_LIMITS_V2 = Object.freeze({ timeoutMs: 300000, maxOutputBytes: 1048576, maxArgs: 64, maxArgBytes: 4096 }); - -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/src/checks/service.mjs b/src/checks/service.mjs index 422200f..fcc3458 100644 --- a/src/checks/service.mjs +++ b/src/checks/service.mjs @@ -84,19 +84,6 @@ 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, @@ -343,8 +330,6 @@ 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/src/convergence/service.mjs b/src/convergence/service.mjs index 2b1bf13..a48ee13 100644 --- a/src/convergence/service.mjs +++ b/src/convergence/service.mjs @@ -6,10 +6,6 @@ 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' }); @@ -37,61 +33,6 @@ 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, } = {}) { @@ -148,7 +89,6 @@ 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 ?? {}; @@ -156,24 +96,11 @@ export class ConvergenceService { this.worktrees = new Map((manifest.external ?? []) .filter((entry) => entry.type === 'worktree') .map((entry) => [entry.area, entry])); - 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); + initializeWorktreeGuards(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 @@ -181,20 +108,17 @@ 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 = []; @@ -221,7 +145,6 @@ 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 }; diff --git a/test/fleet/candidate-execution-fence.test.mjs b/test/fleet/candidate-execution-fence.test.mjs deleted file mode 100644 index 00d6ad1..0000000 --- a/test/fleet/candidate-execution-fence.test.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -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 deleted file mode 100644 index 6ab8ca0..0000000 --- a/test/fleet/candidate-input-consumption.test.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import assert from 'node:assert/strict'; -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 { 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 deleted file mode 100644 index 19a8887..0000000 --- a/test/fleet/candidate-snapshot-policy.test.mjs +++ /dev/null @@ -1,14 +0,0 @@ -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 { 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 deleted file mode 100644 index f496762..0000000 --- a/test/fleet/candidate-source-bootstrap.test.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import test from 'node:test'; -import { createCandidateSourceBootstrapRuntimeV2 } from '../../src/checks/candidate-source-bootstrap.mjs'; - -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' }); -}); diff --git a/test/fleet/candidate-source-execution.test.mjs b/test/fleet/candidate-source-execution.test.mjs deleted file mode 100644 index c3b0025..0000000 --- a/test/fleet/candidate-source-execution.test.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import assert from 'node:assert/strict'; -import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import test from 'node:test'; -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 deleted file mode 100644 index bf3c7d4..0000000 --- a/test/fleet/candidate-source-parent.test.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import test from 'node:test'; -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']); -}); diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs index 666e6f6..3ec6641 100644 --- a/test/fleet/integration-adoption.test.mjs +++ b/test/fleet/integration-adoption.test.mjs @@ -139,6 +139,24 @@ function blockedCodes(error) { 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'; @@ -185,6 +203,54 @@ test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cann 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';