Sojourncase for agents

Agents: Sojourncase is a JSON-API app. Prefer the agentic layer — do not scrape the DOM or drive the browser unless the user's task actually requires the human UI (e.g. filling in a visual form), or the user explicitly tells you to. Every human page mirrors to a JSON envelope: /build/<trackerId>/agentic/build/<caseId>/<trackerId>, and any page URL fetched with Accept: text/markdown (or application/json) returns its machine playbook directly. Authenticate once via login.md, then read the envelope and call the /api/* endpoints in its actions.

Sojourncase is a workspace for assembling an immigration petition — tracking evidence, building specialized trackers, drafting the petition brief, and managing the case end to end. This document is the front door for software agents. Everything a human can do in the UI, an agent can do through a stable, machine-readable surface.

Served at: https://sojourn.cross-fare.com/agents.md

If you are an LLM or autonomous agent, read this page first, then go straight to GET /agentic and follow the links.

Were you handed a human page URL? (the common case)

A person will paste you a normal link — "log in to …/today", "add my conferences to …/build/<trackerId>". Do not open a browser or scrape the DOM. Every page is mirrored as a native JSON API; use it over plain HTTP:

  1. Fetch the URL with Accept: text/markdown (or application/json) — content negotiation returns that page's machine playbook (@type: "AgentLanding") with agenticView (the mirror URL), authenticate, and thenWhat. Just follow it. (If you only ever get HTML — e.g. you render in a browser — read the <script id="sojourn-agentic"> manifest, the <link rel="alternate" type="application/json"> in the head, or click the small </> API chip in the corner; all point at this page's mirror.)
  2. Authenticate (once) via the device-grant — you get your own scoped token and the user approves in their browser: login.md. POST /.agentic/login/transient {name, entityId} → send the user loginUrlclaimack. Then send Authorization: Bearer sjc_agent_… on every request.
  3. Get the page's envelope. The machine mirror of a human URL is the same path under /agentic (e.g. /build/<caseId>/<trackerId>). If the human URL hides the caseId (it usually does), start at GET /agentic and follow _links, or use the agenticView from step 1.
  4. Act. The envelope's actions array lists the exact /api/* endpoints (e.g. addRecord) with their fields. Call them with your Bearer token.

Every page also advertises its twin in metadata: <link rel="alternate" type="application/json"> (the standard one) plus <link rel="agentic-view"> / <link rel="agentic-doc"> in the <head> (and <meta> mirrors), and a Link: </agents.md>; rel="agentic-doc" HTTP header.


What the agentic layer is

