# Onboarding to thatsmyhuman

You are reading the onboarding guide for the **thatsmyhuman** platform — a
human-verified identity system for AI agents. After following this guide you
will have an mTLS certificate that proves your owner verified your identity,
and you will be able to call services using that certificate.

## Prerequisites

- Network access to the URL you fetched this file from (the portal)
- Ability to make HTTP requests (e.g. via curl, requests, fetch)
- Ability to generate an RSA or ECDSA key pair and a CSR (Certificate Signing
  Request)
- Ability to store files on disk (private key, certificate, CA cert)
- `jq` for building/parsing JSON in the shell examples below (any JSON-capable
  runtime works as a fallback, e.g. `python3 -c 'import json,...'` — see
  Step 6 for a full runnable fallback)

## Derive Your URLs

This environment's URLs are fixed for this document:

- The portal is at https://staging.thatsmyhuman.ai
- The certificate authority (CA) is at https://ca.staging.thatsmyhuman.ai

Sanity-check the CA is reachable:

```bash
curl -sf https://ca.staging.thatsmyhuman.ai/ca.crt
```

No demo service is deployed in this environment. The verification steps
below use local certificate checks instead of a live demo call.

A machine-readable copy of these URLs is available at https://staging.thatsmyhuman.ai/.well-known/tmh.json.

## Choose Your Security Level

Before starting enrollment, check if a key-vault is available on your machine:

```bash
curl -sf http://localhost:9999/status
```

If it responds with a JSON object, use the **Key-Vault Flow** below
(recommended — your private key stays protected in hardware).

If it returns a connection error, use the **Direct Flow** further down
(file-based keys). Consider asking your owner:
> "Would you like to set up a key-vault for secure key storage?
> It keeps my private key protected so I can never accidentally expose it."

## Key-Vault Flow (Recommended)

If a key-vault is running at `localhost:9999`, follow this simpler flow.
The key-vault handles all cryptographic operations — you never touch a
private key.

### Step 1: Create a Claim

Same as the Direct Flow — create a claim at the portal:

```bash
curl -X POST https://staging.thatsmyhuman.ai/api/claims \
  -H "Content-Type: application/json" \
  -d '{"agent_name": "my-agent"}'
```

Save the `code` and `pair_words` from the response.

### Step 2: Tell Your Owner

Display the `claim_url` and `pair_words` to your owner. Ask them to open the
URL and approve.

### Step 3: Poll for Approval

```bash
curl https://staging.thatsmyhuman.ai/api/claims/{code}
```

Wait for `"status": "approved"`. Save the `token`.

### Step 4: Onboard via Key-Vault

Send the onboarding token to the key-vault. It will generate a key pair in
its secure storage, get your certificate signed, and activate your agent —
all without exposing the private key.

```bash
curl -X POST http://localhost:9999/onboard \
  -H "Content-Type: application/json" \
  -d '{"onboarding_token": "{token}"}'
```

Response (201 Created):
```json
{
  "session_token": "your-session-token",
  "agent_id": "agent-a1b2c3d4",
  "expires_in": 3600
}
```

Save the `session_token`. You will use it for all subsequent requests.

### Onboarding Complete

The `201 Created` response from `/onboard` in the previous step, containing
your `session_token`, is the terminal result of this flow — there is nothing
further to verify. Re-probing `/status` would not confirm anything new about
the new session.

You are onboarded with secure key storage.

---

## Direct Flow: Claim-Based Enrollment (File-Based Keys)

This is the self-service flow. You initiate enrollment; your owner approves it
in their browser.

### Step 1: Create a Claim

```bash
CLAIM=$(curl -sS --fail-with-body -X POST https://staging.thatsmyhuman.ai/api/claims \
  -H "Content-Type: application/json" \
  -d '{"agent_name": "my-agent"}')
CODE=$(echo "$CLAIM" | jq -r .code)
CLAIM_URL=$(echo "$CLAIM" | jq -r .claim_url)
PAIR_WORDS=$(echo "$CLAIM" | jq -r .pair_words)
EXPIRES_AT=$(echo "$CLAIM" | jq -r .expires_at)
```

Response (201 Created):
```json
{
  "code": "a1b2c3d4e5f6",
  "claim_url": "https://staging.thatsmyhuman.ai/claim/a1b2c3d4e5f6",
  "pair_words": "WOLF-PANDA",
  "expires_at": "2026-03-21T10:10:00Z"
}
```

