# Agent login — getting your own API token

> **Are you a person?** You reached this from **"Login for LLM agents."** You don't
> sign in here yourself — your **agent** does. Hand your LLM agent (Claude, Codex, …)
> this site's URL and tell it to connect; it runs the flow below and asks you to
> approve **once** in your browser. You never paste a password or key into the agent.
>
> **Are you an agent? → Start at [agents.md](/agents.md), not here.** `agents.md` is the
> full playbook for *operating* Casework: **this is a JSON-API app — every human screen
> has a machine-readable mirror, so use the API, don't drive a browser or scrape the
> DOM.** This page is only step one (getting your token). **Read
> [agents.md](/agents.md) first** to learn how to actually work the resource, then use
> the login flow below.

This is how an autonomous agent obtains its **own** scoped Sojourncase API token
without the user ever pasting a key into you. It is a **device-grant** flow (RFC
8628 in spirit): you start a short-lived request, the human approves it in their
browser, you poll for the token, and you **ack** it to make it permanent.

All paths below live under `/.agentic/login/` — e.g.
`https://sojourn.cross-fare.com/.agentic/login/transient`. The
`/api/agentic/login/...` form works too; the SPA uses it for status/approve/deny.

> ## ⚠️ Save the token and reuse it — do NOT log in again per task
> This is the **#1 thing agents get wrong.** Once you ack, the token is **permanent**
> (valid until the user revokes it — not single-use, not per-session). **Persist it in
> your working directory** — a gitignored file like `.casework/token`, your OS secret
> store, or a `CASEWORK_TOKEN` env var — and **load that saved token on every request
> and every future run.** Running this whole grant flow again for each action spams the
> user with approval prompts and is a bug. One token authenticates *all* your future
> requests. Before starting a login, **check whether you already have a saved token.**

## The flow

```
1. POST /.agentic/login/transient {name, entityId}   → grant id (xxxx) + claimSecret + loginUrl + ackUrl
2. tell the user your name; send them to loginUrl
3. user logs in and approves (read or write)
4. poll POST /.agentic/login/transient/xxxx/claim {claimSecret}   → a PROVISIONAL token
5. ACK:  POST /.agentic/login/transient/xxxx/ack   (Authorization: Bearer <token>)   → token is now permanent
6. use the token:  Authorization: Bearer sjc_agent_…
```

The grant lives **5 minutes**. You must claim **and ack** within that window: an
un-acked token is *provisional* and lapses when the grant expires — so you never
end up with a token you can't be sure you received.

### 1 — Request a transient grant

```bash
curl -sX POST https://sojourn.cross-fare.com/.agentic/login/transient \
  -H 'Content-Type: application/json' \
  -d '{"name":"Kant","entityId":"kant-prod-1","scope":"write"}'
```

```jsonc
{
  "transientToken": "9f2c…",                  // the public id (the `xxxx`)
  "claimSecret": "a1b2…c3d4",                 // PRIVATE — keep it; never put in a URL
  "loginUrl":  "https://sojourn.cross-fare.com/login?transient_token=9f2c…",
  "claimUrl":  "https://sojourn.cross-fare.com/.agentic/login/transient/9f2c…/claim",
  "ackUrl":    "https://sojourn.cross-fare.com/.agentic/login/transient/9f2c…/ack",
  "scopeRequested": "write",
  "expiresAt": "2026-06-25T18:05:00Z",
  "pollIntervalSeconds": 3,
  "docs": "https://sojourn.cross-fare.com/agents.dir/login.md"
}
```

- `name` (required, ≤ 64 chars) — your friendly name, shown to the user at consent.
- **`entityId`** (required, ≤ 128 chars) — a **stable identifier for your agent
  identity**. Reuse the same value whenever you re-authenticate: acking a new token
  for this `(user, entityId)` **voids your previous one** — one live token per
  identity per user. Want two independent tokens? Use two different `entityId`s.
- `scope` (optional) — `"read"` or `"write"` (default `"write"`). The user can
  grant less than you ask for.
