Minimal Agent identity

Cloud Agent deliberately keeps identity small. It stores the Gateway user ID and the issuer that minted it, but it does not copy Gateway account metadata into identity claims or derive permissions from account roles. The Gateway makes the current access decision, and Cloud Agent rechecks that decision instead of reading permissions from the session. The Agent admission contract defines those checks.

Tokens and sessions

Credential

Claims retained by Agent

Validation

Gateway identity token

iss, aud, sub, iat, exp

RS256 signature through JWKS, exact configured issuer, cloud-agent audience and expiry

Agent session

sub, gateway_issuer, iat, exp

HS256 signature with AGENT_SESSION_SECRET, exact issuer, expiry and exact claim set

Older Gateways may send additional profile claims. Agent discards them at verification. The session’s gateway_issuer names the upstream identity authority; Agent signs the session itself. Sessions last 30 days. Legacy cookies missing the issuer or containing profile claims, and cookies from another issuer, are rejected. Users must sign in again after upgrading from those formats.

An active account without Agent permission may establish a minimal session for the access-required page. A missing/inactive account is refused through the access contract’s typed subject_unavailable error. Unreadable responses and proxy failures cannot establish access.

GET /v1/session/me returns id and agent_access. My Settings offers a Profile section with a read-only account email, editable first and last names, and an uploaded profile picture. Email is read on demand from the Gateway’s existing /internal/users/{user_id}/status lookup and is never stored in Agent or added to session claims. That response’s role and account-status fields do not confer Agent permissions; current admission is still checked through agent-access.

The names and picture are owner-authored Cloud Agent display data, separate from Gateway login identity and the automatically generated homepage companion. The homepage and Profile editor prefer an uploaded profile picture. When none is saved, they use the owner’s Pokémon companion. Removing a picture restores the companion; memory refreshes and clears continue to update only its habitat and badges, and do not change the saved picture. The Profile editor reuses the memory panel’s current companion, including after a refresh or clear. GET and revision-checked PATCH /v1/agent/profile require current Agent admission and return Cache-Control: no-store. Names allow up to 100 characters each. PNG, JPEG, and WebP uploads are limited to 2 MiB and 16 million pixels; the server re-encodes them as metadata-free PNGs up to 512 by 512 pixels before saving. Migration 0063_user_profiles adds agent_user_profiles; it contains no email, roles, or credentials. Profile editing does not modify Gateway account details.

The frontend shares successful profile and homepage-insights reads in memory, keyed by account. Returning to New task displays the cached avatar immediately while refreshing in the background; a failed refresh preserves the last image. Profile saves update the shared cache immediately and supersede older reads. The cache lasts only within the browser tab and is cleared before sign-out or an account change; it does not change the APIs’ Cache-Control: no-store policy.

Tasks, messages, connection identifiers, encrypted Git/MCP credentials, and other user-authored content remain Agent-owned data.

There is no individual session-revocation table. A copied cookie remains valid until expiry; protected requests still require current Gateway access. Rotating AGENT_SESSION_SECRET invalidates every session. Withdrawing Gateway permission prevents subsequent protected requests.

Database identity boundary

agent_identity_namespace has one record binding the database to a GATEWAY_ISSUER. Both the main API and MCP listener validate the binding before starting workers. The binding never rewrites owner IDs or infers their issuer from old rows. Its table is part of 0001_schema_baseline; the retired migration chain called this change 0029_identity_namespace.

Database state

Startup/check behavior

Empty, no binding, issuer configured

Bind automatically to that issuer

Owner data present, no binding, issuer configured

Refuse until an operator explicitly adopts the original issuer

Bound to the configured issuer

Continue

Bound to another issuer, or issuer removed after binding

Refuse

Neither Gateway configuration nor binding

Allow local infrastructure administration; identity/session authentication stays unavailable

Concurrent initialization is serialized. Adoption can fill an absent binding; it cannot replace one. Use the exact iss signed by the original Gateway. GATEWAY_BASE_URL and JWKS URLs are transport locations and can change without changing the identity namespace. Independent Gateways must use distinct issuers. Replacing the Gateway’s account database with unrelated identities under the same issuer is not a supported migration.

Upgrade an existing deployment

