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:
- Fetch the URL with
Accept: text/markdown(orapplication/json) — content negotiation returns that page's machine playbook (@type: "AgentLanding") withagenticView(the mirror URL),authenticate, andthenWhat. 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</> APIchip in the corner; all point at this page's mirror.) - 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 userloginUrl→claim→ack. Then sendAuthorization: Bearer sjc_agent_…on every request. - 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 thecaseId(it usually does), start atGET /agenticand follow_links, or use theagenticViewfrom step 1. - Act. The envelope's
actionsarray 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
The
/agenticmirror — 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 atGET /agenticand 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./openapi.json+ the API reference — the contract. The full REST contract lives in/openapi.jsonwith stableoperationIds (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 viameta.docs, so you can resolve the exact spec for any link or action you are about to use.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.
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 returns403.New accounts register at
POST /api/auth/register({ name, email, password }). Depending on the operator's policy this returns either201with a token (open signup) or202 { "pending": true, "message": … }(approval required — no token until an operator approves).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.Confirm the token any time with
GET /api/auth/me→ the currentUser.POST /api/auth/logoutis a stateless204no-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. A402response means you ran out — checkGET /api/credits/balanceor the credits screen.
Start here
The loop is always the same — read → link → act → resolve docs:
GET /agenticwith 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.Read
data. Flat, camelCase, JSON-LD–sprinkled state for the screen. Enums appear as{ code, label }; dates are ISO-8601.Follow
_links(safe GET) to reach the screen you need — e.g. a specific case'stoday,build,evidence,draft, orcasemirror. Links to case-scoped screens carry thecaseId. RFC 6570 templated links (templated: true) require you to expand a variable (e.g.{caseId}) before fetching.Perform
actions(unsafe) by calling the real/apiendpoint named in the action'shrefwith itsmethod,contentType, andfields. Actions that cost credits carrycreditsCost; AI/server-authoritative actions are markedserverAuthoritative: true— trust the server's returned result, do not predict it.Resolve
meta.docswhen you need the precise contract for any link or action.meta.docs.endpointsmaps every_linksrel and every actionnameto a deep doc anchor inapi-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/)
- navigation.md — the read → link → act → resolve loop in full: the envelope keys, the rel vocabulary, RFC 6570 templated links, case-scoping, and cursor paging.
- envelope.md — the
exact shape of the agentic envelope (
@context/@type/@id,data,_links,actions,meta) and its invariants. - routes.md — the full
list of
/agenticroutes and which UI screen each one mirrors. - domain-model.md — the entities: cases, trackers, records, fields, tasks, columns, evidence groups, docs, drafts, credits, and how they relate.
- api-reference.md —
every
/apiendpoint byoperationId, with request/response shapes; anchors matchmeta.docs.endpoints. - /openapi.json — the machine
contract;
operationIds are stable and identical to the action names. - /.well-known/llms.txt — the crawler index of this whole set.
House rules
- Never mutate via
/agentic. The mirror is read-only. Mutations always go to the real/api/*endpoint that an action'shrefpoints at. - Only follow
_linksto other/agenticroutes. They are guaranteed safe GETs into the same mirror — never cross-origin, never an unsafe verb. - Respect case scope. Most data lives under
/api/cases/{caseId}/…; the matching mirror is/agentic/<screen>/{caseId}. Always carry thecaseIdyou were given; never fabricate one. - Trust server-authoritative results. For AI actions and the credit balance, the server's returned value is canonical — merge it, don't guess it.