Skip to content

fix(cli): #15 part 3 (fix-15b) — agentkeys scope command#29

Merged
hanwencheng merged 4 commits intomainfrom
fix/issue-15b
Apr 15, 2026
Merged

fix(cli): #15 part 3 (fix-15b) — agentkeys scope command#29
hanwencheng merged 4 commits intomainfrom
fix/issue-15b

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

@hanwencheng hanwencheng commented Apr 14, 2026

Summary

Third and final sub-feature of #15. Adds the agentkeys scope CLI command for editing a child agent's scope from the master.

What changed

CLI

  • New Commands::Scope { agent, add, remove, set, list } in main.rs.
  • cmd_scope in lib.rs: resolves agent, reads current scope, applies add/remove/set, writes via new backend trait methods.
  • --add/--remove repeatable; --set mutually exclusive with both (clean error).
  • --list prints current scope, no write.

Backend trait

  • CredentialBackend::get_scope(session, target_wallet) -> Option<Scope>
  • CredentialBackend::update_scope(session, target_wallet, new_scope) -> ()
  • Implemented in MockHttpClient + InProcessBackend; stubbed in DummyBackend.

Mock server

  • New handlers: PUT /session/scope + GET /session/scope?wallet=<w>.
  • Both ownership-checked (is_owner_of).
  • update_scope bulk-updates sessions.scope_json for all active sessions of the target wallet.

Minor

  • CommandContext::load_session is now pub so 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 passed
  • cargo test -p agentkeys-mock-server → 37 passed
  • 5 new CLI tests: cmd_scope_list_prints_current, cmd_scope_add_appends, cmd_scope_remove_drops_service, cmd_scope_set_replaces, cmd_scope_add_and_set_conflict.
  • Mock-server integration tests for /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 reviewer: approved (commit 7aa9e70 fixes landed; see footer)

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_session pre-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.

@hanwencheng
Copy link
Copy Markdown
Member Author

Codex review (via gstack /codex skill, GPT-5.4 codex-medium reasoning against origin/main)

Verdict

The 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:

  • [P1] Reject scope updates that target the master wallet itself — crates/agentkeys-mock-server/src/handlers/session.rs:324-339
    Because is_owner_of() returns true when caller_wallet == target_wallet, this endpoint lets a master session write scope_json onto its own active sessions. That changes later authorization behavior: credential/read treats any non-null scope as an allowlist, so after agentkeys scope <master-wallet> --set ..., the master can no longer access services outside that list. If scope is only meant for child agents, this handler should explicitly forbid self-targeting.

  • [P2] Percent-encode alias/email when resolving agent identities — crates/agentkeys-cli/src/lib.rs:507-510
    This builds /identity/resolve by interpolating identity_value directly into the query string. For aliases/emails containing reserved characters such as +, &, %, ?, or spaces, the backend receives a different value (for example alice+bot@example.com is decoded as alice bot@example.com), so agentkeys scope <email> ... fails even though the identity is linked. Using reqwest query parameters or explicit URL encoding would avoid that breakage.

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:

  • [P2] URL-encode identity_value in scope resolution — crates/agentkeys-cli/src/lib.rs:507-509
    If the agent is referenced by an alias/email containing reserved query characters (for example +, &, % or spaces), interpolating identity_value directly into the URL changes what /identity/resolve receives, so agentkeys scope can fail to resolve a valid identity or resolve the wrong one. Build this request with proper query encoding before sending it.

  • [P2] Reject scoping the current master session — crates/agentkeys-mock-server/src/handlers/session.rs:324-325
    This endpoint reuses is_owner_of, which returns true when target_wallet == session.wallet_address, so it also lets a master scope its own wallet. In that case the update rewrites the active master session's scope_json, and later /credential/read calls from that master start failing for services outside the accidental scope. Since agentkeys scope is meant for child agents, same-wallet targets should be rejected here.

  • [P2] Parse ScopeChange request_details as canonical CBOR — crates/agentkeys-mock-server/src/handlers/auth_request.rs:230-232
    CredentialBackend::open_auth_request serializes AuthRequestType::ScopeChange with auth_request::canonical_bytes, so request_details arriving here are CBOR, not JSON. serde_json::from_slice(request_details) therefore fails for real scope-change requests, and approval still returns success without updating any session scope. This means the newly added ScopeChange approval path is non-functional for callers using the backend API.

  • [P3] Make --list mutually exclusive with mutating flags — crates/agentkeys-cli/src/lib.rs:561-569
    When --list is combined with --add, --remove, or --set, this early return prints the current scope and exits successfully before applying any update. A command like agentkeys scope 0x... --list --add openrouter therefore looks successful while leaving the scope unchanged, so --list should be validated as mutually exclusive with the mutating options.

— codex review --base main + -c 'model_reasoning_effort="medium"' (initial high-effort run was interrupted; re-run at medium).

