Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ them through an ordinary `cargo update`.

### Fixed

- **`COPILOT_GITHUB_TOKEN` reaches Copilot under `EnvPolicy::Minimal`.** It is the
*highest-precedence* credential variable Copilot accepts and it was missing from the
list, so a host authenticating that way would have failed to authenticate at all once
`Minimal` became the default.
- **Dropping a `Run` now reliably kills the process group.** It signalled the driver and
aborted it, which left the kill waiting on the runtime to poll the aborted task. On Linux
that did not reliably happen and grandchildren survived, while `cancel` and timeouts were
Expand All @@ -47,6 +51,11 @@ them through an ordinary `cargo update`.

### Added

- **`AuthStatus::check(agent)`** answers whether an agent is logged in without spending a
request, which a missing login otherwise only revealed by running a turn and failing.
Claude reports JSON, Codex reports prose, and Copilot offers neither: that case is
`AuthState::Unknown` rather than a logout, since telling someone to re-authenticate a
working setup is worse than admitting the question cannot be answered.
- **`Probe`** reads a CLI's `--version` and compares it against
`Agent::verified_version`, the release its flag mappings were checked against, reporting
`Verified` / `Newer` / `Older` / `Unrecognized` with an `advisory()` written to be shown to
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,30 @@ Two more honest limits: Codex has no true plan mode, so `Plan` maps to its read-
sandbox (writes blocked, execution still permitted), and `unchecked_args` can contradict any
of this by design.

## Is each agent logged in?

Without spending a request:

```rust
for agent in Agent::ALL {
let status = AuthStatus::check(agent).await?;
println!("{agent}: {}", status.summary());
}
```

```text
claude-code: logged in as you@example.com (max)
codex: logged in as ChatGPT
copilot: unknown: copilot exposes no status command, so this cannot be
confirmed without spending a request
```

Claude answers JSON (`claude auth status`), Codex answers prose
(`codex login status`), and **Copilot offers neither**. That third case reports `Unknown`
rather than "logged out", because telling someone to re-authenticate a working setup is
worse than admitting the question cannot be answered. `needs_login()` is true only for a
*confirmed* logout, so gating on it never nags about an agent that simply cannot be asked.

## Environment isolation

**`EnvPolicy::Minimal` is the default.** Inheriting the whole environment is what a CLI gets
Expand Down
40 changes: 39 additions & 1 deletion src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,36 @@ impl Agent {
}
}

/// The command that asks this agent whether it is logged in, or `None`
/// when it offers no way to ask.
///
/// Verified against each CLI: Claude has `auth status`, which answers JSON
/// by default, and Codex has `login status`, which answers prose. Copilot
/// has neither, so its credentials cannot be confirmed without spending a
/// request.
#[must_use]
pub fn auth_status_argv(self) -> Option<&'static [&'static str]> {
match self {
Agent::Claude => Some(&["auth", "status", "--json"]),
Agent::Codex => Some(&["login", "status"]),
Agent::Copilot => None,
}
}

/// The environment variables this agent accepts a credential in, most
/// preferred first.
///
/// Copilot documents its precedence explicitly: `COPILOT_GITHUB_TOKEN`,
/// then `GH_TOKEN`, then `GITHUB_TOKEN`.
#[must_use]
pub fn auth_env_vars(self) -> &'static [&'static str] {
match self {
Agent::Claude => &["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
Agent::Codex => &["CODEX_API_KEY", "OPENAI_API_KEY"],
Agent::Copilot => &["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
}
}

/// The command that resolves a missing login for this agent.
///
/// Verified against each CLI's own help: Codex and Copilot expose a `login`
Expand Down Expand Up @@ -360,7 +390,15 @@ impl Agent {
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
],
Agent::Copilot => &["GH_TOKEN", "GITHUB_TOKEN", "XDG_CONFIG_HOME"],
// `COPILOT_GITHUB_TOKEN` takes precedence over the others per
// Copilot's own docs, and was missing here: a host using it would
// have failed to authenticate under EnvPolicy::Minimal.
Agent::Copilot => &[
"COPILOT_GITHUB_TOKEN",
"GH_TOKEN",
"GITHUB_TOKEN",
"XDG_CONFIG_HOME",
],
};
BASE.iter().chain(WINDOWS).chain(agent).copied().collect()
}
Expand Down
Loading
Loading