a commander's bound is a rule, not a list
the allow list shipped an hour ago was a snapshot. plugin permissions
register at mount, so a plugin installed after a commander was created was
missing from its list and failed as a bare 403 with nothing saying why —
and every future thing a commander is meant to do is exactly that shape:
notify a device, send mail, spawn a sub-agent, hand a transcription to an
idle one, reach a plugin that does not exist yet.
so a credential now narrows two ways, and both only subtract:
permissions an ALLOW list. exact, a snapshot. right for a credential
with one job.
excludedKinds kinds it may never reach, resolved against the LIVE
registry on every request. right for a credential whose
job is open-ended.
a commander gets the second: every kind except admin. bounded away from
installing plugins, managing users and roles and changing server settings,
forever — and open to everything the platform learns to do, with nothing
to re-issue. still intersected with the account, so for a member it
narrows itself against their role.
verified against the running server with a commander-shaped key: tasks,
notify, file-browser and the transcribe PLUGIN all reach their routes;
/api/users, /api/plugins and /api/roles are 403. the plugin case is the
one an allow list could not have passed — it was mounted long after any
key was minted.
the commander's CLAUDE.md says what it cannot do in the rule's own terms,
and that a 403 is an answer rather than a problem to route around.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -69,15 +69,32 @@ export type AuthIdentity = {
|
||||
/** JWT only, seconds since the epoch. */
|
||||
exp?: number;
|
||||
/**
|
||||
* API key only: the permission keys this credential may reach, or null for the holder's full authority.
|
||||
* How this credential is narrowed, or null/absent for one carrying its holder's full authority — which
|
||||
* is every session JWT and every key a human has minted.
|
||||
*
|
||||
* Carried on the identity rather than looked up again by the gate, because the key row was already read
|
||||
* to answer "whose is this" — and because a second lookup is a second chance for the two doors to
|
||||
* disagree about the same credential, which is the failure this whole file exists to prevent.
|
||||
*/
|
||||
permissions?: string[] | null;
|
||||
scope?: CredentialScope | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A narrowing, in the two shapes that are actually useful.
|
||||
*
|
||||
* `allow` is exact and a snapshot; `excludeKinds` is a rule resolved against the live registry. A
|
||||
* commander wants the second: bounded away from administration forever, and still able to reach a plugin
|
||||
* installed long after it was created. See `api-keys/schema.ts` for the full argument.
|
||||
*/
|
||||
export type CredentialScope = {
|
||||
allow?: string[] | null;
|
||||
excludeKinds?: string[] | null;
|
||||
};
|
||||
|
||||
/** Does this narrowing actually narrow anything? Null and two empty rules are the same thing: no. */
|
||||
export const isNarrowed = (scope: CredentialScope | null | undefined): scope is CredentialScope =>
|
||||
!!scope && (Array.isArray(scope.allow) || !!scope.excludeKinds?.length);
|
||||
|
||||
/**
|
||||
* Turn an API key into a caller, or null.
|
||||
*
|
||||
@@ -94,7 +111,14 @@ export async function resolveApiKey(secret: string): Promise<AuthIdentity | null
|
||||
// is debounced to at most once a minute per key inside touchApiKey.
|
||||
void touchApiKey(row.keyId, row.lastUsedAt).catch(() => {});
|
||||
|
||||
return { id: row.userId, email: row.email, username: row.username, via: 'api-key', permissions: row.permissions };
|
||||
const scope: CredentialScope = { allow: row.permissions, excludeKinds: row.excludedKinds };
|
||||
return {
|
||||
id: row.userId,
|
||||
email: row.email,
|
||||
username: row.username,
|
||||
via: 'api-key',
|
||||
scope: isNarrowed(scope) ? scope : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user