From 802860568879e98049cf6cd1f86536075696888d Mon Sep 17 00:00:00 2001 From: gcharang <21151592+gcharang@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:54:47 +0400 Subject: [PATCH] fix(fleet-control): suppress terminal re-entry audit --- .changeset/quiet-fleet-terminal-reentry.md | 5 +++++ docs/fleet-control.md | 4 ++-- packages/fleet-control/README.md | 2 +- packages/fleet-control/src/provision.ts | 1 - packages/fleet-control/test/provision.test.ts | 14 ++++++++++++-- 5 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 .changeset/quiet-fleet-terminal-reentry.md diff --git a/.changeset/quiet-fleet-terminal-reentry.md b/.changeset/quiet-fleet-terminal-reentry.md new file mode 100644 index 0000000..7bb1c3f --- /dev/null +++ b/.changeset/quiet-fleet-terminal-reentry.md @@ -0,0 +1,5 @@ +--- +"@proofoftech/fleet-control": patch +--- + +Delete an already-decommissioned fleet ledger row without emitting a duplicate decommission event mislabeled as forced. diff --git a/docs/fleet-control.md b/docs/fleet-control.md index c54e1b7..7c7e42d 100644 --- a/docs/fleet-control.md +++ b/docs/fleet-control.md @@ -185,7 +185,7 @@ await forceDecommissionDeployment({ }); ``` -The function always acquires `store.withDeploymentLease()` before it reads the ledger. An absent deployment succeeds without a provider mutation. A terminal `decommissioned` record also succeeds and removes the retained ledger row. +The function always acquires `store.withDeploymentLease()` before it reads the ledger. An absent deployment succeeds without a provider mutation. A terminal `decommissioned` record also succeeds, removes the retained ledger row, and emits no audit event. A concurrent provision or decommission receives the same lease-acquisition error as `decommissionDeployment()`. A `database-reserved` row has not authorized provider creation, so force decommission removes that reservation without a provider call. @@ -199,7 +199,7 @@ The force path uses persisted resource identities only. It never fetches an arti Each provider mutation receives the active lease fence. `WranglerLoopBackend` requires route API implementations of both `getDatabase` and `deleteDatabase` before the first D1 lookup. The exact-ID lookup, deletion, and confirmation all run within that route fence; force deletion never falls back to Wrangler. The ledger persists `decommissioning`, `traffic-removed`, `credentials-revoked`, and `database-deleting`, so an interrupted call repeats the incomplete idempotent stage. -Provider 404 responses mean the resource is already absent. After D1 is absent, the function persists `decommissioned`, emits `DecommissionAuditEvent` with `forced: true`, and calls `lease.delete()`. If audit delivery fails, the terminal row remains and a retry emits the event again before deleting the row. Normal decommission uses the same optional event with `forced: false`. +Provider 404 responses mean the resource is already absent. After D1 is absent, the function persists `decommissioned`, emits `DecommissionAuditEvent` with `forced: true`, and calls `lease.delete()`. If initial audit delivery fails, the terminal row remains. A retry deletes it without repeating provider mutations or redelivering the event. Normal decommission uses the same optional event with `forced: false`. Force decommission does not delete the ordinary Worker script, application R2 buckets, or control-plane retention data. It removes the deployment’s ingress, live Worker secrets, database, and fleet ownership record. After the call returns, the host deletes its separate retention row and revokes its gateway key. Workers for Platforms fails closed unless its backend implements equivalent spec-free primitives; its dispatch route and trusted-resource topology cannot use the ordinary Worker route API contract. diff --git a/packages/fleet-control/README.md b/packages/fleet-control/README.md index a3949dc..34086ec 100644 --- a/packages/fleet-control/README.md +++ b/packages/fleet-control/README.md @@ -57,7 +57,7 @@ Use `forceDecommissionDeployment()` only when the host has lost the retained cre Provider 404 responses converge as already absent, so retry the same call after an interrupted teardown. A `database-reserved` row has not authorized provider creation and can be removed without a provider call. A `database-create-authorized` row has an unresolved creation outcome and only a synthetic ID, so force decommission fails closed and retains that row for spec-aware recovery. -Pass `options.audit` to receive a `DecommissionAuditEvent`. Normal decommission emits `forced: false`; force decommission emits the same event with `forced: true`. Fleet Control emits the force event before it deletes the ledger row. If the sink fails, the terminal record remains and the next call retries audit delivery without repeating provider mutations. +Pass `options.audit` to receive a `DecommissionAuditEvent`. Normal decommission emits `forced: false`; an actual force teardown emits the same event with `forced: true`. Fleet Control emits the force event before it deletes the ledger row. If the sink fails, the terminal record remains, and the next call deletes it without repeating provider mutations or redelivering the event. Force decommission of an already-terminal record also deletes the ledger row without emitting an event. The function never reads an artifact, computes a specification digest, deletes host-retained control-plane secrets, or deletes application R2 buckets. `WranglerLoopBackend` requires `PlainWorkerRouteApi.getDatabase` and `deleteDatabase`; it never falls back to Wrangler for force deletion. Other backends fail closed unless they implement the narrow `forceDecommissionStep` contract for equivalent provider primitives. diff --git a/packages/fleet-control/src/provision.ts b/packages/fleet-control/src/provision.ts index 315dd00..6ee7146 100644 --- a/packages/fleet-control/src/provision.ts +++ b/packages/fleet-control/src/provision.ts @@ -1677,7 +1677,6 @@ export async function forceDecommissionDeployment( ); } if (current.phase === 'decommissioned') { - await emitDecommissionAudit(input.options?.audit, current, true); await lease.delete(); return; } diff --git a/packages/fleet-control/test/provision.test.ts b/packages/fleet-control/test/provision.test.ts index 560c405..8b1d8de 100644 --- a/packages/fleet-control/test/provision.test.ts +++ b/packages/fleet-control/test/provision.test.ts @@ -2226,10 +2226,14 @@ describe('fleet provisioning', () => { spec: spec(), secrets, }); + const auditFlags: boolean[] = []; await decommissionDeployment({ backend, store: terminalStore, spec: spec(), + audit: (event) => { + auditFlags.push(event.forced); + }, }); Object.defineProperty(backend, 'forceDecommissionStep', { value: undefined, @@ -2242,11 +2246,17 @@ describe('fleet provisioning', () => { store: terminalStore, tenantTag: 'acme', environment: 'production', + options: { + audit: (event) => { + auditFlags.push(event.forced); + }, + }, }), ).resolves.toBeUndefined(); expect(terminalStore.leaseCalls).toBe(priorLeaseCalls + 1); expect(terminalStore.record).toBeUndefined(); expect(backend.forceSteps).toEqual([]); + expect(auditFlags).toEqual([false]); }); it('uses one audit event shape for normal and forced decommission', async () => { @@ -2388,7 +2398,7 @@ describe('fleet provisioning', () => { await expect(force).resolves.toBeUndefined(); }); - it('retries a completed force teardown when audit delivery fails', async () => { + it('silently removes a completed force teardown after audit delivery fails', async () => { const backend = new FakeBackend('plain-worker'); const store = new MemoryStore(); await provisionDeployment({ backend, store, spec: spec(), secrets }); @@ -2417,7 +2427,7 @@ describe('fleet provisioning', () => { 'delete-database', ]); expect(store.record).toBeUndefined(); - expect(auditAttempts).toBe(2); + expect(auditAttempts).toBe(1); }); it('rejects a concurrent lifecycle operation for the same deployment', async () => {