Panel API development
cloud/api is the real backend behind the daemon’s PanelClient: the signed source of
truth for the MCP / context / capability-pack catalog, minting of scoped upstream
credentials, identity→teams resolution, and an append-only audit. It is built on Node
built-ins — node:http, node:crypto (Ed25519), node:sqlite — with one optional
runtime dependency: the pg driver, imported lazily only when DATABASE_URL selects
Postgres.
Run it locally
Section titled “Run it locally”just panel-run # or: cd cloud/api && node src/index.tsOn boot it generates (or loads PANEL_SIGNING_KEY) an Ed25519 signing key, seeds a demo
org/teams/catalog, and prints the two values the daemon needs:
CHRONOS_PANEL_URL=http://127.0.0.1:8787CHRONOS_PANEL_PUBKEY=<64-hex public key>Wire the daemon to it:
export CHRONOS_PANEL_URL=http://127.0.0.1:8787export CHRONOS_PANEL_PUBKEY=<hex from above> # the daemon pins the PUBLIC key onlyexport CHRONOS_IDENTITY=alice@corpexport CHRONOS_TEAMS=team-a,team-bjust run-daemonThe daemon fetches the Ed25519-signed catalog, verifies it before trusting it, resolves
the cascade, and materializes only loopback MCPs — the real upstream secret stays in the
broker. With CHRONOS_PANEL_URL unset the daemon stays offline (no governed MCPs), so CI
never needs the panel.
Storage backends
Section titled “Storage backends”One async Db interface (src/db.ts), two backends — all SQL lives behind it:
- SQLite (default) —
PANEL_DBpath,panel.dbif unset. Zero dependencies, single-tenant; runs with justnode(22.5+). - Postgres / Neon — set
DATABASE_URL(e.g. a Neon connection string with?sslmode=require); it wins overPANEL_DB. The schema is created on boot and the Ed25519 signing key persists in the database, so daemons keep verifying across restarts and redeploys. Requirespnpm installlocally for thepgdriver.
just panel-test # node --test (SQLite suite; pg suite self-skips)TEST_DATABASE_URL=postgres://… just panel-test # also runs the Postgres suiteThe Postgres suite (pg.e2e.test.ts) drops the panel tables — point
TEST_DATABASE_URL only at a throwaway database. Node↔Rust signature compatibility is
additionally pinned by a cross-language vector test in chronos-broker
(ed25519_verifies_a_node_panel_signature_vector).
Endpoint overview
Section titled “Endpoint overview”The routes mirror PanelClient 1:1:
| Route | Auth | Purpose |
|---|---|---|
GET /v1/catalog?identity&team=… | optional | signed MCP catalog (cascade-filtered) |
GET /v1/context?identity&team=… | optional | signed context bundle |
GET /v1/packs?identity&team=… | optional | signed capability-pack set |
POST /v1/mint/:serverId | bearer | scoped upstream credential (audited, rate-limited) |
POST /v1/auth/token | — | OIDC seam: {subject} → {token, identity, teams} |
GET /v1/keys | — | trusted Ed25519 public keys |
POST /v1/audit | bearer | daemon session-load audit sink |
GET /v1/audit | — | audit tail (panel dashboard) |
Security invariants to preserve when changing this code: the panel signs with the private
key and the daemon only ever verifies with the public key; mint is never anonymous
(bearer required, rate-limited, audited — the audit records which server was minted by
whom, never the token); secrets never appear in a catalog or a log. In production the
signing key comes from PANEL_SIGNING_KEY (KMS/secret); the generated dev key and
panel.db are gitignored.
Admin bootstrap (dev)
Section titled “Admin bootstrap (dev)”Set PANEL_ADMIN_PASSWORD (plus optional PANEL_ADMIN_IDENTITY, default admin@corp)
to create or password-reset the admin account at boot, so a fresh deploy is administrable
without touching the database. PANEL_SEED_PASSWORD gives the demo accounts
(alice/bob/admin) credentials in dev and tests; without it they cannot sign in.