apus devices

Manage device pairing requests and device-scoped tokens.

Common options

When you set --url, the CLI does not fall back to config or environment credentials. Pass --token or --password explicitly, or the command errors.

Commands

apus devices list

List pending pairing requests and paired devices.

apus devices list
apus devices list --json

For a pending request on an already-paired device, the output shows requested access next to the device's current approved access, so scope/role upgrades are visible instead of looking like a lost pairing.

Paired device display names use this precedence: operator label (operatorLabel from devices rename), then client displayName, then clientId, then deviceId. Node approval notices printed by devices commands use the operator label when one is set.

apus devices approve [requestId] [--latest]

Approve a pending pairing request by exact requestId. Omitting requestId, or passing --latest, only previews the newest pending request and exits (code 1); rerun with the exact request ID to approve.

The printed approval command keeps your active profile or container, explicit Gateway URL, nondefault timeout, and JSON output mode. Token and password option values are omitted; supply the same credentials again when the preview asks you to reuse those options.

apus devices approve
apus devices approve <requestId>
apus devices approve --latest

If a device retries pairing with changed auth details (role, scopes, or public key), Apus supersedes the previous pending entry with a new requestId. Run apus devices list right before approval to get the current id.

Approval behavior:

apus devices reject <requestId>

Reject a pending device pairing request.

A connected waiting Control UI browser shows Access request declined and stops retrying automatically. It can submit a new request with Request again; reloading the page can also request again. Rejection does not persist a device ban.

apus devices reject <requestId>

apus devices join-code

Mint a single-use node onboarding URL with administrator access to the Gateway. Paste the printed npx -y apus connect <url> --service --session-host command on the machine to enroll it as a background service that can run agent sessions. Use only trusted shared infrastructure; for a command-only node, omit --session-host. This join URL is not a mobile app setup code; for Android/iOS use apus qr instead.

apus devices join-code
apus devices join-code --json

Join-code creation and redemption are core Gateway operations; no pairing plugin needs to be enabled. The URL must be reachable from the joining machine. Remote join URLs require a TLS Gateway endpoint. Explicitly configured loopback endpoints can use HTTP, provided the joining machine can reach that loopback endpoint, for example through a local tunnel.

With only the default loopback bind and no advertised endpoint, URL discovery refuses to mint a link. For a loopback Gateway behind public HTTPS ingress, set gateway.publicOrigin to the proxy's bare HTTPS origin and include the proxy's source address in gateway.trustedProxies.

Join codes, /pair, and QR setup preserve existing endpoint selection: plugins.entries.device-pair.config.publicUrl, an explicitly preferred gateway.remote.url, Tailscale Serve/Funnel, the non-preferred remote URL, then bind-derived addresses. gateway.publicOrigin is used only as the final fallback before the loopback-only error; it does not replace an existing route. Callers targeting the local Gateway omit the remote URL. HTTP(S) URLs become matching ws:/wss: pairing endpoints.

Join codes preserve the context path of a fully qualified publicUrl: for https://pair.example/extra, the join URL begins with https://pair.example/extra/j/. The device-pair plugin's /pair command instead retains its historical origin-only WebSocket endpoint, wss://pair.example.

Cloud node enrollment uses the same resolver with an explicit public-ingress preference: the pairing-specific override still wins, then gateway.publicOrigin precedes discovery for freshly provisioned workers.

For other deployment prerequisites, see Gateway deployments that cannot host nodes. Plaintext LAN pairing can use a setup code directly instead of an HTTP join URL. See Connect a machine.

apus devices remove <deviceId>

Remove one paired device entry.

apus devices remove <deviceId>
apus devices remove <deviceId> --json

A caller authenticated with a paired device token can remove only its own device entry. Removing another device requires operator.admin.

apus devices rename --device <id> --name <label>

Assign an operator label to a paired device. Labels are owner-side state: they survive pairing repairs and role re-approvals, and they do not change the stable deviceId.

apus devices rename --device <deviceId> --name "Kitchen Mac"
apus devices rename --device <deviceId> --name "Kitchen Mac" --json

apus devices clear --yes [--pending]

Clear paired devices in bulk. Gated by --yes.

apus devices clear --yes
apus devices clear --yes --pending
apus devices clear --yes --pending --json

--pending also rejects all pending pairing requests.

apus devices rotate --device <id> --role <role> [--scope <scope...>]

Rotate a device token for a role, optionally updating its scopes.

apus devices rotate --device <deviceId> --role operator --scope operator.read --scope operator.write

Returns rotation metadata as JSON. If the caller rotates its own token while authenticated with that device token, the response includes the replacement token so the client can persist it before reconnecting. Shared-secret callers and callers rotating another device never receive the bearer token.

When Doctor reports a legacy node token carrying operator scopes, use its explicit recovery command:

apus devices rotate --device <deviceId> --role node --no-scopes

This recovery requires operator.admin and preserves the device's operator pairing and approved scopes. The Gateway removes only a local cached node token that matches the retired legacy token, in the same commit as rotation. For a node host using a separate state directory, provide valid shared Gateway authentication and restart the node to refresh its cache. A retired device token alone cannot authenticate the reconnect.

apus devices revoke --device <id> --role <role>

Revoke a device token for a role.

apus devices revoke --device <deviceId> --role node

A non-admin paired-device caller can revoke only its own device token. Revoking another device's token requires operator.admin. The target scope set must also fit within the caller's own operator scopes; pairing-only callers cannot revoke admin/write operator tokens.

Notes

Token drift recovery checklist

Use this when Control UI or other clients keep failing with AUTH_TOKEN_MISMATCH, AUTH_DEVICE_TOKEN_MISMATCH, or AUTH_SCOPE_MISMATCH.

  1. Confirm current gateway token source:

    apus gateway auth-token --show
    

    Run the command in an interactive terminal on the Gateway host and treat its output as a secret.

  2. List paired devices and identify the affected device id:

    apus devices list
    
  3. Rotate the operator token for the affected device:

    apus devices rotate --device <deviceId> --role operator
    
  4. If rotation is not enough, remove the stale pairing and approve again:

    apus devices remove <deviceId>
    apus devices list
    apus devices approve <requestId>
    
  5. Retry the client connection with the current shared token/password.

Notes:

Related:

Paperclip / apus_gateway first-run approval

Paperclip agents connecting through the apus_gateway adapter go through the same first-run device pairing approval as any other new client. If Paperclip reports apus_gateway_pairing_required, approve the pending device and retry.

apus devices approve --latest

The preview prints the exact apus devices approve <requestId> command; verify the details, then rerun that command with the request ID to approve it. For a remote gateway or explicit credentials, pass the same options while previewing and approving:

apus devices approve --latest --url <gateway-ws-url> --token <gateway-token>

To avoid re-approving after every restart, configure a persistent adapterConfig.devicePrivateKeyPem in Paperclip instead of letting it generate a new ephemeral device identity each run:

{
  "adapterConfig": {
    "devicePrivateKeyPem": "<ed25519-private-key-pkcs8-pem>"
  }
}

If approval keeps failing, run apus devices list first to confirm a pending request exists.

Related