fix(cli): #15 part 3 (fix-15b) — agentkeys scope command#29
fix(cli): #15 part 3 (fix-15b) — agentkeys scope command#29hanwencheng merged 4 commits intomainfrom
agentkeys scope command#29Conversation
|
Codex review (via gstack VerdictThe new scope-management flow has at least one functional regression: it can accidentally apply a restrictive scope to the master session itself. It also mishandles valid linked identities containing reserved URL characters when resolving agents by alias/email. Full review comments:
The new scope functionality has multiple correctness issues: the auth-request-based ScopeChange path cannot apply real requests, and the CLI/server accept inputs that can mis-resolve identities or mutate the wrong session state. Those problems make the patch unsafe to consider fully correct. Full review comments:
— codex |
|
Codex re-review v2 — design tradeoff reply The 2 P1/P2 findings fixed in commit 7aa9e70 (self-wallet guard, URL encoding) are now verified. Codex still flags one remaining concern: CBOR vs JSON parsing in Accepted tradeoff: the Flagging for reviewer awareness rather than cycling in-branch. |
… resolution All 3 codex findings on PR #22 addressed in-branch: **P2 — URL-encode `--parent` aliases** The old code built the query URL via `format!("{}/identity/resolve?identity_type=alias&identity_value={}", ...)` with no encoding. Aliases containing reserved characters (`+`, `&`, `%`, spaces) sent malformed requests or failed to parse. Switched to reqwest's `.query(&[...])` builder, which percent-encodes per RFC 3986. Same pattern as the fixes on PR #20 (aa0cc95) and PR #29 (7aa9e70). **P2 — `0x-`prefixed aliases misclassified as wallets** The old code treated any `--parent` value starting with `0x` as a literal wallet, but `cmd_link` allows aliases like `0x-office` / `0x+bar`. Added `looks_like_raw_wallet(s)` that demands strict `0x + 40 hex digits` before the literal fast-path triggers; everything else goes through `/identity/resolve`. 3 unit tests (accepts canonical hex, rejects 0x-hyphen aliases, rejects short/non-hex). **P3 — Eager `--parent` resolution** Resolution previously ran unconditionally at startup, so a transient backend-down would crash `agentkeys-daemon --session <token>` or `--recover --method passkey` even though those paths never use `parent_wallet`. Moved the call into `resolve_parent_if_set` and invoked only from the pair-flow and master-approval recover branches. The `--session` seam and the `--recover --method …` 2FA path now skip parent resolution entirely. Test: cargo test -p agentkeys-daemon -- --test-threads=1 unit: 3 passed; daemon_tests: 15 passed; pair_tests: 15 passed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… resolution All 3 codex findings on PR #22 addressed in-branch: **P2 — URL-encode `--parent` aliases** The old code built the query URL via `format!("{}/identity/resolve?identity_type=alias&identity_value={}", ...)` with no encoding. Aliases containing reserved characters (`+`, `&`, `%`, spaces) sent malformed requests or failed to parse. Switched to reqwest's `.query(&[...])` builder, which percent-encodes per RFC 3986. Same pattern as the fixes on PR #20 (aa0cc95) and PR #29 (7aa9e70). **P2 — `0x-`prefixed aliases misclassified as wallets** The old code treated any `--parent` value starting with `0x` as a literal wallet, but `cmd_link` allows aliases like `0x-office` / `0x+bar`. Added `looks_like_raw_wallet(s)` that demands strict `0x + 40 hex digits` before the literal fast-path triggers; everything else goes through `/identity/resolve`. 3 unit tests (accepts canonical hex, rejects 0x-hyphen aliases, rejects short/non-hex). **P3 — Eager `--parent` resolution** Resolution previously ran unconditionally at startup, so a transient backend-down would crash `agentkeys-daemon --session <token>` or `--recover --method passkey` even though those paths never use `parent_wallet`. Moved the call into `resolve_parent_if_set` and invoked only from the pair-flow and master-approval recover branches. The `--session` seam and the `--recover --method …` 2FA path now skip parent resolution entirely. Test: cargo test -p agentkeys-daemon -- --test-threads=1 unit: 3 passed; daemon_tests: 15 passed; pair_tests: 15 passed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@claude review this PR again. |
|
Claude finished @hanwencheng's task in 2m 48s —— View job PR Review —
|
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>
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>
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>
7aa9e70 to
57dc9fb
Compare
Rebased onto main + claude code-reviewer P1/P2 fixes (57dc9fb)RebaseMain had advanced with PRs #18, #19, #20, #21, #22, #23, #24 merges. 6 conflicted files resolved:
Claude code-reviewer findings, all fixed in-branch3 P1s:
1 P2 (cheap nit):
Other P2s deferred:
|
|
review it again @claude |
|
Claude finished @hanwencheng's task in 2m 45s —— View job PR Re-Review —
|
| # | Finding | Status |
|---|---|---|
| P1 | Self-target guard in update_scope now uses eq_ignore_ascii_case — EIP-55 mixed-case wallets can no longer bypass the guard (session.rs:331) |
✅ |
| P2 | MockHttpClient::get_scope uses .query() builder (mock_client.rs:697) |
✅ |
| P2 | MockHttpClient::list_credentials uses .query() builder (mock_client.rs:636) |
✅ |
| P2 | DENIED audit rows added for both update_scope (403 path) and get_session_scope (403 path) (session.rs:343–348, session.rs:407–413) |
✅ |
| P2 | --add foo --remove foo overlap now returns a clear error (lib.rs:700–713) |
✅ |
Remaining Low / Nit Issues
Low — Missing test for --list + --add conflict
The guard at lib.rs:690–694 has no test coverage. Test 19 covers --add + --set; there's no analogous test for --list + --add. The guard won't regress silently on the next rebase, but a test would lock it in.
Low — Redundant sort after update_scope (still present)
lib.rs:769 re-sorts new_scope.services after update_scope has already returned. Both the --set path (line 749) and the --add/--remove path (line 760) sort before calling update_scope. The sort on line 769 is a no-op.
// lib.rs:764–770
backend.update_scope(&session, &target_wallet, &new_scope).await...;
new_scope.services.sort_by(|a, b| a.0.cmp(&b.0)); // ← already sorted aboveLow — InProcessBackend::get_scope uses raw URL concatenation
test_client.rs:725 builds the query string via format!() instead of pct_encode, unlike resolve_identity just above it:
// resolve_identity (line 708) — uses pct_encode ✅
let path = format!("/identity/resolve?identity_type={}&identity_value={}", identity_type, pct_encode(&identity_value));
// get_scope (line 725) — raw concat ❌
let path = format!("/session/scope?wallet={}", target_wallet.0);Wallet addresses are hex strings so this won't cause failures in practice, but the inconsistency with the rest of the file is noticeable given pct_encode is defined right at the top of this file. Fix this →
Nit — Two malformed single-slash doc comments
test_client.rs:49:
/ Test helper: insert a row into `identity_links` without going throughshould be ///. And auth_request.rs:306 and 309 have the same single-/ issue in the block comment above mint_scope_change_session.
Acknowledged (no action needed)
mint_scope_change_sessionis a no-op stub — correctly acknowledged in the PR and the comment block above it inauth_request.rs:306–310. The--setCLI path usesPUT /session/scopedirectly and never touches this. The_nowparameter remains unused, cleaned up when the ScopeChange approve-flow is wired end-to-end.- GET/PUT asymmetry on expired agents — PUT returns 404 when no active sessions exist; GET returns empty scope. The comment at
session.rs:360–364now documents the PUT side's intent. Acceptable. - No mock-server integration tests for
/session/scope— acknowledged in the PR; CLI viaInProcessBackendprovides coverage. get_session_scopeallows self-targeting (GET only, benign read) — no guard needed; consistent withis_owner_ofsemantics.
Overall
The feature is correct and the three P1/P2 fixes from the previous round are properly applied. The only substantive items left are small: the missing --list + --add conflict test, the redundant sort, and the pct_encode inconsistency in InProcessBackend::get_scope. None of these are blockers. Ready to merge after those nits, or at your discretion.
…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>
Summary
Third and final sub-feature of #15. Adds the
agentkeys scopeCLI command for editing a child agent's scope from the master.What changed
CLI
Commands::Scope { agent, add, remove, set, list }inmain.rs.cmd_scopeinlib.rs: resolves agent, reads current scope, applies add/remove/set, writes via new backend trait methods.--add/--removerepeatable;--setmutually exclusive with both (clean error).--listprints current scope, no write.Backend trait
CredentialBackend::get_scope(session, target_wallet) -> Option<Scope>CredentialBackend::update_scope(session, target_wallet, new_scope) -> ()MockHttpClient+InProcessBackend; stubbed inDummyBackend.Mock server
PUT /session/scope+GET /session/scope?wallet=<w>.is_owner_of).update_scopebulk-updatessessions.scope_jsonfor all active sessions of the target wallet.Minor
CommandContext::load_sessionis nowpubso integration tests can read the current session.Test plan
cargo test -p agentkeys-cli→ 19 passed (5 new scope tests)cargo test -p agentkeys-core→ 6 passedcargo test -p agentkeys-mock-server→ 37 passedcmd_scope_list_prints_current,cmd_scope_add_appends,cmd_scope_remove_drops_service,cmd_scope_set_replaces,cmd_scope_add_and_set_conflict./session/scope— executor truncated before adding these; relies on CLI integration tests for end-to-end coverage via InProcessBackend.Issue
Closes #15 (parts 1+2 shipped in PR #19, part 3 here).
🤖 Generated with Claude Code
Codex review (2026-04-14):⚠️ Reviewed + fixed (commit 7aa9e70). Self-targeting guard + URL-encoding + --list exclusivity landed. Remaining CBOR/JSON parsing in
mint_scope_change_sessionpre-dates this PR and is not exercised by the scope CLI; tracked as follow-up to land with the AuthRequest::ScopeChange flow end-to-end wiring.