Skip to content

feat(keys): give each coding agent its own key or account - #252

Merged
duyet merged 4 commits into
mainfrom
feat/per-agent-keys
Nov 2, 2026
Merged

duyet merged 4 commits into
mainfrom
feat/per-agent-keys

Conversation

@duyet

@duyet duyet commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Each coding agent can now get its own API key or its own account from plain commands. Before this, the CLI read agents.<id>.api_key at launch, but only the config TUI or a hand edit could write it. A user who wanted separate spend, limits or revocation for Claude Code and Codex had to edit YAML.

New commands:

  • anyr keys create [name] --agent <agent> mints a key in the account that agent launches with, using that account's stored key, and saves it for that agent. It prints the key once. With --quiet it still saves and prints only the secret. It refuses while --key or ANYROUTER_API_KEY is set, because either would mint in another account, and the env var would keep beating the new key at launch. When --profile picks an account other than the agent's bound one, it says so on stderr.
  • anyr keys set --agent <agent> - reads a key from stdin and stores it for that agent. A key typed as an argument is refused, so it stays out of shell history. It accepts only one whole sk-ar-v1- key (16 to 128 characters after the prefix) and never repeats the input.
  • anyr keys unset --agent <agent> removes the agent's key and names the keys revoke <last4> command, since the key still works until it is revoked.
  • anyr account use --agent <agent> --clear drops an account binding.
  • anyr auth status (and whoami) lists each agent that has its own key or account: the masked key it launches with and where that key comes from. --json adds an agents array with agent, key_masked, source and account.

Design

One resolver, resolve_launch_key, returns the key and a KeySource (Flag, Env, AgentKey, AgentAccount, Account). Launch and the status view both call it, so the view cannot drift from what a launch does. The order is --key, then ANYROUTER_API_KEY, then the agent's own key, then its bound account, then --profile / ANYROUTER_PROFILE or the active account. An agent key beats --profile on purpose. That was the existing behavior, and a unit test now pins it.

AgentBinding stays the single source. No config field was renamed or added.

Fixes found along the way

  • keys create minted with the active account even when the agent was bound to another one. A key minted for codex (bound to work) would have billed default. With --agent, it now signs in as the agent's account.
  • account rename left agents bound to the old name, and account remove left them bound to a missing account. That broke their launch. Rename now rewrites the bindings, and remove unbinds them and says so.
  • mask_api_key printed a key of 22 characters or fewer in full, because the prefix and suffix covered all of it. Such keys now show only sk-ar-v1-….
  • The Unknown coding agent error listed 6 agents. It now lists all 12 from KNOWN_AGENTS.

Review

An independent Opus review asked for one change: keys create --agent minted with --key or ANYROUTER_API_KEY ahead of the agent's account. That is fixed, along with its four nits: the --profile warning, the no-key error naming the account, the stdin-only keys set, and the length cap. It found no full-key leak.

A second, fresh Opus review confirmed all five fixes. It found one more should-fix: the minted key was saved before it was printed, so a failed save lost the only copy of a key that already existed on the server. The key now prints first, and the save error says the key above was not saved. A test forces the save to fail and checks that the key was shown (mutation checked). Its nits are also done: a placeholder ANYROUTER_API_KEY is not refused (launch ignores it too), keys set prompts on a terminal, and the help text is re-wrapped. keys create --json printing text was already true on main and is out of scope here.

Tests

  • tests/agent_keys.rs (11 tests) spawns the binary with a fresh ANYROUTER_HOME and no ambient key. Two accounts and four agents prove, through --dry-run, that claude gets its own key, codex gets a different one, opencode gets its bound account and pi gets the active account. Every command's stdout and stderr is checked for full keys. keys create --agent runs against a scripted server, which asserts that the Authorization header carries the bound account's key.
  • Mutation checked: skipping the agent-key branch and minting with the active account together fail 3 of the first 7 tests. Later tests cover the review findings: an env or --key override is refused with no request sent, a bound account with no key is named (exit 4), a --profile mint warns, and a key argument or two joined keys are refused.
  • Unit tests cover resolution order (flag and env beat every binding, --profile beats the agent account but not the agent key, a dummy agent key is skipped), short-key masking and rebind_agents.
  • cargo fmt --check, cargo clippy --locked --all-targets (0 warnings) and cargo test --locked --all-targets (1063 passed, 0 failed, 1 ignored) all pass. The --no-default-features lib build adds no new warnings.

🤖 Generated with Claude Code

duyet and others added 2 commits November 2, 2026 18:18
Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
@github-actions

