Skip to content

CLI/SDK: operator noun - human/email operator-session auth (login, logout, overview, whoami) via browser-delegated RFC 8628 #417

@MajorTal

Description

@MajorTal

Summary

Give the CLI/SDK a second auth principal - the operator, i.e. the human (email identity) - alongside the existing agent (the coding-agent-with-a-wallet, SIWX). The operator noun is human-only: operator login, operator logout, operator overview, operator whoami. The human authenticates in the browser via a device-authorization grant (RFC 8628, the aws sso login model), so the CLI never implements WebAuthn or reads a magic-link fragment - the browser does the auth (magic-link or passkey) and the CLI just brokers the resulting operator-session token.

This is one coherent feature - no phased split. The wallet's own account view is not part of it: that already exists as run402 status. Gated on the gateway device-auth bridge: kychee-com/run402-private#443.

Two-actor model

Actor Principal Auths via "who am I" account view
Operator = the human email browser-delegated session (operator login) operator whoami operator overview (email-union)
Agent = coding-agent-with-a-wallet wallet (SIWX) local allowance signature, headless run402 status / r.whoami() run402 status (active wallet)

The agent keeps doing everything per-wallet with no browser; the human logs in once to see the union across all wallets that verified their email.

Why wallet auth can't return the union

GET /agent/v1/operator/overview scopes by principal:

Principal Proves Returns
SIWX (wallet signature) "I hold this key" that wallet's slice
operator session (email) "I'm the human operating these wallets" union across ALL wallets controlling the email

A wallet signature proves control of one key, so SIWX returns that wallet's slice - never the union (deliberate anti-enumeration: a wallet pooled into an account shouldn't enumerate the others just by signing). The union needs email proof, and the CLI has no email identity today. The browser-delegated device flow is what gives the CLI that email identity. (Surfaced live: run402 billing balance <email> returns 403 ADMIN_REQUIRED; run402 wallets list is local-only.)

The operator noun (human only)

  • operator login - RFC 8628 device flow. POST .../session/device -> print verification_uri + user_code (auto-open verification_uri_complete when stdout is a TTY; --no-open to suppress) -> poll POST .../session/device/token at interval (honoring slow_down) until approved / access_denied / expired_token -> cache the email-scoped session at {base}/operator-session.json (0600). Prints email + wallet count + TTL on success.
  • operator logout - POST .../session/revoke (operator-session bearer; instant server-side revoke) then delete the local cache. Idempotent: no session -> no-op, exit 0; always clears locally even if the revoke call fails.
  • operator overview - GET /agent/v1/operator/overview with the operator-session bearer -> the email-union (scope, rollup, billing_accounts[], wallets[], advisories[]). Requires login; no session -> structured error pointing at operator login. Human only - no SIWX fallback.
  • operator whoami - local cache read: { logged_in, email, wallets[], expires_in, absolute_expires_at }, or { logged_in: false } (nonzero exit). No network.

Not built here (already covered / deliberately excluded)

  • Wallet/agent account view -> use existing run402 status - the active wallet's one-shot state (tier, billing, on-chain balance, projects, label). Loop --wallet a/b/c for the per-wallet sweep. The SIWX path of /operator/overview is not wired into the CLI.
  • No operator status command - GET /agent/v1/operator/status is wallet-authed (a single-wallet health/assurance snapshot that overlaps agent status and run402 status); it is not a human concern. Stays MCP-only (get_operator_status). (Aside: its operator/ path is misleading given it's wallet-authed - a gateway rename/re-scope candidate, out of scope here.)

Contract both sides build to (RFC 8628)

  • POST /agent/v1/operator/session/device -> { device_code, user_code, verification_uri, verification_uri_complete?, expires_in, interval }
  • Browser: user opens verification_uri, authenticates via the existing magic-link/passkey web flows, approves user_code.
  • POST /agent/v1/operator/session/device/token { device_code } -> on approval { operator_session_token, token_type:"Bearer", expires_in, absolute_expires_at, email, wallets[] }; otherwise authorization_pending | slow_down | access_denied | expired_token.
  • POST /agent/v1/operator/session/revoke (operator-session bearer) -> 204, idempotent; operator logout calls this then clears the local cache. Revocation is instant (gateway re-checks revoked_at each request; the session token is the only credential - nothing downstream dangles), so unlike aws sso logout no lingering creds outlive it.
  • GET /agent/v1/operator/overview (operator-session bearer) -> the email-union. Already exists and accepts the bearer - no new server work beyond the device + revoke endpoints in #443.

CLI/SDK scope

  • CLI: new case "operator" in cli/cli.mjs -> cli/lib/operator.mjs with login / logout / overview / whoami. Dispatch + help + e2e help snapshot.
  • Cache: core/src/operator-session.ts (mirrors allowance.ts) - read / save / clear / isExpired for {base}/operator-session.json (0600). Email-scoped at the base config dir, so it is shared across all local named wallets (log in once; every wallet's shell sees the union). Cleared by logout.
  • SDK: new r.operator namespace - overview(), deviceStart(), devicePoll(), revoke() - plus an operator-session-bearer fetch path alongside SIWX. (whoami is a pure local-cache read at the CLI edge; no SDK network method.)
  • MCP: the operator verbs are MCP-null by design - MCP authenticates as the agent (wallet/SIWX), and the human device-login flow must not place the human's email-union session in the agent's hands. get_operator_status stays an MCP tool in r.admin (the agent's wallet-health view) and does not move into r.operator. A future read-only operator_overview MCP tool could consume a CLI-minted session (never performing login itself), but is out of scope here. CLI <-> OpenClaw parity is structural, so all four verbs land in OpenClaw regardless.
  • sync.test.ts SURFACE + OpenClaw parity; docs (llms-cli.txt, SKILL.md, READMEs).
  • Agent-first: operator overview --json / operator whoami --json are clean; operator login is human-in-the-loop by nature (prints URL + code), degrades gracefully headless.

Acceptance criteria

Client side shipped in run402 / @run402/sdk / run402-mcp v2.31.0 (#419). Network paths verified live against the deployed gateway bridge (#443).

  • operator noun (login / logout / overview / whoami) - new case "operator" in cli/cli.mjs + cli/lib/operator.mjs.
  • operator login implements the RFC 8628 device flow (start + poll), prints verification_uri + user_code, auto-opens verification_uri_complete on a TTY, caches {base}/operator-session.json (0600).
  • operator overview sends the operator-session bearer -> email-union; requires login (clear error -> operator login when no session); no SIWX fallback.
  • operator logout calls POST .../session/revoke then clears the cache; idempotent.
  • operator whoami reads the local cache (email, wallets[], expiry); nonzero exit when not logged in; no network.
  • SDK r.operator namespace (overview / deviceStart / devicePoll / revoke) + operator-session-bearer fetch path.
  • core/src/operator-session.ts cache primitive (read / save / clear / isExpired), base-config-dir, 0600.
  • SURFACE + sync + OpenClaw parity; docs updated (cli/llms-cli.txt; SKILL.md / READMEs deferred until #443 makes the flow live).
  • No operator status CLI command; wallet account view stays run402 status.
  • Contract matches the shared device-auth contract in kychee-com/run402-private#443 - verified live: all four endpoints (device / device-token / revoke / overview) return the contract shapes against the deployed gateway.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions