link-assistant-router 0.124.2

Link.Assistant.Router — Claude MAX OAuth proxy and token gateway for Anthropic APIs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Subscription diagnostics shared by the `doctor` CLI command.

use crate::claude_auth::ClaudeAuthMode;
use crate::login::LoginConfig;
use crate::model_catalog::{fetch_provider_catalog, is_credential_rejection};
use crate::subscription::{SubscriptionProvider, all_subscription_readers};

/// One line per Claude login mode, saying whether it can run here and which
/// scopes it would request.
///
/// Issue #193: an operator must be able to see *before* starting a login that
/// the mode they intend to use is actually available in this image, rather than
/// discovering a missing binary from an HTTP 502.
#[must_use]
pub fn login_mode_report(login: &LoginConfig) -> Vec<String> {
    // Both real modes are in-process OAuth, so neither depends on a binary.
    // Only an operator-supplied compatibility backend can be unavailable.
    let uses_external = login.command != "claude";
    let selected = if login.args.iter().any(|argument| argument == "setup-token") {
        ClaudeAuthMode::SetupToken
    } else {
        ClaudeAuthMode::Full
    };

    let mut lines = Vec::new();
    for mode in [ClaudeAuthMode::Full, ClaudeAuthMode::SetupToken] {
        let marker = if mode == selected { " (default)" } else { "" };
        let availability = if uses_external {
            let resolved = resolve_in_path(&login.command);
            resolved.map_or_else(
                || {
                    format!(
                        "UNAVAILABLE — LOGIN_CLI_COMMAND `{}` is not in PATH",
                        login.command
                    )
                },
                |path| format!("via {}", path.display()),
            )
        } else {
            "available (in-process OAuth)".to_string()
        };
        lines.push(format!(
            "login_mode {:<12}: {availability}{marker}; scopes: {}",
            mode.name(),
            mode.scopes()
        ));
    }
    lines
}

/// Exactly which client headers this deployment relays to a vendor.
///
/// The proxy hides the caller's address, so an operator who checks the egress
/// IP — the obvious check — concluded they were private, while the client's
/// OS, architecture, runtime build and a stable session id travelled on. A
/// privacy property nobody can check is one nobody can rely on, and verifying
/// this one meant hand-parsing the request store (issue #332).
#[must_use]
pub fn forwarded_header_report() -> Vec<String> {
    let mut report = vec![format!(
        "{:<23}: {}",
        "upstream_headers",
        crate::proxy::forwarded_client_headers().join(", ")
    )];
    report.push(format!(
        "{:<23}: {}",
        "upstream_user_agent",
        crate::proxy::router_user_agent()
    ));
    report.push(format!(
        "{:<23}: {}",
        "upstream_dropped",
        concat!(
            "every other client header, including x-stainless-*, ",
            "the client user-agent, accept-language, ",
            "x-claude-code-session-id and any x-forwarded-for"
        )
    ));
    report
}

/// Locate an executable on `PATH`, as the process spawner would.
fn resolve_in_path(command: &str) -> Option<std::path::PathBuf> {
    let candidate = std::path::Path::new(command);
    if candidate.is_absolute() {
        return candidate.is_file().then(|| candidate.to_path_buf());
    }
    std::env::var_os("PATH").and_then(|paths| {
        std::env::split_paths(&paths)
            .map(|directory| directory.join(command))
            .find(|path| path.is_file())
    })
}

/// The operator-facing verdict for one credential.
///
/// `expiresAt` is a hint rather than an authority, so an expired-looking token
/// is still probed: the catalog endpoint is what actually knows whether a
/// credential works. That is why "expired" and "rejected" are separate
/// dimensions here, and why a token can be expired on disk yet accepted
/// upstream.
const fn credential_status(was_expired: bool, still_expired: bool, rejected: bool) -> &'static str {
    match (was_expired, still_expired, rejected) {
        (_, true, true) => "found, token EXPIRED and REJECTED",
        (_, true, false) => "found, token EXPIRED on disk but ACCEPTED upstream",
        (true, false, true) => "found, token REJECTED after refresh",
        (false, _, true) => "found, token REJECTED",
        (true, false, false) => "found, token OK (refreshed in memory)",
        (false, false, false) => "found, token OK",
    }
}

/// Where a credential was read from, for the operator-facing line.
///
/// An operator looking at a valid-looking file while the router reports
/// `rejected` has no way to see that the two are reading different places, so
/// the store is named rather than implied (issue #249). A keychain credential
/// is described by its entry, since it has no path to print.
fn credential_location(
    provider: SubscriptionProvider,
    origin: crate::platform_keychain::Origin,
    path: &std::path::Path,
) -> String {
    match origin {
        crate::platform_keychain::Origin::Keychain => {
            crate::platform_keychain::service_name(provider).map_or_else(
                || String::from("platform keychain"),
                |service| format!("keychain {service:?}"),
            )
        }
        crate::platform_keychain::Origin::File => path.display().to_string(),
    }
}

/// Report credential and live-catalog health for every provider.
///
/// Expired credentials are refreshed in memory before their catalogs are
/// fetched. Returns `true` when a present credential cannot become healthy or
/// cannot fetch its catalog.
/// `data_dir`, when given, is where a terminal refusal learned here is
/// recorded, so `accounts list` — which performs no refresh of its own — stops
/// contradicting this command about the same credential (issue #245).
pub async fn subscription_catalog_diagnostics(
    _active_provider: SubscriptionProvider,
    claude_home: &str,
    user_home: &str,
    data_dir: Option<&std::path::Path>,
) -> bool {
    let readers = all_subscription_readers(claude_home, user_home);
    let client = reqwest::Client::new();
    let token_cache = crate::refresh::TokenCache::new();
    if let Some(dir) = data_dir {
        token_cache.persist_rejections_in(dir);
    }
    let now_ms = chrono::Utc::now().timestamp_millis();
    let mut catalog_error = false;
    for reader in readers {
        let provider = reader.provider();
        let label = format!("{provider} subscription");
        // A credential may exist with no file at all: on macOS a recent Claude
        // Code login writes only the Keychain (issue #249). Reporting MISSING
        // on the strength of an absent file would hide a working subscription.
        let path = match reader.discover_credential_path() {
            Some(path) => path,
            None if reader.read_token().is_ok() => reader.home().to_path_buf(),
            None => {
                println!("{label:<23}: {} (MISSING)", reader.home().display());
                continue;
            }
        };
        let (disk_token, origin) = match reader.read_token_from() {
            Ok(found) => found,
            Err(error) => {
                println!("{label:<23}: {} (found, NO TOKEN: {error})", path.display());
                println!(
                    "{:<23}: ERROR (credential is unreadable)",
                    format!("{provider} catalog")
                );
                catalog_error = true;
                continue;
            }
        };
        let was_expired = disk_token.is_expired(now_ms);
        let token = token_cache
            .get_fresh(&client, provider, disk_token, now_ms)
            .await;
        // `expiresAt` is a hint, so a still-expired token is probed rather than
        // declared dead: the catalog endpoint is what actually knows.
        let still_expired = token.is_expired(now_ms);
        let catalog = fetch_provider_catalog(&client, provider, &token, None).await;
        let rejected = catalog
            .as_ref()
            .is_err_and(|error| is_credential_rejection(error));
        let status = credential_status(was_expired, still_expired, rejected);
        let location = credential_location(provider, origin, &path);
        println!(
            "{label:<23}: {location} ({status}, store: {})",
            origin.label()
        );
        if let Some(error) = token_cache.last_refresh_error(provider) {
            println!("{:<23}: {error}", format!("{provider} refresh"));
        }

        match catalog {
            Ok(models) => println!(
                "{:<23}: OK ({} live model(s))",
                format!("{provider} catalog"),
                models.len()
            ),
            Err(error) => {
                println!("{:<23}: ERROR ({error})", format!("{provider} catalog"));
                catalog_error = true;
            }
        }
    }
    catalog_error
}

#[cfg(test)]
mod tests {
    use super::*;

    fn config(command: &str, args: &[&str]) -> LoginConfig {
        LoginConfig {
            command: command.to_string(),
            args: args.iter().map(|value| (*value).to_string()).collect(),
            ..LoginConfig::default()
        }
    }

    /// Both in-process modes are always available, and the report names the
    /// scopes each would request before a login is attempted (issue #193).
    #[test]
    fn both_native_modes_are_reported_available() {
        let report = login_mode_report(&config("claude", &[])).join("\n");
        assert!(report.contains("login_mode full"), "{report}");
        assert!(report.contains("login_mode setup-token"), "{report}");
        assert_eq!(
            report.matches("available (in-process OAuth)").count(),
            2,
            "{report}"
        );
        assert!(report.contains("user:inference"), "{report}");
        assert!(report.contains("org:create_api_key"), "{report}");
    }

    /// The default marker follows `LOGIN_CLI_ARGS`.
    #[test]
    fn the_configured_mode_is_marked_as_the_default() {
        let full = login_mode_report(&config("claude", &[]));
        assert!(full[0].contains("(default)"), "{full:?}");
        assert!(!full[1].contains("(default)"), "{full:?}");

        let narrow = login_mode_report(&config("claude", &["setup-token"]));
        assert!(!narrow[0].contains("(default)"), "{narrow:?}");
        assert!(narrow[1].contains("(default)"), "{narrow:?}");
    }

    /// An operator-supplied backend that is absent is reported as unavailable
    /// rather than failing later with an HTTP 502.
    #[test]
    fn a_missing_external_command_is_reported_unavailable() {
        let report = login_mode_report(&config("definitely-not-on-path-98765", &[])).join("\n");
        assert!(report.contains("UNAVAILABLE"), "{report}");
        assert!(report.contains("definitely-not-on-path-98765"), "{report}");
    }