github-actions Bot commented Nov 2, 2026 •

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.5 MiB 7.46 ms 7.07 ms
anyr-darwin-x86_64 native 2.9 MiB 27.08 ms 32.29 ms
anyr-linux-arm64 native 2.9 MiB 0.9 ms 0.94 ms
anyr-linux-x86_64 native 3.4 MiB 1.04 ms 1.04 ms
anyr-windows-x86_64.exe native 2.9 MiB 5.5 ms 5.43 ms
anyr.wasm wasm 253.9 KiB — —

Budget check: ok

raw timings
[
  {
    "asset": "anyr-darwin-arm64",
    "kind": "native",
    "path": "/Users/runner/work/cli/cli/anyr-darwin-arm64",
    "bytes": 2587192,
    "size": "2.5 MiB",
    "version": "0.1.37 (built 2026-11-02 11:57:40)",
    "target": "aarch64-apple-darwin",
    "os": "macos-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 4.62,
      "median_ms": 7.46,
      "p95_ms": 18.63,
      "mean_ms": 8.46
    },
    "startup_help": {
      "n": 21,
      "min_ms": 4.59,
      "median_ms": 7.07,
      "p95_ms": 13.61,
      "mean_ms": 7.92
    }
  },
  {
    "asset": "anyr-darwin-x86_64",
    "kind": "native",
    "path": "/Users/runner/work/cli/cli/anyr-darwin-x86_64",
    "bytes": 3091112,
    "size": "2.9 MiB",
    "version": "0.1.37 (built 2026-11-02 11:56:57)",
    "target": "x86_64-apple-darwin",
    "os": "macos-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 26.25,
      "median_ms": 27.08,
      "p95_ms": 33.08,
      "mean_ms": 27.58
    },
    "startup_help": {
      "n": 21,
      "min_ms": 27.93,
      "median_ms": 32.29,
      "p95_ms": 34.18,
      "mean_ms": 31.65
    }
  },
  {
    "asset": "anyr-linux-arm64",
    "kind": "native",
    "path": "/home/runner/work/cli/cli/anyr-linux-arm64",
    "bytes": 3085584,
    "size": "2.9 MiB",
    "version": "0.1.37 (built 2026-11-02 11:56:52)",
    "target": "aarch64-unknown-linux-gnu",
    "os": "ubuntu-24.04-arm",
    "startup_version": {
      "n": 21,
      "min_ms": 0.84,
      "median_ms": 0.9,
      "p95_ms": 0.96,
      "mean_ms": 0.91
    },
    "startup_help": {
      "n": 21,
      "min_ms": 0.86,
      "median_ms": 0.94,
      "p95_ms": 0.99,
      "mean_ms": 0.94
    }
  },
  {
    "asset": "anyr-linux-x86_64",
    "kind": "native",
    "path": "/home/runner/work/cli/cli/anyr-linux-x86_64",
    "bytes": 3585680,
    "size": "3.4 MiB",
    "version": "0.1.37 (built 2026-11-02 11:57:04)",
    "target": "x86_64-unknown-linux-gnu",
    "os": "ubuntu-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 1.01,
      "median_ms": 1.04,
      "p95_ms": 1.16,
      "mean_ms": 1.05
    },
    "startup_help": {
      "n": 21,
      "min_ms": 1.01,
      "median_ms": 1.04,
      "p95_ms": 1.1,
      "mean_ms": 1.04
    }
  },
  {
    "asset": "anyr-windows-x86_64.exe",
    "kind": "native",
    "path": "D:\\a\\cli\\cli\\anyr-windows-x86_64.exe",
    "bytes": 3057152,
    "size": "2.9 MiB",
    "version": "0.1.37 (built 2026-11-02T11:57:08Z)",
    "target": "x86_64-pc-windows-msvc",
    "os": "windows-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 5.33,
      "median_ms": 5.5,
      "p95_ms": 5.75,
      "mean_ms": 5.53
    },
    "startup_help": {
      "n": 21,
      "min_ms": 5.39,
      "median_ms": 5.43,
      "p95_ms": 5.5,
      "mean_ms": 5.43
    }
  },
  {
    "asset": "anyr.wasm",
    "kind": "wasm",
    "path": "/home/runner/work/cli/cli/target/wasm-pkg/anyr_cli_bg.wasm",
    "bytes": 259959,
    "size": "253.9 KiB",
    "version": "wasm",
    "target": "wasm32-unknown-unknown",
    "os": ""
  }
]

duyet and others added 2 commits November 2, 2026 18:40
Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
@duyet
duyet merged commit cbbc659 into main Nov 2, 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.

1 participant