plans: #436b design r3 on dewey-0756 (B1-B3, N1-N6), section 7 guard per gizmoduck-0081/-0084 and duckworth-0065, F13 settle probe, Q-B4 closed by mos-1146; no code change

This commit is contained in:
2026-10-20 18:27:52 -05:00
parent be823a6f73
commit fa5658941b
+122 -68
View File
@@ -1,18 +1,34 @@
# Gitea grant path, design draft r2.1 (Vikunja #436b, Gitea #1697 follow-up)
# Gitea grant path, design draft r3 (Vikunja #436b, Gitea #1697 follow-up)
Author: Megavolt. Reviewer: Dewey. GitHub half: Bushroot. Base: Gizmoduck's #436a r4 (1c5705d6d,
Author: Megavolt. Reviewer: Dewey. GitHub half: Bushroot. Base: Gizmoduck's #436a r6 (f9039682e, final,
`agents/gizmoduck/work/machine-grant/DESIGN.md` on origin/gizmoduck/machine-grant-design) and row 199's
DESIGN.md section 7a (e10e03f6b). Mos rulings used here: mos-1130 and mos-1136.
This draft builds nothing. It says how an Approve in Jason's inbox makes an agent's Gitea user, its token and
its forks with no manual step, and it answers mos-1130 item 3 (a to e) on the executor's admin credential.
## r2.1 changes (gizmoduck-0081)
## r3 changes (dewey-0756 on r2 6db7cc45d, gizmoduck-0081 and -0084, duckworth-0065, mos-1144)
- Section 7: the guarded namespace is exactly `grant.*` and `host.enroll.*` (row 210's `host.sudo` and
`host.run` stay allowed). The grant actor is `actor_role NULL, actor_run 'grant'`, a pair no session or
human event can produce. A spend rule: no guarded body carries `decision` before `#consumeGrant`, so step 0's
refusal names it as `reason`. No task refs in guarded bodies. X-G13 and the mutants follow.
- B1 (blocks). Only `#consumeGrant`'s own record carries `decision`. Every other `grant.*` body names the
decision `grantDecision`, so `#spent` sees exactly one spend. A step 0 or step 1 refusal leaves the
decision unspent (section 7, X-G11, a mutant).
- B2 (blocks). Settle with the seat token is proven on scratch (F13): `GET /api/v1/token` answers 200 with the
minted id and 401 after the token is deleted. Only a 401 ends `grant-lost`. 403, 404, 5xx, a timeout, an
unreachable Gitea or a failed C1 to C4 leave the step open as `settle-pending`, delete nothing, and retry
on the next start and in `grants sweep` (section 5, X-G6). `Credentials.reload` is built by #436b's own
row in `packages/bus/src/credentials.mjs`; row 199's candidate doesn't change.
- B3 (blocks). Step 5 compares against the user's forks of every upstream in the agent's `publish` list, not
only `target.projects`. The rendered block names those other upstreams as reach the token may have. A fork
whose upstream has left `publish` refuses `gitea-reach-wider` with its name (section 2, step 5, X-G12).
- Section 7, the guard (gizmoduck-0081, -0084, duckworth-0065): one pattern, `grant.*`, on `body.action` and
on `body.operation` (Dewey N2: the consume record's `operation: 'grant.<method>'`). #436a renames its
`host.enroll.step`, `host.enrolled`, `host.checked` and `host.removed` into `grant.host.*`, so row 210's
`host.sudo` and `host.run` stay outside. The grant actor is `actor_role NULL, actor_run 'grant'`, which no
session or human event can produce (answers Dewey N1, same safety as `_bus`).
- D12 / Q-B4 closed by mos-1146 (section 8).
- N3. Step 5 reads every page of `/user/repos` and checks the total. N4. One nonce format, and the adopt
check compares the lowercased login. N5. `grants run` takes the `#inflight` claim. N6. One open-decision
query, `openGrantDecisions`, for `status`, rotate's void count and the start reconcile (section 7).
## r2 changes (from r1 714c07304, Dewey's dewey-0754, verdict sha256 20d63dfb…)
@@ -100,6 +116,11 @@ F1 to F6 were reported in megavolt-0034, and Mos accepted them in mos-1136.
them. It lists a repository once the account is a collaborator, and an org repository once it joins that
org's team. Joining an org with no repositories leaves it empty, and `GET /user/orgs` gets 403 (no
`read:organization`).
- F13 (`b2.log` b1-b7). Two seat tokens minted (201, ids 2 and 3). `GET /api/v1/token` with each token in the
`token` header gives 200, and the `id` is the minted one for both. After `DELETE` of seat-a's token (204), the
same call with it gives 401. After `prohibit_login` on seat-b (200), the call with seat-b's token gives 403
("This account is prohibited from signing in"), not 401. A value never issued gives 401. So 401 is the only
answer that means the token is gone; 403 means the account is shut, and section 5 leaves it `settle-pending`.
## 2. The binding
@@ -126,14 +147,17 @@ Checks at raise, inside the raise transaction. Each refusal stores nothing:
The rendered block, appended by the broker as #436a R4 does: agent, Gitea user, email, Gitea URL, each fork
URL it will make (`<url>/<user>/<repo>`), the two fixed scopes, the rotate-by date, one of "new Gitea user",
"existing stack-made user, new token" or "token rotation" (step 2), and the line "The token can write every
repository this user can write. Step 5 checks that this is only its forks." The 4096-character cap applies as
in R4.
repository this user can write. Step 5 checks that this is only its forks." Then, when the agent's `publish`
list has projects outside `target.projects`, the line "It may also reach this user's existing forks of:" and
each such upstream. The raise makes no network call, so the block names every such upstream, not only those
with a fork (B3). The 4096-character cap applies as in R4.
The raise event body gets `grant = {method, agent, target, rotateBy, definitionSha256, identitySha256,
adminSha256}`:
- `definitionSha256` as in #436a.
- `identitySha256` over the agent's `identity`, its `publish` list and each named project's `remote`.
- `identitySha256` over the agent's `identity`, its `publish` list and the `remote` of every project in
`publish` (step 5 checks against all of them).
- `adminSha256` over the admin slot's metadata (`url`, `account`, `setAt`), never over the token.
At step 1 any of the three that differs refuses `decision-mismatch`. So a changed identity, publish list,
@@ -283,15 +307,18 @@ nothing.
0. **Start and pre-checks.** X1 starts the executor on Jason's yes. It claims `(agent, "gitea")` in
`#inflight` (#436a X5), then runs C1 to C4 and the admin rotate-by check. A refusal here writes
`grant.failed {code, reason: <decision>, spent: false}`, with no `decision` key (section 7's spend
rule), and leaves the decision unspent. After the fix (a rotation, an upgrade),
Jason runs `mosaic grants run <decision>` (human-only, `verifyHuman`), which starts step 0 again. No other
path starts it. The tag is #436a X2 step 2's, over business, agent and `"gitea"`.
`grant.failed {code, grantDecision}` and leaves the decision unspent (section 7's spend rule). After the
fix (a rotation, an upgrade), Jason runs `mosaic grants run <decision>` (human-only, `verifyHuman`), which
starts step 0 again, claim included. While a start or another `grants run` holds the claim it refuses
`grant-busy` before any call, as #436a X2's `host-busy` does. It refuses `decision-consumed` for a spent
decision. No other path starts it. The tag is #436a X2 step 2's, over business, agent and `"gitea"`.
1. **Recheck and consume**, in one transaction: `#consumeGrant` with `operation: 'grant.gitea'`, the
definition still declares `gitea`, the three hashes match (section 2), no revoke of `(agent, gitea)` newer
than the raise. Any failure is `grant.failed` with its code, and nothing remote has changed.
than the raise. A failure rolls the transaction back, so the decision is not spent, and then writes
`grant.failed {code, grantDecision}`. Nothing remote has changed. `grants run` may retry it, but these
refusals (`decision-mismatch`, a newer revoke, the definition) recur until the chief raises again.
2. **User.** `GET /users/{user}` (F8: case-insensitive).
- 404: write `grant.step {step: 'user', nonce}`, where `nonce` is 16 random hex digits. Then
- 404: write `grant.step {step: 'user', nonce}`, where `nonce` is 16 random lowercase hex digits. Then
`POST /admin/users` with the agent's `gitea.user` and `email`, `full_name` = `<identity.name> [mosaic
<nonce>]`, a random password that is never stored, and `must_change_password: false`. Then, in one
transaction, record `gitea_users {user, id, nonce}` with the `id` from the response (F8) and
@@ -308,17 +335,20 @@ nothing.
4. **Forks.** With the new token, for each project: `GET /repos/{user}/{repo}`. If it exists, its `parent`
must be the project's upstream, or `fork-foreign`. If it's 404, `POST /repos/{owner}/{repo}/forks`.
Push-to-create stays off on the Gitea side (199 N11).
5. **Verify.** With the new token: `GET /api/v1/user` gives `login == user` and the recorded `id`, and
`GET /api/v1/user/repos` lists exactly this user's forks of granted upstreams (F11). Any other repository
refuses `gitea-reach-wider` and names it, so a user someone added as a collaborator or to an org team
doesn't get a token that reaches past the rendered block.
5. **Verify.** With the new token: `GET /api/v1/user` gives `login == user` and the recorded `id`. Then
`GET /api/v1/user/repos?limit=50&page=N` from page 1 until a page holds fewer than 50, with the count
equal to `X-Total-Count`. More than 20 pages, or a count that disagrees, refuses `gitea-reach-unknown`.
Every listed repository must be owned by this user and have as `parent` the upstream of a project in the
agent's `publish` list (F11). Anything else refuses `gitea-reach-wider` and names it: a repository from a
collaborator or org-team add, or the user's old fork of an upstream that has left `publish`. So the token
never reaches past the rendered block (B3).
6. **Go live.** Write `grant.step {step: 'commit', previousTokenId}` (`null` for a first grant). Check the
abort signal, and with no `await` between, call 199 `issue(agent, "gitea", token, rotateBy, commit)`.
`commit` writes the `gitea_grants` record (tokenId, tokenName, projects, rotateBy, decision). The reference
is registered first (#436a X4).
7. **Old token.** For a rotation, `DELETE /users/{user}/tokens/{previousTokenId}` after the new one is live.
A failure adds `serviceLeft: <id>`, which `sweep` clears.
8. **Applied.** `grant.applied {decision, agent, user, userId, tokenId, forks}` and a message to the human and
8. **Applied.** `grant.applied {grantDecision, agent, user, userId, tokenId, forks}` and a message to the human and
the chief. The token appears in no record, message or log.
The claim is released in a `finally`.
@@ -331,15 +361,22 @@ decision (D8). Best effort; a failure adds `serviceLeft`.
**Settle.** Once `issue` has renamed the file, the file may hold the new token or the previous one, and the
record may name either. The executor and the restart never guess. They read the store file and ask Gitea which
token it is: `GET /api/v1/token` with that value in the `token` header (F7, op `token-whoami`). The answer is
an `id`, never a value.
token it is: `GET /api/v1/token` with that value in the `token` header (F7 for admin tokens, F13 for the
two-scope seat token; op `token-whoami`). The answer is an `id`, never a value.
- `id` is the new token: forward. The `gitea_grants` record gets the new token (if `commit` hadn't run),
memory loads the file (`Credentials.reload(instance, service)`, a new 199 method with load's checks), and
the previous token is deleted.
- `id` is the previous token: back. The new token is deleted. The record keeps or gets the previous token.
- 401, or no file: both are gone or unusable. Delete both by id, unregister, delete the record, and end
`grant.failed {code: 'grant-lost'}`. A new decision is needed.
- 200, `id` is the new token: forward. The `gitea_grants` record gets the new token (if `commit` hadn't
run), memory loads the file (`Credentials.reload(instance, service)`, a new method with load's checks,
built in #436b's row; row 199 doesn't change), and the previous token is deleted.
- 200, `id` is the previous token: back. The new token is deleted. The record keeps or gets the previous
token.
- 401: the file's token no longer exists at Gitea (F13 b4). The previous token can't be in the file, so the
agent holds nothing usable. Delete both by id, unregister, delete the record, unlink the file, and end
`grant.failed {code: 'grant-lost'}`. A new decision is needed. No file at all is the same case.
- Anything else is `settle-pending`, and nothing is deleted: 200 with an `id` that is neither token, 403,
404, 5xx, a timeout, Gitea unreachable, or C1 to C4 failing first. The last `grant.step` stays `commit`, the
`grant.call` without a `grant.result` stays listed, and `status` shows `settlePending`. The file stays as
it is, and the agent can use it: it holds one of the two tokens, and both passed step 5. The next start
and `mosaic grants sweep gitea` settle again. Only a 401 deletes.
| Fails at | State left | Cleanup |
|---|---|---|
@@ -364,6 +401,7 @@ the remote part runs after the bus is up, with C1 to C4.
| `mint` or `minted` | none | delete the token by name |
| `commit`, first grant | unlink the file if present, delete the record if present | delete the new token |
| `commit`, rotation | none | settle |
| `commit`, rotation, settle-pending | none; memory loads the file as usual | none. The step stays open, and the next start or `grants sweep` settles again |
Then `grant.failed {code: 'interrupted'}` (or `grant.applied` if a rotation settled forward) and a message to
Jason. A `grant.call` with no `grant.result` is listed in that message. After `interrupted`, the chief raises
@@ -401,42 +439,52 @@ The executor, revoke and `sweep` are the only writers, each in the transaction o
is a file, not a table, so a store backup holds no admin token.
The audit records (`grant.step`, `grant.call`, `grant.result`, `grant.applied`, `grant.failed`,
`grant.revoked`, `grant.sweep`) are `action.allowed` events with that name in `body.action`, the pattern
`#consumeGrant` already uses for `grant.<method>`. They are not new event kinds, so `events` and its CHECK
(`3b.sql:9-30`) aren't rebuilt. This rule is shared with #436a (agreed in gizmoduck-0081, on #436a r6
f9039682e), which writes `grant.step`, `grant.failed`, `grant.revoked`, `grant.revoke.closed`,
`host.enroll.step`, `grant.host.checked` and `grant.host.removed` the same way. Cites are origin/refactor.
`grant.revoked`, `grant.sweep`) are `action.allowed` events with that name in `body.action`. They are not new
event kinds, so `events` and its CHECK (`3b.sql:9-30`) aren't rebuilt. This section is the rule's only home
(mos-1144). #436a r6 (f9039682e) uses it too: #436a row a's BUILD.md cites this section at the commit Duckworth
approves and maps r6's names to `body.action` values, with these renames (gizmoduck-0084):
`host.enroll.step` to `grant.host.enroll.step`, `host.enrolled` to `grant.host.enrolled`, `host.checked` to
`grant.host.checked`, `host.removed` to `grant.host.removed`. Cites are origin/refactor 00d4cc1f6.
**The guarded namespace** is exactly `body.action` GLOB `'grant.*'` or `'host.enroll.*'`. It is not `host.*`:
row 210 writes `action.allowed` with `host.sudo` and `host.run` from agent sessions, and those stay allowed.
#436a's check and remove events are named `grant.host.checked` and `grant.host.removed`, so nothing guarded
sits under a bare `host.*` name.
**The guarded namespace** is one pattern: an `action.allowed` whose `body.action` or `body.operation` GLOBs
`'grant.*'`. `operation` is included because `#consumeGrant`'s consume record is
`{operation: 'grant.<method>', decision}` (#436a X1), and a forged one would spend a decision (Dewey N2). Row
210's `host.sudo` and `host.run`, written from agent sessions, stay outside it, and no grant-path name is left
under `host.*`.
**The grant actor** is `actor_role IS NULL AND actor_run = 'grant'`. `#event` writes the session's role and run
(`packages/bus/src/broker.mjs:558-575`), every session has a role, and the human's events carry NULL for both
(`3b.sql:156-158`). So neither a session nor the human path can produce that pair. No `system` role exists to
reserve. The broker writes guarded events through a private session-less writer added in #436a row a; `#event`
is unchanged. Revoke, sweep and check run for the human, the assistant, the tester or the chief, and their
events still go out under the grant actor, with `body.by` naming the caller the broker checked. One actor rule
covers the whole namespace.
**The grant actor** is `actor_role IS NULL AND actor_run IS 'grant'`. The only production insert into `events`
is `#event` (`packages/bus/src/broker.mjs:568`), which writes the session's role and run. Every session with a
NULL role also has a NULL run: the human (`:537`), the reader (`:549`) and a session-less record (`:1124`). The
tester's role is `tester`. So neither a session nor the human path can produce that pair, as no instance can
be named `_bus` (`:166-168`). A `system` role would not be safe, because `system` matches `ID_PATTERN`
(Dewey N1). The broker writes guarded events, the consume record included, through a private session-less
writer added in #436a row a; `#event` is unchanged. Revoke, sweep and check run for the human, the assistant,
the tester or the chief, and their events still go out under the grant actor, with `body.by` naming the caller
the broker checked.
**Two guards** stop a forged record:
- `recordEvent`, the trusted adapters' path (`broker.mjs:1111`), refuses `reserved-event` for any
`action.allowed` whose `body.action` is in the guarded namespace, as it does today for `git.push.working`
and the merge action.
- The migration adds a trigger: an `action.allowed` event in the guarded namespace aborts unless
`action.allowed` in the guarded namespace, as it does today for `git.push.working` and the merge action.
- The migration adds a trigger: an `action.allowed` in the guarded namespace aborts unless
`actor_role IS NULL AND actor_run IS 'grant'`. That covers any path that misses the first guard.
**Spend rule.** `#spent` (`broker.mjs:811-816`) counts any `action.allowed` that carries `body.decision`
and whose operation isn't `decision.raise`. So no guarded event carries a `decision` key before
`#consumeGrant`'s own event in step 1. Before consume, step 0's records name the decision as `reason`:
its `grant.call` and `grant.result`, and its `grant.failed {code, reason, spent: false}`. From step 1 on,
`grant.step`, `grant.failed` and `grant.applied` carry `decision`. The start reconcile for "resolved yes,
unspent" then keeps reading `#spent` unchanged, and a step 0 refusal stays retryable by `grants run`.
**Spend rule (B1).** `#spent` (`broker.mjs:811-817`) counts any `action.allowed` that names the decision in
`body.decision` and whose `operation` isn't `decision.raise`; a record with no `operation` counts. So the
consume record is the only guarded body with a `decision` key. Every other `grant.*` body names the decision
`grantDecision`. A step 0 or step 1 refusal therefore leaves the decision unspent, and the start reconcile
for "resolved yes, unspent" keeps reading `#spent` unchanged.
**No task refs.** Guarded bodies never carry `target` or `task_ref` holding a task ref, or `#event`'s
`task-subject-required` check (`broker.mjs:561-565`) fires. No other `action.allowed` reader sees these
A wider gap sits under this one and isn't this row's: today any `recordEvent` `action.allowed` that carries
`body.decision` spends that decision. Follow-up for Sage to file.
**Open decisions (N6).** One query, `openGrantDecisions(business, method)`: `credential.mint` decisions
resolved yes by the human whose raise body has `grant.method = method`, and for which `#spent` is false.
`status` lists them, `grants admin rotate` counts the ones it voids (all of them, since `adminSha256`
changes), and the start reconcile reads the same query.
**No task refs.** Guarded bodies never carry `target` or `task_ref` holding a task ref, so they stay clear of
`task-subject-required` (`broker.mjs:561-565`) on any path. No other `action.allowed` reader sees these
events: the rest key on `decision.raise` (`:670`, `:803`), `git.push.working` (`:863`, `:2019`) or
routine (`:2100-2102`).
@@ -456,9 +504,11 @@ Stated once, here, for the #436b approval:
is more than it could get by forging an Approve. A separate uid would close that path. It is deferred on cost
(3c), not because it wouldn't help.
- **For comparison, today (D12).** Every dev seat on dragon-lin already pushes as jason.woltje through
`scripts/git-credential-mosaic.mjs` (mos-1106). If jason.woltje is a site admin on git.mosaicstack.dev, the
admin file adds little exposure there beyond what the seats already have. I can't check that account's admin
flag myself. Sage or Mos should confirm it before this note goes to Jason.
`scripts/git-credential-mosaic.mjs` (mos-1106). jason.woltje is a site admin on git.mosaicstack.dev, with
restricted false (mos-1146: Mos read `/api/v1/users/jason.woltje` with the mos token at 23:05:29Z; Jason's
token wasn't used). So a same-uid process on dragon-lin can already act as a site admin there through that
credential, and the admin file adds little exposure on that Gitea beyond what the seats already have. It
still matters on any other Gitea a business grants on, and once seats stop pushing as jason.woltje (#431).
- **A seat token's reach can grow.** Step 5 checks, at the grant, that the token reaches only its forks. If
someone later adds the user to an org or as a collaborator, the token reaches that too, until the next grant
or rotation checks again.
@@ -480,14 +530,14 @@ or any other live service. No token value is printed.
| X-G3 | on scratch 1.27.1, end to end: user made with the nonce and its `id` recorded, token minted with the two scopes, forks made, step 5 passes, `issue` writes the 0600 file, `grant.applied`, then revoke deletes the token at Gitea (gone from its list) |
| X-G4 | an existing user with no record, a recorded name whose `id` differs (deleted and remade, and a case variant of the name), a site-admin user and a disabled user each refuse before any token call |
| X-G5 | each failure row of section 5 leaves the stated state, rotation rows included. A failed delete leaves `serviceLeft` |
| X-G6 | a restart after `user` adopts a user whose `full_name` carries the nonce, does nothing on 404, and reports `gitea-user-orphan` for a different `full_name`. A restart after `mint`, `minted` and `commit` of a first grant leaves no token at Gitea, no file and no record. A restart after `commit` of a rotation settles forward when the file holds the new token and back when it holds the previous one |
| X-G6 | a restart after `user` adopts a user whose `full_name` ends with ` [mosaic <nonce>]` (16 lowercase hex), whose lowercased login equals `gitea.user` lowercased and who isn't an admin; does nothing on 404; and reports `gitea-user-orphan` for a different `full_name`, a different login or an admin. A restart after `mint`, `minted` and `commit` of a first grant leaves no token at Gitea, no file and no record. A restart after `commit` of a rotation settles forward when the file holds the new token and back when it holds the previous one, ends `grant-lost` only on a 401, and stays `settle-pending` with nothing deleted on 403, 404, 500, a timeout, an unreachable fake and an unknown `id` |
| X-G7 | an identity, publish, upstream or admin slot change between raise and Approve refuses `decision-mismatch` before any Gitea call. An admin rotate voids open grant decisions and `status` lists them |
| X-G8 | every admin-token and seat-token call writes `grant.call` before it and `grant.result` after it, paired by `callId`. A crash between them is listed by reconcile. No record body holds a token |
| X-G9 | admin rotate: C1 to C4, the successor named by time, checked, written, the old token deleted. Two rotations in one second's name refuse cleanly, not half done. Past rotate-by, the raise and the executor refuse |
| X-G10 | seat-token rotation: the new token is live before the old one is deleted |
| X-G11 | a step 0 refusal (rotation due, too old) leaves the decision unspent, and `grants run` by the human runs it. `grants run` from an agent, the chief or the tester refuses |
| X-G12 | step 5 refuses `gitea-reach-wider` when the user was made collaborator on another repository, and when it joined an org team, and deletes the token |
| X-G13 | `recordEvent` refuses an `action.allowed` with `body.action` `grant.call` (and each `grant.*`). A direct store insert of a `grant.*` or `host.enroll.*` action under an agent actor, or under the human's NULL pair, aborts on the trigger. `host.sudo` from an agent session still writes. A step 0 refusal leaves `#spent` false |
| X-G11 | a step 0 refusal (rotation due, too old) and a step 1 refusal leave `#spent` false, and `grants run` by the human gets past `#consumeGrant`. `grants run` from an agent, the chief or the tester refuses. A `grants run` while a start holds the claim refuses `grant-busy` before any Gitea call. No `grant.*` body other than the consume record has a `decision` key |
| X-G12 | step 5 refuses `gitea-reach-wider` when the user was made collaborator on another repository, when it joined an org team, and when it owns a fork of an upstream that has left `publish`, and deletes the token. It passes with an old fork of a `publish` project outside `target.projects`, which the rendered block named. A fake returning 51 forks over two pages is read in full; a total that disagrees refuses `gitea-reach-unknown` |
| X-G13 | `recordEvent` refuses an `action.allowed` with `body.action` `grant.call` (and each `grant.*`) and one with `body.operation` `grant.gitea`. A direct store insert aborts on the trigger for `grant.step` and `grant.host.enrolled` from a session, and for `grant.step` under the human's NULL pair. `host.sudo` from an agent session still writes |
| X-G14 | fire: every token the user has is deleted, the user gets `prohibit_login`, and a failure of either lands in `serviceLeft` |
Mutants, a first list: drop the version floor; compare the version as a string; accept another admin scope;
@@ -496,7 +546,10 @@ at restart without the nonce; create the user before writing `grant.step user`;
`grant.step mint`; delete the old token before `issue`; settle a rotation by the record instead of
`token-whoami`; run C1 to C4 after `#consumeGrant` instead of before; drop the `identitySha256` compare; drop
`adminSha256`; skip the `grant.call` write; write `grant.result` before the call; drop the `grant.` reservation
in `recordEvent`; drop the trigger; widen the trigger to `host.*` (breaks `host.sudo`); put `decision` in step 0's `grant.failed`; send the admin token in a `token` header on the mint route; revoke the
in `recordEvent`; drop the trigger; widen the trigger to `host.*` (breaks `host.sudo`); guard `body.action` only, not
`operation`; key the trigger on `actor_run` alone; put `decision` in step 0's `grant.failed`; delete both
tokens on a settle 403 or 5xx; compare step 5 against `target.projects` instead of `publish`; read one page
of `/user/repos`; let `grants run` skip the claim; send the admin token in a `token` header on the mint route; revoke the
remote token before the local steps; skip the fork `parent` check; skip step 5's reach check; delete only the
recorded token on fire; let `grants run` accept an agent.
@@ -523,7 +576,8 @@ Dewey notes items 2, 3 and 5 of dewey-0754 apply to S3, S4 and S6. The r2 text a
- Q-B2. Answered (dewey-0754 item 10): no organizations. The token's real reach is checked at step 5 and
stated in section 8.
- Q-B3. Bushroot's answer on S1 to S6.
- Q-B4. For Sage or Mos: is jason.woltje a site admin on git.mosaicstack.dev? Section 8's comparison line needs
the answer.
- Q-B5. Answered (gizmoduck-0081): one rule for #436a and #436b, section 7. #436a row a's BUILD.md cites
this commit and names the trigger and its two mutants. Duckworth checks it with that candidate.
- Q-B4. Answered (mos-1146): jason.woltje is a site admin on git.mosaicstack.dev, restricted false. Section 8
says so.
- Q-B5. Answered (gizmoduck-0081, -0084, mos-1144): one rule for #436a and #436b, section 7. #436a row a's
BUILD.md cites section 7 at the commit Duckworth approves, or carries its text in full if row a's candidate
comes first. Duckworth checks it with that candidate.