Skip to content

feat(keys): drop the impossible key picker, name device keys, add keys audit - #232

Merged
duyet merged 2 commits into
mainfrom
feat/keys-simplify-audit
Oct 27, 2026
Merged

duyet merged 2 commits into
mainfrom
feat/keys-simplify-audit

Conversation

@duyet

@duyet duyet commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Why

The owner asked whether device login can ever pick an old key. From the CLI it can't:

  • POST /api/v1/keys/:hash/reveal uses requireAuth, which accepts only a session or an ak_ key. Every CLI login stores an sk-ar- key. A prod probe with the smoke key returned 401 on both of its keys.
  • Device-minted keys are stored with empty ciphertext (token.post.ts binds "", "", ""), so nobody can reveal them, not even from a session.

So keys use, the launcher's key… item, the config TUI's "Switch key" / api key row, and the silent resolve_latest_key call at login never worked: they always failed or did nothing. (The browser consent page can still hand over an existing dashboard key. That is server-side and is unchanged.)

What changes

  • Removed: keys use (it now prints how to get a key), bind_agent_key / switch_agent_key, both launcher key… entries, ConfigureKind::Key, the legacy config "Switch key" item (menu arms re-mapped), resolve_latest_key, reveal_key, save_agent_key, RemoteKey.can_reveal.

  • Kept: local profile switching, which works: auth switch, account use, config use, per-agent account…. The config TUI api key rows stay visible; pressing Enter on one prints the login / --key - hint. A per-agent api_key already in config is still read.

  • Device login names the key <host> anyr <YYYY-MM-DD> (key_label, which the server already accepts, capped at 60 chars). The host comes from gethostname, with env as the fallback.

  • Re-login revoke offer. After a browser or device login, if this profile's previous key is still on the server, has a cli* origin and differs from the new key, the CLI asks Revoke the previous key from this device, "<name>" (…abcd)? [y/N] and revokes only on yes. --yes does not answer this prompt. Scripted runs revoke nothing and print Revoke it: anyr keys revoke abcd.

  • anyr keys audit [--json] lists suggested removals with the reason for each:

    • never used and older than 7 days;
    • no use for 30 days or more;
    • an older device-login key from the same host that a newer key replaced, and that has not been used since the newer key was created.

    It never suggests the key this machine uses, and never a key with requests in the last 30 days. It only suggests and never deletes. The same rules, with the same fixed-date cases, are in duyet/anyrouter apps/dashboard/src/app/keys/-components/key-audit.ts.

  • anyr keys revoke <last4|id|hash>: the existing confirm stays, and the prompt warns when you are revoking the key this machine uses.

  • Output shows only the last 4 characters of a key. Printed hints no longer leak the internal usage marker.

Verification

  • cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, and cargo test --locked --all-targets pass (1012 tests) after rebasing on 0.1.34.
  • New tests: src/cmd/key_audit.rs (rules with fixed dates, current key never suggested, revoke only after yes, no revoke and only a hint when non-interactive), src/civil.rs, auth::device_start_names_the_key_after_the_device, and tests/keys_audit.rs. The last one runs against a mock server: device login sends key_label, re-login sends no DELETE and prints the hint, keys audit skips the current key, keys use is gone, and keys revoke <last4> deletes that hash.
  • Real run against prod with a temp HOME and the smoke key: login --key -, keys list, keys audit ("No unused keys. Checked 2 keys."), keys audit --json, keys use (exit 2 with the hint), keys revoke zzzz (exit 3, not found), and auth status. Nothing was revoked. Device login itself can't be approved unattended, so the label and the revoke offer are covered only by the mocked tests.

Platform side: the dashboard "Unused keys" notice is in duyet/anyrouter (linked in a comment).

🤖 Generated with Claude Code

duyet and others added 2 commits October 28, 2026 00:11
The CLI's sk-ar key cannot reveal another key (reveal is session-only, prod
returns 401), so keys use, the launcher key… item and the config Switch key
never worked. Remove them; local account switching stays.

Device login now names its key <host> anyr <date>. A re-login offers to
revoke this machine's previous device key after a yes, and prints the
command in scripted runs. New: keys audit and keys revoke <last4|id>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
@duyetbot

Copy link
Copy Markdown
Member

Dashboard half: duyet/anyrouter#6665 (Unused keys notice, same rules and fixed-date cases).

@github-actions

Copy link
Copy Markdown
Contributor

anyr size and startup

Startup is wall time for a cold anyr --version / anyr --help (median of 21 runs). Size is the stripped release binary, or the .wasm for the browser demo.

Size budget: anyr-linux-x86_64 must stay ≤ 4.0 MiB (4194304 bytes) stripped.