Prepare each environment before its distribution advances the Agent release. Merging upstream source does not deploy a named environment, and existing owner rows do not reveal the original Gateway issuer.

  1. Verify the original Gateway’s effective issuer from its running environment. In the Gateway implementation used by the original rollout, serving.utils.identity_tokens.issuer() reads IDENTITY_ISSUER, falls back to base_url, and removes trailing slashes. Pin the verified effective value as Gateway IDENTITY_ISSUER and Agent GATEWAY_ISSUER. Verify staging and production separately.

  2. Back up the Agent database. If it still uses the migration chain from before 2026-09-10, follow the conversion/rebuild procedure in operations before running migrations. upgrade head cannot directly convert a retired revision stamp. With the supported schema, migrate to head using the prepared release image.

  3. Adopt existing owner data into its original issuer, then verify the binding. In an environment with the Agent package and database configuration:

    python -m cloud_agent.identity_namespace --adopt-existing --issuer '<original-issuer>'
    python -m cloud_agent.identity_namespace --check
    

    The adoption argument must exactly match GATEWAY_ISSUER. It changes only an absent binding, preserving tasks, connections and owner IDs. There is no persistent auto-adoption switch.

    For Compose, use the existing project name, active environment file and prepared checkout:

    export AGENT_DEPLOYMENT_SHA="$(git rev-parse HEAD)"
    dc() {
      docker compose -p '<existing-project>' --env-file '<active-env-file>' \
        -f deploy/control-plane/docker-compose.yml "$@"
    }
    dc build control-plane migrate
    dc run --rm --no-deps migrate
    dc run --rm --no-deps control-plane python -m cloud_agent.identity_namespace \
      --adopt-existing --issuer '<original-issuer>'
    dc run --rm --no-deps control-plane python -m cloud_agent.identity_namespace --check
    

    These commands require the existing Postgres service to be running. They prepare the binding without replacing the API or MCP relay.

  4. Release Agent backend and web app together through the deployment workflow. Verify fresh login, /me, owner/admin permissions and ownership of existing tasks. Confirm legacy cookies are rejected.

deploy/control-plane/deploy.sh runs the identity check after migration and before bootstrap, host-readiness waiting or API/MCP replacement. Unadopted owner data, a missing required issuer or a conflicting binding stops that deployment. The host rollout can run in parallel, so prepare the identity boundary first. Both Compose listeners use the same issuer.

A permission-capable Gateway may remove profile claims before or after this Agent release; Agent treats them as optional. Older Gateways without the access contract remain incompatible. The local Docker stack uses the fixed issuer freeinference-local; existing unbound demo owner data needs explicit adoption of that issuer too.

Rollback and restore constraints

Do not use alembic downgrade base to undo identity binding: the baseline drops owner tables and agent_identity_namespace together. No partial downgrade preserves owner data while removing only this record.

  • Re-upgrading an empty database automatically binds the currently configured issuer. It cannot protect history already deleted by the downgrade.

  • Restoring owner rows without their matching binding causes the identity check to fail. Restore the original binding or explicitly adopt the verified original issuer using the procedure above.

An older Agent binary may accept profile cookies and ignore the stored issuer. Prefer a compatible forward fix, or stop Agent while keeping the Gateway available. Back up and restore the binding with the data whose owners it names.

The implementation is in identity.py, identity_namespace.py and lifespan.py.

Local verification (2026-09-10)

Gateway make all passed (5,254 tests; 48 skipped). Agent make check passed (2,161 tests), as did its 719 web tests, lint, type checking and production build. Fifteen additional PostgreSQL integration cases exercised initial binding, explicit adoption, preservation of existing owners, concurrent startup and the actual deployment-check/adoption CLI.

Native Gateway and Agent HTTP listeners also completed PKCE code exchange, JWKS verification, minimal session creation, /me and an owner API request against separate fresh PostgreSQL databases with production schemas. The Gateway identity token and Agent cookie had exactly the documented fields; legacy and foreign-issuer cookies were rejected, and no account-status request occurred. Docker Compose rendered each listener’s environment separately; the control plane and MCP relay both started against the bound database with those isolated environments. Compose also refused an omitted issuer. Deploy script tests verified that a failed identity check neither bootstraps nor replaces listeners. This local check supplied a seeded Gateway login dependency and access policy; it did not exercise the main site’s password login or deploy either service.