What has to be true before pairing works.
Access and pairing are two separate gates with two separate lifetimes. This page states both exactly, so you can tell a misconfiguration from a normal wait.
SHADOW_ONLY · AUTHORITY NONE · ENFORCEMENT OFF — Observa observes and reports. It has no authority to allow, deny, approve, block, retry, or change anything in your systems.
Pairing happens last, and only after a person approves you
- Access request — you submit non-sensitive metadata. Nothing is granted.
- Email verification — a one-time code proves the address is yours. Still nothing granted.
- Human review — a person approves or denies. This step is not automatic and cannot be skipped.
- Invitation — approval sends a single-use invitation to your verified address.
- Pairing code — you create one yourself in Hosted Observa after signing in.
- Pair — the CLI redeems that code and stores a delivery credential.
There is no path from observa request-access straight to a paired installation,
and no amount of retrying creates one. Approval also authorizes beta onboarding only: composition approval
and signed-manifest issuance are separate server-side authorities.
The six states, and what each one means
| State | Meaning | Your next action |
|---|---|---|
EMAIL_PENDING | Submitted; the address is not yet verified. | Enter the emailed code: observa request-access --code-file <0600-file>, or run
request-access again for the hidden prompt. |
PENDING_REVIEW | Email verified. Waiting on a human decision. | Wait. observa request-status to check. |
APPROVED | A reviewer approved you; an invitation was sent to the verified address. | Redeem the invitation, sign in, accept the beta documents, create a pairing code, then
observa pair. |
DENIED | A reviewer declined. | You may start over with --new. |
EXPIRED | A lifetime ran out, or verification attempts were exhausted. | observa request-access --new --api-url <https://host>. |
CANCELLED | The request was withdrawn. | You may start over with --new. |
--new is accepted only from DENIED, EXPIRED or
CANCELLED. From any other state it refuses ACCESS_ALREADY_PENDING.
Lifetimes and limits
| Thing | Limit | Detail |
|---|---|---|
Access request in EMAIL_PENDING | 24 hours | From submission. Not extended by anything. |
| Email verification code | 10 minutes | Each code is one-time. |
| Resending a verification code | Does not extend the request | A new code resets the 10-minute code window only. The 24-hour request lifetime is unchanged. |
| Minimum gap between sends | 60 seconds | An earlier resend refuses ACCESS_RATE_LIMITED. |
| Maximum sends per request | 3 | Including the first. After that, no further code can be sent for this request. |
| Failed verification attempts | Retained | A wrong or expired code still counts. The counter is never reset by a resend. |
| Attempt that ends the request | The 5th failed attempt | It moves the request to EXPIRED. Start over with --new. |
Request in PENDING_REVIEW | 30 days | From the moment the email was verified. |
| Beta invitation | 7 days, single-use | Only the invited address can redeem it. When the invitation expires or is revoked, the access
request becomes EXPIRED too. |
Reissuing an invitation (--resend while APPROVED) |
Keeps the original expiry | The previous invitation stops working immediately. Rate limited to 3 per hour. |
| New requests per email address | 3 per hour | — |
| Pairing code | 10 minutes, single-use | Bound to one organization and one workspace. A code for another tenant or workspace is refused as a boundary violation, not a typo. |
These are the values the current implementation enforces. If a command behaves differently from this table, that is worth reporting — see support.
Two things about verification that surprise people
- Your identity is frozen at submission. Name, email, organization and use case cannot be
edited afterwards; attempting it refuses
ACCESS_IDENTITY_IMMUTABLE. A different email address requires a new verified request. - One live request per address. If the same email already has a request in
PENDING_REVIEWorAPPROVED, verifying a second request for that address is refused.
First run. observa request-status reuses the endpoint saved in
your receipt. Run before any request exists, there is nothing to reuse, so it refuses
ACCESS_URL_REQUIRED rather than ACCESS_NO_REQUEST. Pass --api-url,
or run observa request-access first.
The private receipt at ~/.observa/installation/observa-access/request.json is what proves the
request is yours. A request id on its own authenticates nothing. Keep the receipt; if you lose it, you
cannot check or continue that request.
Initial pairing, unpair, and re-pair
Initial pairing
observa pair --base-url https://governance-plane-observa.tailb473db.ts.net --code-stdin
Paste the code, then Ctrl-D. A pairing code is never accepted as a command-line argument:
--code refuses outright, so a code cannot end up in shell history or a process list. The
alternative is --code-file <0600-file>, which must be owner-only.
Pairing is runtime-neutral. The code proves your invite and binds this machine to your workspace; it chooses nothing about what gets installed. The approved composition was recorded for your workspace when you were invited.
Unpair
observa unpair
This stops the service if it is running, asks Hosted to revoke the delivery credential, and reports how many credentials were revoked. It then removes the Hosted base URL, connector credential and installation binding from local config.
Hosted delivery is revoked; the installed identity is retained. The signed installation and its control-plane identity stay intact, and a recovery base URL plus the installation secret are kept locally so you can re-pair without a new code.
Re-pair
observa pair
After an unpair, pair takes no flags — passing any refuses
PAIR_RECOVERY_FLAGS_REFUSED. It self-authenticates the identity you already have and asks
Hosted to mint a replacement credential for the same deployment. No pairing code is redeemed and no new
installation is created.
EXISTING_CREDENTIAL_REQUIRES_REPLACE_FLAG
This refusal belongs to the OpenClaw connector
(@mcphersonai/mcpherson-governance-openclaw), not to Observa Local Node. The Local Node
observa pair has no --replace-existing flag, because its unpair/re-pair path
covers the same ground.
Symptom. Pairing preflight refuses before a single-use code is read or redeemed, with
Pairing preflight refused: EXISTING_CREDENTIAL_REQUIRES_REPLACE_FLAG.
Cause. The selected OpenClaw profile already holds a connector credential. Re-pairing would replace it, and a silent replacement of a working credential is not something the tool will do on its own.
Inspect first, without consuming a code
observa pair --api-url <https://host> --profile default --replace-existing --preflight-only
Preflight reports the profile mode, install layout, connector version, whether the plugin is enabled, and whether a credential is present — and reads no pairing code.
Then migrate, once you have confirmed the endpoint is the one you intend
observa pair --api-url <https://host> --profile default --replace-existing
The old credential is preserved, not overwritten. Before anything is committed, the prior
openclaw.json and the prior credential are written into an owner-only rollback snapshot in the
connector’s own state directory:
<openclaw-profile-state>/mcpherson-governance-connector/pairing-rollbacks/pair_<uuid>/
TRANSACTION.json
openclaw.json.before
deployment-credential.before
The directory is mode 0700. On success the command prints the non-secret transaction id.
Restore that exact prior state with:
observa pair --profile default --rollback pair_<uuid>
Rollback refuses if the profile changed since pairing, rather than restoring over an unknown state. If activation verification fails during pairing, the previous config and credential are restored automatically.
Why a pairing attempt gets refused
| Refusal | What it means | Safe action |
|---|---|---|
PAIR_REFUSED |
Hosted declined the code: unknown, already used, expired, or minted for a different tenant or workspace. These are deliberately one answer, so the refusal is not an oracle. | Create a fresh code and retry once. If a fresh code also fails, stop and contact support — do not loop. |
PAIR_HOSTED_UNREACHABLE |
The connection failed or timed out (10s). Nothing was redeemed. | Check the URL and your network, then retry. The code is not consumed by a failed connection. |
PAIR_BASE_URL_FLAG_REQUIRED / PAIR_BASE_URL_REFUSED |
--base-url is missing or is not https://host[:port] (plain
http is accepted only on loopback). |
Supply the origin only — no path, query or fragment. |
PAIR_CODE_INPUT_REQUIRED |
Neither or both of --code-stdin and --code-file were given. |
Choose exactly one. |
PAIR_CODE_ARGV_REFUSED |
You passed the code as an argument. | Use --code-stdin or an owner-only --code-file. Treat that code as
exposed and mint a new one. |
PAIR_CODE_FILE_PERMISSIONS_REFUSED |
The code file is group- or world-readable. | chmod 600 the file. |
PAIR_RECOVERY_FLAGS_REFUSED |
You passed flags to pair while a re-pair is pending. |
Run observa pair with no flags. |
UNPAIR_ACTIVE_BINDING_REQUIRED |
There is no complete, self-consistent active pairing to unpair. | Run observa diagnose. Do not hand-edit the config file. |
PAIR_CREDENTIAL_SHAPE_REFUSED / PAIR_CREDENTIAL_METADATA_DIVERGED /
PAIR_RESPONSE_MALFORMED |
The response did not match what the CLI independently derived. Nothing was stored. | Do not retry blindly. Contact support with the refusal code. |