Asset Kind Size --version median --help median
anyr-darwin-arm64 native 2.4 MiB 5.66 ms 7.0 ms
anyr-darwin-x86_64 native 2.9 MiB 55.93 ms 54.52 ms
anyr-linux-arm64 native 2.9 MiB 0.89 ms 0.96 ms
anyr-linux-x86_64 native 3.4 MiB 0.66 ms 0.64 ms
anyr-windows-x86_64.exe native 2.9 MiB 8.77 ms 8.94 ms
anyr.wasm wasm 251.8 KiB — —

Budget check: ok

raw timings
[
  {
    "asset": "anyr-darwin-arm64",
    "kind": "native",
    "path": "/Users/runner/work/cli/cli/anyr-darwin-arm64",
    "bytes": 2537224,
    "size": "2.4 MiB",
    "version": "0.1.34 (built 2026-10-27 17:19:43)",
    "target": "aarch64-apple-darwin",
    "os": "macos-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 4.2,
      "median_ms": 5.66,
      "p95_ms": 8.46,
      "mean_ms": 6.05
    },
    "startup_help": {
      "n": 21,
      "min_ms": 3.68,
      "median_ms": 7.0,
      "p95_ms": 11.86,
      "mean_ms": 6.91
    }
  },
  {
    "asset": "anyr-darwin-x86_64",
    "kind": "native",
    "path": "/Users/runner/work/cli/cli/anyr-darwin-x86_64",
    "bytes": 3037616,
    "size": "2.9 MiB",
    "version": "0.1.34 (built 2026-10-27 17:20:31)",
    "target": "x86_64-apple-darwin",
    "os": "macos-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 39.6,
      "median_ms": 55.93,
      "p95_ms": 107.69,
      "mean_ms": 59.87
    },
    "startup_help": {
      "n": 21,
      "min_ms": 40.59,
      "median_ms": 54.52,
      "p95_ms": 68.0,
      "mean_ms": 55.63
    }
  },
  {
    "asset": "anyr-linux-arm64",
    "kind": "native",
    "path": "/home/runner/work/cli/cli/anyr-linux-arm64",
    "bytes": 3020040,
    "size": "2.9 MiB",
    "version": "0.1.34 (built 2026-10-27 17:19:23)",
    "target": "aarch64-unknown-linux-gnu",
    "os": "ubuntu-24.04-arm",
    "startup_version": {
      "n": 21,
      "min_ms": 0.81,
      "median_ms": 0.89,
      "p95_ms": 1.0,
      "mean_ms": 0.89
    },
    "startup_help": {
      "n": 21,
      "min_ms": 0.86,
      "median_ms": 0.96,
      "p95_ms": 1.04,
      "mean_ms": 0.96
    }
  },
  {
    "asset": "anyr-linux-x86_64",
    "kind": "native",
    "path": "/home/runner/work/cli/cli/anyr-linux-x86_64",
    "bytes": 3516632,
    "size": "3.4 MiB",
    "version": "0.1.34 (built 2026-10-27 17:19:21)",
    "target": "x86_64-unknown-linux-gnu",
    "os": "ubuntu-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 0.63,
      "median_ms": 0.66,
      "p95_ms": 0.69,
      "mean_ms": 0.66
    },
    "startup_help": {
      "n": 21,
      "min_ms": 0.61,
      "median_ms": 0.64,
      "p95_ms": 0.9,
      "mean_ms": 0.68
    }
  },
  {
    "asset": "anyr-windows-x86_64.exe",
    "kind": "native",
    "path": "D:\\a\\cli\\cli\\anyr-windows-x86_64.exe",
    "bytes": 2991616,
    "size": "2.9 MiB",
    "version": "0.1.34 (built 2026-10-27T17:20:25Z)",
    "target": "x86_64-pc-windows-msvc",
    "os": "windows-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 8.55,
      "median_ms": 8.77,
      "p95_ms": 9.42,
      "mean_ms": 8.86
    },
    "startup_help": {
      "n": 21,
      "min_ms": 8.67,
      "median_ms": 8.94,
      "p95_ms": 9.63,
      "mean_ms": 9.04
    }
  },
  {
    "asset": "anyr.wasm",
    "kind": "wasm",
    "path": "/home/runner/work/cli/cli/target/wasm-pkg/anyr_cli_bg.wasm",
    "bytes": 257891,
    "size": "251.8 KiB",
    "version": "wasm",
    "target": "wasm32-unknown-unknown",
    "os": ""
  }
]

@duyetbot

Copy link
Copy Markdown
Member

APPROVE. Reviewed head 976922c. Revoke is never silent: the re-login prompt uses term::confirm, which ignores --yes; non-interactive runs only print the revoke command; the new key is never offered. keys audit never lists the key in use. Output shows last 4 only. Rust tests (keys_audit 4, key_audit unit 11) pass; prod run of keys audit with the smoke key returned no suggestions and printed no secret. Plain top-left output.

@duyet
duyet merged commit 710f40c into main Oct 27, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants