# Jsonify agent setup

Jsonify supports `anonymous` registration for free brief preparation and `service_auth` for human-authorized workspace access. Start anonymously to try Jsonify without an email or human verification. Anonymous agents cannot build workspaces, access customer data or start schedules. Follow the matching path in the WorkOS guide below.

Request resource `https://factory.jsonify.com/mcp` and scope `jsonify:prepare` before claiming, or `jsonify:access` after human authorization. Use the resulting bearer access token with the MCP endpoint at that resource. For workspace access, have the user complete the verification link through Jsonify onboarding and give you the displayed code. Users can block access at `/setup/agent`.

When requesting workspace access, ask for the human user's email only if you do not already know which account they want. Anonymous preparation needs no email. The agent does not need its own email. Handle registration, credential storage, exchange and refresh yourself; the human only signs in, authorizes access and returns the displayed code. Preserve the verification URL exactly, including its `token` query.

To prepare: call `start_preparation`, retain its session_id and private preparation_token, then use `request_work(workspaceless=true)` and `get_conversation`. Return the signup_url with the finished brief. The human reviews it and signs in to build through the existing /setup flow; anonymous tokens never authorize a build. Preserve the preparation so the work is not lost. After setup, connect with human-authorized credentials to continue.

For step 4 below, use this Jsonify-specific exchange command instead of the generic example. Both the resource and scope are required:

```bash
SCOPE=jsonify:prepare # use jsonify:access only after human authorization
ASSERTION="$(security find-generic-password -s "$SVC" -a "$ACCT" -w | jq -r .identity.assertion)"
CRED="$(curl -sS "$ISSUER/oauth2/token" -H 'Content-Type: application/x-www-form-urlencoded' \
  -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
  --data-urlencode "assertion=$ASSERTION" \
  --data-urlencode resource=https://factory.jsonify.com/mcp \
  --data-urlencode "scope=$SCOPE")"
ACCESS_TOKEN="$(printf '%s' "$CRED" | jq -er .access_token)"
```

---

# auth.md — agent registration

You are an agent. This service allows you to register for an identity and exchange it for credentials.

Run these commands in order. **Don't print secret values and don't write your own scripts** — the commands below persist secrets and surface only non-secret fields. Commands are macOS (`curl`, `security`, `jq`); adapt as needed for Linux (`secret-tool`, reads stdin) or Windows (PowerShell `Set-Secret`/`Get-Secret`). Only `security` takes the secret on argv — `secret-tool`/`Set-Secret` don't, so don't carry its "rotate if exposed" caveat to them.

Set once:

```bash
ISSUER="https://auth.jsonify.com"
SVC="${ISSUER#https://}"        # keychain service label (issuer host)
EMAIL="<user-email>"           # login_hint, and keychain account for email-based methods
```

## 1. Check for an existing registration — reuse before creating a new one

```bash
if   security find-generic-password -s "$SVC" -a "$EMAIL"  -w >/dev/null 2>&1; then ACCT="$EMAIL"
elif security find-generic-password -s "$SVC" -a anonymous -w >/dev/null 2>&1; then ACCT="anonymous"
fi
# ACCT set -> skip to step 4 (Exchange). ACCT unset -> continue to step 2.
```

## 2. Register — pick the method that fits; capture into `$REG`, never print it

Prefer the lightest method that does the job — don't reach for the user's email by default. A new user can start **anonymous** to trial the service: it grants `jsonify:prepare` immediately, with no email and no human step. If that covers what you're doing, stay anonymous. Step up to **service_auth** (and its claim ceremony) only when you need broader access (the post-claim scopes) or the user already has an account and wants to sign in.

```bash
# service_auth — you have the user's email (claim ceremony required):
REG="$(curl -sS "$ISSUER/agent/identity" -H 'Content-Type: application/json' \
  -d "{\"type\":\"service_auth\",\"login_hint\":\"$EMAIL\"}")"
ACCT="$EMAIL"
# errors: invalid_request -> fix the body; invalid_login_hint -> fix EMAIL; service_auth_registration_disabled -> method not enabled for this environment.

# anonymous — no identity / defer it (claim ceremony optional):
REG="$(curl -sS "$ISSUER/agent/identity" -H 'Content-Type: application/json' \
  -d '{"type":"anonymous"}')"
ACCT="anonymous"
# errors: invalid_request -> fix the body; anonymous_registration_disabled -> method not enabled for this environment.
```

