diff --git a/tests/integration/postgres-worker-agent-revision-credential-sources.test.mjs b/tests/integration/postgres-worker-agent-revision-credential-sources.test.mjs index e3721709c4..a925925417 100644 --- a/tests/integration/postgres-worker-agent-revision-credential-sources.test.mjs +++ b/tests/integration/postgres-worker-agent-revision-credential-sources.test.mjs @@ -279,16 +279,14 @@ test( await startWithGateway(fixture, compute); await fixture.work(active, "succeeded"); const sourceId = owner.harnessAuth.sourceId; - const request = withdrawalRequest(fixture, owner, sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); - const read = () => fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const withdrawal = await requestWithdrawal(fixture, owner, sourceId, fixture.actor.id); const [first] = await withdrawalAttempts(fixture, active); await fixture.work(first, "failed_permanent"); // The outage outlasted the attempt budget. The withdrawal stays pending with its reason, and // the failing pass already queued a retry for 30 s later, so the read still reports an // attempt coming rather than asking for a replay. - const waiting = await read(); + const waiting = await withdrawal.read(fixture.actor.id); assert.equal(waiting.state, "pending"); assert.equal(waiting.lastReason, "DEPENDENCY_UNAVAILABLE"); assert.equal(waiting.withdrawalInProgress, true); @@ -301,7 +299,7 @@ test( gateway.down = false; await runScheduledRetryNow(fixture, retry); await fixture.work(retry, "succeeded"); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.lastReason, "CREDENTIALS_WITHDRAWN"); assert.equal(revoked.withdrawalInProgress, false); @@ -427,9 +425,12 @@ test( const calls = () => gateway.withdrawn.length; await startWithGateway(fixture, compute); await fixture.work(active, "succeeded"); - const request = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); - const read = () => fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const withdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, + fixture.actor.id, + ); const [first] = await withdrawalAttempts(fixture, active); await fixture.work(first, "failed_permanent"); @@ -455,7 +456,7 @@ test( ), [], ); - const exhausted = await read(); + const exhausted = await withdrawal.read(fixture.actor.id); assert.equal(exhausted.state, "pending"); assert.equal(exhausted.lastReason, "DEPENDENCY_UNAVAILABLE"); assert.equal(exhausted.withdrawalInProgress, false); @@ -475,16 +476,13 @@ test( // it before it is read here. await fixture.stop(); gateway.down = false; - const replayed = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - request, - ); + const replayed = await withdrawal.replay(fixture.actor.id); assert.equal(replayed.withdrawalInProgress, true); const replay = await queuedWithdrawal(fixture, active); assert.ok(!replay.idempotencyKey.startsWith(first.idempotencyKey)); await startWithGateway(fixture, compute); await fixture.work(replay, "succeeded"); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.withdrawalInProgress, false); }, @@ -510,14 +508,8 @@ test( // Both share the actor's claim. const offboarded = `withdraw-outage-offboarded-${randomUUID()}`; await fixture.copyActorGrants(offboarded); - await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - withdrawalRequest(fixture, owner, allowed), - ); - await fixture.controller.withdrawAgentCredentialSource( - offboarded, - withdrawalRequest(fixture, owner, deniedSource), - ); + const allowedWithdrawal = await requestWithdrawal(fixture, owner, allowed, fixture.actor.id); + const deniedWithdrawal = await requestWithdrawal(fixture, owner, deniedSource, offboarded); await removeAccessBindings(fixture, offboarded); const [first, ...others] = await withdrawalAttempts(fixture, active); assert.deepEqual(others, []); @@ -533,13 +525,8 @@ test( gateway.down = false; await runScheduledRetryNow(fixture, retry); await fixture.work(retry, "failed_permanent"); - const read = (credentialSourceId) => - fixture.controller.readAgentCredentialWithdrawal( - fixture.actor.id, - withdrawalRequest(fixture, owner, credentialSourceId), - ); - assert.equal((await read(allowed)).state, "revoked"); - const denied = await read(deniedSource); + assert.equal((await allowedWithdrawal.read(fixture.actor.id)).state, "revoked"); + const denied = await deniedWithdrawal.read(fixture.actor.id); assert.equal(denied.state, "pending"); assert.equal(denied.lastReason, "AUTHORIZATION_DENIED"); assert.equal(denied.withdrawalInProgress, false); @@ -584,9 +571,12 @@ test( }); await startWithGateway(fixture, compute); await fixture.work(active, "succeeded"); - const request = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); - const read = () => fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const withdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, + fixture.actor.id, + ); const [first] = await withdrawalAttempts(fixture, active); // The lease runs out during the last attempt, so stale-claim recovery fails the work, not @@ -608,7 +598,7 @@ test( }); assert.equal(retry.idempotencyKey, `${first.idempotencyKey}:recovery:1`); assertRetryDelay(retry, 30_000); - const waiting = await read(); + const waiting = await withdrawal.read(fixture.actor.id); assert.equal(waiting.state, "pending"); assert.equal(waiting.withdrawalInProgress, true); @@ -616,7 +606,7 @@ test( gateway.down = false; await runScheduledRetryNow(fixture, retry); await fixture.work(retry, "succeeded"); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.withdrawalInProgress, false); assert.equal(gateway.withdrawn.length, 3); @@ -643,8 +633,12 @@ test( // the next one for 30 s later; then the operator is offboarded. const offboarded = `withdraw-replay-offboarded-${randomUUID()}`; await fixture.copyActorGrants(offboarded); - const request = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - await fixture.controller.withdrawAgentCredentialSource(offboarded, request); + const withdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, + offboarded, + ); const [first] = await withdrawalAttempts(fixture, active); await fixture.work(first, "failed_permanent"); const series = await queuedWithdrawal(fixture, active); @@ -656,10 +650,7 @@ test( // lets the waiting series run now, without starting a second chain, so the series is not // denied to the offboarded operator and no second replay is needed. gateway.down = false; - const replayed = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - request, - ); + const replayed = await withdrawal.replay(fixture.actor.id); assert.equal(replayed.requestedBy, fixture.actor.id); assert.equal(replayed.withdrawalInProgress, true); const due = await queuedWithdrawal(fixture, active); @@ -669,10 +660,7 @@ test( await startWithGateway(fixture, compute); await fixture.work(due, "succeeded"); - const revoked = await fixture.controller.readAgentCredentialWithdrawal( - fixture.actor.id, - request, - ); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.requestedBy, fixture.actor.id); // The offboarded operator's failed series stays theirs; the revocation is the replaying @@ -724,8 +712,7 @@ test( fixture.actor.id, withdrawalRequest(fixture, owner, allowed), ); - const deniedRequest = withdrawalRequest(fixture, owner, deniedSource); - await fixture.controller.withdrawAgentCredentialSource(offboarded, deniedRequest); + const deniedWithdrawal = await requestWithdrawal(fixture, owner, deniedSource, offboarded); await removeAccessBindings(fixture, offboarded); const [work, ...others] = await withdrawalAttempts(fixture, active); assert.deepEqual(others, []); @@ -734,10 +721,7 @@ test( // when the actor replays the denied withdrawal. The running attempt is its only work. await startWithGateway(fixture, compute); await gatewayCall.promise; - const replayed = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - deniedRequest, - ); + const replayed = await deniedWithdrawal.replay(fixture.actor.id); assert.equal(replayed.requestedBy, fixture.actor.id); assert.equal(replayed.withdrawalInProgress, true); assert.equal((await withdrawalAttempts(fixture, active)).length, 1); @@ -748,10 +732,7 @@ test( await fixture.stop(); assert.equal(completed.attempt_count, 2); assert.deepEqual(withdrawn, [allowed, deniedSource]); - const denied = await fixture.controller.readAgentCredentialWithdrawal( - fixture.actor.id, - deniedRequest, - ); + const denied = await deniedWithdrawal.read(fixture.actor.id); assert.equal(denied.state, "revoked"); assert.equal(denied.requestedBy, fixture.actor.id); assert.deepEqual(await withdrawalAudit(fixture, "actor_id", "outcome", "sources"), [ @@ -786,8 +767,12 @@ test( { convergenceTimeoutMs: 50, transformDrivers: withCredentialGateway }, ); await fixture.work(active, "succeeded"); - const request = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); + const withdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, + fixture.actor.id, + ); const [first] = await withdrawalAttempts(fixture, active); // One attempt, no retry series: retrying cannot change a refusal. @@ -802,7 +787,7 @@ test( (await withdrawalAttempts(fixture, active)).map(({ state }) => state), ["failed_permanent"], ); - const read = await fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const read = await withdrawal.read(fixture.actor.id); assert.equal(read.state, "pending"); assert.equal(read.lastReason, "CREDENTIAL_WITHDRAWAL_MISCONFIGURED"); assert.equal(read.withdrawalInProgress, false); @@ -839,8 +824,12 @@ test( { convergenceTimeoutMs: 50, transformDrivers: withCredentialGateway }, ); await fixture.work(active, "succeeded"); - const request = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); + const withdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, + fixture.actor.id, + ); const [first] = await withdrawalAttempts(fixture, active); // One attempt with the code as its reason: retrying cannot restore the identity's role, @@ -852,7 +841,7 @@ test( (await fixture.workResult(first)).rows[0].reason_code, "SANDBOX_GATEWAY_UNAUTHORIZED", ); - const read = await fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const read = await withdrawal.read(fixture.actor.id); assert.equal(read.state, "pending"); assert.equal(read.lastReason, "SANDBOX_GATEWAY_UNAUTHORIZED"); assert.equal(read.withdrawalInProgress, false); @@ -900,15 +889,18 @@ test( ); gateway.down = false; await fixture.work(active, "succeeded"); - const request = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); - const read = () => fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const withdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, + fixture.actor.id, + ); const [first] = await withdrawalAttempts(fixture, active); // The first attempt is refused and retried with its reason; the second revokes. const settled = await fixture.work(first, "succeeded"); assert.equal(settled.attempt_count, 2); assert.equal(gateway.withdrawn.length, 1); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.lastReason, "CREDENTIALS_WITHDRAWN"); const completed = events.filter( @@ -926,8 +918,12 @@ test( }); refusals = Number.POSITIVE_INFINITY; await fixture.work(other.candidate, "succeeded"); - const otherRequest = withdrawalRequest(fixture, other.owner, other.owner.harnessAuth.sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, otherRequest); + const otherWithdrawal = await requestWithdrawal( + fixture, + other.owner, + other.owner.harnessAuth.sourceId, + fixture.actor.id, + ); const [persistent] = await withdrawalAttempts(fixture, other.candidate); const failed = await fixture.work(persistent, "failed_permanent"); assert.equal(failed.attempt_count, 2); @@ -935,10 +931,7 @@ test( (await fixture.workResult(persistent)).rows[0].reason_code, "SANDBOX_GATEWAY_UNAUTHORIZED", ); - const pending = await fixture.controller.readAgentCredentialWithdrawal( - fixture.actor.id, - otherRequest, - ); + const pending = await otherWithdrawal.read(fixture.actor.id); assert.equal(pending.state, "pending"); assert.equal(pending.lastReason, "SANDBOX_GATEWAY_UNAUTHORIZED"); // Like any withdrawal that ran out of attempts, it enters the recovery series, which is why @@ -1294,12 +1287,8 @@ revisionTest( // The next deployment was admitted with the source; the withdrawal arrives before it runs. const second = await fixture.revision(owner, 2); - const request = withdrawalRequest(fixture, owner, tool); - const requested = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - request, - ); - assert.equal(requested.revisionId, first.id); + const withdrawal = await requestWithdrawal(fixture, owner, tool, fixture.actor.id); + assert.equal(withdrawal.initial.revisionId, first.id); const recorded = () => findWithdrawals(fixture, [first, second], tool); assert.deepEqual( (await recorded()).map((withdrawal) => withdrawal?.state), @@ -1335,7 +1324,7 @@ revisionTest( ); assert.equal((await fixture.currentAgent(owner)).activeRevisionId, second.id); // The read follows the active revision to its own withdrawal instead of answering 404. - const read = await fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const read = await withdrawal.read(fixture.actor.id); assert.equal(read.revisionId, second.id); assert.equal(read.state, "revoked"); const audit = await fixture.observerPool.query( @@ -1434,6 +1423,17 @@ function withdrawalRequest(fixture, owner, credentialSourceId) { return { namespaceId: fixture.namespace.id, agentId: owner.id, credentialSourceId }; } +// Request one Agent source; reads and replays keep that target while naming their own principal. +async function requestWithdrawal(fixture, owner, credentialSourceId, requester) { + const target = withdrawalRequest(fixture, owner, credentialSourceId); + const initial = await fixture.controller.withdrawAgentCredentialSource(requester, target); + return { + initial, + read: (reader) => fixture.controller.readAgentCredentialWithdrawal(reader, target), + replay: (requester) => fixture.controller.withdrawAgentCredentialSource(requester, target), + }; +} + // The revision's withdrawal work rows, oldest first, each ready for fixture.work. Each row is // one queued withdrawal; the worker may run it up to maxAttempts times (its attempt_count). async function withdrawalAttempts(fixture, revision) { @@ -1544,14 +1544,9 @@ test( await fixture.state.transact((unit) => unit.agents.compareAndSetActiveRevision(fixture.namespace.id, owner.id, second.id, third.id), ); - const request = withdrawalRequest(fixture, owner, tool); - const requested = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - request, - ); - assert.equal(requested.revisionId, third.id); - assert.equal(requested.withdrawalInProgress, true); - const read = () => fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const withdrawal = await requestWithdrawal(fixture, owner, tool, fixture.actor.id); + assert.equal(withdrawal.initial.revisionId, third.id); + assert.equal(withdrawal.initial.withdrawalInProgress, true); await fixture.start(compute, options); const [secondAttempt] = await withdrawalAttempts(fixture, second); await fixture.work(secondAttempt, "failed_permanent", 30_000); @@ -1560,7 +1555,7 @@ test( // The active revision's withdrawal is revoked, but the second revision still runs with the // source, so the read reports its withdrawal instead of `revoked`, with its retry queued. - const waiting = await read(); + const waiting = await withdrawal.read(fixture.actor.id); assert.equal(waiting.revisionId, second.id); assert.equal(waiting.state, "pending"); assert.equal(waiting.lastReason, "CREDENTIAL_WITHDRAWAL_PENDING"); @@ -1571,10 +1566,7 @@ test( // A replay while the retry is queued queues nothing more, reports it in progress, and lets // the retry run now instead of at its scheduled time. revoke = true; - const replayed = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - request, - ); + const replayed = await withdrawal.replay(fixture.actor.id); assert.equal(replayed.revisionId, second.id); assert.equal(replayed.state, "pending"); assert.equal(replayed.withdrawalInProgress, true); @@ -1582,7 +1574,7 @@ test( assert.equal((await withdrawalAttempts(fixture, second)).length, 2); await fixture.work(retry, "succeeded"); await fixture.stop(); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.revisionId, third.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.withdrawalInProgress, false); @@ -1619,10 +1611,7 @@ for (const harness of [false, true]) { await fixture.work(first, "succeeded"); await fixture.stop(); second = await admitHeldRevision(fixture, owner, 2); - const request = withdrawalRequest(fixture, owner, sourceId); - const read = () => - fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); + const withdrawal = await requestWithdrawal(fixture, owner, sourceId, fixture.actor.id); await fixture.start(compute, options); const [attempt] = await withdrawalAttempts(fixture, second); await fixture.work(attempt, "failed_permanent", 30_000); @@ -1630,7 +1619,7 @@ for (const harness of [false, true]) { const [work] = await withdrawalAttempts(fixture, first); return work?.state === "succeeded" ? work : undefined; }); - const exhausted = await read(); + const exhausted = await withdrawal.read(fixture.actor.id); assert.equal(exhausted.revisionId, second.id); assert.equal(exhausted.state, "pending"); assert.equal(exhausted.withdrawalInProgress, false); @@ -1644,7 +1633,7 @@ for (const harness of [false, true]) { assert.equal(work.length, 2, "maintenance must queue the successor's withdrawal again"); assert.equal((await withdrawalAttempts(fixture, first)).length, 1); await fixture.work(work[1], "succeeded"); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.revisionId, first.id); assert.equal(revoked.state, "revoked"); @@ -1723,8 +1712,7 @@ for (const successor of [false, true]) { // so each attempt of that request is denied. const requester = `withdraw-denied-requester-${randomUUID()}`; await fixture.copyActorGrants(requester); - const request = withdrawalRequest(fixture, owner, sourceId); - await fixture.controller.withdrawAgentCredentialSource(requester, request); + const withdrawal = await requestWithdrawal(fixture, owner, sourceId, requester); await removeAccessBindings(fixture, requester); await fixture.start(compute, options); for (const revision of withdrawing) { @@ -1747,8 +1735,6 @@ for (const successor of [false, true]) { Promise.all( withdrawing.map(async (revision) => (await withdrawalAttempts(fixture, revision)).length), ); - const read = () => - fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); // Only an operator who still holds operate can complete the withdrawal, so maintenance // queues no further denied attempt and audits no further denial, pass after pass, and @@ -1761,7 +1747,7 @@ for (const successor of [false, true]) { withdrawing.map(() => 1), ); assert.equal(await denials(), withdrawing.length); - const waiting = await read(); + const waiting = await withdrawal.read(fixture.actor.id); assert.equal(waiting.state, "pending"); assert.equal(waiting.withdrawalInProgress, false); assert.equal(waiting.requestedBy, requester); @@ -1771,10 +1757,7 @@ for (const successor of [false, true]) { // unavailable; they record that outage, not the earlier denial, so the next maintenance // pass queues them again. iamUnavailable = true; - const replayed = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - request, - ); + const replayed = await withdrawal.replay(fixture.actor.id); assert.equal(replayed.requestedBy, fixture.actor.id); assert.equal(replayed.withdrawalInProgress, true); const exhaustAttempts = async () => { @@ -1784,7 +1767,7 @@ for (const successor of [false, true]) { } }; await exhaustAttempts(); - const iamOutage = await read(); + const iamOutage = await withdrawal.read(fixture.actor.id); assert.equal(iamOutage.lastReason, "DEPENDENCY_UNAVAILABLE"); assert.equal(iamOutage.withdrawalInProgress, false); iamUnavailable = false; @@ -1796,7 +1779,7 @@ for (const successor of [false, true]) { // Those attempts run out during a gateway outage, and the next pass queues them again. await exhaustAttempts(); - const gatewayOutage = await read(); + const gatewayOutage = await withdrawal.read(fixture.actor.id); assert.equal(gatewayOutage.lastReason, "CREDENTIAL_WITHDRAWAL_PENDING"); assert.equal(gatewayOutage.withdrawalInProgress, false); revoke = true; @@ -1810,7 +1793,7 @@ for (const successor of [false, true]) { await fixture.work(recovered, "succeeded"); } await fixture.stop(); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.requestedBy, fixture.actor.id); assert.equal(await denials(), withdrawing.length); @@ -1851,8 +1834,7 @@ test( transformDrivers: withCredentialGateway, }); await fixture.work(active, "succeeded"); - const request = withdrawalRequest(fixture, owner, tool); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); + const withdrawal = await requestWithdrawal(fixture, owner, tool, fixture.actor.id); const [attempt] = await withdrawalAttempts(fixture, active); await fixture.work(attempt, "failed_permanent"); @@ -1862,18 +1844,17 @@ test( } assert.equal((await withdrawalAttempts(fixture, active)).length, 1); assert.equal(calls, 1); - const read = () => fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); - const waiting = await read(); + const waiting = await withdrawal.read(fixture.actor.id); assert.equal(waiting.lastReason, "CREDENTIAL_WITHDRAWAL_OWNERSHIP_CONFLICT"); assert.equal(waiting.withdrawalInProgress, false); // Once the cause is corrected, a replay revokes the source. refuse = false; - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); + await withdrawal.replay(fixture.actor.id); const replay = (await withdrawalAttempts(fixture, active)).at(-1); await fixture.work(replay, "succeeded"); await fixture.stop(); - assert.equal((await read()).state, "revoked"); + assert.equal((await withdrawal.read(fixture.actor.id)).state, "revoked"); }, ); @@ -1903,8 +1884,7 @@ revisionTest( await fixture.stop(); const second = await fixture.revision(owner, 2); - const request = withdrawalRequest(fixture, owner, sourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, request); + const withdrawal = await requestWithdrawal(fixture, owner, sourceId, fixture.actor.id); await fixture.start(compute, { transformDrivers: withCredentialGateway }); // The successor never re-attaches the model source, so its deployment cannot succeed. await fixture.work(second, "failed_permanent", 30_000); @@ -1927,7 +1907,7 @@ revisionTest( }); assert.deepEqual(prepared, [first.id]); assert.equal((await fixture.currentAgent(owner)).activeRevisionId, first.id); - const read = await fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); + const read = await withdrawal.read(fixture.actor.id); assert.equal(read.revisionId, first.id); assert.equal(read.state, "revoked"); }, @@ -1956,9 +1936,12 @@ revisionTest( // A second operator requests the withdrawal and is offboarded before the worker runs it. const requester = `withdraw-requester-${randomUUID()}`; await fixture.copyActorGrants(requester); - const request = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - const read = () => fixture.controller.readAgentCredentialWithdrawal(fixture.actor.id, request); - await fixture.controller.withdrawAgentCredentialSource(requester, request); + const withdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, + requester, + ); await removeAccessBindings(fixture, requester); const work = await withdrawalAttempts(fixture, active); assert.equal(work.length, 1); @@ -1969,7 +1952,7 @@ revisionTest( // A denial is final on the first attempt and never reaches the gateway. assert.equal(failed.attempt_count, 1); assert.deepEqual(withdrawn, []); - const recorded = await read(); + const recorded = await withdrawal.read(fixture.actor.id); assert.equal(recorded.state, "pending"); assert.equal(recorded.requestedBy, requester); assert.equal(recorded.lastReason, "AUTHORIZATION_DENIED"); @@ -1992,10 +1975,7 @@ revisionTest( // An authorized operator's replay queues the next attempt on its own authority, so the // offboarded requester cannot leave the source usable until a redeploy. await fixture.stop(); - const replayed = await fixture.controller.withdrawAgentCredentialSource( - fixture.actor.id, - request, - ); + const replayed = await withdrawal.replay(fixture.actor.id); assert.equal(replayed.state, "pending"); assert.equal(replayed.requestedBy, fixture.actor.id); assert.equal(replayed.requestedAt, recorded.requestedAt); @@ -2008,7 +1988,7 @@ revisionTest( await startWorker(); await fixture.work(retry[0], "succeeded"); assert.deepEqual(withdrawn, [[active.id, owner.harnessAuth.sourceId]]); - const revoked = await read(); + const revoked = await withdrawal.read(fixture.actor.id); assert.equal(revoked.state, "revoked"); assert.equal(revoked.requestedBy, fixture.actor.id); const revocations = await fixture.observerPool.query( @@ -2025,7 +2005,7 @@ revisionTest( const operator = `withdraw-operator-${randomUUID()}`; await fixture.copyActorGrants(operator); for (const principalId of [operator, fixture.actor.id]) { - const final = await fixture.controller.withdrawAgentCredentialSource(principalId, request); + const final = await withdrawal.replay(principalId); assert.equal(final.state, "revoked"); assert.equal(final.requestedBy, fixture.actor.id); assert.equal(final.withdrawalInProgress, false); @@ -2284,19 +2264,17 @@ revisionTest( // A revoked model source must stop preparation, but an exhausted tool withdrawal still // needs maintenance to recover after a gateway outage. - const modelRequest = withdrawalRequest(fixture, owner, owner.harnessAuth.sourceId); - const modelWithdrawal = await fixture.controller.withdrawAgentCredentialSource( + const modelWithdrawal = await requestWithdrawal( + fixture, + owner, + owner.harnessAuth.sourceId, fixture.actor.id, - modelRequest, ); await waitFor("model source withdrawal to be revoked", async () => { - const withdrawal = await fixture.controller.readAgentCredentialWithdrawal( - fixture.actor.id, - modelRequest, - ); + const withdrawal = await modelWithdrawal.read(fixture.actor.id); return withdrawal.state === "revoked" ? withdrawal : undefined; }); - assert.equal(modelWithdrawal.state, "pending"); + assert.equal(modelWithdrawal.initial.state, "pending"); const preparedBeforeWithdrawalRecovery = dispatched.length; await fixture.stop(); await recordPendingWithdrawal(fixture, owner, active, remainingToolSourceId); @@ -2593,13 +2571,14 @@ revisionTest( ); await fixture.work(active, "succeeded"); for (const credentialSourceId of [toolSourceId, modelSourceId]) { - const target = withdrawalRequest(fixture, owner, credentialSourceId); - await fixture.controller.withdrawAgentCredentialSource(fixture.actor.id, target); + const requestedWithdrawal = await requestWithdrawal( + fixture, + owner, + credentialSourceId, + fixture.actor.id, + ); await waitFor(`withdrawal of ${credentialSourceId} to be revoked`, async () => { - const withdrawal = await fixture.controller.readAgentCredentialWithdrawal( - fixture.actor.id, - target, - ); + const withdrawal = await requestedWithdrawal.read(fixture.actor.id); return withdrawal.state === "revoked" ? withdrawal : undefined; }); }