- **`claimSecret` is your credential for step 4** — it is *not* in `loginUrl`, so a
  bystander who sees the login URL still cannot claim your token. Keep it private.

### 2 — Send the user to log in

Tell the user your name and hand them `loginUrl`. They sign in and see a consent
card: **“Connect Kant? — read-only / full access — Allow / Deny.”** They can also
give you a personal label (alias). You don't watch this screen — just poll `claim`.

### 3 — (the user approves in their browser)

### 4 — Claim (provisional) token

Poll every few seconds (`pollIntervalSeconds`), sending your **claimSecret**:

```bash
curl -sX POST https://sojourn.cross-fare.com/.agentic/login/transient/9f2c…/claim \
  -H 'Content-Type: application/json' -d '{"claimSecret":"a1b2…c3d4"}'
```

| state | HTTP | body |
|---|---|---|
| still waiting | `202` | `{"status":"pending", ...}` — keep polling |
| approved | `200` | `{"status":"approved","token":"sjc_agent_…","scope":"write","ackUrl":"…"}` |
| user denied | `403` | `{"status":"denied", ...}` |
| expired / already confirmed | `410` | `{"error":"…"}` |
| wrong claimSecret | `401` | `{"error":"unauthorized"}` |

The token is returned **once**. If the response is lost to a network failure,
just **claim again** (while not yet acked): re-claiming **reissues a fresh token
and revokes the previous one**, so you converge on a token you actually received —
with no plaintext ever stored server-side.

### 5 — Ack (make it permanent) — REQUIRED

Confirm receipt by calling `ackUrl` **with the token itself** as the Bearer
credential (possession is the signature):

```bash
curl -sX POST https://sojourn.cross-fare.com/.agentic/login/transient/9f2c…/ack \
  -H 'Authorization: Bearer sjc_agent_…'
```

- `200` — done. Body: `{"status":"confirmed","permanent":true,"persist":"…","message":"…"}`.
  The token is now **permanent**, the grant is finalized (no further re-claim can
  rotate it), and your prior token for this `entityId` is voided. **Now save the token
  (see the callout above) and reuse it on every request — do not log in again.**
- `401` — the token was already superseded by a re-claim (ack with your latest one)
  or has lapsed. If it lapsed, start a new grant.

Until you ack, the token expires with the 5-minute grant. **Ack promptly** —
ideally right after claim, before relying on the token.

### 6 — Use the token

```bash
curl -s https://sojourn.cross-fare.com/.agentic/today/<caseId> \
  -H 'Authorization: Bearer sjc_agent_…'
```

The token authenticates you **as the user** for the cases they belong to, and it
deep-links exactly like the rest of the [agentic layer](navigation.md). **Reuse this
same saved token for every subsequent request and run** — only start a new grant if
the token is revoked (`401`) or you genuinely need a token for a different `entityId`.

## Scopes (access restrictions)

| scope | what it can do |
|---|---|
| `read`  | **safe methods only** — every `GET`. Any mutating verb (`POST/PUT/PATCH/DELETE`) → `403`. |
| `write` | full access, the same as the user (subject to their own case roles). |

## Lifecycle & revocation

- An **acked** token is **permanent** until revoked. The user sees and revokes
  connected agents — and can relabel them — under **Account → Connected agents**;
  a revoked token stops authenticating immediately (`401`).
- An **un-acked** token is provisional and lapses with its 5-minute grant.
- Re-authenticating with the same `entityId` voids your prior token once the new
  one is acked.
- Treat the token like a password: never log it, never put it in a URL.

## Errors

| HTTP | meaning |
|---|---|
| `401` | missing/wrong `claimSecret`; or a revoked/lapsed/unknown token on ack or an API call |
| `403` | the user denied the request, or a `read` token tried to mutate |
| `404` | unknown grant id |
| `410` | the grant expired (past its 5-minute TTL) or was already confirmed (acked) |
| `422` | invalid input (missing `name`/`entityId`, a `scope` other than `read`/`write`, or a too-long field) |
