// Package authn answers one question for spec.sr.ht — "who is making this // request?" — and builds the git provenance that answers "who made this write?" // forever after. // // The design has exactly two principals that carry authority: // // - the human owner, recognised by the shared `sr.ht.unified-login.v1` // cookie carrying the instance's [sr.ht] owner-name; // - an agent, recognised by an opaque bearer token stored (hashed) in the // agent_token table. // // Everything else is anonymous. Single-user does not mean "no authorization"; // it relocates it onto agents, which is why Principal distinguishes those two // and nothing finer. Per the confirmed v1 decision there is one agent token and // no per-space scoping: the boundary that matters is the refs rule (agents may // only write proposals/*), and that lives in gitx. // // The two credentials are deliberately asymmetric: // // - A cookie that is missing, forged, expired or unreadable yields an // anonymous principal and never an error. Browsing must keep working. // - A bearer token that is present but unknown, revoked or corrupt is a hard // failure. An agent that presented an explicit credential must not be // silently downgraded to a reader; it would then fail confusingly at the // write instead of clearly at the door. // // Provenance is the other half. Agent identity and session ID are mandatory on // every agent write, and are recorded in the commit itself so that a plain // `git log` on any clone carries the audit trail: // // Author: claude-code/spec-writer (for bigbes) // Committer: bigbes // // Add storage model section // // X-Agent-Session: 8fb9c9a4-b078-4af1-89eb-d97c522f9921 // X-Agent-Base: 1f0c1d1a1e2b3c4d5e6f708192a3b4c5d6e7f809 // // A write missing either field is rejected rather than defaulted: a commit // stamped with a guessed session is worse than no commit, because it launders // unattributable output as attributed. // // This package owns no storage. The agent_token table lives in db/, which is // injected through the TokenStore interface declared here — authn never imports // db, so the dependency arrow keeps pointing downward. package authn import "errors" // Sentinel errors. Callers compare with errors.Is. The split that matters is // permanent (the credential is bad — 401/403) versus transient (the store could // not answer — 503); IsAuthFailure draws it. var ( // ErrNoToken is returned when a bearer credential was expected but the // request carried no Authorization header, or one in another scheme. ErrNoToken = errors.New("no agent token presented") // ErrUnknownToken is the contract a TokenStore must honour: it is what // LookupAgentToken returns (possibly wrapped) when no row matches the // presented hash. Any other error is treated as transient, so a Postgres // outage reads as "try again", never as "your token is bad". ErrUnknownToken = errors.New("unknown agent token") // ErrInvalidToken marks a presented credential that is malformed, or a // stored row whose hash does not actually match what was presented. ErrInvalidToken = errors.New("invalid agent token") // ErrRevokedToken marks a token that resolved to a real row which has been // revoked. Distinct from ErrUnknownToken so operators can tell "you are // using a token I deliberately killed" from "that token never existed". ErrRevokedToken = errors.New("revoked agent token") // ErrNotAgent is returned when agent provenance is demanded of a principal // that is not an agent — the human push path builds no trailers. ErrNotAgent = errors.New("principal is not an agent") // ErrMissingProvenance marks an agent write that omits the agent identity, // the session ID, or the base revision. Mandatory on every agent write; the // design is explicit that these are not defaultable. ErrMissingProvenance = errors.New("missing agent provenance") // ErrInvalidProvenance marks provenance whose values are present but // unusable: control characters or angle brackets that would forge a git // signature line or inject an extra trailer, an over-long field, or a base // revision that is not a hex object name. ErrInvalidProvenance = errors.New("invalid agent provenance") // ErrMissingConfig is returned by InstanceFromConfig when the instance // config lacks a key the provenance identities are built from. It is a // startup failure, not a request failure. ErrMissingConfig = errors.New("missing instance config key") ) // IsAuthFailure reports whether err is a permanent credential failure — the // caller should answer 401/403 — as opposed to a transient backend failure, // which should answer 503 and be retried. Everything not in this set is // transient by definition, which is the fail-closed direction: a store outage // never reads as a valid credential. func IsAuthFailure(err error) bool { return errors.Is(err, ErrNoToken) || errors.Is(err, ErrUnknownToken) || errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrRevokedToken) }