plans: #436b design r4 on dewey-0759: item 1 option (b) per sage-1578 (step 0 refusal unspent only until the next start, cost in D5, X-G11 restart case); item 2 settle 200 unknown id drops memory, deletes nothing, grant-token-unknown, held at start; N7 secretCheck in 3d; duckworth-0067 N1 kind filter, X-G15; section 7 unchanged; no code change
This commit is contained in:
1 parent
fa5658941b
commit
3e66f5acfd
1 file changed
+52
-10
@@ -1,4 +1,4 @@
|
|||||||
# Gitea grant path, design draft r3 (Vikunja #436b, Gitea #1697 follow-up)
|
# Gitea grant path, design draft r4 (Vikunja #436b, Gitea #1697 follow-up)
|
||||||
|
|
||||||
Author: Megavolt. Reviewer: Dewey. GitHub half: Bushroot. Base: Gizmoduck's #436a r6 (f9039682e, final,
|
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
|
`agents/gizmoduck/work/machine-grant/DESIGN.md` on origin/gizmoduck/machine-grant-design) and row 199's
|
||||||
@@ -7,6 +7,26 @@ 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
|
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.
|
its forks with no manual step, and it answers mos-1130 item 3 (a to e) on the executor's admin credential.
|
||||||
|
|
||||||
|
## r4 changes (dewey-0759 on r3 fa5658941, sage-1578, duckworth-0067)
|
||||||
|
|
||||||
|
Section 7 is unchanged from fa5658941, the text Duckworth approved and Gizmoduck cites.
|
||||||
|
|
||||||
|
- Item 1 (blocks), option (b), Sage's ruling in sage-1578. A step 0 refusal leaves the decision unspent only
|
||||||
|
until the next start. #436a r6 X3's start reconcile spends every resolved-yes grant decision with no
|
||||||
|
`action.allowed` as `interrupted`, and this draft no longer claims otherwise. `grants run` works while the
|
||||||
|
bus that refused is still up; after a restart the chief raises again and Jason approves again. `status`
|
||||||
|
says so on each open decision (D5, section 4 step 0, section 5, X-G11). (a) would change #436a r6, which is
|
||||||
|
final. The cost is in D5.
|
||||||
|
- Item 2 (blocks). A settle that gets 200 with an `id` that is neither token no longer leaves the file in use.
|
||||||
|
Memory drops the credential (199 `revoke`'s memory step), the file and both recorded tokens stay, nothing
|
||||||
|
is deleted at Gitea, and the step ends `grant.failed {code: 'grant-token-unknown'}` for Jason. A restart
|
||||||
|
keeps the credential out of memory (`Credentials` `held`, built in #436b's row like `reload`). Revoke clears
|
||||||
|
it (section 5, X-G6).
|
||||||
|
- N7. 3d says the private session-less writer runs `#secretCheck` on every guarded body, as `#event` does
|
||||||
|
(`broker.mjs:559`). The sentence is in 3d, so section 7 doesn't change.
|
||||||
|
- duckworth-0067 N1. Every reader of grant evidence filters on `kind = 'action.allowed'` and the grant actor
|
||||||
|
pair, so a `grant.*` name under another kind is not evidence (section 5, X-G15).
|
||||||
|
|
||||||
## r3 changes (dewey-0756 on r2 6db7cc45d, gizmoduck-0081 and -0084, duckworth-0065, mos-1144)
|
## r3 changes (dewey-0756 on r2 6db7cc45d, gizmoduck-0081 and -0084, duckworth-0065, mos-1144)
|
||||||
|
|
||||||
- B1 (blocks). Only `#consumeGrant`'s own record carries `decision`. Every other `grant.*` body names the
|
- B1 (blocks). Only `#consumeGrant`'s own record carries `decision`. Every other `grant.*` body names the
|
||||||
@@ -46,7 +66,9 @@ its forks with no manual step, and it answers mos-1130 item 3 (a to e) on the ex
|
|||||||
scratch (F10). The separate uid is deferred on cost, and the draft says so.
|
scratch (F10). The separate uid is deferred on cost, and the draft says so.
|
||||||
- D5. The admin checks run before `#consumeGrant`, so a predictable refusal leaves Jason's Approve unspent. A
|
- D5. The admin checks run before `#consumeGrant`, so a predictable refusal leaves Jason's Approve unspent. A
|
||||||
human `mosaic grants run <decision>` starts it again (step 0). The raise refuses when the admin slot is past
|
human `mosaic grants run <decision>` starts it again (step 0). The raise refuses when the admin slot is past
|
||||||
its rotate-by date.
|
its rotate-by date. r4 (sage-1578): unspent only until the next start, when #436a r6 X3's reconcile spends
|
||||||
|
it as `interrupted`. The cost: if a step 0 refusal is followed by a restart before Jason rotates the admin
|
||||||
|
token or upgrades Gitea, the chief raises again and Jason approves again.
|
||||||
- D6. One list of start checks (3a, 3e). The "no organization" claim is now part of the bootstrap promise,
|
- D6. One list of start checks (3a, 3e). The "no organization" claim is now part of the bootstrap promise,
|
||||||
and the check is that `GET /user/repos` is empty, which catches org and collaborator repositories (F11, and
|
and the check is that `GET /user/repos` is empty, which catches org and collaborator repositories (F11, and
|
||||||
F12 for a site-admin token).
|
F12 for a site-admin token).
|
||||||
@@ -260,7 +282,9 @@ grant.result { callId, status } written after it
|
|||||||
- `reason` is the decision id for a grant or its undo, the `grant.revoked` record id for a revoke, the
|
- `reason` is the decision id for a grant or its undo, the `grant.revoked` record id for a revoke, the
|
||||||
`grant.sweep` record id for a sweep, or `admin:<verb>` with the human actor for `set`, `rotate` and `remove`.
|
`grant.sweep` record id for a sweep, or `admin:<verb>` with the human actor for `set`, `rotate` and `remove`.
|
||||||
- No body, no header, no token, no URL query. `Credentials.assertClean` (`packages/bus/src/credentials.mjs:233`,
|
- No body, no header, no token, no URL query. `Credentials.assertClean` (`packages/bus/src/credentials.mjs:233`,
|
||||||
the runtime's `secretCheck`) runs on both.
|
the runtime's `secretCheck`) runs on both. The private session-less writer of section 7 runs `#secretCheck`
|
||||||
|
on every guarded body it takes, `grant.step`, `grant.applied`, `grant.failed`, `grant.revoked`,
|
||||||
|
`grant.sweep` and the consume record included, as `#event` does (`packages/bus/src/broker.mjs:559`) (N7).
|
||||||
- `events` is append-only (`events_no_update`, `packages/bus/src/migrations/3b.sql:131`), so the result is its
|
- `events` is append-only (`events_no_update`, `packages/bus/src/migrations/3b.sql:131`), so the result is its
|
||||||
own record, written in the transaction of the step's next `grant.step` where there is one. A `grant.call`
|
own record, written in the transaction of the step's next `grant.step` where there is one. A `grant.call`
|
||||||
with no `grant.result` means the process died during the call. Reconcile lists each one in its message.
|
with no `grant.result` means the process died during the call. Reconcile lists each one in its message.
|
||||||
@@ -309,7 +333,10 @@ nothing.
|
|||||||
`#inflight` (#436a X5), then runs C1 to C4 and the admin rotate-by check. A refusal here writes
|
`#inflight` (#436a X5), then runs C1 to C4 and the admin rotate-by check. A refusal here writes
|
||||||
`grant.failed {code, grantDecision}` and leaves the decision unspent (section 7's spend rule). After the
|
`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
|
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
|
starts step 0 again, claim included. That holds only while the bus that refused is up: at the next start,
|
||||||
|
#436a r6 X3's reconcile spends every resolved-yes grant decision with no `action.allowed` as
|
||||||
|
`interrupted`, a step 0 refusal included, and the chief raises again (D5, sage-1578). `status` lists each
|
||||||
|
such open decision with `spentAtNextStart: true`. 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
|
`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"`.
|
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
|
1. **Recheck and consume**, in one transaction: `#consumeGrant` with `operation: 'grant.gitea'`, the
|
||||||
@@ -372,15 +399,23 @@ two-scope seat token; op `token-whoami`). The answer is an `id`, never a value.
|
|||||||
- 401: the file's token no longer exists at Gitea (F13 b4). The previous token can't be in the file, so the
|
- 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
|
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.
|
`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,
|
- 200, `id` is neither token: the file holds a live token this grant didn't mint and step 5 never checked.
|
||||||
404, 5xx, a timeout, Gitea unreachable, or C1 to C4 failing first. The last `grant.step` stays `commit`, the
|
Memory drops it (199 `revoke`'s memory step) and the agent gets `credential-unavailable`. The file, the
|
||||||
|
record and both recorded tokens stay, and nothing is deleted at Gitea. The step ends `grant.failed {code:
|
||||||
|
'grant-token-unknown', grantDecision, tokenId}`, where `tokenId` is the id Gitea gave, and Jason gets a
|
||||||
|
message. `status` shows `tokenUnknown`. At each start, before `Credentials` loads, the reconcile passes such
|
||||||
|
`(agent, "gitea")` pairs as `held` (a `Credentials` option built in #436b's row, like `reload`), so memory
|
||||||
|
never loads that file. A revoke (section 6) clears it: it unlinks the file and deletes the recorded token.
|
||||||
|
The unknown token is Jason's to look at.
|
||||||
|
- Anything else is `settle-pending`, and nothing is deleted: 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
|
`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
|
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.
|
and `mosaic grants sweep gitea` settle again. Only a 401 deletes.
|
||||||
|
|
||||||
| Fails at | State left | Cleanup |
|
| Fails at | State left | Cleanup |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 | nothing, the decision unspent | none. `grants run` retries |
|
| 0 | nothing, the decision unspent until the next start | none. `grants run` retries while this bus is up; after a restart the reconcile spends it (`interrupted`) |
|
||||||
| 1 | nothing | none |
|
| 1 | nothing | none |
|
||||||
| 2, before the create | nothing | none |
|
| 2, before the create | nothing | none |
|
||||||
| 2, after the create, before the record | a user with the nonce | none here. Restart adopts it by the nonce (below) |
|
| 2, after the create, before the record | a user with the nonce | none here. Restart adopts it by the nonce (below) |
|
||||||
@@ -402,6 +437,11 @@ the remote part runs after the bus is up, with C1 to C4.
|
|||||||
| `commit`, first grant | unlink the file if present, delete the record if present | delete the new token |
|
| `commit`, first grant | unlink the file if present, delete the record if present | delete the new token |
|
||||||
| `commit`, rotation | none | settle |
|
| `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 |
|
| `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 |
|
||||||
|
| `grant.failed {code: 'grant-token-unknown'}` with no `grant.revoked` after it | pass `(agent, "gitea")` as `held`; memory doesn't load the file | none. Jason's message repeats until a revoke |
|
||||||
|
|
||||||
|
Every reader of grant evidence (this reconcile, `status`, `openGrantDecisions`'s callers, `grants sweep`)
|
||||||
|
reads a `grant.*` record only where `kind = 'action.allowed'` and `actor_role IS NULL AND actor_run IS
|
||||||
|
'grant'`, so a `grant.*` name under another kind or actor is never evidence (duckworth-0067 N1).
|
||||||
|
|
||||||
Then `grant.failed {code: 'interrupted'}` (or `grant.applied` if a rotation settled forward) and a message to
|
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
|
Jason. A `grant.call` with no `grant.result` is listed in that message. After `interrupted`, the chief raises
|
||||||
@@ -530,15 +570,16 @@ 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-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-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-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` 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-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 and an unreachable fake. On 200 with an unknown `id` it drops the credential from memory, deletes nothing at Gitea, keeps the file, ends `grant-token-unknown`, and a second restart still doesn't load the file; a revoke then clears it |
|
||||||
| 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-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-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-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-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) 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-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. A step 0 refusal, then a restart, then `grants run`: the reconcile has spent the decision as `interrupted` and `grants run` refuses `decision-consumed`; before the restart `status` shows `spentAtNextStart` |
|
||||||
| 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-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-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` |
|
| X-G14 | fire: every token the user has is deleted, the user gets `prohibit_login`, and a failure of either lands in `serviceLeft` |
|
||||||
|
| X-G15 | a `grant.step {step: 'commit'}` stored under `action.refused`, and one under the human's NULL pair, are ignored by the restart reconcile: no settle runs and no Gitea call is made |
|
||||||
|
|
||||||
Mutants, a first list: drop the version floor; compare the version as a string; accept another admin scope;
|
Mutants, a first list: drop the version floor; compare the version as a string; accept another admin scope;
|
||||||
skip `gitea-user-foreign`; compare the user by name instead of `id`; skip the admin-user refusal; adopt a user
|
skip `gitea-user-foreign`; compare the user by name instead of `id`; skip the admin-user refusal; adopt a user
|
||||||
@@ -551,7 +592,8 @@ in `recordEvent`; drop the trigger; widen the trigger to `host.*` (breaks `host.
|
|||||||
tokens on a settle 403 or 5xx; compare step 5 against `target.projects` instead of `publish`; read one page
|
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
|
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
|
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.
|
recorded token on fire; let `grants run` accept an agent; keep the file in use on a settle 200 with an
|
||||||
|
unknown `id`; load a `held` pair at start; drop the `kind` filter from the restart reconcile.
|
||||||
|
|
||||||
## 10. Shared with Bushroot's GitHub half (proposed in megavolt-0036, answer pending)
|
## 10. Shared with Bushroot's GitHub half (proposed in megavolt-0036, answer pending)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user