`CODE`, `CLAIM_URL`, `PAIR_WORDS`, and `EXPIRES_AT` are now set for the rest
of this flow.

### Step 2: Tell Your Owner

Display `$CLAIM_URL` and `$PAIR_WORDS` to your owner. Ask them to open the
URL in their browser and verify the pairing code matches. They will click
"Approve" after confirming.

Example message to your owner:
> Please open https://staging.thatsmyhuman.ai/claim/a1b2c3d4e5f6 in your browser.
> Verify the code is WOLF-PANDA, then click Approve.

### Step 3: Poll for Approval

Poll every 3 seconds until your owner approves, bounded by the claim's own
`$EXPIRES_AT` (not a hardcoded duration):

```bash
while true; do
  RESP=$(curl -sS --fail-with-body "https://staging.thatsmyhuman.ai/api/claims/$CODE")
  STATUS=$(echo "$RESP" | jq -r .status)
  case "$STATUS" in
    approved)
      TOKEN=$(echo "$RESP" | jq -r .token)
      AGENT_ID=$(echo "$RESP" | jq -r .agent_id)
      break
      ;;
    expired)
      echo "Claim expired before approval; create a new claim." >&2
      exit 1
      ;;
  esac
  NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  if [[ "$NOW" > "$EXPIRES_AT" ]]; then
    echo "Claim window closed without approval (expires_at=$EXPIRES_AT)." >&2
    exit 1
  fi
  sleep 3
done
```

Responses:
- Waiting: `{"status": "pending"}`
- Approved: `{"status": "approved", "token": "eyJ...", "agent_id": "agent-a1b2c3d4"}`
- Expired: `{"status": "expired"}`

**Important:** The token is returned exactly once. Store it immediately. If you
miss it, the claim is consumed and you must create a new one.

`TOKEN` and `AGENT_ID` are now set for the rest of this flow.

### Step 4: Get the CA Certificate

Fetch the CA certificate. You will need it to verify TLS connections to
services and to complete the mTLS handshake.

```bash
curl -o ca.crt https://ca.staging.thatsmyhuman.ai/ca.crt
```

### Step 5: Generate a Key Pair and CSR

Generate a private key and a Certificate Signing Request (CSR). The CSR's
Common Name (CN) **must** match `$AGENT_ID` from Step 3.

Using ECDSA (P-256, recommended — smaller, faster, and already accepted):
```bash
(umask 077; openssl ecparam -name prime256v1 -genkey -noout -out agent-key.pem)
openssl req -new -key agent-key.pem -out agent.csr \
  -subj "/CN=$AGENT_ID"
chmod 600 agent-key.pem
```

Or using RSA (minimum 3072 bits — the CA rejects smaller keys):
```bash
(umask 077; openssl genrsa -out agent-key.pem 3072)
openssl req -new -key agent-key.pem -out agent.csr \
  -subj "/CN=$AGENT_ID"
chmod 600 agent-key.pem
```

### Step 6: Sign Your CSR

Submit the CSR and your onboarding token to the CA, save the issued
certificate, and capture the serial number for Step 7:

```bash
jq -n --arg csr "$(cat agent.csr)" --arg tok "$TOKEN" \
  '{csr_pem: $csr, onboarding_token: $tok}' \
  | curl -sS --fail-with-body -X POST https://ca.staging.thatsmyhuman.ai/sign \
    -H "Content-Type: application/json" -d @- \
  > sign-response.json

jq -r .certificate_pem sign-response.json > agent-cert.pem
SERIAL=$(jq -r .serial_number sign-response.json)
```

If `jq` is not available, this runnable `python3` fallback does the same
thing (build the request, sign, save the cert, capture the serial):

```bash
TOKEN="$TOKEN" python3 -c "
import json, os
with open('agent.csr') as f:
    csr = f.read()
print(json.dumps({'csr_pem': csr, 'onboarding_token': os.environ['TOKEN']}))
" | curl -sS --fail-with-body -X POST https://ca.staging.thatsmyhuman.ai/sign \
    -H "Content-Type: application/json" -d @- \
  > sign-response.json

SERIAL=$(python3 -c "
import json
d = json.load(open('sign-response.json'))
with open('agent-cert.pem', 'w') as f:
    f.write(d['certificate_pem'])
print(d['serial_number'])
")
```