Persist the whole response now for **anonymous** (carries `identity.assertion`); for **service_auth** keep `$REG` in memory and persist after the claim verifies (step 3):

```bash
security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$REG"   # not after a service_auth registration
```

## 3. Claim ceremony — service_auth (required), anonymous (optional)

Mint an attempt and give the user its link. They sign in there and the page shows them a code; once they read it back to you, complete the claim with it.

```bash
CLAIM_TOKEN="$(printf '%s' "$REG" | jq -r .claim.token)"
ATT="$(curl -sS "$ISSUER/agent/identity/claim" -H 'Content-Type: application/json' \
  -d "{\"type\":\"service_auth\",\"claim_token\":\"$CLAIM_TOKEN\",\"login_hint\":\"$EMAIL\"}")"
# errors: invalid_claim_token -> restart at step 2; invalid_login_hint -> fix EMAIL; claim_expired | claim_revoked | already_claimed -> restart at step 2; auth_method_disabled -> method was disabled; too_many_attempts -> wait for a pending attempt to expire.

# give the user this link (non-secret); the code is shown to them on the page, not here:
printf '%s' "$ATT" | jq '{verification_uri:.attempt.verification_uri}'

# the user reads the code off that page and gives it to you; submit it with the claim token:
USER_CODE="<code the user read off the claim page>"
VER="$(curl -sS "$ISSUER/agent/identity/claim/complete" -H 'Content-Type: application/json' \
  -d "{\"claim_token\":\"$CLAIM_TOKEN\",\"user_code\":\"$USER_CODE\"}")"

# success returns the verified identity once — persist it; an error returns {code,message}:
printf '%s' "$VER" | jq -e .identity.assertion >/dev/null && security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$VER"
# errors: claim_not_confirmed -> user hasn't finished on the page yet, wait and retry; invalid_user_code -> wrong code, ask the user again; user_code_expired -> re-run this step for a fresh link; claim_expired | already_claimed -> restart at step 2; claim_denied -> user denied the claim, restart at step 2; auth_method_disabled -> method was disabled; organization_selection_required | stale_organization_selection -> user must re-select an organization on the claim page.
```

## 4. Exchange the assertion for an access token

```bash
ASSERTION="$(security find-generic-password -s "$SVC" -a "$ACCT" -w | jq -r .identity.assertion)"
RESOURCE="<resource-uri>"   # optional — binds the token's aud to a resource this service recognizes; omit both resource lines for the default audience
CRED="$(curl -sS "$ISSUER/oauth2/token" -H 'Content-Type: application/x-www-form-urlencoded' \
  -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
  --data-urlencode "assertion=$ASSERTION" \
  --data-urlencode "resource=$RESOURCE")"
ACCESS_TOKEN="$(printf '%s' "$CRED" | jq -r .access_token)"
# errors: invalid_request -> assertion could not be decoded; invalid_grant -> assertion expired/revoked, restart at step 2; invalid_target -> resource URI not recognized; unsupported_grant_type -> use the grant above.
```

Send it as `Authorization: Bearer $ACCESS_TOKEN`. **Don't persist the access token** — it's short-lived (~5 minutes). When it expires, re-run this step from the stored assertion; when the assertion itself expires, refresh (step 5).

## 5. Refresh — when the stored assertion nears expiry

```bash
RT="$(security find-generic-password -s "$SVC" -a "$ACCT" -w | jq -r .identity.refresh_token.value)"
REG="$(curl -sS "$ISSUER/agent/identity" -H 'Content-Type: application/json' \
  -d "{\"type\":\"refresh\",\"refresh_token\":\"$RT\"}")"
security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$REG"   # rotates the refresh token; overwrite
# errors: invalid_refresh_token -> restart at step 2.
```

Then re-run step 4 with the fresh assertion.

## Any request

`5xx` -> back off and retry the same request. `rate_limit_exceeded` -> wait `retry_after` seconds. A `4xx` not listed above -> fix per the response body; don't replay.
