# How agents authenticate against Reefy

Reefy's hosted dashboard at reefy.ai uses **Google OAuth** for human
sign-in. There is no public agent-to-agent (M2M) API at the
dashboard layer yet; an agent acting on behalf of a Reefy operator
should drive the same OAuth flow.

## Endpoints

- Sign-in start: `https://reefy.ai/api/auth/login/google`
  Redirects to Google's consent screen. After consent, redirects
  back to Reefy with a session cookie.
- Sign-out: `https://reefy.ai/api/auth/logout`
- Protected resource metadata (RFC 9728):
  `https://reefy.ai/.well-known/oauth-protected-resource`
- OIDC discovery (advisory; identity provider is Google):
  `https://reefy.ai/.well-known/oauth-authorization-server`

## Flow for a human-in-the-loop agent

1. Open `https://reefy.ai/api/auth/login/google` in a browser.
2. User consents on Google's screen.
3. Reefy issues a session cookie named `session_id` valid for the
   browser session.
4. Subsequent calls to `/api/*` succeed as long as the cookie is
   presented.

## Authenticated API endpoints (current)

| Path | Verb | Purpose |
|------|------|---------|
| `/api/devices` | GET | List the user's adopted devices |
| `/api/devices/<uuid>/adopt` | POST | Adopt a pending device |
| `/api/devices/<uuid>/instances` | POST | Install an app on a device |
| `/api/devices/<uuid>/sync` | POST | Force a desired-state push |
| `/api/mqtt/token` | POST | Mint a short-lived MQTT JWT |

A full OpenAPI specification is in flight and will be served at
`/openapi.json`. Agents that need machine-readable schemas should
fall back to scraping endpoints from this page until then.

## Per-device APIs

After adoption, each device exposes its own Flask-based dashboard
at `https://<device-hostname>--ssh--<id>.reefy.ai`. These device
dashboards are also Google-OAuth-protected; the same session
cookie does not transfer (each device has its own auth domain).
Agents that need to drive a device directly should treat each
device as a distinct OAuth-protected resource.

## What is NOT supported (yet)

- Client-credentials / M2M tokens for headless agents
- API keys (every authenticated path is cookie-only today)
- Scoped permissions (sessions grant full account access)

These are tracked in `PLAN-ora-ai-score-improvement.md` and will
ship as part of the OpenAPI + MCP work.

## API versioning

Current major: **v1**. Canonical URLs live under `/api/v1/*`; legacy
`/api/*` requests are rewritten to `/api/v1/*` at the WSGI layer
and remain supported for backwards compatibility.

When breaking changes ship, the new major (`/api/v2/`) is published
alongside the previous one. Deprecation timeline:

1. New major reaches **GA** - both majors live in parallel.
2. The previous major enters **deprecation** - all responses on
   that major carry RFC 8594 `Sunset:` and `Deprecation:` headers
   plus `Link: <migration-url>; rel="sunset"`.
3. **6 months** of deprecation before the previous major is
   retired. Final 30 days: 200 responses become 299 advisories
   carrying the same Sunset metadata.
4. After the Sunset date, the deprecated major returns 410 Gone.

Endpoints introduced and retired within a major use the same
header pattern - no breaking changes without a Sunset header
naming the date.

## Register / mint a credential (planned)

Until personal access tokens ship, agents drive the same OAuth
flow as the dashboard UI:

1. `GET https://reefy.ai/api/auth/login/google` (browser handles
   the consent screen).
2. Reefy sets a `session_id` cookie valid for 30 days.
3. Subsequent `/api/v1/*` calls present that cookie.

When PATs land at `POST /api/v1/account/tokens`, the same
endpoint will mint long-lived bearer tokens (no browser flow),
return JSON `{"token": "reefy_pat_..."}`, and provide list /
revoke siblings under the same path. See the `agent_auth` block
of [/.well-known/oauth-authorization-server](https://reefy.ai/.well-known/oauth-authorization-server)
for the credential endpoint contract.

## Errors

Every `/api/*` response uses the same envelope:

```json
{ "error": "Human-readable error message" }
```

Common codes the API returns:

- `400 Bad Request` - malformed JSON, missing required field, schema mismatch
- `401 Unauthorized` - missing/invalid session cookie. WWW-Authenticate header points at /.well-known/oauth-protected-resource
- `403 Forbidden` - signed in but lacks permission for this resource
- `404 Not Found` - unknown path or hidden by owner scope
- `409 Conflict` - state conflict (e.g. installing an app that's already installed)
- `429 Too Many Requests` - rate-limit cap hit. `RateLimit-Reset` carries the bucket reset epoch
- `500 Internal Server Error` - generic; details in server logs
- `503 Service Unavailable` - upstream dependency down (DB / MQTT broker / Cloudflare API)
