fix(cli): #17 revoke command — self-revoke + revoke-by-wallet#18
fix(cli): #17 revoke command — self-revoke + revoke-by-wallet#18hanwencheng merged 2 commits intomainfrom
Conversation
Closes #17. On main, `cmd_revoke` constructed a fake Session with the wallet address as the token, so the backend's `WHERE token = ?1` lookup never matched and every invocation returned "target session not found". This change introduces two forms: - `agentkeys revoke` — self-revoke + wipe local session - `agentkeys revoke <wallet>` — revoke all active sessions for a wallet Added `CredentialBackend::revoke_by_wallet` trait method; existing `revoke_session` is unchanged. Backend `/session/revoke` now accepts exactly one of `target_session` or `target_wallet`. 66 tests pass. Reviewed by Claude code-reviewer agent (APPROVED after clear_session match-logic fix). Codex reviewer was attempted via codex exec; the process explored the diff but did not emit a verdict within the session timeout — tracked as a follow-up for the PR reviewer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Codex review (via gstack VerdictThe wallet-based revoke path mishandles the documented current-wallet case by leaving a revoked session cached locally, and the new CLI tests introduce a process-wide HOME race that can make CI flaky. Those issues mean the patch is not yet safe to merge as-is. Full review comments:
— codex |
|
Codex review reply — PR #18 Codex flagged 2 × P2 findings:
Both are P2 correctness concerns, not blockers for merge on their own, but should land before the |
… wallet Codex P2 finding on PR #18: `agentkeys revoke <my-own-wallet>` revoked the session on the backend but left the dead token cached locally; every subsequent command loaded the stale revoked token and failed. cmd_revoke's `Some(target_wallet_str)` branch now case-insensitively compares target_wallet against session.wallet. On match: call session_store::clear_session() and return a 'was your own session — re-pair' message (matching the no-arg self-revoke form's UX). Tests added (19 total CLI tests, +2 from previous): - cmd_revoke_with_own_wallet_clears_local_session — verifies the new path wipes the local file and emits the 'was your own session' acknowledgement. - cmd_revoke_with_other_wallet_keeps_local_session — counterpart: revoking someone else's wallet must NOT touch the caller's session. P2-2 (parallel-test HOME race) is tracked separately as issue #34 — needs serial_test crate added cross-PR (#18, #19, #20, #27 all affected). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Codex P2 follow-up Per the discussion above, addressing both findings:
|
Combines the full rebase of fix/issue-16 onto main (post-#18 + #19 merges), with all conflicts resolved in a single clean commit. - `CredentialBackend::list_credentials` + `resolve_identity` trait methods live side-by-side; both `MockHttpClient` and `InProcessBackend` implement both. - `cmd_run` signature is `(ctx, agent: Option<&str>, env_overrides: &[String], cmd: &[String])`: - `agent` from #16 (defaults to session wallet when None) - `env_overrides` from #19 (HashMap cache + pre-flight validation) - `cmd_store` / `cmd_read` take `agent: Option<&str>`. - `cmd_revoke` takes `agent: Option<&str>` (signature kept from #18). - `main.rs` dispatches `Commands::{Store,Read,Run,Revoke} { agent, ... }` via `.as_deref()` since each carries `agent: Option<String>`. - clap derive `Run` has both `--agent` flag AND `--env KEY=SERVICE` flag. - Top-level `long_about` and all sub-command `long_about` strings show the new `--agent` flag form. - `wiki/credential-usage.md` / `wiki/key-security.md` / `wiki/data-classification.md` migrated to the new form with a "breaking change" callout + sed migration recipe. - `docs/contradictions.md` §4.3 resolution narrative corrected. - Integration + CLI tests: 30 cli + 6 core + 46 mock-server passing. Resolves issue #16 (wallet-optional + identity aliases) after the 3-pass codex stalemate landed on the `--agent` flag design (human call). Test: cargo test -p agentkeys-cli -p agentkeys-core -p agentkeys-mock-server -- --test-threads=1
Rebase of fix/issue-12 onto current main (post-PR #18 #19 #21 #23 merges) surfaced two concrete integration issues, fixed in this patch: **1. Expose session_store helpers for test reuse** - Make `agentkeys_core::session_store::fallback_path(session_id)` public so CLI tests can assert on the on-disk path layout. - Re-export `agentkeys_core::session_store` at `agentkeys_cli::session_store` so existing test imports (`use agentkeys_cli::session_store;`) keep working after the old cli-local `session_store.rs` was deleted upstream. **2. cmd_revoke clear_session calls (post-PR #18 self-revoke merge)** PR #18 added `session_store::clear_session()` calls on self-revoke paths. The new signature takes `session_id`, not 0 args. Pass `&ctx.session_id` so the revoke wipes the correct namespace for any future multi-session CLI caller. **3. Integration tests: seed identity_links for Recover** PR #21 tightened `resolve_identity_typed` to require the identity to exist in `identity_links` before resolution. Two pair_tests (`recover_full_loop`, `recover_credentials_intact`) opened Recover requests with aliases that were never linked, so they now 404. Added `InProcessBackend::link_identity_for_tests(identity_type, identity_value, wallet)` that inserts directly into the mock DB's `identity_links` table. The two failing tests now seed the alias link before the Recover flow. Test: cargo test -p agentkeys-core -p agentkeys-daemon -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1 core: 21; daemon: 13 + 15; cli: 30; mock-server: 56 — all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Augments the auto-generated Claude Code + Claude Code Review workflows
with context from 15+ PR review cycles in this repo so the Action
produces findings consistent with recent codex iterations instead of
generic Rust advice.
## claude-code-review.yml
- Scope `on.pull_request.paths` to `crates/**`, `docs/**`, `wiki/**`,
workflows, Cargo.toml, CLAUDE.md, and harness/. Skips cheap Cargo.lock
churn.
- `fetch-depth: 0` so Claude can inspect `git log` / `git blame` during
review (useful for "this finding predates the PR" arguments).
- `dtolnay/rust-toolchain@stable` + `Swatinem/rust-cache@v2` so every
`cargo check` / `cargo test -p <crate>` in-session runs fast.
- Custom prompt injects:
- crate names (agentkeys-types, agentkeys-core, etc)
- pointer to CLAUDE.md for architecture + mock-server design principles
- pointer to the new .github/REVIEW_GUIDELINES.md for agentkeys-specific
review patterns
- `--test-threads=1` requirement (tests mutate shared HOME/keyring)
- the 8-pattern checklist (audit-log DENIED rows, URL-encoding via
reqwest .query(), session-token redaction, case-insensitive wallet
comparison, 30-day TTL, synchronous keychain ops, path-traversal
guards, cross-wallet credential safety)
- `claude_args --allowed-tools` whitelist for cargo/git/gh so the
Action can actually run the cargo commands the prompt tells it to.
## claude.yml (@claude mentions)
- Same Rust toolchain + cache setup so `@claude run tests` /
`@claude check clippy` requests don't pay cold-compile cost.
- `fetch-depth: 0` for git-history tools.
- Same `claude_args --allowed-tools` whitelist plus `gh pr comment:*` /
`gh pr edit:*` so @claude can update PR bodies and comment back with
findings.
## .github/REVIEW_GUIDELINES.md (new)
Single source of truth for agentkeys review patterns, extracted from
PRs #18-#38 (fix/issue-10 through fix/issue-37). Documents:
- Test constraints (`--test-threads=1`, per-crate targeting)
- 10 canonical bug patterns that codex has flagged repeatedly
- Architectural invariants (master→agent single-hop; no users yet)
- Scope-control guidance (no speculative refactors, no backwards-compat
shims pre-launch)
- Policy for codex-vs-claude disagreements
Each pattern has a PR/issue reference so reviewers (and future Claude
runs) can trace why the rule exists.
* "Claude PR Assistant workflow"
* "Claude Code Review workflow"
* ci: wire agentkeys-specific context into Claude Code workflows
Augments the auto-generated Claude Code + Claude Code Review workflows
with context from 15+ PR review cycles in this repo so the Action
produces findings consistent with recent codex iterations instead of
generic Rust advice.
## claude-code-review.yml
- Scope `on.pull_request.paths` to `crates/**`, `docs/**`, `wiki/**`,
workflows, Cargo.toml, CLAUDE.md, and harness/. Skips cheap Cargo.lock
churn.
- `fetch-depth: 0` so Claude can inspect `git log` / `git blame` during
review (useful for "this finding predates the PR" arguments).
- `dtolnay/rust-toolchain@stable` + `Swatinem/rust-cache@v2` so every
`cargo check` / `cargo test -p <crate>` in-session runs fast.
- Custom prompt injects:
- crate names (agentkeys-types, agentkeys-core, etc)
- pointer to CLAUDE.md for architecture + mock-server design principles
- pointer to the new .github/REVIEW_GUIDELINES.md for agentkeys-specific
review patterns
- `--test-threads=1` requirement (tests mutate shared HOME/keyring)
- the 8-pattern checklist (audit-log DENIED rows, URL-encoding via
reqwest .query(), session-token redaction, case-insensitive wallet
comparison, 30-day TTL, synchronous keychain ops, path-traversal
guards, cross-wallet credential safety)
- `claude_args --allowed-tools` whitelist for cargo/git/gh so the
Action can actually run the cargo commands the prompt tells it to.
## claude.yml (@claude mentions)
- Same Rust toolchain + cache setup so `@claude run tests` /
`@claude check clippy` requests don't pay cold-compile cost.
- `fetch-depth: 0` for git-history tools.
- Same `claude_args --allowed-tools` whitelist plus `gh pr comment:*` /
`gh pr edit:*` so @claude can update PR bodies and comment back with
findings.
## .github/REVIEW_GUIDELINES.md (new)
Single source of truth for agentkeys review patterns, extracted from
PRs #18-#38 (fix/issue-10 through fix/issue-37). Documents:
- Test constraints (`--test-threads=1`, per-crate targeting)
- 10 canonical bug patterns that codex has flagged repeatedly
- Architectural invariants (master→agent single-hop; no users yet)
- Scope-control guidance (no speculative refactors, no backwards-compat
shims pre-launch)
- Policy for codex-vs-claude disagreements
Each pattern has a PR/issue reference so reviewers (and future Claude
runs) can trace why the rule exists.
Post-rebase review (claude code-reviewer) flagged 3 P1 issues that map
directly to patterns in `.github/REVIEW_GUIDELINES.md`. All fixed in
this commit plus one P2 nit that was cheap.
**P1 — URL encoding via reqwest `.query()` on scope + list_credentials**
`get_scope` and `list_credentials` in `mock_client.rs` built queries with
raw `format!()` interpolation. Wallet strings with reserved characters
(`&`, `#`, `%`, `+`, spaces) would break the request or smuggle extra
params server-side. Switched both to `.get(self.url("/path")).query(&[...])`
so reqwest percent-encodes per RFC 3986. Same pattern as the fix on PR
#20 `aa0cc95` / PR #22 `ffdc908` / PR #29 `7aa9e70`.
**P1 — Case-insensitive self-target check in `update_scope`**
The self-target reject `session.wallet_address == target_wallet` was
string-equal, but the backend stores wallets in lowercase while EIP-55
checksummed input preserves case. A master passing its own wallet in
mixed-case form would bypass the self-target guard and silently
restrict its own scope (the exact failure the guard was designed to
prevent). Switched to `eq_ignore_ascii_case` matching the pattern
established in PR #18's self-revoke detection.
**P1 — Missing DENIED audit rows on scope endpoints**
`update_scope` and `get_session_scope` returned 403 on ownership
failure without inserting an audit row, breaking the
cross-agent-probing-visibility invariant established for
`list_credentials` / `read_credential` in PR #19. Added `'scope_update'
'DENIED'` and `'scope_read' 'DENIED'` inserts before the 403 return
in both handlers.
**P2 — `--add foo --remove foo` would no-op silently**
`cmd_scope` accepted combinations like `--add X --remove X`. The add
loop would push, then `retain(!remove.contains)` would cancel, and the
backend PUT would still fire with the original scope + misleading
"Scope updated" output. Added an up-front `add ∩ remove` check that
lists overlapping services and rejects.
**P2 — Narrow UPDATE to most-recent active session**
Write-side `update_scope` UPDATEd ALL active sessions for the target
wallet; read-side `get_session_scope` always returned the scope from
the most-recent session via `ORDER BY created_at DESC LIMIT 1`.
Read/write contract mismatched on wallets with multiple active
sessions (paired + recovered). Narrowed the UPDATE to use the same
ORDER/LIMIT subquery.
Test: cargo test -p agentkeys-core -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
core: 22 passed; cli: 35 passed; mock-server: 56 passed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): #15 part 3 (fix-15b) — agentkeys scope command Closes part 3 of #15. Adds `agentkeys scope <AGENT>` subcommand with `--add`/`--remove`/`--set`/`--list` for editing a child agent's scope. Under the hood: new trait methods `CredentialBackend::get_scope` + `update_scope` backed by a new mock-server `PUT /session/scope` endpoint (owner-checked, updates all active sessions for the target wallet). Tests: 5 new CLI integration tests (list/add/remove/set/conflict). 62 total tests across cli/core/mock-server, all green under --test-threads=1. Minor: CommandContext::load_session made pub so integration tests can access the current session token. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli/mock-server): #15b v2 — address codex P1+P2 Codex P1: update_scope handler allowed a master session to target its own wallet, silently writing a restrictive scope_json onto the master's session row. Subsequent credential/read calls would start failing for services outside the accidental scope. Now rejects self-targeting with a clear bad_request. Codex P2 (URL encoding): cmd_scope's identity resolution built the /identity/resolve query string via raw interpolation, breaking aliases/emails with reserved characters ('+', '&', '=', '%', spaces). Switched to reqwest's .query() builder which percent-encodes per RFC. Codex P3 (--list exclusivity): --list now rejects combination with --add/--remove/--set up front instead of silently dropping the mutation. Deferred (tracked as follow-up): CBOR vs JSON parsing of ScopeChange request_details in mint_scope_change_session — the scope CLI uses the direct /session/scope endpoint, not the auth-request flow, so this finding doesn't affect the CLI path. Will be addressed when AuthRequest::ScopeChange approval is wired end-to-end. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli/mock-server): #29 v3 — claude P1+P2 review fixes after rebase Post-rebase review (claude code-reviewer) flagged 3 P1 issues that map directly to patterns in `.github/REVIEW_GUIDELINES.md`. All fixed in this commit plus one P2 nit that was cheap. **P1 — URL encoding via reqwest `.query()` on scope + list_credentials** `get_scope` and `list_credentials` in `mock_client.rs` built queries with raw `format!()` interpolation. Wallet strings with reserved characters (`&`, `#`, `%`, `+`, spaces) would break the request or smuggle extra params server-side. Switched both to `.get(self.url("/path")).query(&[...])` so reqwest percent-encodes per RFC 3986. Same pattern as the fix on PR #20 `aa0cc95` / PR #22 `ffdc908` / PR #29 `7aa9e70`. **P1 — Case-insensitive self-target check in `update_scope`** The self-target reject `session.wallet_address == target_wallet` was string-equal, but the backend stores wallets in lowercase while EIP-55 checksummed input preserves case. A master passing its own wallet in mixed-case form would bypass the self-target guard and silently restrict its own scope (the exact failure the guard was designed to prevent). Switched to `eq_ignore_ascii_case` matching the pattern established in PR #18's self-revoke detection. **P1 — Missing DENIED audit rows on scope endpoints** `update_scope` and `get_session_scope` returned 403 on ownership failure without inserting an audit row, breaking the cross-agent-probing-visibility invariant established for `list_credentials` / `read_credential` in PR #19. Added `'scope_update' 'DENIED'` and `'scope_read' 'DENIED'` inserts before the 403 return in both handlers. **P2 — `--add foo --remove foo` would no-op silently** `cmd_scope` accepted combinations like `--add X --remove X`. The add loop would push, then `retain(!remove.contains)` would cancel, and the backend PUT would still fire with the original scope + misleading "Scope updated" output. Added an up-front `add ∩ remove` check that lists overlapping services and rejects. **P2 — Narrow UPDATE to most-recent active session** Write-side `update_scope` UPDATEd ALL active sessions for the target wallet; read-side `get_session_scope` always returned the scope from the most-recent session via `ORDER BY created_at DESC LIMIT 1`. Read/write contract mismatched on wallets with multiple active sessions (paired + recovered). Narrowed the UPDATE to use the same ORDER/LIMIT subquery. Test: cargo test -p agentkeys-core -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1 core: 22 passed; cli: 35 passed; mock-server: 56 passed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli/mock-server): #29 v4 — claude re-review nits (dead sort, pct_encode consistency, test coverage) Follow-up on the claude-code-action re-review posted against commit 57dc9fb. All 3 substantive findings addressed; the 4th (malformed `/` doc-comments) was a false positive from pre-rebase state — grep for `^\s*/ ` returns nothing in the current tree. **Low — Redundant `sort_by` after `update_scope`** `lib.rs:769` was re-sorting `new_scope.services` after the backend call returned, but both the `--set` branch (line 749) and the `--add`/`--remove` branch (line 760) sort before the `update_scope` call. Dead op; removed. Added a comment so a future reader doesn't re-add it. **Low — `InProcessBackend::get_scope` raw URL concat** `test_client.rs:725` built the query string via `format!()` while the `resolve_identity` method right above uses `pct_encode`. Wallet strings are hex so this was safe in practice, but the inconsistency violates the URL-encoding invariant documented in `.github/REVIEW_GUIDELINES.md` pattern #3. Swapped to `pct_encode`. **Low — Missing test for `--list + --add` conflict** Added `cmd_scope_list_and_add_conflict_errors`. Also added `cmd_scope_add_remove_overlap_errors` (covering the P2 overlap guard from v3) that wasn't yet under test. Test: cargo test -p agentkeys-core -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1 core: 22 passed; cli: 37 passed (+2 new); mock-server: 56 passed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Fixes
agentkeys revokewhich always returned"target session not found"onmainbecausecmd_revokeconstructed a fakeSessionwith the wallet address as the token. Introduces two invocation forms and distinguishes revoke from teardown in the docs.agentkeys revoke(no args): self-revoke current session on backend + wipe local keychain/file. Runinitagain to re-pair.agentkeys revoke <wallet>: revoke all active sessions for that wallet (backend-enforced ownership check). Credentials survive.What changed
Trait (
agentkeys-core/src/backend.rs)CredentialBackend::revoke_by_wallet(session, target_wallet). Existingrevoke_session(session, target: &Session)unchanged.Backend handler (
agentkeys-mock-server/src/handlers/session.rs)/session/revokenow accepts eithertarget_session(token, existing) ortarget_wallet(new) — exactly one; 400 if both/neither.SET revoked = 1 WHERE wallet_address = ?, returns{"ok": true, "sessions_revoked": N}. 404 if no active sessions for that wallet.CLI (
agentkeys-cli/src/lib.rs,main.rs,session_store.rs)cmd_revoke(ctx, agent: Option<&str>)—Noneself-revokes +clear_session();Some(w)callsrevoke_by_wallet.Commands::Revoke.agent: Option<String>withrequired = false; help text documents both forms.session_store::clear_session()— best-effort keyring + file delete; guarded byshould_skip_keyring()so file-only mode skips the keyring path entirely.Docs
docs/manual-test-issue-17.md(new) — 4 test cases including ownership rejection.docs/manual-test-stage4.mdTest 9 — removed BROKEN/SKIPPED caveats.wiki/credential-usage.md— revoke-vs-teardown table added.docs/contradictions.md§4.1 — marked RESOLVED.Test plan
cargo test -p agentkeys-core→ 6 passedcargo test -p agentkeys-cli→ 17 passedcargo test -p agentkeys-mock-server→ 43 passedtarget_sessionstill works,target_walletbulk revoke, both/neither → 400, unowned → 403, no-active → 404, CLINoneself-revoke wipes local file,Some(w)routes correctly, error-clean on no-sessionAPPROVED_WITH_NITSafterclear_session()match-logic fixdocs/manual-test-issue-17.mdwalks the reproduction onmain@f59c3803+ the fix on this branchIssue
Closes #17.
🤖 Generated with Claude Code
Codex review (2026-04-14): ✅ P2-1 fixed in commit 25f9009 (self-by-wallet wipes local state; 2 new tests).⚠️ P2-2 (parallel-test HOME race) tracked as cross-PR follow-up issue #34.