Files
stack/packages/bus/tests/gitea-grant.test.mjs

1464 lines
77 KiB
JavaScript

// Row 236 (#1742, #436b r5 section 9): the Gitea grant executor, its restart, revoke, sweep and admin
// verbs, against the fake Gitea in fake-gitea-api.mjs on 127.0.0.1. Every token is a fake the fake made;
// no test reaches git.mosaicstack.dev or any provider, and no test creates a real account or token
// (Q188). Decisions a test needs outside the human's yes go in by SQL as fixtures. A token value is only
// ever compared, never printed: the checks use assert.ok on a boolean.
import test from 'node:test';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs';
import { Credentials } from '../src/credentials.mjs';
import {
SEAT_SCOPES, adminSha256, definitionSha256, giteaGrant, giteaKeys, giteaStart, identitySha256, openGiteaGrants, tokenName, tokenUnknown, versionAtLeast,
} from '../src/gitea-grant.mjs';
import { Store } from '../src/store.mjs';
import { fakeGiteaApi } from './fake-gitea-api.mjs';
import { grantDecision } from './host-fixtures.mjs';
const AT = '2026-10-21T00:00:00.000Z';
const USER = 'demo-coder';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const dayFrom = (n) => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10);
const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }];
const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
const DEFINITIONS = {
'chief-of-staff': { roleVersion: 2, credentials: [] },
pm: { roleVersion: 2, credentials: [] },
coder: { roleVersion: 2, credentials: [{ service: 'gitea' }] },
research: { roleVersion: 2, credentials: [{ service: 'gitea' }] },
};
async function until(fn, ms = 10000) {
const end = Date.now() + ms;
while (!fn()) {
if (Date.now() > end) throw new Error('timed out');
await sleep(20);
}
}
const rejects = (p, code) => assert.rejects(p, (e) => (assert.equal(e.code, code), true));
function makeBusiness(url) {
return {
id: 'demo',
human: 'jason',
arbiters: { technical: 'pm', delivery: 'pm' },
roles: {
chief: { definition: 'chief-of-staff', ...none },
pm: { definition: 'pm', ...none },
coder: {
definition: 'coder', ...none,
identity: { name: 'Coder', email: '[email protected]', gitea: { url, user: USER } },
publish: ['app', 'lib'],
},
},
definitions: structuredClone(DEFINITIONS),
projects: Object.fromEntries(['app', 'lib', 'old'].map((p) => [p, { remote: `${url}/acme/${p}` }])),
vars: { 'bus.tester': true },
};
}
async function setup(t, opts = {}) {
const dir = mkdtempSync(join(tmpdir(), 'g236-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const gitea = await fakeGiteaApi(t, join(dir, 'tls'), opts.fake);
gitea.addUser('acme');
for (const r of ['app', 'lib', 'old']) gitea.addRepo('acme', r);
const configDir = join(dir, 'cfg');
mkdirSync(configDir, { mode: 0o700 });
const storeRoot = join(configDir, 'secrets', 'agents');
mkdirSync(join(dir, 'data'), { mode: 0o700 });
const store = new Store(join(dir, 'data'));
const credentials = new Credentials({ references: {}, storeRoot, dataRoot: join(dir, 'data') });
t.after(() => {
credentials.close();
store.close();
});
const business = makeBusiness(gitea.url);
opts.business?.(business);
const grantApi = giteaGrant({ configDir, ca: gitea.ca, timeoutMs: opts.timeoutMs ?? 5000 });
const meta = { url: gitea.url, account: 'mosaic-grant', rotateBy: opts.adminRotateBy ?? dayFrom(60), setAt: AT };
if (opts.slot !== false) grantApi.writeAdmin('demo', gitea.adminToken, meta);
const keys = giteaKeys({ credentials, storeRoot });
const calls = [];
const broker = new Broker({
store,
businesses: { demo: business },
secretCheck: (value) => credentials.assertClean(value),
giteaGrant: grantApi,
giteaKeys: keys,
processing: opts.processing,
credentialVerbs: (id, verb, args, actor) => {
calls.push([verb, actor]);
if (verb === 'credentials.revoke' || verb === 'credentials.delete') {
try {
credentials.revoke('demo/' + args.agent, 'gitea');
} catch {}
}
return { ok: true };
},
});
const caps = {};
for (const role of ['chief', 'pm']) {
caps[role] = broker.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi', address: role });
broker.request(caps[role], { verb: 'role.claim' });
}
caps.human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
caps.tester = broker.bindTester({ business: 'demo' });
const call = (who, verb, args = {}) => broker.request(caps[who], { verb, args });
const raise = (grant = {}) =>
call('chief', 'decision.raise', {
action: 'credential.mint', question: 'Grant coder Gitea?', options, recommendation: 'yes', blocking: false,
grant: { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30), ...grant },
});
const grantEvents = () =>
store
.all(`SELECT body FROM events WHERE business='demo' AND ${GRANT_EVIDENCE} ORDER BY seq`)
.map((e) => JSON.parse(e.body));
const ended = (decision) => grantEvents().findLast((e) => e.grantDecision === decision && ['grant.applied', 'grant.failed'].includes(e.action));
const approve = async (grant) => {
const d = await raise(grant);
call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => ended(d.id));
await broker.giteaIdle();
return { id: d.id, end: ended(d.id) };
};
const fixtureGrant = (over = {}, close) => {
const grant = {
method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30),
definitionSha256: definitionSha256(business.definitions.coder),
identitySha256: identitySha256(business.roles.coder, business.projects),
adminSha256: adminSha256(grantApi.adminMeta('demo') ?? meta),
...over,
};
return grantDecision(store, { grant, ...(close !== undefined ? { close } : {}) });
};
const tokenFile = join(storeRoot, 'demo', 'coder', 'gitea');
const fileHolds = (sha1) => existsSync(tokenFile) && readFileSync(tokenFile, 'utf8') === sha1 + '\n';
const record = () => store.get("SELECT * FROM gitea_grants WHERE business='demo' AND agent='coder'");
const userRecord = () => store.get("SELECT * FROM gitea_users WHERE business='demo' AND agent='coder'");
const state = () => credentials.status().find((e) => e.instance === 'demo/coder' && e.service === 'gitea')?.state;
const messages = (decision) =>
store.all("SELECT to_role, body FROM messages WHERE business=? AND decision=? AND from_role='_bus' AND instr(body, ?) ORDER BY rowid", 'demo', decision, `(decision ${decision})`);
const userTokens = () => gitea.tokens(USER) ?? [];
// The newest token the fake made for the seat user: the one a grant minted last.
const seatToken = (id) => gitea.users.get(USER)?.tokens.find((x) => x.id === id) ?? null;
// No token the fake made shows up in an event, a message, a decision or a refusal.
const noLeak = () => {
const text = [
...store.all("SELECT body FROM events WHERE business='demo'").map((r) => r.body),
...store.all("SELECT body FROM messages WHERE business='demo'").map((r) => r.body),
...store.all('SELECT question FROM decisions').map((r) => r.question),
JSON.stringify(gitea.requests),
].join('\n');
assert.ok(gitea.issued.every((tok) => !text.includes(tok)), 'a token value reached the store or the request log');
};
return {
dir, gitea, configDir, storeRoot, store, credentials, business, grantApi, meta, keys, broker, caps, calls,
call, raise, approve, grantEvents, ended, fixtureGrant, tokenFile, fileHolds, record, userRecord, state, messages, userTokens, seatToken, noLeak,
};
}
// A decision the human approved and a killed bus child spent, with its grant.step events up to `steps`.
function interrupted(f, steps, over = {}) {
const id = f.fixtureGrant(over);
writeGrantEvent(f.store, AT, 'demo', { operation: 'grant.gitea', decision: id }, id);
for (const [step, extra = {}] of steps)
writeGrantEvent(f.store, AT, 'demo', { action: 'grant.step', method: 'gitea', grantDecision: id, agent: 'coder', user: USER, step, ...extra }, 'coder');
return id;
}
// An open rotation: a live first grant, then a spent fixture decision whose commit step names the live
// token as previous and `next` (a fake token minted here, never by a grant) as the new one.
async function openRotation(f) {
const first = await f.approve();
assert.equal(first.end.action, 'grant.applied');
const live = f.record();
const next = f.gitea.addToken(USER, tokenName('demo', 'coder', 'rot'), SEAT_SCOPES);
const previous = { tokenId: live.token_id, tokenName: live.token_name, projects: JSON.parse(live.projects), rotateBy: live.rotate_by, decision: live.decision };
const id = interrupted(f, [
['mint', { tokenName: next.name }],
['minted', { tokenId: next.id }],
['commit', { previousTokenId: live.token_id, tokenId: next.id, tokenName: next.name, previous }],
]);
return { first, live, next, id };
}
// A restart as runtime.mjs does it, on the same store: giteaStart, Credentials with its references and
// held list, a new broker, reconcileGrants before the socket opens, then reconcileGitea.
async function restart(t, f, { business = f.business, timeoutMs = 5000 } = {}) {
const references = {};
const { held } = giteaStart(f.store, f.storeRoot, { demo: business }, references);
const credentials = new Credentials({ references, storeRoot: f.storeRoot, dataRoot: join(f.dir, 'data'), held });
t.after(() => credentials.close());
const grantApi = giteaGrant({ configDir: f.configDir, ca: f.gitea.ca, timeoutMs });
const broker = new Broker({
store: f.store,
businesses: { demo: business },
secretCheck: (value) => credentials.assertClean(value),
giteaGrant: grantApi,
giteaKeys: giteaKeys({ credentials, storeRoot: f.storeRoot }),
});
const before = f.gitea.requests.length;
const now = broker.reconcileGrants();
const later = await broker.reconcileGitea();
await broker.giteaIdle();
const state = credentials.status().find((e) => e.instance === 'demo/coder' && e.service === 'gitea')?.state;
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
return { credentials, broker, held, now, later, state, human, requests: f.gitea.requests.slice(before) };
}
test('versionAtLeast compares numbers and refuses a prerelease of 1.27.0', () => {
for (const v of ['1.27.0', '1.27.1', 'v1.30.0', '1.27.0+build', '2.0.0']) assert.equal(versionAtLeast(v), true, v);
for (const v of ['1.26.4', '1.27.0-rc0', '1.9.0', '1.27', 'x', null, '1.27.0 ']) assert.equal(versionAtLeast(v), false, String(v));
});
test('X-G1: a raise that fails a check refuses and stores nothing', async (t) => {
const f = await setup(t);
const decisions = () => f.store.get('SELECT count(*) AS n FROM decisions').n;
const n = decisions();
const bad = [
[{ extra: 1 }, 'invalid-request'],
[{ rotateBy: '2020-01-01' }, 'invalid-request'],
[{ rotateBy: '2026-02-30' }, 'invalid-request'],
[{ target: { projects: [] } }, 'invalid-request'],
[{ target: { projects: ['app', 'app'] } }, 'invalid-request'],
[{ target: { projects: ['app'], repo: 'x' } }, 'invalid-request'],
[{ agent: 'pm' }, 'gitea-not-declared'],
[{ agent: 'nobody' }, 'gitea-not-declared'],
[{ target: { projects: ['old'] } }, 'remote-not-granted'],
];
for (const [over, code] of bad) await rejects(f.raise(over), code);
// Only the chief raises a grant.
await rejects(
f.call('pm', 'decision.raise', {
action: 'credential.mint', question: 'q', options, recommendation: 'yes', blocking: false,
grant: { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30) },
}),
'grant-chief-only',
);
assert.equal(decisions(), n);
assert.equal(f.gitea.requests.length, 0);
});
test('X-G1: a creative role, a role without identity and a missing or due slot refuse at raise', async (t) => {
const creative = await setup(t, { business: (b) => (b.roles.coder.definition = 'research') });
await rejects(creative.raise(), 'gitea-not-declared');
const bare = await setup(t, { business: (b) => delete b.roles.coder.identity });
await rejects(bare.raise(), 'gitea-not-declared');
const noSlot = await setup(t, { slot: false });
await rejects(noSlot.raise(), 'grant-admin-missing');
const due = await setup(t, { adminRotateBy: '2026-01-01' });
await rejects(due.raise(), 'grant-admin-rotation-due');
const elsewhere = await setup(t, { business: (b) => (b.projects.app.remote = 'https://example.test/acme/app') });
await rejects(elsewhere.raise(), 'remote-host-mismatch');
for (const f of [creative, bare, noSlot, due, elsewhere]) assert.equal(f.gitea.requests.length, 0);
});
// Row 285 (#1790, sage-2921): the chief's definition declares gitea for a read grant only, and this path
// mints SEAT_SCOPES, so a chief with identity and publish still refuses at raise and stores nothing.
test('X-G1: the chief of staff refuses at raise, whatever its definition, identity and publish declare', async (t) => {
const f = await setup(t, {
business: (b) => {
b.definitions['chief-of-staff'].credentials = [{ service: 'gitea' }];
b.roles.chief.identity = { name: 'Chief', email: '[email protected]', gitea: { url: b.roles.coder.identity.gitea.url, user: 'demo-chief' } };
b.roles.chief.publish = ['app'];
},
});
const n = f.store.get('SELECT count(*) AS n FROM decisions').n;
await rejects(f.raise({ agent: 'chief' }), 'gitea-not-declared');
assert.equal(f.store.get('SELECT count(*) AS n FROM decisions').n, n);
assert.equal(f.gitea.requests.length, 0);
});
test('X-G2: admin set refuses each failed check and writes nothing, then writes the slot once', async (t) => {
const f = await setup(t, { slot: false });
const set = (token, over = {}) =>
f.call('human', 'grants.admin.set', { service: 'gitea', url: f.gitea.url, account: 'mosaic-grant', rotateBy: dayFrom(60), token, ...over });
const slot = join(f.configDir, 'secrets', 'grants', 'demo', 'gitea');
for (const v of ['1.26.4', '1.27.0-rc0', '1.9.0']) {
f.gitea.state.version = v;
await rejects(set(f.gitea.adminToken), 'gitea-too-old');
}
f.gitea.state.version = '1.27.1';
const plain = f.gitea.addUser('plain', { scopes: ['write:admin', 'write:repository', 'write:user'] }).token.sha1;
await rejects(set(plain, { account: 'plain' }), 'grant-admin-account');
await rejects(set(f.gitea.adminToken, { account: 'someone-else' }), 'grant-admin-account');
const narrow = f.gitea.addToken('mosaic-grant', 'narrow', ['write:admin', 'write:repository']).sha1;
await rejects(set(narrow), 'grant-admin-scope');
const wide = f.gitea.addToken('mosaic-grant', 'wide', ['write:admin', 'write:repository', 'write:user', 'write:organization']).sha1;
await rejects(set(wide), 'grant-admin-scope');
const reach = f.gitea.addRepo('acme', 'shared', { collaborators: ['mosaic-grant'] });
await rejects(set(f.gitea.adminToken), 'grant-admin-not-dedicated');
f.gitea.repos.splice(f.gitea.repos.indexOf(reach), 1);
await rejects(set(f.gitea.adminToken, { rotateBy: '2020-01-01' }), 'invalid-request');
// The url is an instance origin over https (row 199's parser; a trailing slash is the same origin).
await rejects(set(f.gitea.adminToken, { url: f.gitea.url + '/gitea' }), 'invalid-request');
await rejects(set(f.gitea.adminToken, { url: f.gitea.url.replace('https:', 'http:') }), 'invalid-request');
await rejects(f.call('chief', 'grants.admin.set', { service: 'gitea', url: f.gitea.url, account: 'mosaic-grant', rotateBy: dayFrom(60), token: f.gitea.adminToken }), 'human-required');
await rejects(f.call('tester', 'grants.admin.set', { service: 'gitea', url: f.gitea.url, account: 'mosaic-grant', rotateBy: dayFrom(60), token: f.gitea.adminToken }), 'human-required');
assert.equal(existsSync(slot), false);
const meta = await set(f.gitea.adminToken);
assert.deepEqual(Object.keys(meta).sort(), ['account', 'rotateBy', 'setAt', 'url']);
assert.equal(statSync(join(slot, 'admin')).mode & 0o777, 0o600);
assert.equal(statSync(slot).mode & 0o777, 0o700);
assert.ok(readFileSync(join(slot, 'admin'), 'utf8') === f.gitea.adminToken + '\n');
await rejects(set(f.gitea.adminToken), 'grant-admin-exists');
const admin = f.grantEvents().filter((e) => e.action === 'grant.admin');
assert.deepEqual(admin.map((e) => e.verb), ['set']);
f.noLeak();
});
test('X-G3 to X-G8: an approve makes the user, mints the token, forks, checks reach and writes the file, record and grant.applied', async (t) => {
const f = await setup(t);
const d = await f.raise();
const block = f.store.get('SELECT question FROM decisions WHERE id=?', d.id).question;
assert.match(block, new RegExp(`user ${USER}, email [email protected]`));
assert.match(block, /Scopes: read:user, write:repository\./);
assert.match(block, /Kind: new Gitea user\./);
assert.match(block, new RegExp(`It may also reach this user's existing forks of:\\n${f.gitea.url}/acme/lib`));
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
const end = f.ended(d.id);
assert.equal(end.action, 'grant.applied', JSON.stringify(end));
const u = f.gitea.users.get(USER);
const userStep = f.grantEvents().find((e) => e.grantDecision === d.id && e.step === 'user');
assert.match(userStep.nonce, /^[0-9a-f]{16}$/);
assert.equal(u.full_name, `Coder [mosaic ${userStep.nonce}]`);
assert.equal(u.email, '[email protected]');
assert.equal(u.is_admin, false);
assert.deepEqual({ ...f.userRecord() }, { business: 'demo', agent: 'coder', user: USER, id: u.id, nonce: userStep.nonce, decision: d.id });
assert.equal(u.tokens.length, 1);
const tok = u.tokens[0];
assert.deepEqual(tok.scopes, [...SEAT_SCOPES]);
assert.equal(tok.name, tokenName('demo', 'coder', d.id));
assert.deepEqual(end, {
action: 'grant.applied', method: 'gitea', grantDecision: d.id, agent: 'coder', user: USER, userId: u.id, tokenId: tok.id,
forks: [`${f.gitea.url}/${USER}/app`],
});
const fork = f.gitea.repos.find((r) => r.owner === USER && r.name === 'app');
assert.equal(fork.parent, 'acme/app');
assert.equal(statSync(f.tokenFile).mode & 0o777, 0o600);
assert.ok(f.fileHolds(tok.sha1), 'the store file holds the minted token');
assert.deepEqual({ ...f.record() }, {
business: 'demo', agent: 'coder', user: USER, token_id: tok.id, token_name: tok.name, projects: '["app"]', rotate_by: dayFrom(30), decision: d.id,
});
assert.equal(f.state(), 'valid');
const mine = f.grantEvents().filter((e) => e.grantDecision === d.id);
assert.deepEqual(mine.map((e) => e.action + (e.step ? ' ' + e.step : '')), ['grant.step user', 'grant.step mint', 'grant.step minted', 'grant.step commit', 'grant.applied']);
assert.deepEqual(mine.find((e) => e.step === 'commit'), {
action: 'grant.step', method: 'gitea', grantDecision: d.id, agent: 'coder', user: USER, step: 'commit', previousTokenId: null, tokenId: tok.id, tokenName: tok.name, previous: null,
});
// X-G8: each call has its result, in order, and the ops are the section's.
const log = f.grantEvents().filter((e) => e.action === 'grant.call' || e.action === 'grant.result');
const calls = log.filter((e) => e.action === 'grant.call');
for (const c of calls) assert.equal(log.filter((e) => e.action === 'grant.result' && e.callId === c.callId).length, 1, c.op);
assert.deepEqual(calls.map((c) => c.op), [
'version', 'self', 'token-scopes', 'repos-check', 'user-get', 'user-create', 'token-create', 'fork-get', 'fork', 'seat-self', 'seat-repos',
]);
assert.ok(calls.every((c) => c.reason === d.id && c.method === 'gitea'));
// Section 7: the consume record is the only grant body with a `decision` key.
assert.deepEqual(f.grantEvents().filter((e) => 'decision' in e).map((e) => e.operation), ['grant.gitea']);
assert.deepEqual(f.messages(d.id).map((m) => m.to_role), ['human', 'chief']);
// The token routes went with Basic auth, the rest with the token header; nothing as an unknown user.
for (const r of f.gitea.requests) {
if (r.path.includes('/tokens')) assert.equal(r.auth, 'basic', r.path);
else if (r.path !== '/api/v1/version') assert.equal(r.auth, 'token', r.path);
}
f.noLeak();
});
test('X-G4: a user this stack did not make, an admin, a disabled user and a remade user each refuse', async (t) => {
const foreign = await setup(t);
foreign.gitea.addUser(USER);
let r = await foreign.approve();
assert.equal(r.end.code, 'gitea-user-foreign');
assert.equal(foreign.userRecord(), undefined);
assert.equal(foreign.gitea.calls().filter((c) => c.startsWith('POST')).length, 0);
const f = await setup(t);
assert.equal((await f.approve()).end.action, 'grant.applied');
const u = f.gitea.users.get(USER);
u.is_admin = true;
r = await f.approve();
assert.equal(r.end.code, 'gitea-user-admin');
u.is_admin = false;
u.prohibit_login = true;
r = await f.approve();
assert.equal(r.end.code, 'gitea-user-disabled');
u.prohibit_login = false;
const tokenCalls = () => f.gitea.calls().filter((c) => c.includes('/tokens')).length;
const before = tokenCalls();
f.gitea.removeUser(USER);
f.gitea.addUser(USER);
r = await f.approve();
assert.equal(r.end.code, 'gitea-user-foreign');
// A case variant of the name, remade: Gitea matches the login without case, the id differs.
f.gitea.removeUser(USER);
f.gitea.addUser(USER.toUpperCase());
r = await f.approve();
assert.equal(r.end.code, 'gitea-user-foreign');
// None of these minted a token or made a token call, and the live grant stands.
assert.equal(tokenCalls(), before);
assert.equal(f.state(), 'valid');
assert.equal(f.grantEvents().filter((e) => e.step === 'mint').length, 1);
f.noLeak();
});
test('X-G6 and X-G8: the user and mint steps are written before their calls, and each result after its call', async (t) => {
const f = await setup(t);
const seen = {};
// What the store holds while the call is at Gitea: the step, and the call with no result yet.
const at = (op, step) => () => {
const events = f.grantEvents();
const call = events.findLast((e) => e.action === 'grant.call' && e.op === op);
seen[op] = {
step: events.some((e) => e.action === 'grant.step' && e.step === step),
call: Boolean(call),
result: events.some((e) => e.action === 'grant.result' && e.callId === call?.callId),
};
return { pass: true };
};
f.gitea.rule((e) => e.method === 'POST' && e.path === '/api/v1/admin/users', at('user-create', 'user'));
f.gitea.rule((e) => e.method === 'POST' && e.path === `/api/v1/users/${USER}/tokens`, at('token-create', 'mint'));
const r = await f.approve();
assert.equal(r.end.action, 'grant.applied', JSON.stringify(r.end));
assert.deepEqual(seen, {
'user-create': { step: true, call: true, result: false },
'token-create': { step: true, call: true, result: false },
});
// Every result carries the HTTP status Gitea gave (user-get and fork-get answer 404 on a first grant).
const results = f.grantEvents().filter((e) => e.action === 'grant.result');
assert.ok(results.length > 0);
for (const e of results) assert.ok(Number.isInteger(e.status) && e.status >= 100 && e.status < 600, JSON.stringify(e));
assert.ok(results.some((e) => e.status === 404));
});
test('X-G4: a fork by the seat user of another repository than the project refuses fork-foreign', async (t) => {
const f = await setup(t);
f.gitea.addRepo('other', 'app');
f.gitea.addRepo(USER, 'app', { parent: 'other/app' });
const r = await f.approve();
assert.equal(r.end.code, 'fork-foreign', JSON.stringify(r.end));
assert.equal(f.gitea.calls().filter((c) => c.endsWith('/forks')).length, 0);
assert.deepEqual(f.userTokens(), []);
assert.equal(existsSync(f.tokenFile), false);
assert.equal(f.record(), undefined);
});
test('X-G5: a failure after the mint deletes the token, unloads nothing it did not load and leaves no file', async (t) => {
const f = await setup(t);
const off = f.gitea.rule((e) => e.method === 'POST' && e.path.endsWith('/forks'), { status: 500, json: {} });
const r = await f.approve();
off();
assert.equal(r.end.code, 'gitea-failed');
assert.equal(r.end.serviceLeft, undefined);
assert.equal(r.end.user, USER);
assert.deepEqual(f.userTokens(), []);
assert.equal(existsSync(f.tokenFile), false);
assert.equal(f.record(), undefined);
assert.equal(f.state(), undefined);
// The user stays recorded; the next grant reuses it.
assert.ok(f.userRecord());
const again = await f.approve();
assert.equal(again.end.action, 'grant.applied');
assert.equal(f.gitea.calls().filter((c) => c === 'POST /api/v1/admin/users').length, 1);
});
test('X-G5: a token delete that fails is left at Gitea, shown by status and taken by sweep', async (t) => {
const f = await setup(t);
const offFork = f.gitea.rule((e) => e.method === 'POST' && e.path.endsWith('/forks'), { status: 500, json: {} });
const offDelete = f.gitea.rule((e) => e.method === 'DELETE', { status: 500, json: {} });
const r = await f.approve();
offFork();
offDelete();
assert.equal(r.end.code, 'gitea-failed');
const id = f.userTokens()[0].id;
assert.deepEqual(r.end.serviceLeft, [id]);
assert.match(f.messages(r.id)[0].body, new RegExp(`Left at Gitea: ${id}\\. Run: mosaic grants sweep gitea`));
const status = await f.call('human', 'grants.status', { service: 'gitea' });
assert.deepEqual(status.serviceLeft, [{ agent: 'coder', user: USER, id }]);
const swept = await f.call('tester', 'grants.sweep', { service: 'gitea' });
assert.deepEqual(swept.left, []);
assert.deepEqual(f.userTokens(), []);
assert.deepEqual((await f.call('human', 'grants.status', { service: 'gitea' })).serviceLeft, []);
await rejects(f.call('chief', 'grants.sweep', { service: 'gitea' }), 'human-required');
});
test('X-G5: a mint that fails deletes by name, and a refusal at step 0 leaves the decision unspent', async (t) => {
const f = await setup(t);
const off = f.gitea.rule((e) => e.method === 'POST' && e.path.endsWith('/tokens'), { status: 500, json: {} });
const r = await f.approve();
off();
assert.equal(r.end.code, 'gitea-failed');
assert.ok(f.gitea.calls().includes(`DELETE /api/v1/users/${USER}/tokens/${tokenName('demo', 'coder', r.id)}`));
// Step 0: the admin token's scope changed after set.
f.gitea.admin.tokens[0].scopes = ['write:admin'];
const s0 = await f.approve();
assert.equal(s0.end.code, 'grant-admin-scope');
assert.equal(f.grantEvents().some((e) => e.operation === 'grant.gitea' && e.decision === s0.id), false);
f.gitea.admin.tokens[0].scopes = ['write:admin', 'write:repository', 'write:user'];
// The human runs it again once the slot is right.
const run = await f.call('human', 'grants.run', { decision: s0.id });
assert.deepEqual(run, { decision: s0.id, agent: 'coder', started: true });
await f.broker.giteaIdle();
assert.equal(f.grantEvents().filter((e) => e.grantDecision === s0.id).at(-1).action, 'grant.applied');
});
test('X-G5: an agent held for an unknown token is refused at step 0 with no Gitea call', async (t) => {
const f = await setup(t);
const held = f.fixtureGrant();
writeGrantEvent(f.store, AT, 'demo', { action: 'grant.failed', method: 'gitea', grantDecision: held, agent: 'coder', code: 'grant-token-unknown', tokenId: 7 }, 'coder');
const r = await f.approve();
assert.equal(r.end.code, 'grant-token-unknown');
assert.equal(f.gitea.requests.length, 0);
await rejects(f.call('human', 'grants.run', { decision: r.id }), 'grant-token-unknown');
});
test('X-G5 and X-G9 (step 7): a rotation whose old-token delete fails stays applied with the old id left', async (t) => {
const f = await setup(t);
const first = await f.approve();
const oldId = f.record().token_id;
const off = f.gitea.rule((e) => e.method === 'DELETE' && e.path.endsWith(`/tokens/${oldId}`), { status: 500, json: {} });
const second = await f.approve();
off();
assert.equal(second.end.action, 'grant.applied');
assert.deepEqual(second.end.serviceLeft, [oldId]);
assert.equal(f.record().decision, second.id);
assert.notEqual(f.record().token_id, oldId);
assert.match(f.messages(second.id)[0].body, new RegExp(`The previous token ${oldId} is still live`));
const swept = await f.call('human', 'grants.sweep', { service: 'gitea' });
assert.deepEqual(swept.left, []);
assert.deepEqual(f.userTokens().map((x) => x.id), [f.record().token_id]);
assert.notEqual(first.id, second.id);
});
test('X-G10: in a rotation the store file holds the new token before the old one is deleted', async (t) => {
const f = await setup(t);
await f.approve();
const oldId = f.record().token_id;
let seen = null;
f.gitea.rule(
(e) => e.method === 'DELETE' && e.path.endsWith(`/tokens/${oldId}`),
() => {
const newest = f.gitea.users.get(USER).tokens.at(-1);
seen = newest.id !== oldId && f.fileHolds(newest.sha1);
return { pass: true };
},
);
const r = await f.approve();
assert.equal(r.end.action, 'grant.applied');
assert.equal(seen, true);
const commit = f.grantEvents().find((e) => e.grantDecision === r.id && e.step === 'commit');
assert.equal(commit.previousTokenId, oldId);
assert.equal(commit.previous.tokenId, oldId);
assert.deepEqual(f.userTokens().map((x) => x.id), [r.end.tokenId]);
f.noLeak();
});
test('X-G6: a rotation cut before the rename settles back at once, and the old token stays live', async (t) => {
const f = await setup(t);
await f.approve();
const live = f.record();
// The store's tmp path is a directory, so the issue fails before its rename.
mkdirSync(f.tokenFile + '.tmp');
const r = await f.approve();
rmSync(f.tokenFile + '.tmp', { recursive: true });
assert.equal(r.end.code, 'interrupted');
assert.equal(r.end.settled, 'back');
assert.deepEqual({ ...f.record() }, { ...live });
assert.deepEqual(f.userTokens().map((x) => x.id), [live.token_id]);
assert.ok(f.fileHolds(f.seatToken(live.token_id).sha1));
assert.equal(f.state(), 'valid');
});
test('X-G6: a rotation whose step 8 write fails is settle-pending, and a restart settles it forward', async (t) => {
const f = await setup(t);
await f.approve();
const oldId = f.record().token_id;
f.store.run("CREATE TEMP TRIGGER g236_fail BEFORE UPDATE ON gitea_grants BEGIN SELECT RAISE(ABORT, 'test fault'); END");
const d = await f.raise();
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.grantEvents().some((e) => e.grantDecision === d.id && e.step === 'commit'));
await f.broker.giteaIdle();
f.store.run('DROP TRIGGER g236_fail');
assert.equal(f.ended(d.id), undefined);
const status = await f.call('human', 'grants.status', { service: 'gitea' });
assert.deepEqual(status.settlePending, [{ decision: d.id, agent: 'coder' }]);
const newId = f.grantEvents().find((e) => e.grantDecision === d.id && e.step === 'commit').tokenId;
const r = await restart(t, f);
assert.deepEqual(r.held, []);
const end = f.ended(d.id);
assert.equal(end.action, 'grant.applied');
assert.equal(end.settled, 'forward');
assert.equal(end.serviceLeft, undefined);
assert.equal(f.record().token_id, newId);
assert.equal(f.record().decision, d.id);
assert.equal(r.state, 'valid');
assert.deepEqual(f.userTokens().map((x) => x.id), [newId]);
assert.notEqual(newId, oldId);
});
test('X-G6: a restart settles an open rotation back when the file still holds the old token', async (t) => {
const f = await setup(t);
const { live, next, id } = await openRotation(f);
const r = await restart(t, f);
const end = f.ended(id);
assert.equal(end.code, 'interrupted');
assert.equal(end.settled, 'back');
assert.equal(f.record().token_id, live.token_id);
assert.equal(f.record().decision, live.decision);
assert.deepEqual(f.userTokens().map((x) => x.id), [live.token_id]);
assert.equal(f.seatToken(next.id), null);
assert.equal(r.state, 'valid');
});
test('X-G6: a restart settles an open rotation forward when the file holds the new token', async (t) => {
const f = await setup(t);
const { live, next, id } = await openRotation(f);
writeFileSync(f.tokenFile, next.sha1 + '\n');
const r = await restart(t, f);
const end = f.ended(id);
assert.equal(end.action, 'grant.applied');
assert.equal(end.settled, 'forward');
assert.equal(f.record().token_id, next.id);
assert.equal(f.record().decision, id);
assert.deepEqual(f.userTokens().map((x) => x.id), [next.id]);
assert.equal(r.state, 'valid');
assert.notEqual(live.token_id, next.id);
});
test('X-G6: a 401 on the file token loses the grant: both tokens go, the record goes and memory unloads', async (t) => {
const f = await setup(t);
const { live, id } = await openRotation(f);
const u = f.gitea.users.get(USER);
u.tokens.splice(u.tokens.findIndex((x) => x.id === live.token_id), 1);
const r = await restart(t, f);
const end = f.ended(id);
assert.equal(end.code, 'grant-lost');
assert.equal(end.serviceLeft, undefined);
assert.equal(f.record(), undefined);
assert.deepEqual(f.userTokens(), []);
assert.equal(existsSync(f.tokenFile), false);
assert.equal(r.state, undefined);
assert.deepEqual(f.messages(id).map((m) => m.to_role), ['human', 'chief']);
});
test('X-G6: a 403, 404, 500, a hang and a dropped socket each leave the rotation settle-pending with nothing deleted', async (t) => {
for (const answer of [{ status: 403, json: {} }, { status: 404, json: {} }, { status: 500, json: {} }, { hang: true }, { drop: true }]) {
const f = await setup(t);
const { live, next, id } = await openRotation(f);
f.gitea.rule((e) => e.method === 'GET' && e.path === '/api/v1/token' && e.as === USER, answer);
const r = await restart(t, f, { timeoutMs: 400 });
assert.equal(f.ended(id), undefined, JSON.stringify(answer));
assert.deepEqual(f.userTokens().map((x) => x.id).sort(), [live.token_id, next.id].sort());
assert.equal(r.requests.filter((q) => q.method === 'DELETE').length, 0);
assert.equal(f.record().token_id, live.token_id);
assert.deepEqual(openGiteaGrants(f.store, 'demo').map((o) => o.decision), [id]);
}
});
test('X-G6: a file token no grant minted is held; the next restart says so again; a revoke clears it and takes both ids', async (t) => {
const f = await setup(t);
const { live, next, id } = await openRotation(f);
const stray = f.gitea.addToken(USER, 'stray', SEAT_SCOPES);
writeFileSync(f.tokenFile, stray.sha1 + '\n');
const r1 = await restart(t, f);
const end = f.ended(id);
assert.equal(end.code, 'grant-token-unknown');
assert.equal(end.tokenId, stray.id);
assert.equal(r1.state, 'held');
assert.ok(existsSync(f.tokenFile));
assert.deepEqual([...tokenUnknown(f.store, 'demo').keys()], ['coder']);
const before = f.messages(id).length;
const r2 = await restart(t, f);
assert.deepEqual(r2.held, ['demo/coder:gitea']);
assert.equal(r2.state, 'held');
assert.equal(r2.requests.length, 0);
assert.equal(f.messages(id).length, before + 2);
assert.match(f.messages(id).at(-1).body, /still holds a token no grant minted/);
// The revoke deletes the live token and both ids of the last commit step; the stray one stays for Jason.
const out = await r2.broker.request(r2.human, { verb: 'grants.revoke', args: { service: 'gitea', agent: 'coder', reason: 'test' } });
assert.deepEqual(out.serviceLeft, []);
const revoked = f.grantEvents().filter((e) => e.action === 'grant.revoked').at(-1);
assert.deepEqual(revoked.tokens.sort(), [live.token_id, next.id].sort());
assert.deepEqual(f.userTokens().map((x) => x.id), [stray.id]);
assert.deepEqual([...tokenUnknown(f.store, 'demo').keys()], []);
const r3 = await restart(t, f);
assert.deepEqual(r3.held, []);
});
test('N9: a revoke of an open rotation, then a restart: no settle, no Gitea call and no message', async (t) => {
const f = await setup(t);
const { live, next, id } = await openRotation(f);
const out = await f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'test' });
assert.deepEqual(out.serviceLeft, []);
assert.deepEqual(f.grantEvents().find((e) => e.action === 'grant.revoked').tokens.sort(), [live.token_id, next.id].sort());
assert.deepEqual(f.userTokens(), []);
const status = await f.call('human', 'grants.status', { service: 'gitea' });
assert.deepEqual(status.settlePending, []);
const before = f.messages(id).length;
const r = await restart(t, f);
assert.deepEqual(r.requests, []);
const end = f.ended(id);
assert.equal(end.code, 'revoked');
assert.equal(end.settled, undefined);
assert.equal(f.messages(id).length, before);
assert.equal(f.record(), undefined);
assert.equal(r.state, undefined);
});
test('N9: a 404 on a delete by id counts as done', async (t) => {
const f = await setup(t);
const { next } = await openRotation(f);
const u = f.gitea.users.get(USER);
u.tokens.splice(u.tokens.findIndex((x) => x.id === next.id), 1);
const out = await f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'test' });
assert.deepEqual(out.serviceLeft, []);
assert.ok(f.gitea.calls().includes(`DELETE /api/v1/users/${USER}/tokens/${next.id}`));
});
test('X-G6: a restart undoes each step a killed child left, and spends an approved unspent decision', async (t) => {
const f = await setup(t);
// user: a user made with the nonce is adopted; one without it is an orphan; none is interrupted.
const nonce = 'aaaaaaaaaaaaaaaa';
f.gitea.addUser(USER, { full_name: `Coder [mosaic ${nonce}]` });
const u1 = interrupted(f, [['user', { nonce }]]);
let r = await restart(t, f);
assert.equal(f.ended(u1).code, 'interrupted');
assert.equal(f.userRecord().id, f.gitea.users.get(USER).id);
assert.equal(f.userRecord().nonce, nonce);
const g = await setup(t);
g.gitea.addUser(USER, { full_name: 'Someone' });
const u2 = interrupted(g, [['user', { nonce }]]);
await restart(t, g);
assert.equal(g.ended(u2).code, 'gitea-user-orphan');
assert.equal(g.userRecord(), undefined);
const h = await setup(t);
const u3 = interrupted(h, [['user', { nonce }]]);
await restart(t, h);
assert.equal(h.ended(u3).code, 'interrupted');
assert.equal(h.userRecord(), undefined);
// mint by name, minted by id, a first grant's commit by id with its file unlinked.
const k = await setup(t);
k.gitea.addUser(USER);
const named = k.gitea.addToken(USER, tokenName('demo', 'coder', 'x1'), SEAT_SCOPES);
const m1 = interrupted(k, [['mint', { tokenName: named.name }]]);
// X-G8: a call the child wrote before it was killed, with no result, is listed in the end.
writeGrantEvent(k.store, AT, 'demo', { action: 'grant.call', callId: 'c-open', reason: m1, method: 'gitea', op: 'token-create' }, 'coder');
r = await restart(t, k);
assert.equal(k.ended(m1).code, 'interrupted');
assert.deepEqual(k.ended(m1).unclosedCalls, [{ callId: 'c-open', op: 'token-create' }]);
assert.match(k.messages(m1)[0].body, /Calls with no result: token-create\./);
assert.deepEqual(k.userTokens(), []);
// One executor per agent (grant-busy), so a killed child leaves one open grant per agent: its own store.
const m = await setup(t);
m.gitea.addUser(USER);
const byId = m.gitea.addToken(USER, 'x2', SEAT_SCOPES);
const m2 = interrupted(m, [['mint', { tokenName: 'x2' }], ['minted', { tokenId: byId.id }]]);
await restart(t, m);
assert.equal(m.ended(m2).code, 'interrupted');
assert.equal(m.ended(m2).serviceLeft, undefined);
assert.ok(m.gitea.calls().includes(`DELETE /api/v1/users/${USER}/tokens/${byId.id}`));
assert.deepEqual(m.userTokens(), []);
const c = await setup(t);
c.gitea.addUser(USER);
const tok = c.gitea.addToken(USER, 'x3', SEAT_SCOPES);
const c1 = interrupted(c, [['commit', { previousTokenId: null, tokenId: tok.id, tokenName: 'x3', previous: null }]]);
mkdirSync(join(c.storeRoot, 'demo', 'coder'), { recursive: true, mode: 0o700 });
writeFileSync(c.tokenFile, tok.sha1 + '\n', { mode: 0o600 });
r = await restart(t, c);
assert.deepEqual(r.held, ['demo/coder:gitea']);
assert.equal(c.ended(c1).code, 'interrupted');
assert.equal(existsSync(c.tokenFile), false);
assert.deepEqual(c.userTokens(), []);
assert.equal(r.state, undefined);
// An approved decision no child spent is spent at start and ends interrupted, with no Gitea call.
const o = await setup(t);
const open = o.fixtureGrant();
const status = await o.call('human', 'grants.status', { service: 'gitea' });
assert.deepEqual(status.open, [{ decision: open, agent: 'coder', spentAtNextStart: true }]);
r = await restart(t, o);
assert.deepEqual(r.now, [open]);
assert.equal(o.ended(open).code, 'interrupted');
assert.ok(o.grantEvents().some((e) => e.operation === 'grant.gitea' && e.decision === open));
assert.deepEqual(r.requests, []);
});
test('X-G6: a restart with no admin slot leaves the user an orphan and the token left', async (t) => {
const f = await setup(t);
const id = interrupted(f, [['mint', { tokenName: 'x1' }], ['minted', { tokenId: 41 }]]);
rmSync(join(f.configDir, 'secrets', 'grants'), { recursive: true });
await restart(t, f);
assert.deepEqual(f.ended(id).serviceLeft, [41]);
const u = interrupted(f, [['user', { nonce: 'bbbbbbbbbbbbbbbb' }]]);
await restart(t, f);
assert.equal(f.ended(u).code, 'gitea-user-orphan');
});
test('X-G7: a change to the definition, identity, publish remotes or admin slot after the raise ends decision-mismatch', async (t) => {
// The broker holds its own copy of the business, so each change is a binding the raise saw before it
// (as row 234's G9 does): the decision's hash differs from what step 1 hashes now.
const f = await setup(t);
const b = f.business;
const meta = f.grantApi.adminMeta('demo');
for (const over of [
{ definitionSha256: definitionSha256({ ...b.definitions.coder, roleVersion: 3 }) },
{ identitySha256: identitySha256({ ...b.roles.coder, identity: { ...b.roles.coder.identity, email: '[email protected]' } }, b.projects) },
{ identitySha256: identitySha256(b.roles.coder, { ...b.projects, lib: { remote: `${f.gitea.url}/acme/lib2` } }) },
{ adminSha256: adminSha256({ ...meta, setAt: '2026-01-01T00:00:00.000Z' }) },
]) {
const id = f.fixtureGrant(over);
await f.call('human', 'grants.run', { decision: id });
await f.broker.giteaIdle();
assert.equal(f.ended(id).code, 'decision-mismatch', JSON.stringify(over));
// The spend rolled back with the recheck.
assert.equal(f.grantEvents().some((e) => e.operation === 'grant.gitea' && e.decision === id), false);
}
assert.equal(f.gitea.calls().some((c) => c.includes('/admin/users') || c.includes('/tokens')), false);
});
test('X-G7 and X-G9: an admin rotate voids open decisions, and rotate --finish deletes what was left', async (t) => {
const f = await setup(t);
const open = f.fixtureGrant();
const offDelete = f.gitea.rule((e) => e.method === 'DELETE' && e.path.endsWith('/tokens/mosaic-grant-bootstrap'), { status: 500, json: {} });
const out = await f.call('human', 'grants.admin.rotate', { service: 'gitea', rotateBy: dayFrom(90) });
offDelete();
assert.deepEqual(out.voided, [open]);
assert.equal(out.adminTokenLeft, 'mosaic-grant-bootstrap');
assert.match(out.tokenName, /^mosaic-grant-\d{8}T\d{6}Z$/);
assert.equal(f.grantApi.adminMeta('demo').rotateBy, dayFrom(90));
const successor = f.gitea.admin.tokens.find((x) => x.name === out.tokenName);
assert.ok(readFileSync(join(f.configDir, 'secrets', 'grants', 'demo', 'gitea', 'admin'), 'utf8') === successor.sha1 + '\n');
assert.equal((await f.call('human', 'grants.status', { service: 'gitea' })).adminTokenLeft, 'mosaic-grant-bootstrap');
await f.call('human', 'grants.run', { decision: open });
await f.broker.giteaIdle();
assert.equal(f.ended(open).code, 'decision-mismatch');
const done = await f.call('human', 'grants.admin.rotate', { service: 'gitea', finish: true });
assert.deepEqual(done, { deleted: 'mosaic-grant-bootstrap' });
assert.deepEqual(f.gitea.admin.tokens.map((x) => x.name), [out.tokenName]);
await rejects(f.call('human', 'grants.admin.rotate', { service: 'gitea', finish: true }), 'grant-admin-nothing-left');
assert.deepEqual(f.grantEvents().filter((e) => e.action === 'grant.admin').map((e) => e.verb), ['rotate', 'rotate-finish']);
f.noLeak();
});
test('X-G9: a rotate whose name is taken refuses and writes nothing; remove takes the slot away', async (t) => {
const f = await setup(t);
const slotFile = join(f.configDir, 'secrets', 'grants', 'demo', 'gitea', 'admin');
const before = readFileSync(slotFile);
const now = Date.now();
for (let s = -1; s < 6; s++) {
const stamp = new Date(now + s * 1000).toISOString().slice(0, 19).replace(/[-:]/g, '') + 'Z';
f.gitea.addToken('mosaic-grant', `mosaic-grant-${stamp}`, ['write:admin', 'write:repository', 'write:user']);
}
await rejects(f.call('human', 'grants.admin.rotate', { service: 'gitea', rotateBy: dayFrom(90) }), 'grant-admin-name-taken');
assert.ok(readFileSync(slotFile).equals(before));
await rejects(f.call('human', 'grants.admin.rotate', { service: 'gitea', rotateBy: '2020-01-01' }), 'invalid-request');
await rejects(f.call('human', 'grants.admin.rotate', { service: 'gitea', rotateBy: dayFrom(90), finish: true }), 'invalid-request');
await rejects(f.call('chief', 'grants.admin.remove', { service: 'gitea' }), 'human-required');
assert.deepEqual(await f.call('human', 'grants.admin.remove', { service: 'gitea' }), { removed: true });
assert.equal(existsSync(join(f.configDir, 'secrets', 'grants', 'demo', 'gitea')), false);
await rejects(f.call('human', 'grants.admin.remove', { service: 'gitea' }), 'grant-admin-missing');
await rejects(f.raise(), 'grant-admin-missing');
});
test('X-G9: a slot past its rotate-by date refuses a run at step 0, leaving the decision unspent', async (t) => {
const f = await setup(t);
const open = f.fixtureGrant();
f.grantApi.writeAdmin('demo', f.gitea.adminToken, { ...f.meta, rotateBy: '2026-01-01' });
const status = await f.call('human', 'grants.status', { service: 'gitea' });
assert.equal(status.admin.rotationDue, true);
await f.call('human', 'grants.run', { decision: open });
await f.broker.giteaIdle();
assert.equal(f.ended(open).code, 'grant-admin-rotation-due');
assert.equal(f.gitea.requests.length, 0);
const before = await f.call('human', 'grants.status', { service: 'gitea' });
assert.deepEqual(before.open, [{ decision: open, agent: 'coder', spentAtNextStart: true }]);
// X-G11: a restart spends it as interrupted, and the human's run then refuses decision-consumed.
const r = await restart(t, f);
assert.deepEqual(r.now, [open]);
assert.equal(f.ended(open).code, 'interrupted');
await rejects(r.broker.request(r.human, { verb: 'grants.run', args: { decision: open } }), 'decision-consumed');
});
test('X-G11: run refuses every caller but the human, an unapproved, a spent and a busy decision', async (t) => {
const f = await setup(t);
const open = f.fixtureGrant();
await rejects(f.call('chief', 'grants.run', { decision: open }), 'human-required');
await rejects(f.call('tester', 'grants.run', { decision: open }), 'human-required');
const unapproved = f.fixtureGrant({}, null);
await rejects(f.call('human', 'grants.run', { decision: unapproved }), 'not-authorized');
const tester = f.fixtureGrant({}, { op: 'resolved', by: 'tester', choice: 'yes', via: 'broker' });
await rejects(f.call('human', 'grants.run', { decision: tester }), 'not-authorized');
const signal = f.keys.claim('demo', 'coder');
await rejects(f.call('human', 'grants.run', { decision: open }), 'grant-busy');
f.keys.release('demo', 'coder', signal);
await f.call('human', 'grants.run', { decision: open });
await f.broker.giteaIdle();
assert.equal(f.ended(open).action, 'grant.applied');
await rejects(f.call('human', 'grants.run', { decision: open }), 'decision-consumed');
await rejects(f.call('human', 'grants.run', { decision: open, extra: 1 }), 'invalid-request');
// status: the human, the tester and the chief; no other role.
for (const who of ['human', 'tester', 'chief']) assert.ok(await f.call(who, 'grants.status', { service: 'gitea' }));
await rejects(f.call('pm', 'grants.status', { service: 'gitea' }), 'grant-chief-only');
assert.deepEqual(f.grantEvents().filter((e) => 'decision' in e).map((e) => e.operation), ['grant.gitea']);
});
test('kill switch (plan :16): while halted the chief still reads grants.status, and an agent grants.revoke refuses', async (t) => {
const control = { halted: false, version: 1, generation: randomUUID() };
const f = await setup(t, { processing: { read: () => ({ ...control }) } });
await f.approve();
control.halted = true;
const status = await f.call('human', 'grants.status', { service: 'gitea' });
assert.deepEqual(await f.call('chief', 'grants.status', { service: 'gitea' }), status);
// The gate throws before dispatch, as in processing.test.mjs, so the call is wrapped.
await rejects(async () => f.call('pm', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'r' }), 'processing-halted');
assert.equal(f.record().agent, 'coder');
assert.equal(f.state(), 'valid');
});
test('X-G11: a revoke while the executor runs aborts it and the grant ends revoked', async (t) => {
const f = await setup(t);
let release;
const held = new Promise((r) => (release = r));
f.gitea.rule((e) => e.method === 'POST' && e.path.endsWith('/forks'), async () => {
await held;
return { pass: true };
});
const d = await f.raise();
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.gitea.calls().some((c) => c.endsWith('/forks')));
const revoke = f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'stop' });
release();
await revoke;
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
assert.equal(f.ended(d.id).code, 'revoked');
assert.equal(f.record(), undefined);
assert.deepEqual(f.userTokens(), []);
});
test('X-G11: a revoke unloads memory and removes the file before its Gitea delete', async (t) => {
const f = await setup(t);
const live = (await f.approve()).end;
assert.equal(live.action, 'grant.applied');
assert.ok(existsSync(f.tokenFile));
let release;
const held = new Promise((r) => (release = r));
const seen = {};
f.gitea.rule((e) => e.method === 'DELETE' && e.path === `/api/v1/users/${USER}/tokens/${live.tokenId}`, async () => {
seen.file = existsSync(f.tokenFile);
seen.state = f.state();
await held;
return { pass: true };
});
const out = f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'stop' });
await until(() => 'file' in seen);
release();
assert.deepEqual((await out).serviceLeft, []);
assert.equal(seen.file, false);
assert.notEqual(seen.state, 'valid');
assert.deepEqual(f.userTokens(), []);
});
test('X-G11: a revoke during a rotation step 7 ends it revoked, and step 8 writes no record back', async (t) => {
const f = await setup(t);
await f.approve();
const oldId = f.record().token_id;
let release;
const held = new Promise((r) => (release = r));
f.gitea.rule((e) => e.method === 'DELETE' && e.path === `/api/v1/users/${USER}/tokens/${oldId}`, async () => {
await held;
return { pass: true };
});
const d = await f.raise();
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.gitea.calls().includes(`DELETE /api/v1/users/${USER}/tokens/${oldId}`));
const revoke = f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'stop' });
release();
await revoke;
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
assert.equal(f.ended(d.id).code, 'revoked');
assert.equal(f.grantEvents().some((e) => e.action === 'grant.applied' && e.grantDecision === d.id), false);
assert.equal(f.record(), undefined);
assert.deepEqual(f.userTokens(), []);
});
test('X-G11: a revoke while a sweep settles a rotation, forward or back, ends it revoked with no record', async (t) => {
for (const forward of [true, false]) {
const f = await setup(t);
const { live, next, id } = await openRotation(f);
if (forward) writeFileSync(f.tokenFile, next.sha1 + '\n');
// Forward deletes the old token, back the new one: hold that delete.
const target = forward ? live.token_id : next.id;
let release;
const held = new Promise((r) => (release = r));
f.gitea.rule((e) => e.method === 'DELETE' && e.path === `/api/v1/users/${USER}/tokens/${target}`, async () => {
await held;
return { pass: true };
});
const sweep = f.call('human', 'grants.sweep', { service: 'gitea' });
await until(() => f.gitea.calls().includes(`DELETE /api/v1/users/${USER}/tokens/${target}`));
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' }], forward ? 'forward' : 'back');
assert.equal(f.ended(id).code, 'revoked');
assert.equal(f.grantEvents().some((e) => e.grantDecision === id && e.settled), false);
assert.equal(f.record(), undefined);
assert.deepEqual(f.userTokens(), []);
}
});
// 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) => {
const collab = await setup(t);
collab.gitea.addRepo('other', 'secret', { collaborators: [USER] });
let r = await collab.approve();
assert.equal(r.end.code, 'gitea-reach-wider');
assert.equal(r.end.repository, 'other/secret');
assert.match(collab.messages(r.id)[0].body, /It reaches other\/secret\./);
assert.deepEqual(collab.userTokens(), []);
assert.equal(existsSync(collab.tokenFile), false);
// A fork of a project that left publish.
const f = await setup(t);
assert.equal((await f.approve()).end.action, 'grant.applied');
f.gitea.addRepo(USER, 'old', { parent: 'acme/old' });
r = await f.approve();
assert.equal(r.end.code, 'gitea-reach-wider');
assert.equal(r.end.repository, `${USER}/old`);
assert.equal(f.state(), 'valid');
// An old fork of a publish project outside target.projects passes.
const ok = await setup(t);
assert.equal((await ok.approve()).end.action, 'grant.applied');
ok.gitea.addRepo(USER, 'lib', { parent: 'acme/lib' });
r = await ok.approve();
assert.equal(r.end.action, 'grant.applied');
assert.deepEqual(ok.userTokens().map((x) => x.id), [r.end.tokenId]);
});
test('X-G12: reach is paged by X-Total-Count and a count that does not add up refuses', async (t) => {
const f = await setup(t);
assert.equal((await f.approve()).end.action, 'grant.applied');
for (let i = 0; i < 51; i++) f.gitea.addRepo(USER, `app${i}`, { parent: 'acme/app' });
const before = f.gitea.requests.length;
let r = await f.approve();
assert.equal(r.end.action, 'grant.applied');
const pages = f.gitea.requests.slice(before).filter((q) => q.path.startsWith('/api/v1/user/repos') && q.as === USER).map((q) => q.path);
assert.deepEqual(pages, ['/api/v1/user/repos?limit=50&page=1', '/api/v1/user/repos?limit=50&page=2']);
const off = f.gitea.rule((e) => e.path.startsWith('/api/v1/user/repos') && e.as === USER, { status: 200, json: [], total: 5 });
r = await f.approve();
off();
assert.equal(r.end.code, 'gitea-reach-unknown');
const noTotal = f.gitea.rule((e) => e.path.startsWith('/api/v1/user/repos') && e.as === USER, { status: 200, json: [] });
r = await f.approve();
noTotal();
assert.equal(r.end.code, 'gitea-reach-unknown');
});
test('X-G13: an agent cannot record a grant event, and the trigger refuses one from any writer but the grant writer', async (t) => {
const f = await setup(t);
for (const body of [
{ action: 'grant.applied', method: 'gitea', agent: 'coder' },
{ action: 'grant.revoked', method: 'gitea', agent: 'coder' },
{ operation: 'grant.gitea', decision: randomUUID() },
])
assert.throws(() => f.broker.recordEvent(f.caps.chief, { kind: 'action.allowed', body }), (e) => (assert.equal(e.code, 'reserved-event'), true));
assert.throws(() =>
f.store.run(
"INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,'action.allowed',NULL,NULL,'coder',?)",
randomUUID(), AT, 'demo', JSON.stringify({ action: 'grant.step', method: 'gitea' }),
),
/only the grant writer/);
// The grant writer takes grant.* bodies only.
assert.throws(() => writeGrantEvent(f.store, AT, 'demo', { action: 'host.sudo', host: 'web1' }, 'web1'), /grant event shape/);
});
test('X-G14: credentials delete fires the user: tokens go, login is prohibited, and grant.revoke.closed records it', async (t) => {
const f = await setup(t);
await f.approve();
f.gitea.addToken(USER, 'extra', SEAT_SCOPES);
const out = f.call('human', 'credentials.delete', { agent: 'coder', reason: 'fired' });
assert.match(out.giteaRevoke, /^[0-9a-f-]{36}$/);
await until(() => f.grantEvents().some((e) => e.action === 'grant.revoke.closed'));
await f.broker.giteaIdle();
const closed = f.grantEvents().find((e) => e.action === 'grant.revoke.closed');
assert.deepEqual(closed.serviceLeft, []);
assert.equal(closed.revoke, out.giteaRevoke);
assert.deepEqual(f.userTokens(), []);
assert.equal(f.gitea.users.get(USER).prohibit_login, true);
assert.equal(f.record(), undefined);
const revoked = f.grantEvents().find((e) => e.action === 'grant.revoked');
assert.equal(revoked.fire, true);
assert.equal(revoked.by, 'jason');
// A plain revoke through the credentials verb takes the grant but leaves the user.
const g = await setup(t);
await g.approve();
g.call('human', 'credentials.revoke', { agent: 'coder', service: 'gitea', reason: 'r' });
await until(() => g.grantEvents().some((e) => e.action === 'grant.revoke.closed'));
await g.broker.giteaIdle();
assert.equal(g.gitea.users.get(USER).prohibit_login, false);
assert.deepEqual(g.userTokens(), []);
f.noLeak();
});
test('X-G14: a fire whose token delete or login PATCH fails lands it in serviceLeft', async (t) => {
const f = await setup(t);
await f.approve();
const extra = f.gitea.addToken(USER, 'extra', SEAT_SCOPES);
const offDelete = f.gitea.rule((e) => e.method === 'DELETE' && e.path.endsWith(`/tokens/${extra.id}`), { status: 500, json: {} });
const offPatch = f.gitea.rule((e) => e.method === 'PATCH', { status: 500, json: {} });
f.call('human', 'credentials.delete', { agent: 'coder', reason: 'fired' });
await until(() => f.grantEvents().some((e) => e.action === 'grant.revoke.closed'));
await f.broker.giteaIdle();
offDelete();
offPatch();
const closed = f.grantEvents().find((e) => e.action === 'grant.revoke.closed');
assert.deepEqual(closed.serviceLeft, ['tokens', 'login']);
assert.equal(f.gitea.users.get(USER).prohibit_login, false);
assert.deepEqual(f.userTokens().map((x) => x.id), [extra.id]);
});
test('X-G14: a fire whose user is gone or remade skips the fire steps and says why', async (t) => {
const f = await setup(t);
await f.approve();
f.gitea.removeUser(USER);
f.call('human', 'credentials.delete', { agent: 'coder', reason: 'fired' });
await until(() => f.grantEvents().some((e) => e.action === 'grant.revoke.closed'));
await f.broker.giteaIdle();
assert.equal(f.grantEvents().find((e) => e.action === 'grant.revoke.closed').fireSkipped, 'gitea-user-gone');
const g = await setup(t);
await g.approve();
g.gitea.removeUser(USER);
g.gitea.addUser(USER);
g.call('human', 'credentials.delete', { agent: 'coder', reason: 'fired' });
await until(() => g.grantEvents().some((e) => e.action === 'grant.revoke.closed'));
await g.broker.giteaIdle();
assert.equal(g.grantEvents().find((e) => e.action === 'grant.revoke.closed').fireSkipped, 'gitea-user-foreign');
assert.equal(g.gitea.users.get(USER).prohibit_login, false);
});
test('X-G14: a sweep retries a fire only for the user it fired: a remade or gone user is skipped and named', async (t) => {
for (const reason of ['gitea-user-foreign', 'gitea-user-gone']) {
const f = await setup(t);
await f.approve();
// The fire's user check answers 500, so both fire steps are left for the sweep.
const off = f.gitea.rule((e) => e.method === 'GET' && e.path === `/api/v1/users/${USER}`, { status: 500, json: {} });
f.call('human', 'credentials.delete', { agent: 'coder', reason: 'fired' });
await until(() => f.grantEvents().some((e) => e.action === 'grant.revoke.closed'));
await f.broker.giteaIdle();
off();
assert.deepEqual(f.grantEvents().find((e) => e.action === 'grant.revoke.closed').serviceLeft, ['tokens', 'login']);
f.gitea.removeUser(USER);
if (reason === 'gitea-user-foreign') f.gitea.addUser(USER, { scopes: SEAT_SCOPES, tokenName: 'theirs' });
const before = f.gitea.requests.length;
const swept = await f.call('human', 'grants.sweep', { service: 'gitea' });
const after = f.gitea.requests.slice(before).map((r) => `${r.method} ${r.path.split('?')[0]}`);
assert.equal(after.some((c) => c.startsWith('PATCH ') || c.startsWith('DELETE ') || c.endsWith('/tokens')), false, reason);
assert.deepEqual(swept.left, []);
const skipped = [{ agent: 'coder', user: USER, id: 'tokens', reason }, { agent: 'coder', user: USER, id: 'login', reason }];
assert.deepEqual(swept.skipped, skipped);
assert.deepEqual(f.grantEvents().findLast((e) => e.action === 'grant.swept').skipped, skipped);
if (reason === 'gitea-user-foreign') {
assert.equal(f.gitea.users.get(USER).prohibit_login, false);
assert.deepEqual(f.userTokens().map((x) => x.name), ['theirs']);
}
// Skipped is not left: the next sweep has nothing to retry.
assert.deepEqual((await f.call('human', 'grants.status', { service: 'gitea' })).serviceLeft, []);
}
});
test('X-G14: a revoke Gitea cannot reach stays open, and a restart closes it', async (t) => {
const f = await setup(t);
await f.approve();
const tokenId = f.record().token_id;
const off = f.gitea.rule(() => true, { status: 500, json: {} });
const out = await f.call('tester', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'r' });
assert.deepEqual(out.serviceLeft, [tokenId]);
off();
const status = await f.call('human', 'grants.status', { service: 'gitea' });
assert.deepEqual(status.serviceLeft, [{ agent: 'coder', user: USER, id: tokenId }]);
await f.call('human', 'grants.sweep', { service: 'gitea' });
assert.deepEqual(f.userTokens(), []);
await rejects(f.call('pm', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'r' }), 'revoke-refused');
});
// Row 281 (#1786, d6 section 8, T8): grants.revoke shares its Gitea steps with a fire's step G now. This
// pins the tester's dev-only refusal, the verb's records and their order, and that it starts no job.
test('row 281 T8: a tester grants.revoke refuses tester-dev-only once bus.tester is off; the human revoke keeps its records and starts no job', async (t) => {
const f = await setup(t);
await f.approve();
f.broker.reloadBusiness('demo', { ...f.business, vars: { 'bus.tester': 'yes' } });
const before = f.grantEvents().length;
await rejects(f.call('tester', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'r' }), 'tester-dev-only');
assert.equal(f.grantEvents().length, before);
assert.ok(f.record());
await f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'r' });
const after = f.grantEvents().slice(before)
.filter((e) => e.action !== 'grant.call' && e.action !== 'grant.result')
.map((e) => [e.action, e.fire ?? false]);
assert.deepEqual(after, [['grant.revoked', false], ['grant.revoke.closed', false]]);
assert.deepEqual(f.userTokens(), []);
const jobs = f.store.all("SELECT body FROM events WHERE business='demo' AND kind='action.allowed'").map((e) => JSON.parse(e.body).action)
.filter((a) => /^(fire|grants)\./.test(a));
assert.deepEqual(jobs, []);
});
test('X-G15: look-alike commit steps outside the grant writer are ignored at start, by revoke and by status', async (t) => {
const f = await setup(t);
const first = await f.approve();
const live = f.record();
const fake = { action: 'grant.step', method: 'gitea', grantDecision: first.id, agent: 'coder', user: USER, step: 'commit', previousTokenId: 999, tokenId: 998, tokenName: 'x', previous: null };
f.store.run(
"INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,'action.refused',NULL,'grant','coder',?)",
randomUUID(), AT, 'demo', JSON.stringify(fake),
);
f.store.run(
"INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,'human.input',NULL,NULL,'coder',?)",
randomUUID(), AT, 'demo', JSON.stringify({ ...fake, action: 'grant.failed', code: 'grant-token-unknown' }),
);
assert.deepEqual(openGiteaGrants(f.store, 'demo'), []);
assert.deepEqual([...tokenUnknown(f.store, 'demo').keys()], []);
const r = await restart(t, f);
assert.deepEqual(r.held, []);
assert.deepEqual(r.requests, []);
assert.equal(r.state, 'valid');
const out = await r.broker.request(r.human, { verb: 'grants.revoke', args: { service: 'gitea', agent: 'coder', reason: 'r' } });
assert.deepEqual(out.serviceLeft, []);
assert.deepEqual(f.grantEvents().find((e) => e.action === 'grant.revoked').tokens, [live.token_id]);
});
test('X-G15: a look-alike spend under another kind with the grant writer pair is not an open grant', async (t) => {
const f = await setup(t);
const id = f.fixtureGrant({}, null);
f.store.run(
"INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,'action.refused',NULL,'grant',?,?)",
randomUUID(), AT, 'demo', id, JSON.stringify({ operation: 'grant.gitea', decision: id }),
);
assert.deepEqual(openGiteaGrants(f.store, 'demo'), []);
// The same body as the grant writer's spend is open.
writeGrantEvent(f.store, AT, 'demo', { operation: 'grant.gitea', decision: id }, id);
assert.deepEqual(openGiteaGrants(f.store, 'demo').map((o) => o.decision), [id]);
});
test('status lists the slot, users, grants and nothing secret, and every grants verb needs service gitea', async (t) => {
const f = await setup(t);
await f.approve();
const s = await f.call('chief', 'grants.status', { service: 'gitea' });
assert.deepEqual(s.admin, { ...f.meta, rotationDue: false });
assert.equal(s.adminTokenLeft, null);
assert.deepEqual(s.users.map((u) => u.agent), ['coder']);
assert.deepEqual(s.grants.map((g) => [g.agent, g.projects]), [['coder', ['app']]]);
for (const k of ['open', 'running', 'settlePending', 'tokenUnknown', 'serviceLeft', 'revoking']) assert.deepEqual(s[k], [], k);
await rejects(f.call('human', 'grants.status', { service: 'host' }), 'invalid-request');
await rejects(f.call('human', 'grants.sweep', {}), 'invalid-request');
const bare = new Broker({ store: f.store, businesses: { demo: f.business } });
const human = bare.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
await rejects(bare.request(human, { verb: 'grants.status', args: { service: 'gitea' } }), 'gitea-not-configured');
f.noLeak();
});
// C2-21: these catch library forking, wrong collaborator/token level, widened reach, and stale bindings.
function libraryBusiness(b, { agent = 'coder', definition = 'research', publish = ['app'], library = ['app'] } = {}) {
const original = b.roles.coder;
b.roles[agent] = { ...original, definition, publish,
identity: { ...original.identity, gitea: { ...original.identity.gitea, user: agent === 'chief' ? 'demo-chief' : USER } } };
b.definitions[definition] = { roleVersion: 2, credentials: [{ service: 'gitea',
scopes: definition === 'chief-of-staff' ? ['read:repository', 'read:user'] : ['read:user', 'write:repository'] }] };
for (const project of library) b.projects[project].kind = 'library';
}
for (const [definition, agent, level, scopes] of [
['research', 'coder', 'write', ['read:user', 'write:repository']],
['chief-of-staff', 'chief', 'read', ['read:repository', 'read:user']],
['coder', 'coder', 'read', ['read:user', 'write:repository']],
]) {
test(`C2-21 L1/L2/L9: ${definition} gets ${level} on her repository with declared scopes through raise and yes`, async (t) => {
const f = await setup(t, { business: (b) => {
libraryBusiness(b, { agent, definition });
b.launch = { by: 'chief', instances: ['coder', 'pm'] };
} });
const user = agent === 'chief' ? 'demo-chief' : USER;
const puts = [];
f.gitea.rule((e) => e.method === 'PUT' && e.path.includes('/collaborators/'), (e, body) => {
puts.push({ path: e.path, as: e.as, auth: e.auth, body });
return { pass: true };
});
const d = await f.raise({ agent });
const block = f.store.get('SELECT question FROM decisions WHERE id=?', d.id).question;
assert.ok(block.includes(`Repository: ${f.gitea.url}/acme/app, level ${level}.`));
assert.doesNotMatch(block, /\nForks:/);
assert.ok(block.includes(`Scopes: ${scopes.join(', ')}.`));
assert.ok(block.includes(level === 'write'
? "The token can write every path of this repository. Only the bus's path check limits it to supporting folders."
: "The token can read every path of this repository. It can't push to it, because the user is a read collaborator."));
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
const end = f.ended(d.id);
assert.equal(end.action, 'grant.applied', JSON.stringify(end));
assert.deepEqual(end.forks, []);
assert.deepEqual(end.repositories, [{ project: 'app', repository: `${f.gitea.url}/acme/app`, level }]);
assert.deepEqual(puts, [{ path: `/api/v1/repos/acme/app/collaborators/${user}`, as: 'mosaic-grant', auth: 'token', body: { permission: level } }]);
assert.equal(f.gitea.calls().filter((c) => c.endsWith('/forks')).length, 0);
assert.deepEqual(f.gitea.tokens(user)[0].scopes, scopes);
const messages = f.messages(d.id).map((m) => m.body).join('\n');
assert.ok(messages.includes(`repositories: ${f.gitea.url}/acme/app (${level})`));
assert.doesNotMatch(messages, /forks: \./);
const results = f.grantEvents();
assert.ok(results.some((e) => e.action === 'grant.call' && e.op === 'collaborator-add'));
f.noLeak();
// L5: token revocation leaves the collaborator, just as legacy forks remain.
await f.call('human', 'grants.revoke', { service: 'gitea', agent, reason: 'C2-21' });
await f.broker.giteaIdle();
assert.deepEqual(f.gitea.tokens(user), []);
assert.ok(f.gitea.repos.find((r) => r.owner === 'acme' && r.name === 'app').collaborators.includes(user));
});
}
for (const [agent, definition] of [['coder', 'research'], ['chief', 'chief-of-staff']]) {
for (const projects of [['lib'], ['app', 'lib']]) {
test(`C2-21 L7/L7b: ${definition} refuses non-library targets ${projects.join(',')}`, async (t) => {
const f = await setup(t, { business: (b) => libraryBusiness(b, { agent, definition, publish: ['app', 'lib'] }) });
await rejects(f.raise({ agent, target: { projects } }), 'gitea-not-declared');
assert.equal(f.store.get('SELECT count(*) AS n FROM decisions').n, 0);
assert.equal(f.gitea.requests.length, 0);
});
}
}
for (const [name, permissions, fork] of [
['read widened to write', { admin: false, push: true, pull: true }, false],
['admin privilege', { admin: true, push: false, pull: true }, false],
['missing pull', { admin: false, push: false, pull: false }, false],
['missing permission object', null, false],
['library fork', { admin: false, push: false, pull: true }, true],
]) {
test(`C2-21 L4: ${name} refuses gitea-reach-wider and deletes the minted token`, async (t) => {
const f = await setup(t, { business: (b) => libraryBusiness(b, { agent: 'chief', definition: 'chief-of-staff' }) });
const repository = fork ? 'demo-chief/app' : 'acme/app';
f.gitea.rule((e) => e.as === 'demo-chief' && e.path.startsWith('/api/v1/user/repos?'), { status: 200, total: 1, json: [{
full_name: repository, owner: { login: fork ? 'demo-chief' : 'acme' },
parent: fork ? { full_name: 'acme/app' } : null, permissions,
}] });
const { end } = await f.approve({ agent: 'chief' });
assert.equal(end.code, 'gitea-reach-wider', JSON.stringify(end));
assert.equal(end.repository, repository);
assert.deepEqual(f.gitea.tokens('demo-chief'), []);
assert.equal(f.store.get("SELECT 1 FROM gitea_grants WHERE agent='chief'"), undefined);
f.noLeak();
});
}
test('C2-21 L8: untargeted library collaborator reach is disclosed and accepted publish-wide', async (t) => {
const f = await setup(t, { business: (b) => libraryBusiness(b, { publish: ['app', 'lib'], library: ['app', 'lib'] }) });
f.gitea.repos.find((r) => r.owner === 'acme' && r.name === 'lib').collaborators.push(USER);
const d = await f.raise();
const block = f.store.get('SELECT question FROM decisions WHERE id=?', d.id).question;
assert.ok(block.includes(`It may also reach these repositories as a collaborator: ${f.gitea.url}/acme/lib (write)`));
assert.ok(block.includes('Step 5 checks that this is only its forks and the repositories above at their level.'));
assert.doesNotMatch(block, /existing forks of:/);
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
assert.equal(f.ended(d.id).action, 'grant.applied');
});
test('C2-21 L10: project kind or library level changing after raise refuses decision-mismatch', async (t) => {
for (const change of ['kind', 'level']) {
const f = await setup(t, { business: (b) => {
b.definitions.coder.credentials = [{ service: 'gitea', scopes: ['read:user', 'write:repository'] }];
if (change === 'level') libraryBusiness(b);
} });
const d = await f.raise();
const next = structuredClone(f.business);
if (change === 'kind') next.projects.app.kind = 'library';
else {
next.definitions['chief-of-staff'] = structuredClone(next.definitions.research);
next.roles.coder.definition = 'chief-of-staff';
}
f.broker.reloadBusiness('demo', next);
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
assert.equal(f.ended(d.id).code, 'decision-mismatch');
assert.deepEqual(f.gitea.calls(), ['GET /api/v1/version', 'GET /api/v1/user', 'GET /api/v1/token', 'GET /api/v1/user/repos']);
assert.ok(f.gitea.requests.every((r) => r.method === 'GET' && !/\/admin\/users|\/tokens|\/collaborators/.test(r.path)));
assert.equal(existsSync(f.tokenFile), false);
assert.equal(f.record(), undefined);
}
});
test('C2-21 L4: creative write requires push true but ignores unrelated permission keys', async (t) => {
for (const push of [false, true]) {
const f = await setup(t, { business: (b) => libraryBusiness(b) });
f.gitea.rule((e) => e.as === USER && e.path.startsWith('/api/v1/user/repos?'), { status: 200, total: 1, json: [{
full_name: 'acme/app', owner: { login: 'acme' }, parent: null,
permissions: { admin: false, push, pull: true, issues: true },
}] });
const { end } = await f.approve();
assert.equal(end.action, push ? 'grant.applied' : 'grant.failed');
if (!push) assert.equal(end.code, 'gitea-reach-wider');
}
});
// mos-1590: the L10 fixture must not hide the no-declared-scopes refusal.
test('C2-21: a library grant with no declared scopes refuses gitea-not-declared at raise', async (t) => {
const f = await setup(t, { business: (b) => {
libraryBusiness(b);
delete b.definitions.research.credentials[0].scopes;
} });
await rejects(f.raise(), 'gitea-not-declared');
assert.equal(f.store.get('SELECT count(*) AS n FROM decisions').n, 0);
assert.equal(f.gitea.requests.length, 0);
});