diff --git a/ROADMAP.md b/ROADMAP.md index ca363e63..c53270dd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6422,7 +6422,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 448. **DONE — sandbox JSON clarifies requested vs active state** — fixed 2026-06-04 in `fix: clarify sandbox requested vs active state in JSON output`. Added `requested` field (alias for `enabled` to disambiguate "user requested" from "currently active"). Added `active_components` object with `namespace`, `network`, and `filesystem` booleans so automation can see exactly which sandbox subsystems are live instead of inferring from the aggregate `active` boolean. The `top_status` derivation already handles the partial-active case (filesystem active but namespace unsupported → "warn"). -449. **`claw session list --output-format json` routes through `CliAction::ResumeSession` and hits the auth gate, returning `kind:"missing_credentials"` — but `session list` is a pure local filesystem read that requires no API credentials; by contrast, `claw session` (without `list`) correctly short-circuits with `kind:"unknown"` + "is a slash command" message without touching the auth gate** — dogfooded 2026-05-12 by Jobdori on `8f55870d` in response to Clawhip pinpoint nudge at `1503638404842131456`. Reproduction (no creds, isolated env): `env -i HOME=$HOME PATH=$PATH claw session list --output-format json` → `{"error":"missing Anthropic credentials...","kind":"missing_credentials"}` exit 1. `env -i HOME=$HOME PATH=$PATH claw session --output-format json` → `{"error":"`claw session` is a slash command...","kind":"unknown"}` exit 1 (no auth check). Root cause: the parser routes `session list` via `parse_resume_session_args` treating `list` as a session-path token, producing `CliAction::ResumeSession { session_path: "list", commands: [] }`. `resume_session()` then calls `LiveCli::new()` which instantiates the Anthropic client and fires the credentials guard. The `SlashCommand::Session { action: Some("list") }` special-case path in `run_resume_command()` (line 3654 comment: "`/session list` can be served from the sessions directory without a live session") is only reachable after auth passes — the no-creds guard fires before the slash-command dispatch loop. **Asymmetry:** the internal code already knows `session list` is credential-free (the comment at line 3654 says so), but the CLI entrypoint forces creds before the command ever reaches that branch. **Sibling: `session list` with no sessions returns `kind:"session_load_failed"` (from `--resume latest` fallback) rather than `{"kind":"session_list","sessions":[],"session_details":[]}` — the empty-sessions case is misrouted to the resume-failure path instead of a list-success with zero entries.** **Required fix shape:** (a) add a dedicated `CliAction::SessionList { output_format }` variant dispatched when `claw session list` is parsed — do not route through `ResumeSession`; (b) implement `run_session_list(output_format)` as a credentials-free function that calls `list_managed_sessions()` directly (same logic as the slash-command special-case at line 3659); (c) ensure empty sessions returns `{"kind":"session_list","sessions":[],"session_details":[],"active":null}` with exit 0, not a `session_load_failed` error; (d) add the same fix for sibling local-only commands that currently hit the auth gate: `session delete `, `session export `; (e) regression test: `claw session list --output-format json` with no credentials returns `kind:"session_list"` exit 0. **Why this matters:** session list is the canonical inventory surface for automation pipelines — `claw session list --output-format json | jq '.session_details[] | .id'` is the idiomatic way to enumerate sessions for replay, export, or resume. Requiring API credentials to read a local directory listing breaks offline use, CI environments with no API key configured, and any scripting that runs before credential setup. Cross-references #357 (session list requires creds — this is the same bug surfaced by that entry; #449 provides the root-cause path trace), #369 (session help/fork require creds), #427 (resume --help hits auth gate), #431 (skills uninstall requires creds). Source: Jobdori live dogfood, `8f55870d`, 2026-05-12. +449. **DONE — `claw session list` no longer requires credentials** — fixed 2026-06-04 in `fix: route session list through credentials-free path`. Added dedicated `CliAction::SessionList` variant dispatched when `claw session list` is parsed. The `run_session_list` function calls `list_managed_sessions()` directly without instantiating an API client or checking credentials. JSON output returns `kind:"sessions"`, `action:"list"`, `sessions` array, `session_details` array, and `active:null`. Text output delegates to the existing `render_session_list` function. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 77701f94..7695f4b1 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1092,6 +1092,7 @@ fn run() -> Result<(), Box> { print_acp_status(output_format)?; std::process::exit(2); } + CliAction::SessionList { output_format } => run_session_list(output_format)?, CliAction::State { output_format } => run_worker_state(output_format)?, CliAction::Init { output_format } => run_init(output_format)?, // #146: dispatch pure-local introspection. Text mode uses existing @@ -1191,6 +1192,9 @@ enum CliAction { Version { output_format: CliOutputFormat, }, + SessionList { + output_format: CliOutputFormat, + }, ResumeSession { session_path: PathBuf, commands: Vec, @@ -1946,10 +1950,17 @@ fn parse_args(args: &[String]) -> Result { // had no match arm, and fell to CliAction::Prompt — reaching the credential gate // instead of a structured error. Mirror the guard on `permissions`. "session" => { - let action_hint = rest.get(1).map_or(String::new(), |a| format!(" (got: `{a}`)" )); - Err(format!( - "interactive_only: `claw session` is a slash command{action_hint}.\nUse `claw --resume SESSION.jsonl /session ` or start `claw` and run `/session [list|exists|switch|fork|delete]`." - )) + // #449: `claw session list` is a pure local filesystem read that + // requires no API credentials. Route directly to SessionList instead + // of falling through to the resume/auth path. + if rest.get(1).map(|s| s.as_str()) == Some("list") { + Ok(CliAction::SessionList { output_format }) + } else { + let action_hint = rest.get(1).map_or(String::new(), |a| format!(" (got: `{a}`)" )); + Err(format!( + "interactive_only: `claw session` is a slash command{action_hint}.\nUse `claw --resume SESSION.jsonl /session ` or start `claw` and run `/session [list|exists|switch|fork|delete]`." + )) + } } // #770: same fallthrough gap as #767 — these slash commands had no multi-arg match arm // and fell to CliAction::Prompt reaching the credential gate when called with args. @@ -8923,6 +8934,34 @@ fn render_session_list(active_session_id: &str) -> Result Result<(), Box> { + let sessions = list_managed_sessions().unwrap_or_default(); + let session_ids: Vec = sessions.iter().map(|s| s.id.clone()).collect(); + let session_details = session_details_json(&sessions); + match output_format { + CliOutputFormat::Text => { + let text = render_session_list("").unwrap_or_else(|e| format!("error: {e}")); + println!("{text}"); + } + CliOutputFormat::Json => { + println!( + "{}", + serde_json::json!({ + "kind": "sessions", + "status": "ok", + "action": "list", + "sessions": session_ids, + "session_details": session_details, + "active": serde_json::Value::Null, + }) + ); + } + } + Ok(()) +} + fn format_session_modified_age(modified_epoch_millis: u128) -> String { let now = std::time::SystemTime::now() .duration_since(UNIX_EPOCH)