fix: route session list through credentials-free path

Added dedicated CliAction::SessionList variant for claw session list so
it no longer requires API credentials. run_session_list() calls
list_managed_sessions() directly without instantiating an API client.

Generated with https://github.com/Yeachan-Heo/gajae-code
Co-authored-by: Gajae Code <dev@gajae-code.com>
This commit is contained in:
bellman
2026-06-05 01:06:28 +09:00
parent 6fcd0c57ae
commit db56498460
2 changed files with 44 additions and 5 deletions

View File

@ -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 <id>`, `session export <id>`; (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.

View File

@ -1092,6 +1092,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
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<String>,
@ -1946,10 +1950,17 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// 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 <action>` 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 <action>` 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<String, Box<dyn std::e
Ok(lines.join("\n"))
}
/// #449: credentials-free session list that works without API keys.
/// `claw session list --output-format json` should work in CI/offline.
fn run_session_list(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let sessions = list_managed_sessions().unwrap_or_default();
let session_ids: Vec<String> = 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)