row 281 design d2: Q1/Q2 rulings (sage-2573), the finding in the Why, T3b/T4b and M8/M9 (#1786)

This commit is contained in:
jason.woltje committed 2026-10-22 04:56:39 -05:00
1 parent dfebd16cc1
commit c746ce378c
1 file changed
+36 -10
+36 -10
View File
@@ -1,6 +1,7 @@
# Row 281 build notes: a fire or a delete revokes the instance's grants (#1786) # 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. approved. The results sections are filled in after the runs on Sage's gate.
## Sources ## 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. 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). - 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 - **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 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 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 `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 "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 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
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
shorter than they can take, the design needs a change before code. shorter than they can take, the design needs a change before code.
## 3. Step G: what it revokes, and how ## 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 `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 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. 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 - A host line left by an earlier manual `hosts.revoke` doesn't block the fire (Q1 below). These are the
record is already gone and `mosaic hosts sweep` owns it. The fire's result names it (section 6), so the human agent's `grant.revoke.closed` records with `hostLineLeft` and no later line removal for that tag. For each
sees it. **Question Q1 for Dewey and Sage:** should it block instead, as the Gitea leftovers do? Blocking would one whose host record still exists, step G sends `remove <user> <tag>` once. On success it records
make the two halves match. It would also make a fire wait on a host the human may already have retired. `grant.revoke.closed {revoke: <the old revoke id>, hostLine: 'removed', fire: true}`, so a later fire doesn't
I lean toward naming it without blocking. 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) ## 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 token deleted and login prohibited at the fake Gitea. Afterwards that agent's `host.run` and Gitea use both
refuse. refuse.
- T3: the same through the delete (`lifecycleTake` then `lifecycleDelete`). - 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, - 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 `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 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. - 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. - 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. - 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 ## 10. Order and rebase