    /// An absolute path that exists resolves; one that does not is reported.
    #[test]
    fn an_absolute_command_path_is_probed_directly() {
        let existing = std::env::current_exe().expect("test binary path");
        let report = login_mode_report(&config(&existing.to_string_lossy(), &[])).join("\n");
        assert!(report.contains("via "), "{report}");
        assert!(!report.contains("UNAVAILABLE"), "{report}");

        let missing = login_mode_report(&config("/nonexistent/router/login-cli", &[])).join("\n");
        assert!(missing.contains("UNAVAILABLE"), "{missing}");
    }

    #[test]
    fn resolve_in_path_finds_a_real_executable() {
        // `sh` exists on every platform this test runs on.
        assert!(
            resolve_in_path("sh").is_some() || cfg!(windows),
            "sh should be resolvable"
        );
        assert!(resolve_in_path("definitely-not-on-path-98765").is_none());
    }

    /// A keychain credential is described by its entry, not by a file path it
    /// does not have -- naming the store is the diagnosability fix in #249.
    #[test]
    fn a_keychain_credential_is_described_by_its_entry() {
        let location = credential_location(
            SubscriptionProvider::Claude,
            crate::platform_keychain::Origin::Keychain,
            std::path::Path::new("/Users/someone/.claude/.credentials.json"),
        );

        if cfg!(target_os = "macos") {
            assert_eq!(location, "keychain \"Claude Code-credentials\"");
        } else {
            assert_eq!(location, "platform keychain");
        }
        assert!(
            !location.contains(".credentials.json"),
            "a keychain credential must not be reported as a file: {location}"
        );
    }

    /// A provider with no named store still reports a store rather than a path
    /// it did not read, so the line never claims the wrong origin.
    #[test]
    fn a_storeless_provider_reports_a_generic_store() {
        let location = credential_location(
            SubscriptionProvider::Gemini,
            crate::platform_keychain::Origin::Keychain,
            std::path::Path::new("/home/someone/.gemini/oauth_creds.json"),
        );

        assert_eq!(location, "platform keychain");
    }

    /// A file credential keeps reporting its path, which is what every
    /// non-macOS platform and every other provider sees.
    #[test]
    fn a_file_credential_is_described_by_its_path() {
        let location = credential_location(
            SubscriptionProvider::Codex,
            crate::platform_keychain::Origin::File,
            std::path::Path::new("/home/someone/.codex/auth.json"),
        );

        assert_eq!(location, "/home/someone/.codex/auth.json");
    }

    /// A credential that refreshed successfully must say so, rather than being
    /// reported by the expiry that sent it to be refreshed.
    #[test]
    fn a_refreshed_credential_reads_as_ok() {
        assert_eq!(
            credential_status(true, false, false),
            "found, token OK (refreshed in memory)"
        );
        assert_eq!(credential_status(false, false, false), "found, token OK");
    }

    /// The case from issue #249: expired on disk and refused upstream.
    #[test]
    fn an_expired_and_refused_credential_says_both() {
        let status = credential_status(true, true, true);

        assert!(status.contains("EXPIRED"), "{status}");
        assert!(status.contains("REJECTED"), "{status}");
    }

    /// An expiry is a hint, so a token the upstream still accepts must not be
    /// reported as dead -- this is what stops `doctor` condemning a credential
    /// the vendor client is happily using.
    #[test]
    fn an_expired_but_accepted_credential_is_not_condemned() {
        let status = credential_status(true, true, false);

        assert!(status.contains("ACCEPTED upstream"), "{status}");
        assert!(!status.contains("REJECTED"), "{status}");
    }

    /// A refusal after a successful refresh is distinct from one before it:
    /// the first means the chain is dead, the second that it never worked.
    #[test]
    fn a_refusal_names_whether_a_refresh_preceded_it() {
        assert_eq!(
            credential_status(true, false, true),
            "found, token REJECTED after refresh"
        );
        assert_eq!(
            credential_status(false, false, true),
            "found, token REJECTED"
        );
    }

    /// The privacy property is checkable without reading the source.
    ///
    /// Verifying what reached the vendor previously meant hand-parsing
    /// `requests.jsonl`, so an operator who checked the egress IP concluded
    /// they were private while the client's machine identity travelled on
    /// (issue #332).
    #[test]
    fn the_report_names_what_is_forwarded_and_what_is_not() {
        let report = forwarded_header_report().join("\n");
        // What travels.
        for forwarded in crate::proxy::forwarded_client_headers() {
            assert!(
                report.contains(forwarded),
                "the report must name {forwarded}: {report}"
            );
        }
        assert!(
            report.contains(crate::proxy::router_user_agent()),
            "the report must name the identity sent upstream: {report}"
        );
        // And what does not, named explicitly rather than left to inference.
        for dropped in [
            "x-stainless",
            "accept-language",
            "x-claude-code-session-id",
            "x-forwarded-for",
        ] {
            assert!(
                report.contains(dropped),
                "the report must say {dropped} is dropped: {report}"
            );
        }
    }
}