feat(bus,webui): bus read of credential status; the Console shows it (#1549, row 60)

The credentials read returns service, account, role, date kind, date and
state for the capability's business, to human and reader sessions only.
It never returns a token, file path or variable name. Author Filbert,
reviewers Dewey (27558) and Darkwing (27575), candidate manifest 13541657.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 19:37:06 -05:00
co-authored by Claude Opus 5.5
parent 6a0d43854a
commit 1771bf98b2
9 changed files with 313 additions and 15 deletions
+27 -2
View File
@@ -86,12 +86,15 @@ export class Broker {
#runs = new Map();
#ended = new Set();
#secretCheck;
#credentialStatus;
#clock = 0;
constructor({ store, businesses, secretCheck = () => {} }) {
// credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use().
constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [] }) {
if (!object(businesses) || !Object.keys(businesses).length) fail('invalid-business');
this.#businesses = structuredClone(businesses);
this.#store = store;
this.#secretCheck = secretCheck;
this.#credentialStatus = credentialStatus;
for (const table of [
'events',
'role_claims',
@@ -489,7 +492,7 @@ export class Broker {
if (!object(args)) fail('invalid-request');
this.#secretCheck(args);
return this.#store.transaction(() => {
if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions'].includes(request.verb))
if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials'].includes(request.verb))
fail('read-only');
const result = this.#dispatch(s, request.verb, args);
this.#secretCheck(result);
@@ -875,6 +878,28 @@ export class Broker {
.map((d) => ({ ...this.#decisionView(d), closed: this.#closed(d.id) }));
fail('invalid-request');
}
// Row 60 (#1549): what the running host loaded, checked against its clock, for the session's
// business. Service, account, role, date and state only; no token, file path or variable name.
case 'credentials': {
if (!s.human && !s.reader) fail('human-required');
keys(a, [], []);
const prefix = s.business + '/';
return this.#credentialStatus()
.filter((c) => c.instance.startsWith(prefix))
.map(({ instance, service, date, state }) => {
const role = instance.slice(prefix.length),
sync = role === '@sync',
bot = sync ? b.tracker?.sync?.bot : service === 'vikunja' ? b.roles?.[role]?.tracker?.bot : null;
return {
service,
account: bot ?? null,
role: sync ? 'tracker sync' : role,
dateKind: service === 'gitea' ? 'rotateBy' : 'expires',
date,
state,
};
});
}
default:
fail('unknown-verb');
}
+6 -1
View File
@@ -31,7 +31,12 @@ export async function startBroker({
];
credentials = new Credentials({ references, repoRoots: protectedRoots, dataRoot });
store = new Store(dataRoot);
const broker = new Broker({ store, businesses, secretCheck: (value) => credentials.assertClean(value) });
const broker = new Broker({
store,
businesses,
secretCheck: (value) => credentials.assertClean(value),
credentialStatus: () => credentials.status(),
});
// Process identities must be supplied for every managed run when using the human transport.
const launchRecords = [];
function bindLaunch(r) {
+1
View File
@@ -6,5 +6,6 @@ export function views(client) {
agents: () => client.call('agents'),
trail: (subject) => client.call('trail', { subject }),
decisions: (list) => client.call('decisions', { list }),
credentials: () => client.call('credentials'),
});
}