machine-grant design r6 (#436): duckworth-0063 C1 update-only undo after revoke, N1 start sweep of unrecorded key files

This commit is contained in:
jason.woltje committed 2026-10-20 17:38:57 -05:00
1 parent 7686fd783e
commit f9039682eb
1 file changed
+36 -7
+36 -7
View File
@@ -1,16 +1,29 @@
# Machine grant path, design draft r5 (Vikunja #436, row 210 follow-up)
# Machine grant path, design draft r6 (Vikunja #436, row 210 follow-up)
Owner: Gizmoduck. Reviewer: Duckworth. Sizing and row assignment: Sage. Design only. Code starts when
Duckworth approves the design and row 210 lands (sage-1527).
Sources: Q188 (DECISIONS.md:1642, jason-master-restart 8ecf252), mos-1115, mos-1120, mos-1125, mos-1127, mos-1131, mos-1133, mos-1139,
sage-1527, Duckworth's r1 review (REVIEW-r1.md, duckworth/reviews d025ef492, corrected at 749080115), Duckworth's
r3 and r4 review (REVIEW-r3.md, duckworth/reviews 9dceb2148, duckworth-0060),
r3 and r4 review (REVIEW-r3.md, duckworth/reviews 9dceb2148, duckworth-0060), Duckworth's r5 diff check
(REVIEW-r5.md, duckworth/reviews c3dc58f30, duckworth-0063),
Honker's PRD 1.8 draft (honker/row194-b2 3c30f1036, REQ-TRUST-2 and REQ-GRANT-1 to -4), row 210 DESIGN r3
(e021dfa26) and its approved candidate 455964f23. Code cites are `path:line` at origin/refactor f41729178
unless they say "210:", which means the row 210 candidate 455964f23 (the same code as the branch head
a2fd30390). Host facts come from ops-01's read-only check of .53 and .54 at 22:21Z (mos-1127).
## r6 changes (from r5 7686fd783)
Duckworth approved r5 with C1 and one note (duckworth-0063). The design is approved for code once row 210
lands. r6 carries C1 and the note, for a diff check on those lines.
- C1: a revoke can't be undone by a commands-change undo. After step 5 commits, the executor ignores an
abort (X5). Both undo paths that set a record back to `previous` only update a record that exists, never
insert one, and do nothing when a `grant.revoked` is newer than the decision (X3). G28 and an upsert
mutant cover it.
- N1: a failed unlink at revoke step 2 no longer depends on the marker. At start, after the grant
reconcile, the bus unlinks every `host.<id>` key file that has no `host_grants` record (section 6, G27).
## r5 changes (from r4 1c5705d6d)
Duckworth's review of r3 and r4 (duckworth-0060) asked for changes, B1 and B2, with notes N1 to N11. mos-1139
@@ -540,6 +553,11 @@ The steps are ordered so a failure leaves nothing an agent can use:
| 5 before `commit` | a line, a key file, no grant record | unlink the key file, unregister (X4), `remove` |
| 5 after `commit` | a line, a grant record, a key file | the record back to `previous` (deleted for a new key), 210 `revoke`, `remove` |
Setting a record back to `previous`, here and in the restart table below, only updates a record that
exists. It never inserts one, and it does nothing when a `grant.revoked` for the (agent, host) is newer than
the decision. So a revoke that deleted the record stays done, and the next start loads no reference for it
(C1).
A best-effort `remove` that fails adds `hostLineLeft: <tag>` to `grant.failed`, and `status` shows it until
`mosaic hosts sweep <hostId>` (human or tester) succeeds. `sweep` runs `list <user>` and removes each line
whose tag is in this business's recorded `grant.step` tags and has no live grant. A line with any other tag
@@ -554,7 +572,7 @@ is left alone, so a sweep can't touch another business's line on the same user (
| none | none (the tmp dir goes in 210's run-dir sweep) | none |
| `install` | none | `remove <user> <tag>` |
| `commit`, new key | unlink the key file if present, delete the grant record if present | `remove <user> <tag>` |
| `commit`, commands change | set the record's `commands` back to `previous` | none, the key and line didn't change |
| `commit`, commands change | set the record's `commands` back to `previous`, by the update-only rule above | none, the key and line didn't change |
| enrollment `registry` | delete the installer key if present, delete the host record | none |
Then it records `grant.failed {code: 'interrupted', tag}` with the tag listed every time, and `hostLineLeft:
@@ -610,7 +628,8 @@ it and returns the signal, and `release` deletes it.
A mutant covers it.
- On abort the executor undoes as X3's failure table does at that step, then ends
`grant.failed {code: 'revoked'}`.
- A revoke after step 5 is an ordinary revoke (section 6).
- A revoke after step 5 is an ordinary revoke (section 6). Once step 5 commits, the executor ignores an
abort and goes on to step 6, so its undo never runs against a revoke that came after the commit (C1).
While the executor holds the claim, a `host.run` for the same agent and host refuses `host-busy`. That only
matters for a commands change on a live key, and it lasts until the executor ends.
@@ -691,8 +710,8 @@ Order, local first so the part that matters can't wait on the network:
2. Right after the commit, with no `await` between: 210 `revoke` if `Credentials` holds a reference
(memory, abort the `host.run` or the executor in flight, unlink the key file), or `abortClaim` if it
holds only a claim (X5). Steps 1 and 2 are the "instant" part, and neither touches the network. If the
unlink fails, the verb reports it. The key is already out of memory, and the start reconcile unlinks the
file.
unlink fails, the verb reports it. The key is already out of memory, and the next start removes the
file (below).
3. `remove <user> <tag>` on the host through the installer. Success records `grant.revoke.closed {tag,
hostLine: 'removed'}`. If the host can't be reached, it records `grant.revoke.closed {tag, hostLineLeft:
<tag>}`, and `sweep` clears it later.
@@ -702,6 +721,11 @@ A restart between the steps (N4) leaves a `grant.revoked` with `hostLine: 'pendi
there. No reference loads for it, because the record is gone (X4). After the bus is up, the reconcile runs
step 3 and closes the marker. `status` shows an open marker as a revoke in progress.
Step 3 closes the marker even when step 2's unlink failed. So the file doesn't wait on the marker: at every
start, after the grant reconcile (X3) and before `Credentials` loads, the bus unlinks each `host.<id>` key
file in the agent store that has no `host_grants` record (N1 of duckworth-0063). The grant reconcile runs
first, so a key an interrupted executor wrote is already handled there.
Step 3 is defense in depth. After step 1 the stack holds no copy of the key. The line only matters if
something copied the key file before the revoke, which section 9's same-user limit allows.
@@ -808,7 +832,8 @@ such argument. No real host is contacted.
| G23 | a revoke during install (a scratch `mosaic-keys` that waits on a fifo) ends `grant.failed revoked` with no key file, no grant and no line. A revoke before the executor starts, with no reference and no claim, records `grant.revoked`, and step 1 then ends `grant.failed revoked`. A grant whose claim finds a running `host.run` ends `grant.failed host-busy` with its decision spent and a message (B2) |
| G24 | agent and host names at 64 characters with `.` and `_` give distinct tags of the fixed length. `sweep` leaves another business's line and an unrecorded `mosaic-` line on the same user (B4) |
| G25 | the writer refuses a symlinked `/etc/ssh/mosaic-keys`, a symlinked user file, a group-writable dir, and a tmp that is a symlink or not a regular file. A leftover regular tmp is unlinked and the write goes ahead. A first `add` creates the file. A written file is root-owned 0644 (B5, N6) |
| G27 | `hosts.revoke` works for an agent whose business file has no credentials reference (N3). A restart after revoke step 1, and one after step 2, each end at start with no key file, no record, the line removed and the marker closed (N4) |
| G27 | `hosts.revoke` works for an agent whose business file has no credentials reference (N3). A restart after revoke step 1, and one after step 2, each end at start with no key file, no record, the line removed and the marker closed (N4). A key file left by a failed step 2 unlink is gone after the next start, though the marker closed |
| G28 | a commands change commits at step 5, a revoke follows before `grant.applied`, and the bus restarts: the start ends with no grant record, no reference and no key file. The same with a failure after step 5 in place of the restart (C1) |
Mutants, a first list. The row's BUILD.md numbers them and adds more:
@@ -851,6 +876,10 @@ Mutants, a first list. The row's BUILD.md numbers them and adds more:
- make `commit` async
- route `hosts.revoke` through `credential-verbs.mjs`
- skip the revoke marker reconcile
- let the undo to `previous` insert a record (an upsert)
- let the undo to `previous` run past a newer `grant.revoked`
- let the executor act on an abort after step 5 commits
- skip the start sweep of key files with no record
- treat a missing `bus.tester` as dev
## 11. Not in this draft