diff --git a/agents/megavolt/work/queue-281/BUILD.md b/agents/megavolt/work/queue-281/BUILD.md index 01a5172be..dab420e49 100644 --- a/agents/megavolt/work/queue-281/BUILD.md +++ b/agents/megavolt/work/queue-281/BUILD.md @@ -1,6 +1,7 @@ # Row 281 build notes: a fire or a delete revokes the instance's grants (#1786) -Builder Megavolt, reviewer Dewey, gate owner Sage. Design draft d1 for Dewey's approval. No code until it is +Builder Megavolt, reviewer Dewey, gate owner Sage. Design draft d2 for Dewey's approval. d2 adds Sage's +Q1/Q2 rulings (sage-2573) to d1 (dfebd16cc). No code until it is approved. The results sections are filled in after the runs on Sage's gate. ## Sources @@ -13,7 +14,11 @@ approved. The results sections are filled in after the runs on Sage's gate. goes through `lifecycleDelete` and the same `revoke()`, so this design covers it unchanged. - Row 235 r6 section 6 (host revoke) and #436b r5 section 6 (Gitea revoke and the fire steps). -## 1. What happens today (read from the code, to be pinned by a test before the change) +## 1. Why: what happens today (read from the code, to be pinned by a test before the change) + +In short: today a delete of a granted instance unlinks the key files but leaves both grant records, the host +lines and the Gitea token live, and a fire refuses `credential-revoke-failed` (sage-2573 asked for this here +and on #1786). The details: - **Fire.** `revoke()` (runtime.mjs:151) revokes only the roles entry's `credentials` services, then lists the agent's store directory and refuses `credential-revoke-failed` if any file is left. A host grant's key is @@ -63,9 +68,7 @@ awaits the reply, and `process.mjs` already returns async ops (`mayLaunch`). A r launcher with the refusal. `remove()` returns `{refusal: e.code}`, and the launcher closes the request through `lifecycleDone` with that code, as it does today for a writer refusal. The request is then closed, so the "repeat" for a delete is the human's new delete request. Grants revoked before the refusal stay revoked, and -the new request's step G goes on from there. Q2 for Sage: is it fine that a refused delete needs a new request? -The other choice is leaving the request taken, which today's launcher pass doesn't do for any refusal. Also to -check at build time: the launcher's `host.op` reply timeout against an SSH remove and the Gitea calls. If it's +the new request's step G goes on from there (Q2, ruled by sage-2573 below). Also to check at build time: the launcher's `host.op` reply timeout against an SSH remove and the Gitea calls. If it's shorter than they can take, the design needs a change before code. ## 3. Step G: what it revokes, and how @@ -158,11 +161,26 @@ A repeat runs the whole fire again: the checks, step A (nothing left to stop), s `grant.swept` with `by` set to the firing chief. That's an internal caller of an existing method, not a new verb. An entry left by an earlier manual `mosaic grants revoke` of the same agent blocks the fire the same way. That's intended: no grant outlives its instance, whichever verb started its revoke. -- A host line left by an earlier manual `hosts.revoke` (`hostLineLeft`, no row) doesn't block the fire. Its - record is already gone and `mosaic hosts sweep` owns it. The fire's result names it (section 6), so the human - sees it. **Question Q1 for Dewey and Sage:** should it block instead, as the Gitea leftovers do? Blocking would - make the two halves match. It would also make a fire wait on a host the human may already have retired. - I lean toward naming it without blocking. +- A host line left by an earlier manual `hosts.revoke` doesn't block the fire (Q1 below). These are the + agent's `grant.revoke.closed` records with `hostLineLeft` and no later line removal for that tag. For each + one whose host record still exists, step G sends `remove ` once. On success it records + `grant.revoke.closed {revoke: , hostLine: 'removed', fire: true}`, so a later fire doesn't + find it again. On failure it records nothing new and the fire goes on. The host and the tag go in + `grantsLeft` (section 6). + +## Rulings + +- **Q1 (sage-2573):** a leftover line doesn't block the fire. If the host is reachable, the fire tries the + removal once, because a fired agent's key line still on a host is the exact risk this row closes. If that + try fails, the fire doesn't refuse. It names the host and the leftover line in the result and in the event + the human sees. The human accepted that residue when their manual `hosts.revoke` left it, so it shouldn't + stop a fire forever on a dead host. Dewey may push back on the retry if it widens step G more than it's + worth. +- **Q2 (sage-2573):** a delete refused at step G closes its request through the launcher's normal refusal + path, and the human raises it again. A request left taken would strand, which is the shape DEFERRED already + records for the >64-stops done. Conditions: the refusal names the instance, the grant and the reason (the + `grant.revoke.refused` event, section 4). The repeat delete skips grants already revoked, as the repeat fire + does. T3b tests both. ## 6. The result and the event (D6) @@ -191,6 +209,12 @@ Bus, with the fake installer and the fake Gitea the row 235 and 236 tests use: token deleted and login prohibited at the fake Gitea. Afterwards that agent's `host.run` and Gitea use both refuse. - T3: the same through the delete (`lifecycleTake` then `lifecycleDelete`). +- T3b (Q2): a delete refused at step G closes its request with `grant-revoke-failed`. The file and the + instance are unchanged, and `grant.revoke.refused` names the instance, the grant and the reason. A new + delete request then completes and skips the grants the first one revoked. +- T4b (Q1): an earlier manual `hosts.revoke` left a line on host C. A fire with C reachable removes it and + records the close. With C unreachable, the fire completes and names C and the tag in `grantsLeft` and in + `fire.done`. - T4: host B unreachable. The fire refuses `grant-revoke-failed`, and `grant.revoke.refused` names the agent, `host:B` and `host-remove-failed`. The file is unchanged. Host A is revoked and recorded, host B's row and key file are still there, the Gitea grant is untouched (it comes after B in the order), and the credentials are @@ -214,6 +238,8 @@ G closes the request with `grant-revoke-failed`, if the launcher's tests stub `l - M5: the fire's host revoke goes local first (`lineFirst: false`). Killed by T4, because B's row is gone. - M6: a Gitea revoke with something in `serviceLeft` counts as done. Killed by T5b. - M7: open decisions left out of the set. Killed by T6. +- M8: step G skips the leftover-line retry. Killed by T4b. +- M9: a failed leftover-line retry refuses the fire. Killed by T4b. ## 10. Order and rebase