row 281 design d6: the job's step C without the chief's claim, fire.failed for a step B or C failure, boot closes interrupted jobs, Q1 lines in the set (dewey-0825, #1786)
This commit is contained in:
1 parent
59142e9fbb
commit
97535587b2
1 file changed
+101
-15
@@ -1,6 +1,6 @@
|
|||||||
# 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 d5 for Dewey's approval. No code until it is
|
Builder Megavolt, reviewer Dewey, gate owner Sage. Design draft d6 for Dewey's approval. 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.
|
||||||
|
|
||||||
History: d1 dfebd16cc. d2 c746ce378 added Sage's Q1/Q2 rulings (sage-2573). d3 36c2d98b8 added the
|
History: d1 dfebd16cc. d2 c746ce378 added Sage's Q1/Q2 rulings (sage-2573). d3 36c2d98b8 added the
|
||||||
@@ -13,6 +13,14 @@ approved-before-fire tests (sage-2574). d4 answers Dewey's CHANGES on d3 (dewey-
|
|||||||
d5 adds sage-2575: two requests per granted delete, with its five conditions. It also adds a `job` id to every
|
d5 adds sage-2575: two requests per granted delete, with its five conditions. It also adds a `job` id to every
|
||||||
answer, event and message, and tests T3, T3b and T3c with mutants M16-M18.
|
answer, event and message, and tests T3, T3b and T3c with mutants M16-M18.
|
||||||
|
|
||||||
|
d6 answers Dewey's CHANGES on d5 (dewey-0825):
|
||||||
|
- C1: the job's step C records authority without the chief's claim (`#authorityFor`, as a landing's record
|
||||||
|
does), processing-halted doesn't hold the job, and a step B or C failure records `fire.failed {job, step,
|
||||||
|
code}` with its own message. Tests T11 and T12, mutants M19 and M20.
|
||||||
|
- N4: boot closes a job a restart interrupted, with one message. Test T13, mutant M21.
|
||||||
|
- N5: section 7 names the refusals the manual verbs gain.
|
||||||
|
- N6: the set counts Q1 leftover lines, in fire step 3 and in the delete text.
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
- Brief: `docs/plans/2026-10-22_fire-revokes-grants.md`, "Brief: no grant outlives its instance" (sage-2568,
|
- Brief: `docs/plans/2026-10-22_fire-revokes-grants.md`, "Brief: no grant outlives its instance" (sage-2568,
|
||||||
@@ -76,14 +84,17 @@ the request that started it.
|
|||||||
2. Step A, as today, with one addition. In the same transaction, after `fire.stopped`, the grant set is read
|
2. Step A, as today, with one addition. In the same transaction, after `fire.stopped`, the grant set is read
|
||||||
(section 3). Reading it there means a grant raise either committed before step A, and is in the set, or
|
(section 3). Reading it there means a grant raise either committed before step A, and is in the set, or
|
||||||
comes after `fire.stopped` and refuses (section 3d).
|
comes after `fire.stopped` and refuses (section 3d).
|
||||||
3. If the set is empty and the agent has no Gitea leftover (section 5), steps B and C run synchronously,
|
3. If the set is empty, steps B and C run synchronously, exactly as today. A fire with no grant answers
|
||||||
exactly as today. A fire with no grant answers today's result with today's events (T7).
|
today's result with today's events (T7). The set counts everything step G would touch (d6, N6): grant
|
||||||
|
rows, open decisions, the agent's Gitea leftovers (section 5) and its Q1 leftover host lines (section 5).
|
||||||
|
Any of them makes the set non-empty and goes to the job.
|
||||||
4. Otherwise the verb takes the agent's step G lock (section 3e), records `fire.revoking {job, grants}` in
|
4. Otherwise the verb takes the agent's step G lock (section 3e), records `fire.revoking {job, grants}` in
|
||||||
step A's transaction, starts the job, and answers at once:
|
step A's transaction, starts the job, and answers at once:
|
||||||
`{...stopped, job, revoking: ['host:<id>' | 'gitea', ...], done: false}`. There's no `transaction`, since
|
`{...stopped, job, revoking: ['host:<id>' | 'gitea', ...], done: false}`. There's no `transaction`, since
|
||||||
step B hasn't run.
|
step B hasn't run.
|
||||||
5. The job runs step G, then step B (`write('instance.fire')`) and step C (`consumeAuthority`, then
|
5. The job runs step G, then step B (`write('instance.fire')`) and step C (`#recordAuthority`, below, then
|
||||||
`fire.done {job, grants}`). It releases the lock when it ends, whichever way it ends.
|
`fire.done {job, grants}`). It releases the lock when it ends, whichever way it ends, `fire.failed`
|
||||||
|
included.
|
||||||
|
|
||||||
What the chief sees meanwhile: the instance is "firing". That's an existing state (`#firing`, broker.mjs:645):
|
What the chief sees meanwhile: the instance is "firing". That's an existing state (`#firing`, broker.mjs:645):
|
||||||
`fire.stopped` with no later `fire.done`. Starts already refuse `instance-firing`.
|
`fire.stopped` with no later `fire.done`. Starts already refuse `instance-firing`.
|
||||||
@@ -94,8 +105,42 @@ What the chief sees meanwhile: the instance is "firing". That's an existing stat
|
|||||||
The human sees the outcome either way: `fire.done` names the job and the grants, and a refusal sends the
|
The human sees the outcome either way: `fire.done` names the job and the grants, and a refusal sends the
|
||||||
inbox message in section 4.
|
inbox message in section 4.
|
||||||
|
|
||||||
To check at build time: step C's `consumeAuthority(s, ...)` runs after the verb has answered, so the job keeps
|
**Steps B and C in the job (d6, C1).** d4 left "step C without the chief's run" as a build-time check, and
|
||||||
the fields of `s` that step C needs. It must not depend on the chief's run still being live.
|
Dewey checked it: it fails.
|
||||||
|
- `#consumeAuthority` calls `#checkAuthority` (broker.mjs:2852). That runs `#processingWork()` and
|
||||||
|
`#agent(s)`, and `#agent` refuses `not-holder` once the chief's run no longer holds its claim (:749).
|
||||||
|
- After the verb answers, the job can take a minute or more. If the chief's run ends meanwhile, step C would
|
||||||
|
throw after step B had delisted. The instance would be left delisted with no `fire.done`.
|
||||||
|
|
||||||
|
So the verb does the full check, and the job only records:
|
||||||
|
- The verb runs `#checkAuthority(s, action, authority)`, as today, before step A. The chief holds its claim
|
||||||
|
then.
|
||||||
|
- The job's step C runs `#authorityFor(s, action, authority)`, the check without the holder test. It then
|
||||||
|
runs the `#spent` check and the `action.allowed` event, as `#consumeAuthority` does. That's the shape a
|
||||||
|
landing's record already uses (`recordPublish`, broker.mjs:3174, row 137): the run that asked held its
|
||||||
|
claim when it asked, and may have ended since. It's one small private method, `#recordAuthority`, shared
|
||||||
|
with nothing else.
|
||||||
|
- The job keeps a copy of the session fields `#authorityFor` and `#event` read (business, role, thread,
|
||||||
|
run). It doesn't keep the live session object.
|
||||||
|
- `processing-halted` doesn't hold the job. A halt stops agent work. Step G only removes access, and steps
|
||||||
|
B and C finish a decision the chief made while processing ran. Holding them would leave a delisted or
|
||||||
|
half-revoked instance "firing" for the length of the halt. The verb itself still refuses during a halt,
|
||||||
|
through `#checkAuthority`, as today.
|
||||||
|
|
||||||
|
**A step B or C failure in the job** gets its own record, not `grant.revoke.refused`, since every grant in the
|
||||||
|
set was revoked by then:
|
||||||
|
- It records `fire.failed {job, step: 'B' | 'C', code}`. `code` is the thrown code, for example the writer's
|
||||||
|
`credential-revoke-failed`, or `decision-consumed`.
|
||||||
|
- It sends an inbox message:
|
||||||
|
|
||||||
|
> Fire of <agent>: job <job> revoked its grants (<list>) but stopped at step <B|C> (<code>). The instance
|
||||||
|
> is still firing. Run the fire again once <code> is cleared.
|
||||||
|
|
||||||
|
- The instance stays firing. A repeat fire finds an empty set and takes today's path for that state, with the
|
||||||
|
chief's own claim: B and C after a step B failure (T12), the delisted path (webby-0094 N1) after a step C
|
||||||
|
failure.
|
||||||
|
|
||||||
|
`grant.revoke.refused` with reason `internal` stays only for an unexpected throw inside step G.
|
||||||
|
|
||||||
### The delete (sage-2575)
|
### The delete (sage-2575)
|
||||||
|
|
||||||
@@ -129,8 +174,10 @@ grant set.
|
|||||||
|
|
||||||
It starts a new job for what's left. The broker knows the last job refused because the agent's latest job
|
It starts a new job for what's left. The broker knows the last job refused because the agent's latest job
|
||||||
end event is a `grant.revoke.refused` with verb `lifecycle.delete`, and no `grants.revoked.done` came after
|
end event is a `grant.revoke.refused` with verb `lifecycle.delete`, and no `grants.revoked.done` came after
|
||||||
it. A delete after the human cleared the grant by hand finds an empty set, or only leftover lines (Q1), and
|
it. A delete after the human cleared the grant by hand doesn't find an empty set if a manual revoke left a
|
||||||
goes on.
|
line (Q1). The leftover line makes the set non-empty, so that delete starts a job for the line removal
|
||||||
|
and refuses `grants-revoking` (d6, N6, and T3b's second case). If nothing is left, the set is empty and the
|
||||||
|
delete goes on.
|
||||||
|
|
||||||
The tradeoff (ruled by sage-2575): deleting a granted instance now always takes two requests. That keeps the
|
The tradeoff (ruled by sage-2575): deleting a granted instance now always takes two requests. That keeps the
|
||||||
cli and the host channel out of row 281, and a delete of an instance with a grant is rare. Holding the
|
cli and the host channel out of row 281, and a delete of an instance with a grant is rare. Holding the
|
||||||
@@ -311,8 +358,25 @@ the grant and the reason, with the manual way out beside the retry (N2):
|
|||||||
The delete's message has the same shape, with "raise the delete again". For Gitea, the way out is
|
The delete's message has the same shape, with "raise the delete again". For Gitea, the way out is
|
||||||
`mosaic grants sweep gitea` once Gitea is back.
|
`mosaic grants sweep gitea` once Gitea is back.
|
||||||
|
|
||||||
A job that throws something unexpected records `grant.revoke.refused` with reason `internal`, sends the same
|
A job that throws something unexpected inside step G records `grant.revoke.refused` with reason `internal`,
|
||||||
message, and releases the lock. There's no unhandled rejection.
|
sends the same message, and releases the lock. There's no unhandled rejection. A failure at step B or C is
|
||||||
|
`fire.failed` (section 2).
|
||||||
|
|
||||||
|
**A restart mid-job (d6, N4).** The lock and the job are in memory, so a restart ends them with no end
|
||||||
|
record. A fire would stay "firing" until the chief happened to repeat it. A delete's request is already
|
||||||
|
closed, so no one would hear anything. Boot closes that gap:
|
||||||
|
- A job is a start record (`fire.revoking` or `grants.revoking`) whose `job` has no end record. The end
|
||||||
|
records are `fire.done`, `fire.failed`, `grants.revoked.done`, `grant.revoke.refused` and the new
|
||||||
|
`grants.job.interrupted`.
|
||||||
|
- When the broker starts, for each such job of each business, it records `grants.job.interrupted {job,
|
||||||
|
agent, verb}` and sends the human one inbox message:
|
||||||
|
|
||||||
|
> <Fire|Delete> of <agent>: job <job> was interrupted by a bus restart. Some grants may be revoked. Run the
|
||||||
|
> fire again, or raise the delete again. The repeat goes on from what's left.
|
||||||
|
|
||||||
|
- Recording the end record in the same transaction as the message means a second boot finds nothing to
|
||||||
|
send.
|
||||||
|
- Boot reads only events, with no network call, so it adds nothing to the start time beyond one query.
|
||||||
|
|
||||||
## 5. A repeat fire (D5)
|
## 5. A repeat fire (D5)
|
||||||
|
|
||||||
@@ -377,15 +441,19 @@ way through the human's new request.
|
|||||||
|
|
||||||
## 7. What doesn't change (brief item 4)
|
## 7. What doesn't change (brief item 4)
|
||||||
|
|
||||||
- `hosts.revoke`, `grants.revoke` and `grants.sweep`: same callers, the same refusals (`tester-dev-only`
|
- `hosts.revoke`, `grants.revoke` and `grants.sweep`: same callers and the same records. Their refusals stay
|
||||||
included), and the same records. hosts.revoke's own order stays local first.
|
the same (`tester-dev-only` included) with one addition from section 3e (d6, N5): while a step G job holds
|
||||||
|
the agent's lock, `hosts.revoke` and `grants.revoke` for that agent refuse `grant-revoke-running`, and
|
||||||
|
`grants.sweep` skips the agent and names it as `running`. hosts.revoke's own order stays local first.
|
||||||
- No new verb, no Console route, no CLI change, no schema change. Every new record is an `events` row in the
|
- No new verb, no Console route, no CLI change, no schema change. Every new record is an `events` row in the
|
||||||
existing grant-evidence shape.
|
existing grant-evidence shape.
|
||||||
- `revoke()` in runtime.mjs and the organization writer don't change.
|
- `revoke()` in runtime.mjs and the organization writer don't change.
|
||||||
- `server.mjs`, `process.mjs`, `cli/src/host.mjs` and `cli/src/launcher.mjs` don't change. Both verbs stay
|
- `server.mjs`, `process.mjs`, `cli/src/host.mjs` and `cli/src/launcher.mjs` don't change. Both verbs stay
|
||||||
synchronous. The launcher already closes a request with whatever code `lifecycleDelete` refuses.
|
synchronous. The launcher already closes a request with whatever code `lifecycleDelete` refuses.
|
||||||
- New codes: `fire-in-progress`, `grants-revoking`, `grant-revoke-running`, `instance-fired`, and
|
- New codes: `fire-in-progress`, `grants-revoking`, `grant-revoke-running`, `instance-fired`, and
|
||||||
`grant-revoke-failed` in events and messages. `lifecycleDone` checks a refusal against `LIFECYCLE_CODE =
|
`grant-revoke-failed` in events and messages. New event types: `fire.revoking`, `grants.revoking`,
|
||||||
|
`grants.revoked.done`, `grant.revoke.refused`, `fire.failed` and `grants.job.interrupted`.
|
||||||
|
- The broker's start gains one events query for interrupted jobs (section 4, N4). `lifecycleDone` checks a refusal against `LIFECYCLE_CODE =
|
||||||
/^[a-z-]{1,64}$/` (broker.mjs:149), and `grants-revoking` matches it.
|
/^[a-z-]{1,64}$/` (broker.mjs:149), and `grants-revoking` matches it.
|
||||||
|
|
||||||
## 8. Tests (each in the owning package, whole files)
|
## 8. Tests (each in the owning package, whole files)
|
||||||
@@ -471,9 +539,24 @@ same shape as `giteaIdle()`. Unless a test says otherwise, every fire or delete
|
|||||||
- After release, the jobs complete.
|
- After release, the jobs complete.
|
||||||
- A cli test drives the launcher's delete pass against a bus whose remove is held. The pass closes the
|
- A cli test drives the launcher's delete pass against a bus whose remove is held. The pass closes the
|
||||||
request `grants-revoking` and the host channel stays up.
|
request `grants-revoking` and the host channel stays up.
|
||||||
|
- T11 (d6, C1): a fire whose job is held on a remove. While it's held, the chief's run ends and its claim is
|
||||||
|
released, and processing is halted. After the release, the job completes: `fire.done` is recorded with the
|
||||||
|
job, the consumed `action.allowed` event is there, a second use of the same authority refuses
|
||||||
|
`decision-consumed`, and the instance is out of the file. No `fire.failed` and no `grant.revoke.refused`.
|
||||||
|
- T12 (d6, C1): a fire with a grant whose step B refuses in the job. The test uses T1's setup, where
|
||||||
|
`revoke()` refuses `credential-revoke-failed`. After the job:
|
||||||
|
- `fire.failed` carries the job, step `B` and `credential-revoke-failed`, and the inbox message names the
|
||||||
|
job, the revoked grants, the step and the code;
|
||||||
|
- there's no `grant.revoke.refused` and no `fire.done`;
|
||||||
|
- the instance is still firing and still in the file.
|
||||||
|
With the cause cleared, a repeat fire finds an empty set, runs B and C synchronously and completes.
|
||||||
|
- T13 (d6, N4): a fire and a delete, each with its job held, then the broker is closed and a new one opened
|
||||||
|
on the same data root. The new broker records `grants.job.interrupted` for both jobs and sends two inbox
|
||||||
|
messages, one saying to fire again and one to raise the delete again. A third broker on the same root sends
|
||||||
|
nothing more. A job that ended before the restart gets no interrupted record.
|
||||||
|
|
||||||
Files (N3). Every file listed runs whole in the manifest round, touched or not:
|
Files (N3). Every file listed runs whole in the manifest round, touched or not:
|
||||||
- New: `packages/bus/tests/fire-revokes-grants.test.mjs` holds T2-T10.
|
- New: `packages/bus/tests/fire-revokes-grants.test.mjs` holds T2-T13.
|
||||||
- Changed:
|
- Changed:
|
||||||
- `grant-host.test.mjs` and `gitea-grant.test.mjs`: T1's pins, and the T8 regressions next to their
|
- `grant-host.test.mjs` and `gitea-grant.test.mjs`: T1's pins, and the T8 regressions next to their
|
||||||
existing cases;
|
existing cases;
|
||||||
@@ -508,6 +591,9 @@ Files (N3). Every file listed runs whole in the manifest round, touched or not:
|
|||||||
- M16: the delete's first refusal sends no start message. Killed by T3.
|
- M16: the delete's first refusal sends no start message. Killed by T3.
|
||||||
- M17: a delete after a refused job refuses `grants-revoking` instead of `grant-revoke-failed`. Killed by T3b.
|
- M17: a delete after a refused job refuses `grants-revoking` instead of `grant-revoke-failed`. Killed by T3b.
|
||||||
- M18: a delete of an instance with no grant goes through the job. Killed by T3c.
|
- M18: a delete of an instance with no grant goes through the job. Killed by T3c.
|
||||||
|
- M19: the job's step C uses `#consumeAuthority`, with the holder and processing checks. Killed by T11.
|
||||||
|
- M20: a step B or C failure in the job is recorded as `grant.revoke.refused internal`. Killed by T12.
|
||||||
|
- M21: boot skips the interrupted-job check. Killed by T13.
|
||||||
|
|
||||||
## 10. Order and rebase
|
## 10. Order and rebase
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user