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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user