@hanwencheng
Copy link
Copy Markdown
Member Author

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 mint_scope_change_session (the AuthRequest::ScopeChange approval path).

Accepted tradeoff: the agentkeys scope CLI added in this PR uses the direct PUT /session/scope endpoint, not the AuthRequest flow. The CBOR-parsing bug in mint_scope_change_session pre-dates this PR and is not exercised by any CLI path introduced here. Tracked as a follow-up — will be fixed when the AuthRequest::ScopeChange approval flow is wired end-to-end (likely in the same PR that #9 + #6 land their on-chain transport).

Flagging for reviewer awareness rather than cycling in-branch.

hanwencheng added a commit that referenced this pull request Apr 15, 2026
… 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>
hanwencheng added a commit that referenced this pull request Apr 15, 2026
… 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>
@hanwencheng
Copy link
Copy Markdown
Member Author

@claude review this PR again.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 15, 2026

Claude finished @hanwencheng's task in 2m 48s —— View job


PR Review — agentkeys scope command

Tasks

  • Gather context from PR description and prior reviews
  • Read changed files (CLI lib, mock server handlers, tests, backend)
  • Verify P1/P2 fixes from prior Codex review
  • Check for new issues or regressions
  • Post final review

P1 / P2 Fix Verification

P1 — Self-targeting guard (session.rs:328-332): Confirmed. The update_scope handler now explicitly rejects session.wallet_address == target_wallet before the is_owner_of check. ✅

P2 — URL encoding for identity resolution (lib.rs:510-513): Confirmed. The resolve_agent_to_wallet helper now uses reqwest's .query(&[...]) builder, which percent-encodes per RFC 3986 (comment in the code also documents this). ✅

P3 (Codex, previously informational) — --list mutual exclusivity (lib.rs:548-552): Confirmed added. The guard correctly returns an error when --list is combined with --add, --remove, or --set. ✅


New Issues Found

Medium — GET/PUT scope asymmetry when agent has no active sessions

GET /session/scope returns an empty Scope (200 OK) when the target wallet has no active sessions (scope_json is NULL or absent). But PUT /session/scope returns 404 when rows_affected == 0 (session.rs:354-357).

// GET — silently returns empty scope:
None => agentkeys_types::Scope { services: vec![], read_only: false },

// PUT — fails with 404:
if rows_affected == 0 {
    return Err(AppError::not_found("no active sessions for target wallet"));
}

This means agentkeys scope <expired-agent> --list succeeds (shows empty scope), but agentkeys scope <expired-agent> --add openrouter fails with a 404. For a management command used by masters, the 404 on PUT is arguably the right choice to surface that the agent has no sessions to update, but the asymmetry is surprising and undocumented. At minimum a comment explaining the design choice would help; ideally the error message on PUT could say "agent has no active sessions — nothing to update" rather than generic "not found".

Low — Missing test for the --list + mutating-flag conflict

The guard at lib.rs:548-552 has no test exercising it. Tests 15–19 cover --add, --remove, --set, --list, and --add+--set conflict — but not the --list+--add conflict path. The analogous cmd_scope_add_and_set_conflict_errors test (test 19) exists only for --add+--set. Consider adding a test cmd_scope_list_and_add_conflict_errors for completeness and to prevent the guard from being accidentally deleted. Fix this →

Low — Redundant sort in cmd_scope success path

Services are sorted at lib.rs:588 (set path) and lib.rs:599 (add/remove path) before update_scope is called. Then they are sorted again at lib.rs:608 before building the success message. The second sort is a no-op. Harmless but noise.

backend.update_scope(&session, &target_wallet, &new_scope).await...;

new_scope.services.sort_by(|a, b| a.0.cmp(&b.0));  // ← already sorted above

Low — get_session_scope is asymmetric with update_scope on self-targeting

update_scope explicitly rejects caller == target with a helpful error, but get_session_scope has no such guard — it allows a master to query its own scope via the API. This is benign (read-only), but a master querying its own wallet via --list would succeed and show its own scope (typically empty). This inconsistency could be confusing if someone mistakenly targets their own wallet with agentkeys scope <MY-WALLET> --list. Consider adding the same guard to get_session_scope or at least a comment explaining why it's intentionally asymmetric.

Info — _now parameter unused in mint_scope_change_session

fn mint_scope_change_session(
    state: &SharedState,
    request_details: &[u8],
    _now: u64,                 // ← never used; remove or use it
) -> (Option<String>, Option<String>) {

This is dead weight in the signature. It won't cause issues but suggests the function signature was drafted for something not yet implemented. Remove or use it when the end-to-end ScopeChange flow is wired up.

Info — CBOR parsing issue in mint_scope_change_session (acknowledged tradeoff)

As noted in the PR description: mint_scope_change_session at auth_request.rs:231 uses serde_json::from_slice on request_details but the canonical format from open_auth_request is CBOR. This path is never exercised by the agentkeys scope CLI (which uses the direct PUT /session/scope endpoint), so it's a non-blocking issue for this PR. Tracked as a follow-up. ✅

Info — No mock-server integration tests for /session/scope endpoints

The +14/-0 in tests/integration.rs is entirely the put_json_auth helper function; no tests were added for GET /session/scope or PUT /session/scope at the mock-server integration level. The CLI integration tests via InProcessBackend provide meaningful coverage, but a direct mock-server test would catch handler-level regressions without going through the full CLI stack. Acknowledged in the PR as a gap; should be addressed in a follow-up.


Overall Assessment

The core feature is correct. P1 and P2 from the Codex review are properly fixed. Mutex exclusivity guards are in place and tests cover the main flows (add, remove, set, list, add+set conflict). The code follows the existing patterns and the new trait methods are cleanly defined.

The two substantive items to address before merge are: documenting or fixing the GET/PUT asymmetry on empty-session wallets (at minimum a comment clarifying intent), and adding the missing test for the --list + --add conflict path. Everything else is informational or cleanup.

hanwencheng and others added 3 commits April 15, 2026 12:22
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>
@hanwencheng
Copy link
Copy Markdown
Member Author

Rebased onto main + claude code-reviewer P1/P2 fixes (57dc9fb)

Rebase

Main had advanced with PRs #18, #19, #20, #21, #22, #23, #24 merges. 6 conflicted files resolved:

  • crates/agentkeys-cli/src/lib.rs + tests/cli_tests.rs — kept main's test set alongside this PR's scope tests; merged session_store re-export + Scope import
  • crates/agentkeys-core/src/backend.rs + mock_client.rs — kept both trait method sets: list_credentials / resolve_identity from main AND get_scope / update_scope from this branch (orthogonal)
  • crates/agentkeys-mock-server/src/test_client.rs — same dual-impl merge
  • crates/agentkeys-mock-server/src/handlers/auth_request.rs — dropped the duplicate mint_scope_change_session stub (main's PR fix(mock-server): #13 refactor — typed params + shared resolve_identity + modular handlers #21 version kept; the /session/scope PUT endpoint is what actually backs cmd_scope, not the ScopeChange approve-flow)

Claude code-reviewer findings, all fixed in-branch

3 P1s:

  1. URL encoding on get_scope + list_credentials — both built queries via raw format!(). Switched to reqwest .query(&[...]) builder for RFC 3986 percent-encoding. Same pattern as PR fix(cli): #16 wallet-optional + identity aliases #20 / fix(daemon): #14 --parent flag to bind pair requests to a specific master #22 / fix(cli): #15 part 3 (fix-15b) — agentkeys scope command #29 history.
  2. Case-insensitive self-target check in update_scope== comparison would miss EIP-55 mixed-case wallets that the backend stores lowercase, bypassing the self-target guard. Switched to eq_ignore_ascii_case.
  3. Missing DENIED audit rows on scope endpointsupdate_scope and get_session_scope returned 403 on ownership failure without inserting an audit row. Added 'scope_update'/'DENIED' and 'scope_read'/'DENIED' inserts.

1 P2 (cheap nit):

  • --add foo --remove foo silently no-op'd. Added an up-front add ∩ remove overlap check with a clear error.

Other P2s deferred:

  • Skip backend PUT when scope unchanged (ergonomic, not a bug)
  • ScopeChange approve-flow dead path (caller works via PUT /session/scope direct; the AuthRequestType::ScopeChange path is not exercised end-to-end)
  • Log-redaction pass (cross-PR scope)
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

@hanwencheng
Copy link
Copy Markdown
Member Author

review it again @claude

@claude
Copy link
Copy Markdown

claude Bot commented Apr 15, 2026

Claude finished @hanwencheng's task in 2m 45s —— View job


PR Re-Review — agentkeys scope command

Tasks

  • Gather context from PR description and prior reviews
  • Read current state of changed files
  • Verify fixes from last round (URL encoding, case-insensitive self-target, audit rows, add∩remove overlap)
  • Check for new issues
  • Post final review

Confirmed Fixed (commit 57dc9fb)

# 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.

Fix this →

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 above

Fix this →

Low — 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 through

should 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_session is a no-op stub — correctly acknowledged in the PR and the comment block above it in auth_request.rs:306–310. The --set CLI path uses PUT /session/scope directly and never touches this. The _now parameter 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–364 now documents the PUT side's intent. Acceptable.
  • No mock-server integration tests for /session/scope — acknowledged in the PR; CLI via InProcessBackend provides coverage.
  • get_session_scope allows self-targeting (GET only, benign read) — no guard needed; consistent with is_owner_of semantics.

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>
@hanwencheng hanwencheng merged commit 69ea799 into main Apr 15, 2026
1 check 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.

CLI run command broken for master sessions + missing scope edit and --env flag

1 participant