From c6b883dda1f133aef2cec1210b5fe4506231264c Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Mon, 10 Aug 2026 00:57:33 +0300 Subject: [PATCH 1/2] fix: enforce deployment attempt plan lineage Agent: Aurelianus --- src/deployment.ts | 45 +++++++++++++++++++++++++++++++++++++-- tests/deployment.test.ts | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/deployment.ts b/src/deployment.ts index a0ead05..b356175 100644 --- a/src/deployment.ts +++ b/src/deployment.ts @@ -1573,13 +1573,18 @@ export function validateDeploymentContractSet( attempts.forEach((attempt) => { requireLinkedRecord(attempt.plan, planMap, `deploymentAttempts.${attempt.id}.plan`, issues); const plan = planMap.get(attempt.plan.id) as - | (LinkedRecord & { request: DeploymentRecordReference }) + | (LinkedRecord & { + request: DeploymentRecordReference; + actions: Array<{ id: string }>; + }) | undefined; const request = plan ? requestMap.get(plan.request.id) as | (LinkedRecord & { environment: DeploymentRecordReference }) | undefined : undefined; + const linkedApprovalActorKeys = new Set(); + let linkedApprovalCount = 0; attempt.approvals.forEach(( approval: { decision: LinkedRecord; @@ -1594,7 +1599,7 @@ export function validateDeploymentContractSet( requireLinkedRecord(approval.decision, approvalMap, approvalPath, issues); const linkedApproval = approvalMap.get(approval.decision.id) as | (LinkedRecord & { - decision: { status: string }; + decision: { status: string; actor: ActorPointer }; scope: string; actionId: string | null; phaseId: string | null; @@ -1607,6 +1612,10 @@ export function validateDeploymentContractSet( if (!linkedApproval) { return; } + linkedApprovalCount += 1; + linkedApprovalActorKeys.add( + `${linkedApproval.decision.actor.kind}:${linkedApproval.decision.actor.id}`, + ); if (linkedApproval.decision.status !== "allowed") { issues.push( `${approvalPath}.decision: linked approval decision is not allowed`, @@ -1650,6 +1659,38 @@ export function validateDeploymentContractSet( ); } }); + if (linkedApprovalCount === attempt.approvals.length) { + const attemptDecisionActorKeys = new Set( + attempt.decisionActors.map( + (actor: ActorPointer) => `${actor.kind}:${actor.id}`, + ), + ); + if ( + attemptDecisionActorKeys.size !== linkedApprovalActorKeys.size + || [...attemptDecisionActorKeys].some( + (actorKey) => !linkedApprovalActorKeys.has(actorKey), + ) + ) { + issues.push( + `deploymentAttempts.${attempt.id}.decisionActors: decision actors do not match linked approval actors`, + ); + } + } + if (plan) { + const planActionIds = new Set( + plan.actions.map((action) => action.id), + ); + attempt.actionSteps.forEach(( + step: { actionId: string }, + index: number, + ) => { + if (!planActionIds.has(step.actionId)) { + issues.push( + `deploymentAttempts.${attempt.id}.actionSteps.${index}.actionId: action is not present in linked deployment plan ${plan.id}`, + ); + } + }); + } if ( attempt.state === "succeeded" && attempt.actionSteps.some( diff --git a/tests/deployment.test.ts b/tests/deployment.test.ts index 0798695..193e3dd 100644 --- a/tests/deployment.test.ts +++ b/tests/deployment.test.ts @@ -593,6 +593,31 @@ describe("deployment contract records", () => { expect(result.issues).toContain(expected(fixtures)); }); + test("attempt decision actors must match the linked approval actors", () => { + const fixtures = createDeploymentFixtureSet(); + const contractSet = deploymentFixtureSetToContractSet(fixtures); + const attemptDraft = clone(fixtures.deploymentAttempt); + attemptDraft.decisionActors = [{ + kind: "agent", + id: "unrelated-decision-actor", + name: "Unrelated Decision Actor", + }]; + const downstream = recomputeAttemptDownstream(fixtures, attemptDraft); + + contractSet.deploymentAttempts = [downstream.deploymentAttempt]; + contractSet.providerReceipts = [downstream.providerReceipt]; + contractSet.deploymentReceipts = [downstream.deploymentReceipt]; + contractSet.launchEvidence = [downstream.launchEvidence]; + + const result = validateDeploymentContractSet(runtimeSchemas, contractSet); + expect(result).toEqual({ + success: false, + issues: [ + `deploymentAttempts.${downstream.deploymentAttempt.id}.decisionActors: decision actors do not match linked approval actors`, + ], + }); + }); + test("action inputs must resolve to a linked deployment record", () => { const fixtures = createDeploymentFixtureSet(); const contractSet = deploymentFixtureSetToContractSet(fixtures); @@ -620,6 +645,27 @@ describe("deployment contract records", () => { ); }); + test("attempt action steps must exist in the linked deployment plan", () => { + const fixtures = createDeploymentFixtureSet(); + const contractSet = deploymentFixtureSetToContractSet(fixtures); + const attemptDraft = clone(fixtures.deploymentAttempt); + attemptDraft.actionSteps[0]!.actionId = "unapproved-action"; + const downstream = recomputeAttemptDownstream(fixtures, attemptDraft); + + contractSet.deploymentAttempts = [downstream.deploymentAttempt]; + contractSet.providerReceipts = [downstream.providerReceipt]; + contractSet.deploymentReceipts = [downstream.deploymentReceipt]; + contractSet.launchEvidence = [downstream.launchEvidence]; + + const result = validateDeploymentContractSet(runtimeSchemas, contractSet); + expect(result).toEqual({ + success: false, + issues: [ + `deploymentAttempts.${downstream.deploymentAttempt.id}.actionSteps.0.actionId: action is not present in linked deployment plan ${fixtures.deploymentPlan.id}`, + ], + }); + }); + test("a succeeded attempt cannot contain a failed action step", () => { const fixtures = createDeploymentFixtureSet(); const contractSet = deploymentFixtureSetToContractSet(fixtures); From 725250a50ab5e95949362258ac11760233b93cec Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Mon, 10 Aug 2026 01:07:50 +0300 Subject: [PATCH 2/2] fix: fail closed on missing approval actors Agent: Aurelianus --- src/deployment.ts | 21 ++++++++++++++++----- tests/deployment.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/deployment.ts b/src/deployment.ts index b356175..ca1bdbe 100644 --- a/src/deployment.ts +++ b/src/deployment.ts @@ -1585,6 +1585,7 @@ export function validateDeploymentContractSet( : undefined; const linkedApprovalActorKeys = new Set(); let linkedApprovalCount = 0; + let linkedApprovalActorCount = 0; attempt.approvals.forEach(( approval: { decision: LinkedRecord; @@ -1599,7 +1600,7 @@ export function validateDeploymentContractSet( requireLinkedRecord(approval.decision, approvalMap, approvalPath, issues); const linkedApproval = approvalMap.get(approval.decision.id) as | (LinkedRecord & { - decision: { status: string; actor: ActorPointer }; + decision: { status: string; actor?: ActorPointer }; scope: string; actionId: string | null; phaseId: string | null; @@ -1613,9 +1614,16 @@ export function validateDeploymentContractSet( return; } linkedApprovalCount += 1; - linkedApprovalActorKeys.add( - `${linkedApproval.decision.actor.kind}:${linkedApproval.decision.actor.id}`, - ); + if (linkedApproval.decision.actor) { + linkedApprovalActorCount += 1; + linkedApprovalActorKeys.add( + `${linkedApproval.decision.actor.kind}:${linkedApproval.decision.actor.id}`, + ); + } else { + issues.push( + `${approvalPath}.decision: linked approval decision is missing an actor`, + ); + } if (linkedApproval.decision.status !== "allowed") { issues.push( `${approvalPath}.decision: linked approval decision is not allowed`, @@ -1659,7 +1667,10 @@ export function validateDeploymentContractSet( ); } }); - if (linkedApprovalCount === attempt.approvals.length) { + if ( + linkedApprovalCount === attempt.approvals.length + && linkedApprovalActorCount === attempt.approvals.length + ) { const attemptDecisionActorKeys = new Set( attempt.decisionActors.map( (actor: ActorPointer) => `${actor.kind}:${actor.id}`, diff --git a/tests/deployment.test.ts b/tests/deployment.test.ts index 193e3dd..ffad47b 100644 --- a/tests/deployment.test.ts +++ b/tests/deployment.test.ts @@ -618,6 +618,34 @@ describe("deployment contract records", () => { }); }); + test("linked approval decisions without actors fail closed", () => { + const fixtures = createDeploymentFixtureSet(); + const contractSet = deploymentFixtureSetToContractSet(fixtures); + const approvalDraft = clone(fixtures.deploymentApprovalDecision); + delete approvalDraft.decision.actor; + const downstream = recomputePlanDownstream( + fixtures, + fixtures.deploymentPlan, + approvalDraft, + ); + + contractSet.deploymentApprovalDecisions = [ + downstream.deploymentApprovalDecision, + ]; + contractSet.deploymentAttempts = [downstream.deploymentAttempt]; + contractSet.providerReceipts = [downstream.providerReceipt]; + contractSet.deploymentReceipts = [downstream.deploymentReceipt]; + contractSet.launchEvidence = [downstream.launchEvidence]; + + const result = validateDeploymentContractSet(runtimeSchemas, contractSet); + expect(result).toEqual({ + success: false, + issues: [ + `deploymentAttempts.${downstream.deploymentAttempt.id}.approvals.0.decision: linked approval decision is missing an actor`, + ], + }); + }); + test("action inputs must resolve to a linked deployment record", () => { const fixtures = createDeploymentFixtureSet(); const contractSet = deploymentFixtureSetToContractSet(fixtures);