Files
platform/src/servers/create-router.ts
T
pastilhasandClaude Opus 5 12a9605b01 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
2026-08-24 13:47:46 +00:00

30 lines
1.4 KiB
TypeScript

import type { User } from 'types';
import { Hono } from 'hono';
import type { CredentialScope } from './auth-token';
export type HonoVariables = {
body: Record<string, unknown>;
origin: string;
/**
* The authenticated account, plus — on routes that resolve it — the home directory this request is
* confined to.
*
* `homeDir` is optional because only the file browser's `confineToHome` middleware sets it, and it lives
* on `user` rather than in its own variable for a blunt reason: `getRootDir(user, root)` is called from
* fifteen places in that router, and the cost of missing one is serving the OWNER'S home to a member.
* Carrying it on the object those call sites already receive means none of them can forget.
*/
user: User & { homeDir?: string };
/**
* The narrowing carried by the credential that made this request, or null when it carries its holder's
* full authority (every session JWT, every key a human minted).
*
* Set by the permission gate, which has already resolved the token and is the only place that should.
* A route reads it when it hands authority ONWARD — `POST /tasks/:name/run` mints a token for the
* script it spawns, and that token must not be wider than the credential that asked for the run.
*/
credentialScope: CredentialScope | null;
};
export const createRouter = () => new Hono<{ Variables: HonoVariables }>();