Agent admission policy
The Gateway decides who may use Cloud Agent. Cloud Agent represents the account
with its user ID and issuer, then checks the Gateway again on each protected
owner/admin request and on /v1/session/me.
To get the current decision, Agent reads
GET /internal/users/{user_id}/agent-access over HTTP with its dispatch
credential. The response contains user_id, allowed, and permissions.
Account roles and profile fields are not authorization inputs.
Permissions and request checks
Permission |
Access |
|---|---|
|
Owner APIs, subject to per-resource ownership checks |
|
Gateway-authorized administration, including runner-host APIs; requires |
allowed must agree with the presence of agent.use. Permissions must be
unique, use recognized values and include agent.use whenever agent.admin
is present. A decision for another user, or any malformed or contradictory
response, cannot grant access.
The FreeInference policy documented by the original rollout grants both permissions to active admin accounts. Another deployment can register a policy that admits other accounts without changing Agent. A neutral Gateway with no registered policy denies access. Verify the policy in the deployment being operated; this repository consumes its decision.
An active account denied Agent permission can still sign in to see the
access-required page. Missing or inactive accounts cannot create a session.
Existing sessions recheck permissions on each protected owner/admin request and
on /v1/session/me. Owner requests may be admitted on a grant the Gateway gave
up to 10 seconds earlier (AGENT_ACCESS_ALLOW_TTL_S): a page load sends a
burst of owner requests, and concurrent lookups for one account share a single
Gateway call. Only a grant is kept. A denial, an unknown account and an
unavailable Gateway are asked again on every request. Sign-in, /v1/session/me,
the admin surface and the checks made when a turn starts never use a kept
grant, and a denial /v1/session/me receives ends the kept grant at once. The session response exposes
agent_access: {allowed, permissions}. The web app denies access if that field
is missing, including responses from an older backend.
Checks govern admission of each request. A policy change does not itself close an open stream or cancel a running job. An owner stream (a job’s events, the account’s change feed, a terminal’s output) is admitted when it opens and ends after about 15 minutes. The client reconnects from where it left off, and that request is admitted again, so a withdrawn permission or an expired session stops a stream within that bound. Agent keeps no permissions in a session, and an owner request’s kept grant lasts at most 10 seconds; the Gateway’s account cache can still delay direct database edits until its expiry. Ordinary Gateway account-management operations invalidate that cache.
Failure behavior
Condition |
Result |
|---|---|
Explicit policy denial |
Protected APIs refuse access; an active account can see the access-required page |
Gateway 403 typed |
Refuse a new login; existing sessions receive a denied access decision |
Missing identity configuration, missing access endpoint or rejected dispatch credential |
HTTP 503, typed |
Transport/server failure, unreadable decision or untyped/proxy 403 |
HTTP 503; retry after the Gateway recovers |
Expired or invalid login-flow state |
Return to login and start a fresh flow |
When /me cannot confirm current access, the web app hides protected content
and offers a retry without forcing another sign-in. A callback outage returns
to the public login page with the original destination and a retry action; the
consumed login-flow cookie is cleared. Configuration failures ask the operator
to correct the deployment. An invalid /me payload has its own explanation.
Without a configured web destination, the API retains its HTTP error response.
Unverified state and consumed authorization codes are never reused.
Infrastructure credentials
ADMIN_TOKEN independently authorizes infrastructure administration even when
the Gateway is unavailable. The on-call dispatch credential must also pass the
configured service account’s agent.use check before creating a job; its
polling and cancellation access remains available for cleanup. Host/worker
credentials, attempt lease fences and owner checks are separate controls.
Sessions carry minimal identity only. See minimal identity for cookie contents, issuer binding and upgrade constraints.
Verify the Gateway contract
The schema is in
internal-lookups.openapi.yaml.
Local tests validate its error types against the Agent client and permission
resolver. To validate an isolated or otherwise authorized Gateway over HTTP:
# Set both values securely in the environment before running; never commit them.
export AGENT_CONTRACT_GATEWAY_URL
export AGENT_CONTRACT_DISPATCH_TOKEN
uv run pytest -q -m external tests/external/test_gateway_access_contract.py
The probe queries a randomly named missing account and checks the real 403
against the schema and consumer. It creates no accounts and opens neither
database. It requires explicit Gateway credentials and is excluded from default
CI. Historical verification exercised the production Gateway router/error
handler from c7987680 (hybridInference#1408) through an isolated empty-store
fixture over loopback HTTP; it did not verify a live deployment.
Rollout
For a deployment first adopting permission-based access:
Configure its Gateway policy registration.
Release the Gateway policy hook and access endpoint together with compatible distribution backend/frontend artifacts. Verify an admitted account, an active account without permission, and a missing/inactive account. Ensure the deployment’s proxy routes the internal access path correctly.
Release Agent backend and web app together. Check fresh login, existing sessions, owner APIs and administration.
A Gateway without the access endpoint is incompatible and causes protected requests to fail closed. Do not add role-based fallback to compensate. After adopting minimal identity, follow its upgrade and rollback constraints: older Agent binaries do not enforce the database issuer, and profile-bearing cookies require a fresh login. Source changes alone do not update pinned images or deploy either service.
The implementation is in
agent_access.py,
deps.py and
session.py.
Local verification (2026-09-10)
The candidate sources were exercised through native HTTP listeners: the Gateway’s
production internal router and PostgreSQL operational store, the actual
FreeInference backend extension loader, and the Agent’s create_app control
plane. Gateway and Agent used separate fresh PostgreSQL databases with their
production schemas. The Agent database contained no users table.
Twenty-five assertions passed: current FreeInference admission, inactive/missing
account refusal, owner and host APIs, old-session revocation, /me permissions,
a custom policy admitting non-admin accounts, separate use/admin permissions,
neutral-policy denial, policy exception handling, an old Gateway lacking the
endpoint, and local infrastructure-token access in each mode.
This run checks local HTTP authorization and database separation. It does not claim a staging deployment, a browser login redirect round trip, or execution of a sandbox job. No deployment lock or published image was changed.
The login follow-up was also checked in an actual browser against the production Next.js build and Agent application, using a local HTTP Gateway fixture with PKCE code exchange and signed identity tokens. An active account without Agent permissions reached the access-required page. An existing session stayed on a retry page during a Gateway outage and recovered without signing in again. An outage during the callback returned a retry link that started a fresh login; an HTML proxy 403 showed the temporary-failure page. This fixture-based browser run does not claim staging deployment or sandbox execution.