Response (200 OK):
```json
{
  "certificate_pem": "-----BEGIN CERTIFICATE-----\n...",
  "serial_number": "abc123...",
  "not_before": "2026-03-21T10:00:00Z",
  "not_after": "2026-04-20T10:00:00Z"
}
```

The pipeline above already saved `agent-cert.pem` and set `$SERIAL`.

### Step 7: Activate Your Agent

Tell the portal that you have your certificate:

```bash
ACTIVATE_RESPONSE=$(jq -n --arg aid "$AGENT_ID" --arg serial "$SERIAL" \
  '{agent_id: $aid, cert_serial: $serial}' \
  | curl -sS --fail-with-body -X POST https://staging.thatsmyhuman.ai/api/agents/activate \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" -d @-)

API_TOKEN=$(echo "$ACTIVATE_RESPONSE" | jq -r .api_token)
```

Response (200 OK):
```json
{
  "status": "active",
  "message": "Agent activated successfully",
  "api_token": "a1b2c3..."
}
```

`API_TOKEN` is now set — store it, it is used for future agent-authenticated
API calls.

### Verify Your Certificate

Confirm the certificate is valid and matches your identity:

```bash
openssl verify -CAfile ca.crt agent-cert.pem
openssl x509 -in agent-cert.pem -noout -subject -dates
```

The subject CN must equal your `agent_id`.

Then confirm the portal itself recognizes you as active — a `200` here
proves activation landed, `$API_TOKEN` works, and the portal considers this
agent active (a check no local openssl command can make):

```bash
curl -sS --fail-with-body -H "Authorization: Bearer $API_TOKEN" \
  https://staging.thatsmyhuman.ai/api/agents/$AGENT_ID/allowlist
```

On failure: re-check that Step 7 completed successfully; see Troubleshooting.

### Next steps

- `chmod 600 agent-key.pem` if you have not already done so.
- Your certificate's `not_after` date (from the `openssl x509 -dates` output
  above) is when it expires — renew it via `POST https://staging.thatsmyhuman.ai/api/agents/cert-renewal`
  before then.
- `$API_TOKEN` is a Bearer token for future agent-authenticated API calls,
  such as `GET https://staging.thatsmyhuman.ai/api/agents/$AGENT_ID/allowlist` above.

## Alternative: Token-Based Enrollment

If your owner gives you a pre-generated JWT token (instead of using the claim
flow above), skip Steps 1-3 and go directly to Direct Flow, Step 4. First set
the two variables that Steps 1-3 would otherwise have set — the later steps
use them as-is:

```bash
TOKEN=...      # the JWT token your owner provided
AGENT_ID=...   # the agent ID your owner assigned
```

## Known Limitations

- **Python SDK:** The claim-based flow (Steps 1-3) is not yet implemented in
  the Python SDK. Use the REST API directly (as shown above) or use
  token-based enrollment.
- **Claim expiry:** Claims expire after 10 minutes. If your owner does not
  approve in time, create a new claim.
- **One-time token:** The onboarding token from Step 3 is returned exactly
  once. If you poll again after receiving it, you will get a 404.

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `POST /api/claims` returns 429 | Rate limited — create/approve/set-password share one claim-lifecycle budget | Wait the `retry_after_seconds` value from the response body (also sent as the `Retry-After` header), then create a **new** claim — any claim created earlier will likely have expired by the time the window resets. To capture the header instead of the body: `curl -sS -D headers.txt -o body.json -X POST https://staging.thatsmyhuman.ai/api/claims -d '{"agent_name": "my-agent"}'; grep -i '^retry-after' headers.txt` |
| Poll stays `pending` until expiry even though the owner clicked Approve | The owner's approval attempt was rate-limited by the same shared budget | Wait, then create a new claim and re-send the pairing URL to your owner |
| Poll returns `{"status": "expired"}` | Owner did not approve within the claim's window | Create a new claim |
| Poll returns 404 | Token already delivered (one-time read) or claim unknown | Create a new claim |
| `POST /sign` returns 403 "invalid onboarding token" | Token expired or malformed | Get a new token via claim flow |
| `POST /sign` returns 403 "token agent_id does not match CSR CN" | CSR CN does not match agent_id | Regenerate CSR with correct CN |
| mTLS call returns TLS error | Wrong CA cert or missing client cert | Ensure `--cacert ca.crt --cert agent-cert.pem --key agent-key.pem` are passed on every mTLS request to any service |
| `POST /api/agents/activate` returns 401 | Missing or invalid Authorization header | Include `Bearer $TOKEN` header |
