Documentation
¶
Overview ¶
Package api is the control plane's whole HTTP surface: every route the platform serves, and the dispatcher deciding which credential may reach which route. Five surfaces share one http.ServeMux, four credentials reach them, and dispatchAuth (server.go) picks the credential by path and runs it before the router — splitting the routes across nested muxes would let ServeMux's own path-cleaning and subtree-slash redirects answer an unauthenticated request before auth had run. Surfaces and credentials do not line up one to one, and the exceptions are the part a reader has to carry: they are collected at the end of the map below rather than left implicit in it.
The public /v1 wire — agents, environments, sessions with their events and resources, vaults and credentials, skills, files — is wire-compatible with Anthropic Managed Agents down to paths, JSON shapes, ID prefixes and the pagination and error envelopes, and takes the management x-api-key (auth.go) everywhere the dual-auth set at the end does not say otherwise; anthropic-version / anthropic-beta and ?beta=true are accepted and ignored. The work API under /v1/environments/{id}/work is the second credential: a BYOC worker presents an environment key as Authorization: Bearer, and workScope asserts that key's environment against the path's, so one key drives one queue and no other (envauth.go, workapi.go). The off-wire /api console namespace mirrors the reference console's own private paths rather than inventing a namespace (consoleapi.go, consoleapikeys.go); it is reached by a verified human whose asserted role must clear the route's declared minimum (identitylane.go), or by the management key, which no dispatch can confine to the console: one credential cannot distinguish its callers, so "console-only" means off the wire and built for the console, nothing more (consoleapi.go). That human lane is not confined to /api — it is the management arm's alternate credential everywhere, tried only when no x-api-key is offered, which is why every route declares its role beside its path in server.go. Fourth, the internal gate-config endpoint, and with it the fourth credential: a session's egress gate authenticates with its per-session gtk_ token and fetches the networking policy and resolved credentials it needs (gateauth.go, gateconfig.go) — off /v1, and registered as a divergence in docs/DIVERGENCES.md. Fifth is the event layer (events.go): the SSE tail of a session's log, and the state-machine triggers a posted batch fires — waking an idle session, resuming a suspended turn, clearing a confirmation gate — which is why sending events is a transaction holding the session row rather than a bare append. Both sit in the dual-auth set below.
The exceptions, then, and they run one way: the environment key is not confined to the work API. Every route a BYOC worker needs to set up and drive its session is dual-auth (dualAuth) — reached by a worker's Bearer environment key, or by a management or human caller, whichever the request carries. That is the session events subtree; the bare GET /v1/sessions/{id}; the GET skill reads at and under /v1/skills/{id} — the skill itself, its versions, a version, and a version's /content (isSkillReadPath, server.go); and the GET /v1/files/{id}/content download, which is the worker's own SetupSkills and SetupFiles path. Skill content and file content therefore do NOT need a management key. What keeps the lane narrow is per-resource scoping inside the handlers rather than the dispatcher: a session route's key must own the session (requireEnvironmentKeyForSession), a file download's key must belong to an environment in which some session mounts that file (downloadFile), and skills, workspace-global resources every environment's sandboxes consume, need no scoping at all. Everything else on /v1 — the collections, the file metadata read, every mutation — is management-only, the work API excepted: it runs the other way, taking the environment key and nothing else, since resolveEnvironmentKey demands a Bearer and a management x-api-key never satisfies it (envauth.go). Outside the events subtree the dual-auth routes are GET-only.
The cross-cutting fact no single file makes obvious: dispatchAuth classifies on r.URL.EscapedPath() while ServeMux matches the DECODED path, and the asymmetry cuts both ways on purpose. An encoded %2F cannot forge a segment the router does not also see, so an environment key can never be admitted to a management-only handler. In the other direction a percent-encoded spelling of a machine route (/%77ork) fails the lane predicates, falls through to the management arm, and is then decoded and routed to that machine registration anyway — arriving there, with identity enabled, on the human lane with a real principal. What refuses it is identity.RoleNone on those registrations, which no role satisfies. RoleNone on the work and gate routes is therefore a live denial, not a placeholder (TestAnEncodedPathCannotSlipPastTheWorkLane).
Index ¶
- Constants
- func EnsureAPIKey(ctx context.Context, pool *pgxpool.Pool, name, key string) error
- func IssueEnvironmentKey(ctx context.Context, pool *pgxpool.Pool, environmentID, name string) (string, error)
- func NewHandler(pool *pgxpool.Pool, blobs blob.Store, cipher secrets.Cipher, ...) http.Handler
- func RevokeEnvironmentKey(ctx context.Context, pool *pgxpool.Pool, environmentID, keyID string) (found bool, err error)
- type EnvironmentKey
- type ImportSummary
- type ManagementKey
Constants ¶
const ( KeyStatusActive = "active" KeyStatusInactive = "inactive" KeyStatusArchived = "archived" KeyStatusExpired = "expired" )
The three states an operator may set, and the fourth the server computes.
The settable set is the reference's own, quoted from the 400 its server returned to a `{"status":"deleted"}` probe: "status: Input should be 'active', 'inactive' or 'archived'" (#378). `expired` is **rendered, never stored** — storing it would need a sweeper to keep true, and a key that lapsed while the sweeper was down would keep authenticating.
const ( MetricFileUploads = "files.uploads" MetricFileUploadBytes = "files.upload.bytes" MetricFileDownloadBytes = "files.download.bytes" )
Files registry instruments (docs/plan/08_files.md observability table), mirroring the skills registry names. Attribute cardinality is bounded: outcome only — file ids belong in logs and span attributes, never in metric labels. Exported so filesmetric_test.go can assert the exact names and labels.
const ( MetricSkillUploads = "skills.uploads" MetricSkillUploadBytes = "skills.upload.bytes" MetricSkillDownloadBytes = "skills.download.bytes" )
Skill registry instruments (docs/plan/06_skills.md observability table). Attribute cardinality is bounded: outcome only — skill ids belong in logs and span attributes, never in metric labels. Exported so skillsmetric_test.go can assert the exact names and labels.
const EnvironmentKeyTTL = 365 * 24 * time.Hour
EnvironmentKeyTTL is how long an issued environment key authenticates. It matches the reference console's own year — its issuance response reports expires_in: 31536000 — and is not configurable: an operator wanting a shorter-lived credential revokes it, and the alternative, a per-key TTL, is a policy the reference does not offer and nobody has asked for.
const IssuedKeyPrefix = "sk-map-api01-"
IssuedKeyPrefix marks a management key this platform minted, beside plan 30's `sk-map-env01-` for worker credentials. It is public by construction: it is the same for every key, so showing it identifies the *kind* of credential without revealing anything about a particular one.
const MetricSessionResources = "session.resources"
MetricSessionResources counts session resource mutations (create-attach, add, delete, token rotation) by outcome. Outcome-only labels: session/resource/file ids ride the structured logs, never the metric (plan decision 9). Exported so the integration test can assert the name and labels.
Variables ¶
This section is empty.
Functions ¶
func EnsureAPIKey ¶
EnsureAPIKey makes key the one live credential for the named logical key: it inserts (or reactivates) the hash and archives every other live key under the same name. That gives rotation-by-restart semantics — changing CONTROLPLANE_API_KEY and restarting cmd/controlplane retires the previous key instead of leaving it valid forever. All replicas must therefore share one key value per name; replicas booting with *different* values for one name race, and api_keys_one_live_unissued resolves that by failing the loser's transaction rather than leaving the name with two live credentials.
It only ever writes rows with created_by NULL, which is what puts them under that index and marks them env-var-managed. A key issued over the console records its issuer and is deliberately outside the one-live rule (plan 32).
func IssueEnvironmentKey ¶ added in v0.3.0
func IssueEnvironmentKey(ctx context.Context, pool *pgxpool.Pool, environmentID, name string) (string, error)
IssueEnvironmentKey mints a worker credential for an environment and returns it in plaintext — the only moment it exists anywhere but the caller's memory, since the database receives nothing but its hash. name labels the key so an operator can tell one host's credential from another's when revoking one.
An environment may hold any number of live keys: issuing does not disturb the ones already out on other hosts. That is the reference console's model, and the reason migration 0021 retired the one-live-key invariant.
Issuance itself is a deliberate divergence: the reference mints environment keys on its console's private backend, with no public wire endpoint, so a self-hostable platform owns the primitive. The consuming side — resolving a Bearer token to its environment — stays wire-locked by the real `ant beta:worker`.
func NewHandler ¶
func NewHandler(pool *pgxpool.Pool, blobs blob.Store, cipher secrets.Cipher, verifier *identity.Verifier) http.Handler
NewHandler assembles the control-plane HTTP surface over the given pool. blobs is the object store backing skill archives; nil deploys without object storage — everything serves except the storage-backed skill routes, which answer with a configuration error. cipher seals vault credential secrets; nil deploys without one — vault metadata CRUD serves, while the secret-bearing paths (credential create/update with secret fields, the validate probe) answer with a configuration error (fails closed, plan 12 D1). verifier authenticates humans; nil is IDENTITY_MODE=disabled, and the surface is then what it was before plan 31 — no lane, no role check — on every request shape but one: requireAPIKey refuses a repeated x-api-key field in every mode, deliberately (see dispatchManagementAuth).
func RevokeEnvironmentKey ¶ added in v0.3.0
func RevokeEnvironmentKey(ctx context.Context, pool *pgxpool.Pool, environmentID, keyID string) (found bool, err error)
RevokeEnvironmentKey retires one key and leaves the environment's others authenticating. found reports whether the environment held such a key at all, which is how the caller tells a real revocation from a 404 — a key id that belongs to another environment is simply not found here, so revocation cannot reach across environments or confirm that an id exists elsewhere.
Revoking an already-revoked key succeeds and still reports found, so a retry is not an error; coalesce keeps the original revocation timestamp rather than sliding it forward on every repeat.
Types ¶
type EnvironmentKey ¶ added in v0.3.0
type EnvironmentKey struct {
ID string
Name string
CreatedAt time.Time
// ExpiresAt is nil for a key minted before keys carried expiries; such a
// key stays live until revoked, which is what it was promised on issue.
ExpiresAt *time.Time
}
EnvironmentKey is an issued credential's metadata — everything about a key that outlives its issuance. The secret is not part of it: only the hash is stored, so no surface can render the key a second time.
func ListEnvironmentKeys ¶ added in v0.3.0
func ListEnvironmentKeys(ctx context.Context, pool *pgxpool.Pool, environmentID string, limit, offset int) ([]EnvironmentKey, int, error)
ListEnvironmentKeys returns an environment's un-revoked credentials, newest first, with the total that matched so the caller can page. A revoked key is omitted rather than shown as retired — revocation is final, and the reference console drops the row too. An expired key is still listed: an operator whose worker has stopped connecting needs to see the credential it is failing on.
type ImportSummary ¶ added in v0.2.0
type ImportSummary struct {
Imported []string // "name version" pairs landed this run
Skipped []string // already present at this version
Failed map[string]error // directory → why it did not import
}
ImportSummary reports one operator-import run.
func ImportAnthropicSkills ¶ added in v0.2.0
func ImportAnthropicSkills(ctx context.Context, pool *pgxpool.Pool, blobs blob.Store, dirs []string, version string) (*ImportSummary, error)
ImportAnthropicSkills is the controlplane's run-once operator import (docs/plan/06_skills.md slice 3): each dir is a skill directory from a local checkout of github.com/anthropics/skills, validated exactly like an upload and landed as a source='anthropic' skill whose id is the SKILL.md name (the reference catalog's short-name ids) at the given date-based version. Idempotent per (skill, version): an existing version is skipped without touching storage. A directory that fails to validate is logged and skipped; the returned error reports that some directories failed.
The checkout's content is read at the operator's machine and never enters this repository — the reference document skills are source-available, not open source (the plan's license red lines).
type ManagementKey ¶ added in v0.3.0
type ManagementKey struct {
ID string
Name string
// Status is the rendered status, so it may read `expired`, which is not a
// value any row holds.
Status string
PartialKeyHint string
CreatedAt time.Time
// CreatedBy is nil for a key nobody issued — one seeded from
// CONTROLPLANE_API_KEY. That is the same predicate api_keys_one_live_unissued
// keys on, so "nil issuer" and "env-var-managed" are one fact, not two.
CreatedBy *string
// ExpiresAt is nil for a key that never expires: the console's own "Never".
ExpiresAt *time.Time
}
ManagementKey is an `x-api-key` credential's metadata: everything about a key that outlives its issuance. The secret is not part of it — only the hash is stored, so no surface can render the key a second time, and PartialKeyHint is the only trace of the plaintext that survives.
func IssueManagementKey ¶ added in v0.3.0
func IssueManagementKey(ctx context.Context, pool *pgxpool.Pool, name string, expiresAt *time.Time, createdBy string) (string, ManagementKey, error)
IssueManagementKey mints a management credential and returns it in plaintext — the only moment it exists anywhere but the caller's memory, since the database receives nothing but its hash — alongside the row a caller renders.
createdBy must be non-empty, and every caller is an authenticated route, so it always is: it is the `principal_` id of the human who issued the key over SSO, or the `apikey_` id of the machine credential that did. Beyond audit it is load-bearing, because non-NULL is exactly what puts this row **outside** api_keys_one_live_unissued — issued keys may share a name, as the reference allows, while EnsureAPIKey keeps one-live-per-name over the rows it owns.
expiresAt is the caller's choice and may be nil, which means never. That is a deliberate divergence from environment keys' server-fixed EnvironmentKeyTTL, and it is the reference's: an operator issuing a management credential picks its lifetime; a worker credential's lifetime is the platform's business.
func ListManagementKeys ¶ added in v0.3.0
ListManagementKeys returns every management key, newest first.
Every one, including archived rows and including the key seeded from CONTROLPLANE_API_KEY. Archived rows are returned because the reference returns them — its console filters them client-side — and because an archive that erased the row would erase the evidence that a credential once existed. The bootstrap key is returned because it is a management key: a listing that hid the credential the operator is authenticating with would be lying about what can reach this API. It renders with a null issuer, which is the honest answer to "who created this" for a key an environment variable did.
No paging. The reference's console list is a bare array with no pagination at all, and this table holds one row per issued credential plus the bootstrap one — an operator-paced count, not a growing log.
And no index, which settles a question migration 0024 left open. Its closing comment declined `api_keys (created_at DESC)` as build-ahead and said "the listing that would want one lands in slice 2"; the listing has landed and does not want one. A migration is immutable once merged, so the reconsideration has to be recorded where a reader will meet it rather than by editing that file: the sort is over an operator-paced table with no growth driver — issuance is a deliberate admin action, and nothing else writes rows — so an index would be paid for on every write to speed a read that has nothing to scan. If a deployment ever does accumulate keys, the missing bound is the *response size*, which an index does not fix; that would be paging, and the reference's own surface has none to mirror.
Source Files
¶
- agents.go
- apikeys.go
- auth.go
- consoleapi.go
- consoleapikeys.go
- doc.go
- envauth.go
- environments.go
- envkeys.go
- errors.go
- events.go
- files.go
- filesmetrics.go
- filesupload.go
- gateauth.go
- gateconfig.go
- identitylane.go
- page.go
- principals.go
- server.go
- sessionresources.go
- sessions.go
- skills.go
- skillsimport.go
- skillsmetrics.go
- skillsupload.go
- vaultcredauth.go
- vaultcredentials.go
- vaults.go
- vaultvalidate.go
- wire.go
- workapi.go