Repository navigation
feat(keys): give each coding agent its own key or account - #252
Merged
Merged
Conversation
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>
Contributor
anyr size and startupStartup is wall time for a cold Size budget:
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": ""
}
] |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_keyat 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--quietit still saves and prints only the secret. It refuses while--keyorANYROUTER_API_KEYis set, because either would mint in another account, and the env var would keep beating the new key at launch. When--profilepicks 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 wholesk-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 thekeys revoke <last4>command, since the key still works until it is revoked.anyr account use --agent <agent> --cleardrops an account binding.anyr auth status(andwhoami) lists each agent that has its own key or account: the masked key it launches with and where that key comes from.--jsonadds anagentsarray withagent,key_masked,sourceandaccount.Design
One resolver,
resolve_launch_key, returns the key and aKeySource(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, thenANYROUTER_API_KEY, then the agent's own key, then its bound account, then--profile/ANYROUTER_PROFILEor the active account. An agent key beats--profileon purpose. That was the existing behavior, and a unit test now pins it.AgentBindingstays the single source. No config field was renamed or added.Fixes found along the way
keys createminted with the active account even when the agent was bound to another one. A key minted for codex (bound towork) would have billeddefault. With--agent, it now signs in as the agent's account.account renameleft agents bound to the old name, andaccount removeleft them bound to a missing account. That broke their launch. Rename now rewrites the bindings, and remove unbinds them and says so.mask_api_keyprinted a key of 22 characters or fewer in full, because the prefix and suffix covered all of it. Such keys now show onlysk-ar-v1-….Unknown coding agenterror listed 6 agents. It now lists all 12 fromKNOWN_AGENTS.Review
An independent Opus review asked for one change:
keys create --agentminted with--keyorANYROUTER_API_KEYahead of the agent's account. That is fixed, along with its four nits: the--profilewarning, the no-key error naming the account, the stdin-onlykeys 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_KEYis not refused (launch ignores it too),keys setprompts on a terminal, and the help text is re-wrapped.keys create --jsonprinting text was already true on main and is out of scope here.Tests
tests/agent_keys.rs(11 tests) spawns the binary with a freshANYROUTER_HOMEand 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 --agentruns against a scripted server, which asserts that theAuthorizationheader carries the bound account's key.--keyoverride is refused with no request sent, a bound account with no key is named (exit 4), a--profilemint warns, and a key argument or two joined keys are refused.--profilebeats the agent account but not the agent key, a dummy agent key is skipped), short-key masking andrebind_agents.cargo fmt --check,cargo clippy --locked --all-targets(0 warnings) andcargo test --locked --all-targets(1063 passed, 0 failed, 1 ignored) all pass. The--no-default-featureslib build adds no new warnings.🤖 Generated with Claude Code