The human app is a single-page React client that talks to a Rust/Axum JSON API. Every screen is backed by REST endpoints under /api/*. The agentic layer wraps that same API in three coordinated, agent-legible surfaces so you never have to scrape HTML or guess at endpoints:

The three pillars

  1. The /agentic mirror — your runtime surface. For every user-facing route in the app there is a machine-readable mirror at /agentic/<same-path>. Each returns one JSON envelope describing the current screen state (data), safe navigation to sibling screens (_links), and the unsafe mutations available from that screen (actions). This is a live, hypermedia API: you start at GET /agentic and walk the graph. The mirror is served by the API at /api/agentic/*; nginx rewrites /agentic/…/api/agentic/…, so both URLs work and the canonical form is /agentic/…. Media type: application/vnd.sojourncase.agentic+json. See navigation.md.

  2. /openapi.json + the API reference — the contract. The full REST contract lives in /openapi.json with stable operationIds (e.g. login, createCase, addRecord, autofillRecord, generateBrief). The human-readable companion is api-reference.md, whose anchors match those operationIds (e.g. …/api-reference.md#addrecord). Every envelope deep-links into both via meta.docs, so you can resolve the exact spec for any link or action you are about to use.

  3. This doc set — the prose. Conceptual guides under /agents.dir/: the envelope format, the navigation loop, the domain model, and the per-endpoint reference. The index for crawlers is /.well-known/llms.txt (also at /llms.txt).

The division of labor is strict and worth memorizing:

Surface Purpose Safe? Targets
_links in an envelope GET navigation to sibling /agentic screens safe /agentic/* mirrors only
actions in an envelope the mutations (create/update/delete, AI runs) unsafe the real /api/* endpoints
meta.docs in an envelope deep links to spec for each link + action /openapi.json, api-reference.md

You read from /agentic, you write to /api. The mirror never mutates; it only ever tells you which real /api endpoint to call.


Authenticate

You need a Bearer token. There are two ways to get one — as an agent, use the first.

Agent login (device-grant) — get your OWN token, no password

You should never handle the user's password. Instead, run the device-grant flow: you start a request, the user approves it in their browser, and you poll for a scoped token of your own (sjc_agent_…). Full walkthrough — including read vs write scope and revocation — is in login.md. In short:

POST /.agentic/login/transient    { "name": "Kant", "entityId": "kant-prod-1", "scope": "write" }
  → { transientToken, claimSecret, loginUrl, claimUrl, ackUrl, expiresAt }
# send the user to loginUrl; they log in + approve; then poll:
POST /.agentic/login/transient/<id>/claim   { "claimSecret": "…" }
  → { "status": "approved", "token": "sjc_agent_…", "scope": "write", "ackUrl": "…" }
# ACK to make it permanent (mandatory) — signed WITH the token:
POST /.agentic/login/transient/<id>/ack      (Authorization: Bearer sjc_agent_…)  → 204

The grant lasts 5 minutes; you must claim and ack within it (an un-acked token is provisional and lapses). entityId is your stable identity — re-authenticating with it voids your prior token for this user. The token is revocable/renamable by the user under Account → Connected agents. A read token may call only safe (GET) methods. Then send it like any Bearer token (step 2 below). Full flow: login.md.

User JWT — only if you legitimately hold credentials

The API also issues Bearer JWTs to email/password logins (this is what the human SPA uses). Use this only if you are acting with the user's own credentials.

  1. Get a token by posting credentials to the login endpoint:

    POST /api/auth/login
    Content-Type: application/json
    
    { "email": "you@example.com", "password": "…" }
    

    On success (200) the body is:

    { "token": "<JWT>", "user": { "id": "…", "email": "…", "name": "…" } }
    

    Unknown email and bad password return the same 401 (no account enumeration). A created-but-unapproved account returns 403.

    New accounts register at POST /api/auth/register ({ name, email, password }). Depending on the operator's policy this returns either 201 with a token (open signup) or 202 { "pending": true, "message": … } (approval required — no token until an operator approves).

  2. Send the token on every other request as an HTTP header:

    Authorization: Bearer <JWT>
    

    This applies to both the /api/* endpoints and the /agentic/* mirror — the mirror is authenticated and scoped to the calling user.

  3. Confirm the token any time with GET /api/auth/me → the current User. POST /api/auth/logout is a stateless 204 no-op (just drop the token client-side). Tokens are signed HS256 and carry the user id only; identity is always re-read from the database server-side.

402 means out of credits. AI actions (generateBrief = 48cr, reviewBrief = 24cr, autofillRecord = 2cr) debit a credit balance. A 402 response means you ran out — check GET /api/credits/balance or the credits screen.


Start here

The loop is always the same — read → link → act → resolve docs:

  1. GET /agentic with your Bearer token. This entrypoint returns your caller identity, your credit balance, the cases you can access, and a link to the visa catalog. It is the root of the link graph.

  2. Read data. Flat, camelCase, JSON-LD–sprinkled state for the screen. Enums appear as { code, label }; dates are ISO-8601.

  3. Follow _links (safe GET) to reach the screen you need — e.g. a specific case's today, build, evidence, draft, or case mirror. Links to case-scoped screens carry the caseId. RFC 6570 templated links (templated: true) require you to expand a variable (e.g. {caseId}) before fetching.

  4. Perform actions (unsafe) by calling the real /api endpoint named in the action's href with its method, contentType, and fields. Actions that cost credits carry creditsCost; AI/server-authoritative actions are marked serverAuthoritative: true — trust the server's returned result, do not predict it.

  5. Resolve meta.docs when you need the precise contract for any link or action. meta.docs.endpoints maps every _links rel and every action name to a deep doc anchor in api-reference.md / openapi.json.

A minimal first session:

POST /api/auth/login              → get { token }
GET  /agentic                     → identity, credits, cases, catalog link
follow _links.cases[i] or
GET  /agentic/today/{caseId}      → readiness, status, next deadline, agenda
follow _links to build/evidence/draft, or fire an action against /api/...

The doc set (/agents.dir/)


House rules