From 530afb3b6749d84dee1478544386ccb115eeeccd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:34:09 -0400 Subject: [PATCH] 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);