From 8f894e9863e6be5bb11cb7e5548bf68ccac980fc Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:08:19 -0400 Subject: [PATCH] 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' });