bus: row 236 r3, one N5 test per settle branch; README notes for a left admin token and a running executor (sage-2388, #1742)

This commit is contained in:
2026-10-21 22:42:26 -05:00
parent 352eb2d00f
commit 8f51900b5c
3 changed files with 42 additions and 32 deletions
+6 -4
View File
@@ -81,7 +81,9 @@ Each is a reading of the design where it left room. Dewey may rule otherwise.
## Named for DEFERRED (r2, Dewey N1-N4)
None is a small local change, so none is in r2.
None is a small local change, so none is in r2. sage-2388: all four DEFERRED; N3 and N4 get a bus README
sentence each in r3 (a left admin token is deleted by hand at Gitea; a running executor keeps the admin
api it started with).
- N1: an admin rotate cut between the mint and the slot write leaves a token at Gitea that no event names.
A `grant.admin rotate-begin {oldName}` before the mint would close it; that is a new event shape.
@@ -130,6 +132,6 @@ r2 tests, each red on 8bf442be9 by reading:
interrupted settled:back` after the revoke deleted it.
- B2: "X-G14: a sweep retries a fire only for the user it fired: a remade or gone user is skipped and
named". On 8bf442be9 the retry fires the remade user (token list, delete, PATCH) and has no `skipped`.
- N5 (r3): "X-G11: a revoke during a settle whoami that finds a foreign token or a 401 ends it revoked, not
held or lost". On 77dd43c9e the branches end `grant-token-unknown` (the agent back in tokenUnknown) and
`grant-lost`.
- N5 (r3, sage-2388): "X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not
held" and "X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost". On 77dd43c9e
they end `grant-token-unknown` (the agent back in tokenUnknown) and `grant-lost`.
+1 -1
View File
@@ -229,7 +229,7 @@ an operator inspect content and delivery evidence after an uncertain result.
| `grants.run` | `{decision}`; the human only. Starts the Gitea executor of a `credential.mint` decision with a gitea grant the human said yes to, as the yes itself does (X1). Step 0's refusals come back with no record: `not-authorized`, `decision-consumed`, `grant-token-unknown`, `grant-busy`, `gitea-not-configured`. Returns `{decision,agent,started:true}`; the outcome is a `grant.applied` or `grant.failed` and a notice. |
| `grants.revoke` | `{service:"gitea",agent,reason}`; the human, the business's assistant, or the tester when `bus.tester` is exactly `true`. The token leaves memory and its file goes, an executor in flight aborts, then one transaction deletes the `gitea_grants` record and writes `grant.revoked` with the token ids to delete. The deletes run with the admin token after C1 to C4, and `grant.revoke.closed` records `serviceLeft`, a list. `credentials.revoke AGENT gitea` and `credentials.delete` do the same for a stack-made user; delete also deletes every token of the user and sets `prohibit_login` (fire). |
| `grants.sweep` | `{service:"gitea"}`; the human or the tester. Closes open revokes, settles open rotations again, and retries each `serviceLeft` entry. A fire retry (`tokens`, `login`) first checks the user's id as the revoke close does, and a gone or remade user is skipped with `gitea-user-gone` or `gitea-user-foreign`. Writes `grant.sweep` and `grant.swept {left,skipped,settled,closed}`. |
| `grants.admin.set`, `grants.admin.rotate`, `grants.admin.remove` | The human only (#436b r5 section 3). set takes `{service,url,account,rotateBy,token}`, checks C1 to C4 with the token, then writes the slot under `<configDir>/secrets/grants/<business>/gitea`; a slot already there is `grant-admin-exists`. rotate `{service,rotateBy}` mints a successor admin token and names the old one in `adminTokenLeft`; `{service,finish:true}` deletes it. Rotation voids the approved decisions not yet run, as their `adminSha256` no longer matches. remove drops the slot. Each writes `grant.admin` with no secret. |
| `grants.admin.set`, `grants.admin.rotate`, `grants.admin.remove` | The human only (#436b r5 section 3). set takes `{service,url,account,rotateBy,token}`, checks C1 to C4 with the token, then writes the slot under `<configDir>/secrets/grants/<business>/gitea`; a slot already there is `grant-admin-exists`. rotate `{service,rotateBy}` mints a successor admin token and names the old one in `adminTokenLeft`; `{service,finish:true}` deletes it. Rotation voids the approved decisions not yet run, as their `adminSha256` no longer matches. remove drops the slot. Each writes `grant.admin` with no secret. A left admin token that set or remove drops from `adminTokenLeft`, or one shown as `unknown`, has to be deleted by hand at Gitea. A running executor, revoke close or sweep keeps the admin api it started with: rotate voids only decisions not yet run, and remove takes effect from the next run. |
Each of the three leaves one receipt in the organization's `changes.jsonl`: the actor and a
`detail` of `{verb,agent,kind,date or reason}`, never a value. A change to the store alone writes
+35 -27
View File
@@ -991,33 +991,41 @@ test('X-G11: a revoke while a sweep settles a rotation, forward or back, ends it
}
});
test('X-G11: a revoke during a settle whoami that finds a foreign token or a 401 ends it revoked, not held or lost', async (t) => {
for (const kind of ['grant-token-unknown', 'grant-lost']) {
const f = await setup(t);
const { id } = await openRotation(f);
// A token this grant did not mint, or one Gitea does not know.
const file = kind === 'grant-token-unknown' ? f.gitea.addToken(USER, 'foreign', SEAT_SCOPES).sha1 : 'f'.repeat(40);
writeFileSync(f.tokenFile, file + '\n');
let release;
const held = new Promise((r) => (release = r));
const seat = (e) => e.method === 'GET' && e.path === '/api/v1/token' && e.as !== 'mosaic-grant';
f.gitea.rule(seat, async () => {
await held;
return { pass: true };
});
const sweep = f.call('human', 'grants.sweep', { service: 'gitea' });
await until(() => f.gitea.requests.some(seat));
const revoke = f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'stop' });
release();
await revoke;
const swept = await sweep;
await f.broker.giteaIdle();
assert.deepEqual(swept.settled, [{ decision: id, outcome: 'revoked' }], kind);
assert.equal(f.ended(id).code, 'revoked');
assert.equal(f.grantEvents().some((e) => e.grantDecision === id && e.code === kind), false);
assert.deepEqual([...tokenUnknown(f.store, 'demo').keys()], []);
assert.equal(f.record(), undefined);
}
// A sweep settles the open rotation while the seat whoami is held with `file` in the store file; a revoke
// lands during the hold. Checks the rotation ended revoked with no record; returns the fixture and its decision.
async function revokeDuringWhoami(t, file) {
const f = await setup(t);
const { id } = await openRotation(f);
writeFileSync(f.tokenFile, file(f) + '\n');
let release;
const held = new Promise((r) => (release = r));
const seat = (e) => e.method === 'GET' && e.path === '/api/v1/token' && e.as !== 'mosaic-grant';
f.gitea.rule(seat, async () => {
await held;
return { pass: true };
});
const sweep = f.call('human', 'grants.sweep', { service: 'gitea' });
await until(() => f.gitea.requests.some(seat));
const revoke = f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'stop' });
release();
await revoke;
const swept = await sweep;
await f.broker.giteaIdle();
assert.deepEqual(swept.settled, [{ decision: id, outcome: 'revoked' }]);
assert.equal(f.ended(id).code, 'revoked');
assert.equal(f.record(), undefined);
return { f, id };
}
test('X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not held', async (t) => {
const { f, id } = await revokeDuringWhoami(t, (g) => g.gitea.addToken(USER, 'foreign', SEAT_SCOPES).sha1);
assert.equal(f.grantEvents().some((e) => e.grantDecision === id && e.code === 'grant-token-unknown'), false);
assert.deepEqual([...tokenUnknown(f.store, 'demo').keys()], []);
});
test('X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost', async (t) => {
const { f, id } = await revokeDuringWhoami(t, () => 'f'.repeat(40));
assert.equal(f.grantEvents().some((e) => e.grantDecision === id && e.code === 'grant-lost'), false);
});
test('X-G12: a token that reaches wider than its forks fails with the repository named', async (t) => {