65 KiB
Row 281 build notes: a fire or a delete revokes the instance's grants (#1786)
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.
History: d1 dfebd16cc. d2 c746ce378 added Sage's Q1/Q2 rulings (sage-2573). d3 36c2d98b8 added the
approved-before-fire tests (sage-2574). d4 answers Dewey's CHANGES on d3 (dewey-0823):
- B1: the set takes every grant decision with no outcome, and a raise refuses a fired or firing agent.
- B2: step G runs detached from the verb, so neither caller waits on the network.
- B3: one step G per agent, with named refusals for everything that could race it.
- N1-N3 are folded in.
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.
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 recordsfire.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
- Brief:
docs/plans/2026-10-22_fire-revokes-grants.md, "Brief: no grant outlives its instance" (sage-2568, mos-1271, Vikunja 524). - Code read at
47a35bf43:packages/bus/src/broker.mjs,runtime.mjs,credentials.mjs,host-grant.mjs,gitea-grant.mjs,packages/business/src/instance-settings.mjs,tools/host/mosaic-keys-write. - Row 256's delete (honker/row256-r2 at
1e761962a, not landed) changes only the delete's checks. Its write still goes throughlifecycleDeleteand the samerevoke(), so this design covers it unchanged. - Row 235 r6 section 6 (host revoke) and #436b r5 section 6 (Gitea revoke and the fire steps).
1. Why: what happens today (read from the code, to be pinned by a test before the change)
In short: today a delete of a granted instance unlinks the key files but leaves both grant records, the host
lines and the Gitea token live, and a fire refuses credential-revoke-failed (sage-2573 asked for this here
and on #1786). The details:
- Fire.
revoke()(runtime.mjs:151) revokes only the roles entry'scredentialsservices, then lists the agent's store directory and refusescredential-revoke-failedif any file is left. A host grant's key is<store>/<business>/<agent>/host.<id>(host-grant.mjs:102), and a Gitea grant's token is.../gitea(gitea-grant.mjs:379). Neither is a roles-entry service, so a fire of an instance with a grant refuses at step B with "host. still in the store". It doesn't leak, but it can't finish until a human runsmosaic hosts revokeormosaic grants revokeby hand. If the business file also namesgiteaas a roles-entry credential,credentials.revoke(instance, 'gitea')unlinks the granted token file with nogitea_grantschange and no Gitea call. - Delete.
credentials.remove(instance)(credentials.mjs:400) revokes every registered reference,host.<id>andgiteaincluded, and unlinks the directory. Thehost_grantsandgitea_grantsrecords stay, the host line stays on each host, and the Gitea token, user and login stay live. This is the leak. The next start then finds a grant record whose key file is gone. What the start does with that isn't read yet. The T1 test below pins it. - An open grant decision. A fire delists the instance but keeps its roles entry. An approved host or Gitea
grant decision for that agent that hasn't run yet still passes
#hostDefinitionand#giteaRole, so it can still apply after the fire. A delete removes the roles entry, so its executor refusesunknown-roleorgitea-not-declared. - A raise after the fire (dewey-0823 B1).
#raiseGrant(broker.mjs:1529) checks only that the caller is the chief, and the grant bindings check only the roles entry. So the chief can raise a new grant for a fired agent, and if the human approves it, it applies.
2. Where the grant revokes sit (D1, rewritten in d4 for B2)
d3 awaited step G inside the verb. Dewey's B2 showed that neither caller can wait that long:
- Delete. The launcher's
host.opwaitsREQUEST_TIMEOUT_MS = 10000(cli/src/host.mjs:31). On a timeout,breakChannelstops the whole bus host. One SSH remove can take 30 s (host-grant.mjs:231, :311), and each Gitea call 15 s (CALL_MS, gitea-grant.mjs:38). - Fire. server.mjs answers a verb that isn't on its async list synchronously (:154), and the async list gives 120 s (:150). The chief's runner client times out at 30 s (harness/src/runner.mjs:449). Two hosts plus the Gitea calls can pass both limits.
So step G never runs inside a verb. The fire and the delete both use the same answer-at-once job, for the same
timeout reason (sage-2575 condition 5). Both verbs stay synchronous and answer at once. Step G runs as tracked
broker work after the answer, as row 236's grant executors do (#track, setImmediate). No caller waits on
the network, so server.mjs, process.mjs, cli/src/host.mjs and the launcher don't change.
Each job gets an id, job (a UUID), when it starts. The verb's answer, the job's start event, its end event
and every inbox message about it carry that id. The human and the chief can then match a later outcome to
the request that started it.
The fire
write('instance.fireCheck'), as today.- 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 comes afterfire.stoppedand refuses (section 3d). - If the set is empty, steps B and C run synchronously, exactly as today. A fire with no grant answers 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.
- 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:{...stopped, job, revoking: ['host:<id>' | 'gitea', ...], done: false}. There's notransaction, since step B hasn't run. - The job runs step G, then step B (
write('instance.fire')) and step C (#recordAuthority, below, thenfire.done {job, grants}). It releases the lock when it ends, whichever way it ends,fire.failedincluded.
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.
- A repeat fire while the job runs refuses
fire-in-progress, a new code. - A repeat after the job finishes takes the delisted path (webby-0094 N1) and answers done.
- A repeat after a refused step G runs step G again (section 5).
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.
Steps B and C in the job (d6, C1). d4 left "step C without the chief's run" as a build-time check, and Dewey checked it: it fails.
#consumeAuthoritycalls#checkAuthority(broker.mjs:2852). That runs#processingWork()and#agent(s), and#agentrefusesnot-holderonce 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#spentcheck and theaction.allowedevent, as#consumeAuthoritydoes. 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
#authorityForand#eventread (business, role, thread, run). It doesn't keep the live session object. processing-halteddoesn'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}.codeis the thrown code, for example the writer'scredential-revoke-failed, ordecision-consumed. -
It sends an inbox message:
Fire of : job revoked its grants () but stopped at step <B|C> (
). The instance is still firing. Run the fire again onceis 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)
lifecycleDelete (broker.mjs:3293) stays synchronous. After its running and starting checks, it reads the
grant set.
-
No grant. An empty set: instances.set delete, one request, exactly as today (condition 4, T3c).
-
The first request. For a non-empty set, it takes the lock and records grants.revoking {job, agent, verb: 'lifecycle.delete', grants}. It starts the job and refuses grants-revoking at once. The launcher
closes the request with that code, well inside its 10 s.
The launcher can carry only the code, so the broker also sends the human an inbox message (condition 1):
Delete of is waiting on its grants. Revoking started (job ) for: host:, ..., gitea.
Raise the delete again when the inbox says the revoke is done.
-
A delete raised while the job runs. It refuses grants-revoking again and starts no second job
(condition 2, the 3e lock). No second message goes out either: the first one still applies.
-
The job ends. The human gets one inbox message (condition 3).
- If it completed, the message names each grant revoked, with any
grantsLeft (section 5), then "Raise the
delete again." It records grants.revoked.done {job, agent, verb, grants, grantsLeft}.
- If it refused, the message names the grant, the reason and how to clear it by hand, as section 4
describes. It records
grant.revoke.refused {job, ...}.
-
The delete raised after a completed job finds an empty set and goes as today.
-
The delete raised after a refused job doesn't complete over a live grant. Its set is non-empty, since
the refused grant's record is still there. It refuses grant-revoke-failed, naming the earlier job's
reason in a message:
Delete of refused: job couldn't revoke (). Retrying now as job . Raise
the delete again when the inbox says it's done, or clear by hand first.
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
it. A delete after the human cleared the grant by hand doesn't find an empty set if a manual revoke left a
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
cli and the host channel out of row 281, and a delete of an instance with a grant is rare. Holding the
launcher's request open past its 10 s would need a host channel change and a strand path, and polling would
widen the row into the launcher for a one-time wait.
While a delete's step G runs, the instance is still listed and not firing, so it could be started. That's
harmless: its grants are being revoked, and the repeat delete refuses instance-running as today. No new
start check.
Why not in beforeWrite or revoke()
Both are synchronous and run inside the organization writer, after its checks. A host remove is SSH and a
Gitea revoke is HTTP, so they can't run there. The business package also has no reach into the broker's grant
tables, and shouldn't get any.
Why after step A
A fire is the decision to stop the agent. If step G came first, a host that can't be reached would keep the
fired agent running. After step A, a refused step G leaves the same state as a failed step B today: the
instance is "firing" (stopped, still listed) until a repeat. Row 194 already handles that state. The repeat
fire's delisted path (webby-0094 N1) also runs step G, since the set is read before step B on both paths.
3. Step G: what it revokes, and how
The set, for (business, agent), is read in one store transaction: step A's for a fire, the delete's checks
for a delete (section 2). It holds:
- each
host_grants row of the agent;
- each host named by an open host grant decision for the agent;
- the Gitea grant if the agent has a
gitea_grants row, a gitea_users row, or an open Gitea grant decision.
An open decision (d4, B1) is any grant decision raised for the agent with no outcome yet. That is: not denied,
not expired, no grant.applied or grant.failed, and no grant.revoked of the same (agent, host), or of the
agent's Gitea, recorded after its raise. A raised decision the human hasn't decided is open. d3 required
"approved", which left that one out.
An empty set means no step G and no new records. A fire of an instance with no grant is then exactly today's
fire, with no new events (brief tests, fourth bullet).
3a. A host grant: the host line first (D2)
hosts.revoke goes local first: the record drops, the key leaves memory and its file, and only then is
remove <user> <tag> sent. If the host can't be reached, the revoke closes with hostLineLeft and the
record is already gone. The brief says a fire may not drop a grant record while its key could still be on the
host. So the fire's host revoke reverses one step and keeps the rest the same:
- Abort an executor in flight for (agent, host), with no record change (
hostKeys.abortClaim, as the
credential-unknown branch of #revokeHost already does).
remove <user> <tag> through the installer, pinned to the host record's keys. mosaic-keys-write says
"a missing tag changes nothing", so this is safe to repeat and safe for a host named only by an open
decision. If the host record is gone, or the remove doesn't exit 0, step G refuses for this host
(section 4). Nothing local has changed for this grant: the record, the key file and the memory copy stay.
- hosts.revoke's step 1 transaction, unchanged: the
host_grants row goes and grant.revoked {revoke, agent, host, tag, hostLine: 'pending', granted, reason, by} is recorded, plus fire: true (or delete: true)
so the human can tell it from a manual revoke.
- hosts.revoke's step 2, unchanged:
hostKeys.revoke, so the key leaves memory and its file goes.
grant.revoke.closed {hostLine: 'removed'} with no second remove. Step 2 already did it.
This is one shared method, #revokeHostGrant(business, agent, host, reason, by, { lineFirst }).
hosts.revoke calls it with lineFirst: false and its behavior and records don't change (brief item 4). The
fire and the delete call it with lineFirst: true.
The tradeoff: hosts.revoke goes local first so that "the part that matters can't wait on the network" (r6
section 6). For a fire, step A has already stopped the agent's runs, and a refused fire keeps the instance
"firing". So waiting on the host costs nothing in exposure, and the record never lies about the host.
3b. The Gitea grant (D3)
The Gitea revoke stays grants.revoke's steps with fire: true, which is what credentials delete already
runs (#436b section 6). Step 1 takes the token out of memory and unlinks the file. Step 2 is
#giteaRevokeLocal(..., fire: true). Step 3 is #giteaCloseRevoke: every token deleted and
prohibit_login: true. The fire awaits step 3 instead of leaving it to setImmediate.
The Gitea side can't go remote first the way a host does. The record has to drop and grant.revoked has to
name the tokens before the DELETE calls, or a restart in between would find a live record whose tokens are
gone (section 5's reconcile). Two changes keep the fire closed:
- Preflight. Before step 1,
#giteaAdminChecked runs. If the admin token can't be read or checked,
step G refuses with nothing changed.
- Left means not done. If
grant.revoke.closed lists anything in serviceLeft, step G refuses. The
token ids stay in the evidence, which is exactly what mosaic grants sweep gitea retries, so nothing
forgets them. The record is gone, but no key of this stack's is still usable: the file and memory copy are
gone, and only the Gitea side is left. That is the case the brief's host rule guards against, and it is
kept visible here, not hidden.
A Gitea executor in flight (d3 T6c, and B3 seen from the executor's side). Step G's Gitea part goes in
this order:
- Record
grant.revoked {granted: false} for each open Gitea decision (3c).
- Await this agent's tracked Gitea executors and remote undos, if any are running. Each one ends
revoked at
its next #giteaRevokedSince check, or it committed applied before step 1.
- Read the
gitea_grants row again and run the steps above.
#giteaCloseRevoke with fire: true lists the user's tokens and deletes every one. The list comes after
step 2, so it includes any token an in-flight executor created. #giteaRuns is a single Set today. It
becomes keyed by (business, agent) so step G can await one agent's executors alone.
3c. Open decisions
A host named only by an open decision has no record. Step 3a still runs remove (a no-op) and records
grant.revoked {granted: false}, as hosts.revoke does for a revoke with no record (N4). The executor's
#revokedSince then ends that decision revoked. The Gitea side is the same through
#giteaRevokedSince.
This is the case where a decision is approved before the fire and executed after it (sage-2574). Both checks
look for a grant.revoked of the agent recorded after the decision was raised (broker.mjs:1156 and :1678 at
47a35bf43). A fire's grant.revoked is always later than the approval, so the executor ends the decision
whenever it runs. The fired agent keeps its roles entry, so #hostDefinition and #giteaRole still pass, and
this check is the only thing that stops the decision. T6 and T6b test it. A delete also removes the roles
entry, so the executor refuses there as well.
The same holds for a decision raised but not yet approved (B1). It's in the set, so it gets its
grant.revoked {granted: false}. If the human approves it later, its executor ends revoked (T6d). The
decision stays approvable in the inbox. Approving it does nothing but end it, which grant.failed reports as
it does for any revoked grant.
3d. A raise for a fired agent (d4, B1)
#hostGrantBinding and #giteaGrantBinding both run inside the raise transaction (broker.mjs:1539 and
:1548; the host raise's keyscan is awaited before that transaction). Both gain one check. The raise refuses
instance-fired if the agent is:
- firing (
#firing);
- fired, by the writer's own rule (instance-settings.mjs, hire): a roles entry, not in
launch.instances,
and not launch.by.
A rehire lists the name again, so a rehired agent can be granted again.
Because the check is in the transaction that records the decision, there's no gap. A raise that commits
before step A's transaction is in the set. One that commits after it refuses. A raise for an agent whose
delete's step G is running refuses grant-revoke-running (3e).
No executor recheck is added. A raise can't commit after fire.stopped, and every decision raised before it
is in the set and gets its grant.revoked.
3e. One step G per agent (d4, B3)
The broker keeps #revoking, a map from (business, agent) to the verb whose step G is running. The verb sets
it synchronously, in the same turn as the set read, before it answers. The job clears it in a finally.
While it's set:
Caller
Answer
instance.fire
refuses fire-in-progress
lifecycleDelete
refuses grants-revoking, no second job
hosts.revoke / grants.revoke for the agent
refuse grant-revoke-running
grant raise for the agent
refuses grant-revoke-running
grants.sweep
skips this agent's entries and names it as running in its result
Two fires started together (the chief retrying after a client timeout): the first takes the lock before it
answers, and the second refuses. The map is in memory. After a broker restart the job is gone and so is the
lock. The instance is still firing, or the delete still has a non-empty set, and the repeat starts a fresh
step G (section 5).
A manual revoke can't race step G, since it refuses while the lock is held. Step G's transactions still
check what they read: a host_grants row already gone at 3a step 3 records grant.revoked {granted: false}
and goes on, as hosts.revoke does today for a revoke with no record.
4. A refusal (D4)
Step G stops at the first grant that can't be revoked. Grants already revoked in this step G stay revoked and
recorded, and the rest stay as they were. The job doesn't run step B: the organization file is unchanged, and
the credentials are unrevoked, since revoke() never ran. A fire stays "firing". A delete's request was
already closed with grants-revoking (section 2).
The verb has already answered, so the refusal is a record, not a thrown code:
- One grant-evidence event,
grant.revoke.refused {job, agent, grant: 'host:<id>' | 'gitea', reason, verb: 'instance.fire' | 'lifecycle.delete', by}.
reason is one of host-unknown, host-remove-failed, gitea-admin-unavailable and gitea-left:<ids>.
grant-revoke-failed is the code in the event and in the inbox message.
A message lands in the human's inbox, as grant.failed does today (broker.mjs:1139). It names the instance,
the grant and the reason, with the manual way out beside the retry (N2):
Fire of stopped: its grant on couldn't be revoked (). Run the fire again once
is reachable. If won't come back, run mosaic hosts revoke <agent> <host> first: the grant record
goes, the line is named as left, and the next fire completes and names that line (section 5).
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.
A job that throws something unexpected inside step G records grant.revoke.refused with reason internal,
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 : 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)
A repeat runs the whole fire again: the checks, step A (nothing left to stop), the set read, then step G, B
and C in the job. A repeat while the job runs refuses fire-in-progress (3e). A repeat delete works the same
way through the human's new request.
- A host grant already revoked has no row and no open decision, so it isn't in the set. It's done.
- A host grant that refused still has its row, so it's retried from 3a step 1.
- Gitea (d4, N1): if the agent has entries in
#giteaLeft or #giteaOpenRevokes, step G retries only those.
It calls #giteaCloseRevoke for each, as #sweepGitea does per entry. It doesn't run the business-wide
sweep, which would retry every agent's entries and settle every open rotation under the chief's name. Any
entry still left for this agent refuses gitea-left. An entry left by an earlier manual
mosaic grants revoke of the same agent blocks the fire the same way. That's intended: no grant outlives
its instance, whichever verb started its revoke. These entries count toward a non-empty set (section 2,
fire step 3), so they go to the job and never run inside the verb.
- A host line left by an earlier manual
hosts.revoke doesn't block the fire (Q1 below). These are the
agent's grant.revoke.closed records with hostLineLeft and no later line removal for that tag. For each
one whose host record still exists, step G sends remove <user> <tag> once. On success it records
grant.revoke.closed {revoke: <the old revoke id>, hostLine: 'removed', fire: true}, so a later fire doesn't
find it again. On failure it records nothing new and the fire goes on. The host and the tag go in
grantsLeft (section 6). These leftover lines also make the set non-empty, so the retry runs in the job,
inside B2's answer (dewey-0823 accepted the single retry on that condition).
Rulings
- Q1 (sage-2573): a leftover line doesn't block the fire. If the host is reachable, the fire tries the
removal once, because a fired agent's key line still on a host is the exact risk this row closes. If that
try fails, the fire doesn't refuse. It names the host and the leftover line in the result and in the event
the human sees. The human accepted that residue when their manual
hosts.revoke left it, so it shouldn't
stop a fire forever on a dead host. Dewey may push back on the retry if it widens step G more than it's
worth.
- Q2 (sage-2573): a delete refused at step G closes its request through the launcher's normal refusal
path, and the human raises it again. A request left taken would strand, which is the shape DEFERRED already
records for the >64-stops done. Conditions: the refusal names the instance, the grant and the reason (the
grant.revoke.refused event, section 4). The repeat delete skips grants already revoked, as the repeat fire
does. T3b tests both.
- Q2 extended (sage-2575): every delete of a granted instance takes two requests. The first starts the
revoke job and refuses
grants-revoking, and the human raises the delete again when the inbox says it's
done. This keeps the cli and the host channel out of the row, and such a delete is rare. Conditions, each
with a test:
- The first refusal tells the human what happened and what's next: the named grants, and to raise the
delete again when the inbox says done. A bare code isn't enough (T3).
- A delete raised while the job runs refuses
grants-revoking again and starts no second job (T3, T9).
- The job's end message names each grant revoked. If one failed, it names the grant, the reason and the
manual way out (
mosaic hosts revoke or mosaic grants revoke). The next delete then refuses with that
reason and doesn't complete over a live grant (T3b).
- A delete of an instance with no grant completes in one request, exactly as today (T3c).
- Section 2 says the fire uses the same answer-at-once job, and the fire's result and event name the job
(T2).
6. The result and the event (D6)
- A fire with a non-empty set answers at once with
job, revoking and done: false, and records
fire.revoking {job, grants} (section 2).
- The job's outcome is recorded, not returned.
fire.done (step C) carries job and grants: [{grant: 'host:<id>' | 'gitea', revoke, granted}], plus grantsLeft for any leftover from section 5's last bullet.
So the event the human sees names them.
- A delete's job records
grants.revoking {job, ...} at the start and grants.revoked.done {job, agent, verb: 'lifecycle.delete', grants, grantsLeft} at the end. It sends the inbox messages in section 2.
- A fire with an empty set carries none of these keys, so its shape is exactly today's.
7. What doesn't change (brief item 4)
hosts.revoke, grants.revoke and grants.sweep: same callers and the same records. Their refusals stay
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
existing grant-evidence shape.
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
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
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.
8. Tests (each in the owning package, whole files)
The bus tests use the fake installer and the fake Gitea from rows 235 and 236. They wait on the job with a new
trusted test API, grantsIdle(), which resolves once every step G job and Gitea executor has ended. It's the
same shape as giteaIdle(). Unless a test says otherwise, every fire or delete below is awaited that way.
- T1 (pins section 1 before the change): a fire of an instance with a host grant refuses
credential-revoke-failed today. A delete leaves the host_grants row. Then: what the next start does with
a record whose key file is gone.
- T2: a fire with a host grant on two hosts and a Gitea grant. The verb answers with a
job id, revoking
with all three, and done: false, and fire.revoking carries the same job (sage-2575, condition 5). After
the job:
- all three are revoked, each with
grant.revoked and grant.revoke.closed as the manual verbs record
them, plus fire: true;
- the line is gone on both fake hosts, every token is deleted, and login is prohibited at the fake Gitea;
fire.done names all three;
- that agent's
host.run and Gitea use both refuse.
- T3 (sage-2575, conditions 1-3): the same grants through the delete (
lifecycleTake then
lifecycleDelete).
- The first delete refuses
grants-revoking and starts the job. The human's inbox gets the start message
naming the job and all three grants, and saying to raise the delete again when it's done.
- A delete raised while the job is held refuses
grants-revoking, starts no second job and sends no second
message.
- After the job, the end message names the job and each grant revoked.
- A third request completes as today.
- T3b (Q2, sage-2575 condition 3): a delete whose job refuses (host B unreachable).
- The end message and
grant.revoke.refused name the instance, host:B, the reason and
mosaic hosts revoke.
- The file and the instance are unchanged.
- The next delete refuses
grant-revoke-failed, not grants-revoking. Its message names the earlier job's
reason and the new job, and the instance is not deleted.
- With B back, that retry job revokes only B and Gitea, skipping host A, and the delete after it completes.
- A second case clears B by hand with
hosts.revoke before the next delete. That delete starts a job only
for Gitea and the leftover line.
- T3c (sage-2575 condition 4): a delete of an instance with no grant completes in one request. Same result,
same events counted, no inbox message.
- T4: host B unreachable in a fire. The job records
grant.revoke.refused naming the agent, host:B and
host-remove-failed, and the inbox message names mosaic hosts revoke (N2). Afterwards:
- the file is unchanged and the instance is still firing;
- host A is revoked and recorded, and host B's row and key file are still there;
- the Gitea grant is untouched, since it comes after B in the order;
- the credentials are unrevoked.
A repeat with B reachable completes and revokes only B and Gitea.
- T4b (Q1): an earlier manual
hosts.revoke left a line on host C. A fire with C reachable removes it and
records the close. With C unreachable, the fire completes and names C and the tag in grantsLeft and in
fire.done.
- T5: Gitea's admin token can't be checked. The job refuses with the Gitea grant intact.
- T5b: a token DELETE fails. The job refuses
gitea-left. A repeat with Gitea back retries only this agent's
entries and completes. Another agent's left entry is still left afterwards (N1).
- T6 (sage-2574): a host grant decision approved before the fire, executed after it. The fire records
grant.revoked {granted: false} for (agent, host). The executor runs after fire.done and ends the decision
revoked: no host_grants row, no key file, no add sent to the host.
- T6b (sage-2574): the same for a Gitea grant decision. The executor ends it
revoked before any Gitea call:
no gitea_grants row, no token file, and the fake Gitea saw no token create.
- T6c: a host or Gitea executor already in flight when step G starts.
- The host side aborts its claim (3a step 1) and ends
revoked.
- The Gitea side: step G awaits the executor (3b), and then the fake Gitea holds no token for the user. That
includes one the executor created mid-flight.
- T6d (B1): a host decision and a Gitea decision, raised before the fire and still undecided. The fire puts
both in the set. The human approves both after
fire.done, and both executors end revoked, with nothing
applied.
- T6e (B1): a host raise and a Gitea raise refuse
instance-fired:
- while the instance is firing;
- after
fire.done.
After a rehire of the same name, a raise goes through. While a delete's job runs, a raise refuses
grant-revoke-running.
- T7: an instance with no grant fires exactly as today, synchronously. Same result keys, no
revoking, and
the same events, counted.
- T8: hosts.revoke and grants.revoke keep their records, their order and their tester refusals.
- T9 (B3): with the job held on a remove:
- a second fire refuses
fire-in-progress;
- a delete of the same agent refuses
grants-revoking and starts no second job;
hosts.revoke and grants.revoke for the agent refuse grant-revoke-running;
grants.sweep names the agent as running.
After a release, one set of grant.revoked records exists per grant.
- T10 (B2): the fake installer holds a remove and the fake Gitea holds a DELETE, neither answering until
released.
instance.fire and lifecycleDelete both answer before either is released. The test checks this with a
timer, not a wall-clock bound.
- Meanwhile the broker answers another verb from another session.
- 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
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:
- New:
packages/bus/tests/fire-revokes-grants.test.mjs holds T2-T13.
- Changed:
grant-host.test.mjs and gitea-grant.test.mjs: T1's pins, and the T8 regressions next to their
existing cases;
packages/cli/tests/lifecycle.test.mjs: the T10 launcher pass;
fake-gitea-api.mjs and host-fixtures.mjs, if T10's holds need a hook there.
- Run whole because they call
instance.fire or lifecycleDelete at 47a35bf43:
- bus:
chief-of-staff, lifecycle-revoke, lifecycle-verbs, reload;
- cli:
lifecycle, instances;
- harness:
tools.
- Also run:
grant-executor.test.mjs (the executors' #giteaRuns change) and gitea-grant-docker.test.mjs
(brief gate).
server.mjs, process.mjs, host.mjs and launcher.mjs are unchanged, so no test changes there for this
row.
9. Mutants
- M1: step G skips the host revokes. Killed by T2.
- M2: step G skips the Gitea revoke. Killed by T2.
- M3: a refused grant revoke is ignored and the job goes on to step B. Killed by T4.
- M4:
lifecycleDelete skips step G. Killed by T3.
- M5: the fire's host revoke goes local first (
lineFirst: false). Killed by T4, because B's row is gone.
- M6: a Gitea revoke with something in
serviceLeft counts as done. Killed by T5b.
- M7: open decisions left out of the set. Killed by T6 and T6b.
- M8: step G skips the leftover-line retry. Killed by T4b.
- M9: a failed leftover-line retry refuses the fire. Killed by T4b.
- M10: the set takes only approved decisions (d3's rule). Killed by T6d.
- M11: the raise bindings skip the fired check. Killed by T6e.
- M12: no
#revoking lock. Killed by T9.
- M13: the verb awaits the job before it answers. Killed by T10.
- M14: step G's Gitea part doesn't await the in-flight executor. Killed by T6c.
- M15: step G's Gitea retry runs the business-wide
#sweepGitea. Killed by T5b.
- 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.
- 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
Built from 47a35bf43 for this read. The code goes on whatever refactor is when the build starts. Rows 274, 256
and 194's follow-ups touch the broker and instance-settings, so expect a rebase and a whole rerun. The row lands
before the .53 Case 1 install hires an instance with a grant. It doesn't hold the dragon-lin re-pin.
Build notes
Dewey approved d6 (dewey-0829, 10:11Z 10-22) with two build-time items. Both are test details, so there's no
new draft:
- N7, T12's setup. T1's setup makes
revoke() refuse because the grant's host.<id> file is still in the
store. Step G's hostKeys.revoke removes that file before step B, so it can't be T12's cause. T12 uses a
step B refusal that survives step G, for example a stray file in the agent's store directory or a roles-entry
credential whose revoke fails. The test asserts it reaches fire.failed with step B.
- N8,
decision-consumed in T11. A chief's fire is within-role: #checkAuthority returns { class } with no
decision, and the lifecycle verbs take none (broker.mjs:3712). #spent never applies to a fire. T11 drops the
second-use decision-consumed assertion. Instead, a repeat fire after fire.done takes the delisted path and
answers done. Section 2's decision-consumed example for fire.failed can't occur, so the build uses only
codes that can.
Choices made in the code (b1, broker.mjs only so far). None changes an approved section; each fills in a
detail the design left open. Dewey, flag any you read differently.
- Record writers.
writeGrantEvent takes only grant.* actions. So fire.revoking, fire.done, fire.failed
and grants.job.interrupted are not grant evidence. fire.* goes through #event under the chief's session
(the job keeps a copy, sc). grants.revoking, grants.revoked.done and grants.job.interrupted go under
the bus session { role: null, run: null }, subject the agent. grant.revoke.refused is grant evidence, as
section 4 has it.
- A fired Gitea user. The
gitea_users row survives a fire (#436b section 6), so on its own the row would
never let a fired agent's set empty. The user counts as pending until a later Gitea grant.revoked with
fire: true and a non-null user names the agent, and counts again after a new grant step (a rehire).
- Open Gitea decisions (3c) end with a
grant.revoked that has user: null, tokens: [], fire: false and
decisions: [ids], plus an immediate close with serviceLeft: []. #giteaRevokedSince then ends them. Since
user is null, the pair never counts as the user's fire.
- Leftover lines (section 5, Q1). A line is in the set as
line:<host> when its tag is in #hostLinesLeft, its
host isn't one the agent holds a grant on, and no fire.done or grants.revoked.done for the agent since the
line was last named left lists it in grantsLeft. A line removed in step G gets grant.revoke.closed with
the old revoke id, hostLine: 'removed' and the flag. #hostLinesLeft now clears a tag on a later close
with hostLine: 'removed' and fire or delete set, so the line also leaves hosts sweep's list. The
removed lines appear in grants as { grant: 'line:<host>', revoke, tag }.
- 3a's wait for host executors.
#runHostGrant marks itself in #hostRuns from its claim to its release. Step
G's host part runs abortClaim, waits for every mark on (agent, host), and sends the remove. If a new mark
appeared during the remove, it runs again, up to five rounds, and then refuses host-remove-failed. From the
last check to the step 1 commit there's no await.
hosts sweep gitea (3e) skips the open revokes, settles and left entries of every agent a job holds, and
keeps those entries in left. Its answer has running: [agents] only when there's at least one. The retry
of one left entry is now #giteaRetry, which the sweep and step G share. The behavior is unchanged.
- Step G's Gitea retries record a sweep-shaped pair for the agent:
grant.sweep { agent, job }, then the
closes and retries, then grant.swept whose left keeps every other agent's entries. The pair is written
first, so a serviceLeft from a close between the two can't drop out of #giteaLeft.
#giteaRuns is now a Map from each promise to its business/agent key, or null for business-wide work.
Step G waits for its own agent's keys and the null ones. #track adds .catch to its finally chain, so a
rejected executor can't leave an unhandled rejection behind.
- A delete repeat after a refusal.
grant-revoke-failed is returned only when the agent's last delete job end
was a grant.revoke.refused whose grant is still in the set. If that grant was cleared by hand and only a
leftover line remains, the repeat answers grants-revoking.
- The delete job's reason, in
grant.revoked, is lifecycle.delete <request>.
- Step G's messages go to the human's inbox alone (no decision, so no raising role to thread to), through the
same insert and route as
#grantNotice.
#recordAuthority is #consumeAuthority less #checkAuthority's holder and processing checks, for a
fire's step C (C1).
Choices made in the tests (b2). Each changes where a test sits or how it checks, not what it checks:
- T1 is in
lifecycle-revoke.test.mjs, not grant-host.test.mjs and gitea-grant.test.mjs. T1 pins the
runtime's revoke() (step B's credential check) and the start sweep, and only lifecycle-revoke builds a
broker through startBroker with the real credential store. Its setup takes a new seed hook that writes
a host record and grant before the start.
- T8 is in
grant-executor.test.mjs for hosts.revoke, where G12, G13, G26 and G27 already are, and in
gitea-grant.test.mjs for grants.revoke. G12 already pins the host verb's two records, their bodies and
their order, and G26 its role refusals, so T8 adds what no test had: tester-dev-only once bus.tester is
off, and that a verb revoke writes no fire.* or grants.* record.
- T2-T13 use a fake installer and a fake organization in
fire-revokes-grants.test.mjs. The organization's
instance.fire delists and reloads the business, as the writer does, and can throw a set error, which is
how T12 gets its step B cause (N7): a credential-revoke-failed that survives step G.
- "A fired agent's
host.run and Gitea use refuse" is checked through the state those uses read: no
host_grants row, no key file, a revoked credential, and no Gitea token for the user.
- T10's cli case runs the real runtime and launcher. The seeded grant is on a TEST-NET host
(192.0.2.10), so the job's remove fails or waits. The first request closes
grants-revoking either way, and
the case waits on grantsIdle() before close.
- The mutants are
scratch/megavolt-r281/r281-mutants.sh, one sed each on broker.mjs, run against
fire-revokes-grants.test.mjs. M13's verbs are synchronous, so its mutant makes the fire return the job's
promise, and the call waits for the job. That's what the verb awaiting its job would look like.
Rebase onto 46dedf925, after row 256 (sage-2597). It's on branch megavolt/row281-b, and
megavolt/row281 stays where it is. git range-diff 0165be266..bd0bb96ae origin/refactor..HEAD gives = for
all nine commits, and no path changes on both sides. Row 256 threads the actor through planDelete,
checkDelete and instances.check, and adds protectedRefusal. That doesn't change what a delete calls in
this row:
- At the verb,
instances.set op delete already ran instances.check with s.human before 256. A
creative instance's delete by anyone but the human now refuses there, before any request exists, so no
step G job starts for it.
- In the pass, step G runs in
lifecycleDelete before #organization('instances.set'), and the actor
is still r.by. planDelete there can still refuse after step G has revoked. Before 256 a references or
last-listed refusal could do the same thing, so the order is unchanged. A delete the verb accepted only
meets a new refusal in the pass if the file changed between the two.
read(business, actor) changes only the listing's endProtected. The broker and these tests don't read it.
The T10 cli case deletes sec-1 (security-reviewer), which isn't END_PROTECTED.
Rebase onto 980cec88c, after row 277 (sage-2605). It's on branch megavolt/row281-c.
git range-diff 46dedf925..7454be493 980cec88c..c174f37ac gives = for all ten commits. Row 277's
thread.claims change to broker.mjs doesn't touch step G or any line the mutants change:
scratch/megavolt-r281/anchors.sh counts each M1-M21 sed anchor in the rebased broker.mjs, and all 28
counts are 1. The mutants script runs it after a green base and stops if a count isn't 1.
b3, after the first base run on c174f37ac (11:24:47Z) came back red. These tests had never run before: b2
was written under git and docs only. bus: 120 tests, 116 pass, 4 fail. cli lifecycle: 24 tests, 23 pass,
1 fail. Every older test in those files passed. No mutant ran. Each failure is a test fault, and b3 changes
no code:
- T4b,
fire-revokes-grants: test changed. The test read manual.revoke, but the hosts.revoke
answer has never carried revoke: #revokeHost drops it, so the verb's answer keeps its pre-281 shape.
The fire's close names the manual revoke, as section 3a says. The test now takes that id from the
manual's grant.revoked event.
- T5b,
fire-revokes-grants: test changed. The job refused gitea-left:9,tokens, and the test
expected gitea-left:<digits>. Section 4 sets the reason as gitea-left:<ids>, the ids left at Gitea.
For a fire, a close's serviceLeft is the N10 list from before 281: each token id whose DELETE failed,
then tokens or login for a fire step that failed (#giteaCloseRevoke). With the DELETE failing,
#giteaFire's token list never clears, so tokens is a real entry left at Gitea. It belongs in the
reason. The test now expects gitea-left:<id>,tokens.
- T6b,
fire-revokes-grants: test changed. After the fire, the executor made 4 Gitea calls where the
test expected none. They are step 0's admin checks C1 to C4: version, self, token scopes and the repo
check, all GET. Step 1's #giteaRevokedSince then ends the decision revoked before any write. Section
3c of d6 names that step 1 check as the only thing that stops the decision. T6b's line in section 8 says
"before any Gitea call", but the evidence it lists is no row, no token file and no token create. That
phrase was wrong, because step 0 always runs first. The approved text stays as it is, and this note is
the correction. The test now checks that every call after the fire is a GET and that no token create
happened. Moving the revoked check ahead of step 0 would change the executor for every revoke, not just
step G's, and the design doesn't ask for that.
- T8,
gitea-grant: test changed. The human revoke's events between grant.revoked and
grant.revoke.closed include its 5 grant.call/grant.result pairs (#giteaLog logs both around every
Gitea call). T8 checks that a human revoke writes no job records, not that it makes no calls. It now
leaves the call log out before comparing.
- T10, cli
lifecycle: test changed (sage-2615 asked about this one). The credential-file came from
startBroker, not from the delete and not from Gitea. The seeded host grant gives sec-1 a host.web1
store reference. Credentials checks the store root (<config dir>/secrets/agents) with privateDir at
start, and the fixture had no store root. That check is row 199's, and it is right to fail closed. The
test now makes the store root at 0700 under dirname(p.f.config), as the bus host fixtures do, never
under the real config. sage-2575's condition still holds. This business has no Gitea grant, so
#grantSet adds no gitea entry (#giteaPending and #giteaUnfired read only the store), and step G
never reaches #stepGGitea. A delete with no Gitea grant reads no Gitea credential file.
b4a: the M1 hang's cause (mos-1273, sage-2636)
The first M1-M21 run on 06ee5ed6b hung on M1. All 22 tests ended in about 80 s (16 fail, 5 pass, 1
cancelled), but the file's process stayed alive until the harness killed it at 1762 s. This is a test fault,
and no code changed.
- Cause: T6c holds
web1:add, and the fake install loops in pause() on sleep(5) while the key is
held. Only the fire's abort signal can end that loop, and M1 removes the host revoke that sends the
abort. In M1's log, T6c fails at its until on line 683, before it deletes the key on line 691. So the loop keeps a
timer alive forever. That reading comes from the log and the file. The rerun checks it: before M1-M21 it runs M1
once without --test-force-exit, and that run must exit by itself inside 180 s.
- Fix: setup's
t.after clears held and failing before it closes the store, so any held loop ends
with its test. The base is unchanged because every passing test already releases its keys.
- Sage's second candidate, the Gitea
gate in T6c and T10, isn't a live handle. The fake server's
t.after closes all its connections, and a parked promise with no timer doesn't keep node alive. Both
tests now also release the gate in t.after (go() only, not off(): a second off() would splice
index -1 and remove another rule).
- The runner's 180 s timeout with
--test-force-exit (sage-2633) still caps any future hang.
b4: T6b pins the four reads (dewey-0839, sage-2627)
- T6b: test changed, stricter.
slice(calls).every(GET) also passed with zero calls. It now
deep-equals the exact list in order: GET /api/v1/version, GET /api/v1/user, GET /api/v1/token,
GET /api/v1/user/repos?limit=1&page=1 (step 0's C1 to C4, gitea-grant.mjs adminChecks). It also fails
if step 1 stops ending the run and step 2's reads go out. The title says "before any Gitea write".
T14 and M22: a declined decision on a removed host (sage-2653, dewey-0844, N1)
Test only, written before the slot. Not run yet.
- T14 (new, in
fire-revokes-grants.test.mjs): coder's web1 decision is raised and left undecided. The human
then runs hosts.remove web1, which goes through because hosts.remove doesn't check open decisions (N1).
The fire starts a job whose set is host:web1. Step G refuses host-unknown with one
grant.revoke.refused, and one inbox message names the job, web1 and host-unknown. There's no fire.done.
The human resolves the decision no. No executor runs (#approvedGrant is null for a no), so the closed
check in #openAgentDecisions is the only thing that takes it out of the set. The repeat fire answers
with no job and records one fire.done with no job.
- M22 deletes that closed check (
broker.mjs:1684). The declined decision stays open, so the repeat fire
starts a second job and refuses host-unknown again, and T14 fails on repeat.job. M10 mutates the same
line in another way (approved-only), and T6d kills it. M22 runs on the fire-revokes file like M1-M21, and
its anchor makes 29.
- N1's hint text (hosts.remove naming the open decision) is a source change. It stays out of this row and
goes to a DEFERRED line that Sage files at landing.
- If T14 is red on its first run, it goes to Sage and then Mos untouched, because 281 has had its one placed red
(mos-1272).
Gate plan (sage-2649, dewey-0840, mos-1275)
Written before the slot. The slot runs on row281-e after the rebase onto 278's landing, and every step
stops at the first red.
- The base: the touched files whole (fire-revokes-grants, lifecycle-revoke, grant-executor, gitea-grant
in bus; lifecycle in cli). Then the 29 anchors, the M1 exit check without
--test-force-exit, and
M1-M22 under timeout -k 10 180 with --test-force-exit.
- dewey-0840's files whole: bus chief-of-staff, lifecycle-verbs, reload, grant-host and
gitea-grant-docker; cli instances; harness tools. If gitea-grant-docker skips, the report names the skip
and its reason.
- bus, business and cli whole.
- Every
scripts/test-*.sh, each as timeout -k 10 LIMIT bash scripts/test-X.sh, and no script is
edited. LIMIT is Gander's row 278 limit (agents/gander/work/queue-278/SHELL-LIMITS.json, sage-2646): 3x
the longest successful run from Gander's row 196 receipts, plus 120 s headroom. I checked it against two
more records. One is my row 236 gate at bd5f11ded (10-22). The other is 278's run 3
(run3/whole/results.json). Neither has a longer run, so every limit is at least 3x the longest of all
three. A script with no limit stops the gate before it starts. Any timeout goes to Sage, then Mos,
unplaced.
Script
Row 236
196 receipts
278 run 3
Longest
3x
LIMIT
test-auth
1 s
1.9 s
0.8 s
1.9 s
6 s
126 s
test-conductor
6 s
8.1 s
6.1 s
8.1 s
25 s
145 s
test-config
1 s
3.2 s
1.9 s
3.2 s
10 s
130 s
test-discord
8 s
8.8 s
7.4 s
8.8 s
27 s
147 s
test-extension-package
1 s
3.0 s
1.4 s
3.0 s
10 s
130 s
test-foundation
39 s
39.7 s
39.9 s
39.9 s
120 s
240 s
test-land-work
11 s
12.6 s
11.3 s
12.6 s
38 s
158 s
test-queue
20 s
23.4 s
21.2 s
23.4 s
71 s
191 s
test-release
2 s
12.2 s
2.7 s
12.2 s
37 s
157 s
test-task
14 s
36.8 s
13.8 s
36.8 s
111 s
231 s
Runs
The manifest-round slot on row281-e 7979fb41e (base 385a7194c, row 278's landing), CLEAR sage-2661. Runner
r281-slot.sh (scratch), from scratch/megavolt-r222: 12:43:17Z to 12:59:25Z, 10-22, SLOT_EXIT 0. No red,
no timeout. Every step ran in a detached worktree at the candidate, with TMPDIR under
/mnt/storage/scratch/tmp. The gate worktree was clean at the end, and the trap removed both worktrees.
- The base, each file whole, plain
node --test: bus fire-revokes-grants, lifecycle-revoke,
grant-executor and gitea-grant 121/121, 0 skipped, 22 s; cli lifecycle 24/24, 0 skipped, 13 s. T14 passed
on its first run. All 29 anchors matched once. The M1 exit check exited by itself: rc 1, 17 fails (the 16
on 3f29375a8, plus T14), 63 s. M1-M22: 22 killed, 0 survived (Mutants).
- dewey-0840's files whole: bus chief-of-staff, lifecycle-verbs, reload, grant-host and gitea-grant-docker
76/76, 0 skipped, 11 s (gitea-grant-docker ran, with no skip); cli instances 14/14, 0 skipped, 3 s; harness
tools 23/23, 0 skipped.
- Whole packages: bus 819/819, 23 s; business 231/231, 3 s; cli 837/837, 591 s. Each has 0 skipped.
- The shells, each under
timeout -k 10 LIMIT, all rc 0:
Script
Result
Time
LIMIT
test-auth
15 passed, 0 failed
1 s
126 s
test-conductor
17 passed, 0 failed
6 s
145 s
test-config
31 passed, 0 failed
1 s
130 s
test-discord
81 passed, 0 failed
8 s
147 s
test-extension-package
18 passed, 0 failed
1 s
130 s
test-foundation
44 passed, 0 failed
36 s
240 s
test-land-work
86 passed, 0 failed
10 s
158 s
test-queue
node 156/156; 28 passed, 0 failed
20 s
191 s
test-release
14 passed, 0 failed
3 s
157 s
test-task
91 passed, 0 failed
13 s
231 s
Skips, from a grep of every shell log for ^skip, # skip and SKIP (dewey-0845):
- test-task, lines 87 and 93:
skip live provider cases (row 244: no provider call in the gate). Row 244
keeps provider calls out of the gate by design. The log has no Docker lines and no other skip.
- test-queue, line 34:
skip queue verify and render --check: this checkout (.../r281-gate-wt) is not the queue's canonical root (/mnt/storage/src/mosaic-stack). The gate runs in a worktree, so the canonical-root
checks are skipped.
- No other shell log, and no node suite above, has a skip.
Mutants
Run on b4a 3f29375a8 (base 6af85df4b), 12:16:18Z to 12:20:15Z, 10-22, slot sage-2643; runner
r281-mutants.sh (scratch). Base first, plain node --test, each file whole: bus fire-revokes-grants,
lifecycle-revoke, grant-executor and gitea-grant 120/120, 0 skipped; cli lifecycle 24/24, 0 skipped. The
worktree was clean after the base, and all 28 sed anchors matched once in broker.mjs.
The M1 exit check (mos-1273) applied M1 and ran fire-revokes-grants without --test-force-exit. It exited by
itself, rc 1, with 16 fails, in 63 s. The first run on 06ee5ed6b hung there until it was killed at 1762 s
(b4a above).
Each mutant ran fire-revokes-grants.test.mjs whole under timeout -k 10 180 node --test --test-force-exit
(sage-2633). Every one ran all 21 tests, with no load error and no timeout.
Mutant
Change
Verdict
rc
Fails of 21
Time
Section 9 names
Failing tests (from the log)
M1
skip-host-revokes
KILLED
1
16
63 s
T2
T2 T3 T3b×2 T4 T4b×2 T6 T6c T6d T6e T9 T10 T11 T12 T13
M2
skip-gitea-revoke
KILLED
1
12
12 s
T2
T2 T3 T3b×2 T4 T5 T5b T6b T6c T6d T9 T10
M3
refusal-goes-on
KILLED
1
3
3 s
T4
T4 T5 T5b
M4
delete-skips-step-g
KILLED
1
6
3 s
T3
T3 T3b×2 T6e T10 T13
M5
host-local-first
KILLED
1
4
3 s
T4
T2 T3b×2 T4
M6
service-left-done
KILLED
1
1
3 s
T5b
T5b
M7
set-no-open-decisions
KILLED
1
4
3 s
T6, T6b
T6 T6b T6c T6d
M8
skip-line-retry
KILLED
1
3
4 s
T4b
T3b T4b×2
M9
line-retry-refuses
KILLED
1
2
3 s
T4b
T3b T4b
M10
approved-only
KILLED
1
1
3 s
T6d
T6d
M11
raise-no-fired-check
KILLED
1
3
3 s
T6e
T4 T6e T12
M12
no-lock
KILLED
1
3
3 s
T9
T3 T6e T9
M13
verb-awaits-job
KILLED
1
14
3 s
T10
T2 T4 T4b×2 T5 T5b T6 T6b T6c T6d T10 T11 T12 T13
M14
no-executor-wait
KILLED
1
1
3 s
T6c
T6c
M15
business-wide-sweep
KILLED
1
1
4 s
T5b
T5b
M16
no-start-message
KILLED
1
2
3 s
T3
T3 T3b
M17
repeat-grants-revoking
KILLED
1
1
3 s
T3b
T3b
M18
empty-set-job
KILLED
1
4
4 s
T3c
T3 T3b×2 T3c
M19
step-c-consume
KILLED
1
1
4 s
T11
T11
M20
bc-as-internal
KILLED
1
2
3 s
T12
T6e T12
M21
no-interrupted-check
KILLED
1
1
4 s
T13
T13
Every mutant fails the test section 9 names for it; none was killed only by a different test. "T3b×2" and "T4b×2" are the two tests that share each id. The failing-test ids are taken from each log's "failing tests" list (dewey-0841).
21 killed, 0 survived. b4 makes one T6b assertion stricter after this run, and fire-revokes-grants is
rerun whole on b4 (Runs).
On row281-e 7979fb41e (sage-2661)
12:43:17Z to 12:47:12Z, 10-22, the first part of the manifest-round slot (Runs). Same runner and flags as
above, with M22 added (T14 and M22 in the build notes). Every mutant ran all 22 tests, with no load error and
no timeout.
Mutant
Change
Verdict
rc
Fails of 22
Time
Section 9 names
Failing tests (from the log)
M1
skip-host-revokes
KILLED
1
17
62 s
T2
T2 T3 T3b×2 T4 T4b×2 T6 T6c T6d T6e T9 T10 T11 T12 T13 T14
M2
skip-gitea-revoke
KILLED
1
12
13 s
T2
T2 T3 T3b×2 T4 T5 T5b T6b T6c T6d T9 T10
M3
refusal-goes-on
KILLED
1
4
3 s
T4
T4 T5 T5b T14
M4
delete-skips-step-g
KILLED
1
6
3 s
T3
T3 T3b×2 T6e T10 T13
M5
host-local-first
KILLED
1
4
3 s
T4
T2 T3b×2 T4
M6
service-left-done
KILLED
1
1
3 s
T5b
T5b
M7
set-no-open-decisions
KILLED
1
5
3 s
T6, T6b
T6 T6b T6c T6d T14
M8
skip-line-retry
KILLED
1
3
3 s
T4b
T3b T4b×2
M9
line-retry-refuses
KILLED
1
2
3 s
T4b
T3b T4b
M10
approved-only
KILLED
1
2
3 s
T6d
T6d T14
M11
raise-no-fired-check
KILLED
1
3
3 s
T6e
T4 T6e T12
M12
no-lock
KILLED
1
3
3 s
T9
T3 T6e T9
M13
verb-awaits-job
KILLED
1
15
3 s
T10
T2 T4 T4b×2 T5 T5b T6 T6b T6c T6d T10 T11 T12 T13 T14
M14
no-executor-wait
KILLED
1
1
3 s
T6c
T6c
M15
business-wide-sweep
KILLED
1
1
3 s
T5b
T5b
M16
no-start-message
KILLED
1
2
3 s
T3
T3 T3b
M17
repeat-grants-revoking
KILLED
1
1
3 s
T3b
T3b
M18
empty-set-job
KILLED
1
4
3 s
T3c
T3 T3b×2 T3c
M19
step-c-consume
KILLED
1
1
3 s
T11
T11
M20
bc-as-internal
KILLED
1
2
3 s
T12
T6e T12
M21
no-interrupted-check
KILLED
1
1
3 s
T13
T13
M22
no-closed-check
KILLED
1
1
3 s
T14
T14
22 killed, 0 survived. Every mutant fails the test it's named for, and M22 fails T14 alone. Compared with
3f29375a8, the only new failures are T14's (under M1, M3, M7, M10, M13 and M22), and every other failing set is
unchanged.