Skip to content

web-sign-in — The deployed surfaces know who is asking, and the identity providers are data

One canonical identity-provider kind under .ai/repo/identity/providers/ describes each provider as data: its issuer, its endpoints or its discovery document, the scopes requested, the claim that carries a stable subject and the environment variable names its credentials arrive in. A single OpenID-Connect-shaped flow in the executable serves every provider in that directory; Google and Facebook are two objects, not two code paths. Sessions are cookie-borne, signed, short and revocable; identity is attached to a request once, in the router, and every surface — HTTP, Cockpit, OpenAPI, MCP — reads it from the same place. The capability registry's effect classification, not a per-route list, decides what an anonymous reader may do, and the OpenAPI document and the Cockpit both state that decision because both project it from the registry. Striking a provider is deleting one file and regenerating.

PLANNED 0 of 8 issues done

ready
1
blocked
7
active
0
verify
0
done
0
cancelled
0

Problem

Everything this repository serves is served to whoever reaches the port. That was safe while the server bound loopback and the only reader was the person standing in front of the checkout, and it stops being safe the moment the deployment milestone puts a public HTTPS URL in front of the same router: the Cockpit, the API, the OpenAPI document and the MCP surface would all be readable by anyone who found the host, and no mutating capability could ever be exposed through the browser because there would be nobody to attribute the mutation to. The obvious repair is the wrong one twice over. Hand-written provider clients for Google and for Facebook would be two implementations of one flow, and a compiled-in list of which providers exist would put an identity decision in Rust source where every other decision of this repository is a declarative object under .ai/repo/. The provider set is expected to change — both of the first two may be struck and replaced once the audience is known — and a change of that kind must be an edit to one object, not a release of the executable.

Outcome

One canonical identity-provider kind under .ai/repo/identity/providers/ describes each provider as data: its issuer, its endpoints or its discovery document, the scopes requested, the claim that carries a stable subject and the environment variable names its credentials arrive in. A single OpenID-Connect-shaped flow in the executable serves every provider in that directory; Google and Facebook are two objects, not two code paths. Sessions are cookie-borne, signed, short and revocable; identity is attached to a request once, in the router, and every surface — HTTP, Cockpit, OpenAPI, MCP — reads it from the same place. The capability registry's effect classification, not a per-route list, decides what an anonymous reader may do, and the OpenAPI document and the Cockpit both state that decision because both project it from the registry. Striking a provider is deleting one file and regenerating.

Current state

There is no authentication of any kind: no session, no cookie, no identity type, no login route, no credential handling and no notion of a caller anywhere in apps/majordomus-cli/src/http. The lease file records a URL and the server warns when it binds anything but loopback. The deployment milestone (fly-deployment) states as non-scope that mutating deployment operations must not be exposed to unauthenticated HTTP or to the browser, which is a constraint this milestone is what removes.

Desired state

A reader who opens the public URL is served the public read-only surfaces as they are today. A reader who signs in with any provider declared in .ai/repo/identity/providers/ gets a session, is named in the Cockpit, and may invoke the capabilities the effect policy allows a signed-in reader to invoke. Adding a third provider is one YAML file; removing the first two is deleting two files; neither is a code change, and the generated documentation, the Cockpit's sign-in offer and the OpenAPI security scheme all follow from the same objects.

Scope

  • .ai/repo/identity
  • .ai/repo/adrs
  • .ai/repo/rules/project
  • .ai/repo/policy.yaml
  • apps/majordomus-cli/src/identity
  • apps/majordomus-cli/src/capability/builtin
  • apps/majordomus-cli/src/http
  • apps/majordomus-cli/src/cockpit
  • share/kinds.yaml
  • share/allow
  • share/schemas
  • docs
  • site
  • test/cases
  • .github/workflows

Out of scope

  • A user database, a profile store, or any record of a reader beyond the session needed to answer the next request
  • Passwords, password reset, e-mail verification or any credential this repository would itself have to store
  • A provider-specific code path, client library or SDK for Google, for Facebook or for any successor
  • Authorisation roles, groups or an access-control language beyond what the capability registry's effect classification already decides
  • Any credential, client secret or token committed to the repository, written into a generated artifact, rendered into the site or logged
  • Exposing mutating capabilities to a signed-in reader before the effect classification says which ones are safe to expose at all
  • Signing in to the local development server as a requirement; loopback stays open so the existing workflow is unchanged

