docs(spec): match the re-login spec to the implementation
This commit is contained in:
@@ -61,13 +61,15 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
`Task<string?> AskForCodeAsync(string url, int attempt, CancellationToken)`, which returns null when there's no
|
||||
answer or it was cancelled. It also has `void Report(string message)`.
|
||||
- For each attempt (max 3):
|
||||
1. Start `claude auth login --claudeai` through `ClaudeProcess`, with the allow-listed environment plus
|
||||
`BROWSER=/bin/false` on Unix (`BROWSER=none` on Windows). Stdout is read as text, not JSONL.
|
||||
1. Start `claude auth login --claudeai` as a plain `System.Diagnostics.Process` (not `ClaudeProcess`, which is
|
||||
built around the JSONL stream and its idle/total timeouts), with the allow-listed environment plus
|
||||
`BROWSER=/bin/false` on Unix (`BROWSER=none` on Windows). Stdout is read as text in chunks, since the paste
|
||||
prompt has no trailing newline.
|
||||
2. Read stdout until a line contains `https://` followed by `oauth/authorize`. Take the URL up to the next
|
||||
whitespace. Allow 30 s (`ClaudeLoginOptions.UrlTimeout`). If there's no URL, or the process exits early,
|
||||
the result is `Failed("claude auth login printed no login URL: <stderr excerpt>")`.
|
||||
3. `prompt.AskForCodeAsync(url, attempt)`. A null result kills the process and returns
|
||||
`Failed("no code was entered")`.
|
||||
3. `prompt.AskForCodeAsync(url, attempt)`, bounded by `ClaudeLoginOptions.CodeTimeout` (10 min) whatever the
|
||||
prompt does. A null result (or the timeout) kills the process and returns `Failed("no code was entered")`.
|
||||
4. Trim the code and write it plus `\n` to stdin, then wait up to 60 s (`ClaudeLoginOptions.ExchangeTimeout`)
|
||||
for exit.
|
||||
5. Run `claude auth status` (through `ClaudeInstallation.RunAsync`). The attempt succeeds only when the exit
|
||||
@@ -77,6 +79,9 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
- The process is always killed (process tree) on cancel, timeout or exception. The code and URL never appear in
|
||||
exception messages or logs.
|
||||
- `LoginOutcome` is `record LoginOutcome(bool Success, string? Error)`.
|
||||
- `ClaudeLoginOptions` holds the budget: `MaxAttempts` (3), `UrlTimeout` (30 s), `ExchangeTimeout` (60 s),
|
||||
`StatusTimeout` (10 s), `CodeTimeout` (10 min, per code prompt) and `OwnerTimeout` (30 s, for resolving
|
||||
`LoginUi`).
|
||||
|
||||
### 3. Single flight — `ClaudeLoginCoordinator`
|
||||
|
||||
@@ -87,6 +92,8 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
waiter's `/stop` doesn't kill the flow for everyone else. It stores the task.
|
||||
- Every caller awaits the stored task with `WaitAsync(ct)`. A cancelled caller only stops waiting.
|
||||
- When the task completes, the slot clears. The next expired-session request starts a new flow.
|
||||
- `long Generation` counts successful flows; it is advanced before the slot clears. A request reads it before
|
||||
its attempt starts (see §5) so a late expiry after someone else's successful login is recognised as stale.
|
||||
- The flow is abandoned when the owner doesn't answer: `ClaudeLoginOptions.CodeTimeout` (10 min) bounds each code
|
||||
prompt whatever the UI does, so the flow fails and every waiter fails. Resolving `LoginUi` is bounded to 30 s.
|
||||
|
||||
@@ -95,8 +102,9 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
- New option: `Func<CancellationToken, Task<IUserInterface?>>? LoginUi`. It returns where the owner is reached,
|
||||
or null if nobody can be reached right now.
|
||||
- An adapter, `UiLoginPrompt(IUserInterface ui)`, implements `ILoginPrompt`:
|
||||
- `AskForCodeAsync`: `ui.Notify("Claude Code needs you to sign in again.\n1. Open: <url>\n2. Sign in with the Claude subscription account.\n3. Paste the code shown on the page as a reply.")`,
|
||||
then `ui.InputAsync("Paste the Claude login code", "code", null, ct)`.
|
||||
- `AskForCodeAsync`: a single `ui.InputAsync("Claude Code needs you to sign in again.\n1. Open: <url>\n2. Sign in with the Claude subscription account.\n3. Paste the Claude login code shown on the page as a reply.", "code", null, ct)`.
|
||||
The link is part of the question's title rather than a separate `Notify`: on Telegram notifications go through
|
||||
the renderer's async queue while the question is sent directly, so separate messages could arrive out of order.
|
||||
- `Report`: `ui.Notify(message, NotifyKind.Warning)`.
|
||||
- After success the owner gets `Notify("Claude Code login complete ✓")`. After failure:
|
||||
`Notify("Claude Code login failed: <reason>", Error)`.
|
||||
@@ -104,20 +112,27 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
`[auth] Claude Code session expired; run "claude auth login" on the host (or /claude-code-login from a chat UI)`.
|
||||
- **Telegram host:** `Program.cs` registers a `ClaudeCodeProviderOptions` singleton. `ActivatorUtilities` hands it
|
||||
to the extension's constructor. Its `LoginUi` resolves lazily through the service provider:
|
||||
`ct => (await sp.GetRequiredService<ChatRegistry>().GetAsync(options.AllowedUserIds[0], ct)).Ui`. This
|
||||
resolves at call time to avoid a construction cycle. In a private chat, the chat id is the user id.
|
||||
- **Owner unreachable:** if `LoginUi` returns null (or doesn't resolve within 30 s), the flow fails with
|
||||
`the owner could not be reached; the first allowed user must start a chat with the bot`. If the owner's chat
|
||||
resolves but sending fails (the owner never started the bot), `InputAsync` returns null and the flow fails with
|
||||
`no code was entered`.
|
||||
`ct => sp.GetRequiredService<OwnerLoginChannel>().ResolveAsync(ct)`. `OwnerLoginChannel` returns the UI of the
|
||||
private chat of `AllowedUserIds[0]` (created on demand through `ChatRegistry`; in a private chat the chat id is
|
||||
the user id), or null when the allow-list is empty. It resolves at call time to avoid a construction cycle.
|
||||
While the code prompt is pending, the owner's next plain text message (not a command, not a photo) answers it
|
||||
instead of reaching the agent.
|
||||
- **Owner unreachable:** only when `LoginUi` returns null (e.g. an empty allow-list) or doesn't resolve within
|
||||
`OwnerTimeout` (30 s) does the flow fail with
|
||||
`the owner could not be reached; the first allowed user must start a chat with the bot`. An owner who never
|
||||
started the bot still resolves (the chat is created on demand), but sending the prompt fails, `InputAsync`
|
||||
returns null and the flow fails with `no code was entered`.
|
||||
|
||||
### 5. Provider integration — `ClaudeCodeApi`
|
||||
|
||||
- `RunRequestAsync` gets split: `RunAttemptAsync` does one process run and reports its outcome, which is either
|
||||
a failure exception or success, plus whether any event reached the channel. The outer method decides on the
|
||||
retry.
|
||||
- Before attempt 1 the request reads the coordinator's `Generation`.
|
||||
- If attempt 1 fails with `LoginRequired`, **no event has been written**, the request wasn't cancelled and a
|
||||
login UI is available:
|
||||
0. If `Generation` has advanced since (a login completed while this attempt ran), the expiry is stale: run
|
||||
attempt 2 directly, with no notice and no new flow. Still exactly one replay.
|
||||
1. Notify the requesting session (through its registry `Notify`):
|
||||
`Claude Code session expired; waiting for the bot owner to sign in…`
|
||||
2. `await coordinator.EnsureLoggedInAsync(flow, ct)`, where `flow` resolves `LoginUi`, then runs `ClaudeLogin`
|
||||
@@ -128,8 +143,9 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
- Cancelling while waiting publishes the usual `aborted` failure.
|
||||
- Waiting for login counts against neither the idle nor the total timeout. Those are per-process, and attempt 2
|
||||
gets fresh ones.
|
||||
- `ClaudeCodeApiOptions` gains `LoginUi` and `Coordinator`. `ClaudeCodeProviderExtension` fills them in from its
|
||||
options.
|
||||
- `ClaudeCodeApiOptions` gains `LoginUi`, `Coordinator` and `LoginOptions` (`ClaudeLoginOptions`: `CodeTimeout`,
|
||||
`OwnerTimeout` and the rest of §2's budget). `ClaudeCodeProviderExtension` fills them in from the same-named
|
||||
`ClaudeCodeProviderOptions`.
|
||||
|
||||
### 6. `/claude-code-login` command
|
||||
|
||||
@@ -138,6 +154,9 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
a UI (`!ctx.HasUi`), it appends a message saying the command needs an interactive UI.
|
||||
- The result is reported with `Notify`. It also runs when the session is still valid, to switch accounts or
|
||||
refresh early.
|
||||
- If a flow is already running (driven through another UI, e.g. the owner chat for another chat's expired
|
||||
request), the command joins it and reports its outcome in its own UI:
|
||||
`A Claude Code login was already in progress elsewhere; it succeeded ✓` or `…; it failed: <reason>`.
|
||||
- The doctor's "not logged in" line becomes: `run /claude-code-login (or claude auth login on the host)`.
|
||||
|
||||
## Error handling summary
|
||||
@@ -149,8 +168,9 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
| owner doesn't answer in 10 min (`CodeTimeout`) | every waiting request fails `[auth] Claude Code login failed: no code was entered` |
|
||||
| requesting user sends `/stop` while waiting | that request aborts; the flow continues for the others |
|
||||
| no login UI (headless) | `[auth] Claude Code session expired; run "claude auth login"…` |
|
||||
| `LoginUi` returns null | `[auth] Claude Code login failed: the owner could not be reached…` |
|
||||
| `LoginUi` returns null / doesn't resolve in 30 s (e.g. empty allow-list) | `[auth] Claude Code login failed: the owner could not be reached…` |
|
||||
| owner never started the bot | `[auth] Claude Code login failed: no code was entered` |
|
||||
| expiry reported after another request's login already succeeded | replays directly, no second login |
|
||||
| expired again right after login | attempt 2's error is published, no loop |
|
||||
| `apiKeySource != none` / denied env var | unchanged `[auth]` failure, no login |
|
||||
|
||||
@@ -163,14 +183,16 @@ state, so a code only works with the process that printed its link. `claude auth
|
||||
the right one (the prompt sees attempt 2 with a new URL), three wrong codes, no URL, a null answer (process
|
||||
killed) and cancellation. These are Unix-only, like the existing script-based tests.
|
||||
- **Coordinator:** two concurrent callers share one flow invocation. Both see success or both see failure. A
|
||||
cancelled caller doesn't cancel the flow. A new flow starts after completion.
|
||||
cancelled caller doesn't cancel the flow. A new flow starts after completion. `Generation` advances only on
|
||||
success; a request whose attempt spanned a completed login replays without a second flow.
|
||||
- **Provider:** a fake `claude` whose first `-p` run emits the 401 fixture and whose second emits the tool-ok
|
||||
fixture. With a fake `LoginUi`, the request ends `Done`, the flow ran once and the session was notified. Two
|
||||
more cases: expired again after login is an error with no third run, and no `LoginUi` gives the headless
|
||||
`[auth]` message.
|
||||
- **Telegram:** with the fake bot client, a request in a non-owner chat hits an expired session. The link and the
|
||||
prompt appear in the owner's chat and the non-owner chat gets the waiting notice. A reply in the owner's chat
|
||||
resumes the request.
|
||||
- **Telegram:** with the fake bot client and the real extension (per session, as in `Program.cs`) over a fake
|
||||
`claude` script, a request in a non-owner chat hits an expired session. The link and the prompt appear in the
|
||||
owner's chat as one message and the non-owner chat gets the waiting notice. A reply in the owner's chat resumes
|
||||
the request. A photo never answers a pending prompt.
|
||||
|
||||
## Out of scope
|
||||
|
||||
|
||||
Reference in New Issue
Block a user