Acceptance criteria

  • Every fact about a provider — issuer, endpoints, scopes, subject claim, credential variable names, display name, ordering — lives in one object under .ai/repo/identity/providers/ and nowhere else
  • Google and Facebook are each one such object, and the executable contains no identifier, endpoint or branch naming either of them
  • Deleting a provider object removes the provider from the sign-in offer, from the generated documentation, from the OpenAPI security scheme and from the Cockpit, with no other edit
  • The flow validates state and nonce, rejects a callback whose state it did not issue, pins the session to the issuer and subject, and refuses a provider whose credentials are absent rather than starting a flow that cannot complete
  • The session cookie is signed, HttpOnly, SameSite-constrained, Secure when the surface is served over HTTPS, and expires; signing out revokes it server-side rather than only clearing it in the browser
  • Identity is resolved once in the router and read by every surface from that one place; no surface parses the cookie a second time
  • What an anonymous reader may do is derived from the capability registry's effect classification rather than from a list of routes
  • No secret sentinel placed in the credential environment variables appears in any response, generated artifact, log line, cache, site output or OpenAPI example
  • The documentation states how to add a provider and how to remove one, and both statements are executed by a test rather than only written

Validation

  • bash test/run.sh
  • bin/majordomus doctor
  • cargo test --workspace --all-features
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • majordomus generate --check

Evidence required

  • provider_objects_canonical
  • provider_removal_proved
  • flow_hardened
  • session_hardened
  • single_identity_resolution
  • effect_derived_exposure
  • no_secret_leakage
  • documented_and_tested

Risks

  • An OpenID Connect flow written twice — once for the provider that is standards-shaped and once for the one that is not — is the failure this milestone exists to prevent. Facebook's endpoints differ from Google's in their spelling, not in their shape; anything that genuinely cannot be expressed as data about one flow is a defect in the provider object's schema and is fixed there.
  • Credentials are the one part of this that cannot be data in the repository. The objects name the environment variables; the values arrive from the deployment's secret store and are never read into any structure that is serialised, cached or rendered.
  • Sign-in invites scope creep towards accounts, profiles and roles. The session exists to answer the next request and nothing more; anything beyond that belongs to a milestone that has a reason for it.
  • The provider set is expected to be struck. A design that makes Google and Facebook cheap to add but expensive to remove has failed even if it works.

Issues

Each one is an execution contract with its own acceptance criteria, validation command and required evidence. The wave column is the layer of the dependency graph it sits in.

issuestatuswavedepends ontitle
I1201 READY 0 An identity provider is a declarative object, and Google and Facebook are two of them
I1202 BLOCKED 1 I1201 One typed provider model reads the objects, and no code names a provider
I1203 BLOCKED 2 I1202 One flow serves every provider, and it refuses everything it did not start
I1204 BLOCKED 3 I1203 The session is signed, short, and revocable from the server
I1205 BLOCKED 4 I1204 Identity is resolved once, in the router, and every surface reads it from there
I1206 BLOCKED 5 I1205 What an anonymous reader may do is derived from the effect classification, not from a list
I1207 BLOCKED 4 I1204 No credential reaches any output, and hostile provider metadata renders safely
I1208 BLOCKED 6 I1202, I1206 Adding a provider is one file, removing one is one deletion, and both are executed rather than described

The graph

web-sign-in dependency graphfrom .ai/repo/project
flowchart LR
    I1201["I1201<br/>An identity provider is a declarative object, and Google and Facebook are two of them"]:::ready
    I1202["I1202<br/>One typed provider model reads the objects, and no code names a provider"]:::blocked
    I1203["I1203<br/>One flow serves every provider, and it refuses everything it did not start"]:::blocked
    I1204["I1204<br/>The session is signed, short, and revocable from the server"]:::blocked
    I1205["I1205<br/>Identity is resolved once, in the router, and every surface reads it from there"]:::blocked
    I1206["I1206<br/>What an anonymous reader may do is derived from the effect classification, not from a list"]:::blocked
    I1207["I1207<br/>No credential reaches any output, and hostile provider metadata renders safely"]:::blocked
    I1208["I1208<br/>Adding a provider is one file, removing one is one deletion, and both are executed rather than described"]:::blocked
    I1201 --> I1202
    I1202 --> I1203
    I1202 --> I1208
    I1203 --> I1204
    I1204 --> I1205
    I1204 --> I1207
    I1205 --> I1206
    I1206 --> I1208
    classDef done stroke:#16a34a,fill:#052e16,stroke-width:2px
    classDef active stroke:#2563eb,fill:#eff6ff,stroke-width:2px
    classDef verify stroke:#7c3aed,fill:#f5f3ff,stroke-width:2px
    classDef ready stroke:#0891b2,fill:#ecfeff,stroke-width:2px
    classDef blocked stroke:#b45309,fill:#fffbeb,stroke-width:2px
    classDef cancelled stroke:#6b7280,fill:#f9fafb,stroke-width:2px

Canonical record: .ai/repo/project/milestones/web-sign-in.yaml. Read it back with majordomus plan show web-sign-in.