use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;
mod auth_cli;
mod auth_import;
mod bin_doctor;
#[path = "logs_cli.rs"]
mod logs_cli;
mod shutdown;
use axum::middleware::from_fn_with_state;
use link_assistant_router::accounts::{AccountRouter, AccountRouterOptions};
use link_assistant_router::cli::{AccountOp, Command, TokenOp};
use link_assistant_router::config::{Config, RoutingMode};
use link_assistant_router::crater::{ForgeFedTaskProvider, TaskProvider};
use link_assistant_router::login::LoginManager;
use link_assistant_router::metrics::Metrics;
use link_assistant_router::oauth::OAuthProvider;
use link_assistant_router::providers::ProviderStore;
use link_assistant_router::proxy::AppState;
use link_assistant_router::storage::{TokenStore, build_token_store};
use link_assistant_router::token::{ADMIN_SCOPE, IssueRequest, TokenManager};
use log_lazy::LogLazy;
use tower_http::trace::TraceLayer;
type SharedState = (Arc<dyn TokenStore>, Option<AccountRouter>);
type AnyError = Box<dyn std::error::Error>;
fn main() -> ExitCode {
link_assistant_router::entrypoint::run_on_a_deep_stack(run)
}
async fn run() -> ExitCode {
match link_assistant_router::codex_loopback_bridge::daemon_request_from_env() {
Ok(Some(request)) => {
return match link_assistant_router::codex_loopback_bridge::run_persistent_daemon(
request,
)
.await
{
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("error: {error}");
ExitCode::from(1)
}
};
}
Ok(None) => {}
Err(error) => {
eprintln!("error: {error}");
return ExitCode::from(1);
}
}
let arguments =
link_assistant_router::cli::protect_client_arguments(std::env::args_os().collect(), true);
let cli = link_assistant_router::cli::parse_arguments(arguments);
match cli.command.as_ref() {
Some(Command::With(args)) => {
return link_assistant_router::with_command::run(args).await;
}
Some(Command::Server { op }) => {
return link_assistant_router::server_command::run(op).await;
}
Some(Command::Configure(args)) => {
return link_assistant_router::configure::run_with_home(args, cli.home.as_deref())
.await;
}
_ => {}
}
let verbose = cli.verbose;
let request_log = cli.request_log.clone();
let request_log_max_bytes = cli.request_log_max_bytes;
let request_log_max_total_bytes = cli.request_log_max_total_bytes;
link_assistant_router::logging::init(verbose);
let logger = link_assistant_router::logging::build_lazy(verbose);
tracing::info!("Link.Assistant.Router v{}", link_assistant_router::VERSION);
if verbose {
tracing::info!("Verbose logging enabled");
}
let cli = link_assistant_router::remote_command::relax_token_secret_for_cli(cli);
let config = match cli.into_config() {
Ok(c) => c,
Err(e) => {
tracing::error!("Configuration error: {e}");
return ExitCode::from(2);
}
};
if let Some(command) = cli.command.as_ref()
&& let Some(code) = link_assistant_router::remote_command::refuse_managed(command)
{
return code;
}
if let Some(command) = cli.command.as_ref()
&& link_assistant_router::remote_command::may_be_remote(command)
&& let Some(target) = link_assistant_router::remote_command::target_of(command)
&& (target.server.is_some()
|| !link_assistant_router::remote_command::names_local_state(&cli))
{
match link_assistant_router::remote_command::resolve(target).await {
Ok(link_assistant_router::remote_command::Target::Remote(server)) => {
return run_remote_command(&server, command).await;
}
Ok(link_assistant_router::remote_command::Target::Local) => {}
Err(code) => return code,
}
}
match cli.command.as_ref() {
None | Some(Command::Serve) => match run_server(
config,
logger,
request_log.as_deref(),
request_log_max_bytes,
request_log_max_total_bytes,
)
.await
{
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!("server error: {e}");
ExitCode::from(1)
}
},
Some(Command::Tokens { op }) => run_tokens(&config, op),
Some(Command::Accounts { op }) => run_accounts(&config, op),
Some(Command::Providers { op }) => {
link_assistant_router::providers_cli::run(&config, op).await
}
Some(Command::Clients { op }) => {
link_assistant_router::client_command::run(&config, cli.home.as_deref(), op).await
}
Some(Command::With(_) | Command::Server { .. } | Command::Configure(_)) => {
unreachable!("handled before config")
}
Some(Command::Auth { op }) => {
auth_cli::run(
&config,
op,
link_assistant_router::remote_command::names_local_state(&cli),
)
.await
}
Some(Command::Usage { provider, json, .. }) => {
let token = std::env::var("LINK_ASSISTANT_ROUTER_TOKEN")
.or_else(|_| std::env::var("LINK_ASSISTANT_TOKEN"))
.ok();
let base_url = format!("http://{}", config.listen_addr);
link_assistant_router::subscription_usage_cli::run(
&base_url,
token.as_deref(),
*provider,
*json,
)
.await
}
Some(Command::Doctor { .. }) => bin_doctor::run_doctor(&config).await,
Some(Command::Tls { op }) => link_assistant_router::tls_cli::run(&config, op),
Some(Command::Logs { op }) => logs_cli::run(&config, request_log.as_deref(), op),
}
}
fn build_shared_state(config: &Config) -> Result<SharedState, AnyError> {
if !config.data_dir.exists() {
std::fs::create_dir_all(&config.data_dir)?;
}
let store = build_token_store(config.storage_policy, &config.data_dir)?;
let account_router =
if config.additional_account_dirs.is_empty() && config.account_request_limits.is_empty() {
None
} else {
let (provider, primary) = config.subscription_pool();
let options = AccountRouterOptions {
strategy: config.account_routing_strategy,
cooldown: Duration::from_secs(config.account_cooldown_secs),
session_affinity_ttl: Duration::from_secs(config.session_affinity_ttl_secs),
request_limits: config
.account_request_limits
.iter()
.map(|limit| (*limit != 0).then_some(*limit))
.collect(),
};
Some(AccountRouter::new_for_provider(
primary,
&config.additional_account_dirs,
provider,
options,
))
};
Ok((store, account_router))
}
const BOOTSTRAP_ADMIN_TTL_HOURS: i64 = 24 * 365;
const BOOTSTRAP_ADMIN_LABEL: &str = "bootstrap-admin";
fn announce_admin_access(config: &Config, token_manager: &TokenManager) {
if config.allow_anonymous_admin {
tracing::warn!(
"--allow-anonymous-admin is set: /api/management/tokens*, /api/management/providers* and /api/management/login* accept unauthenticated requests"
);
return;
}
if config.admin_key.is_some() {
tracing::info!("Admin access: TOKEN_ADMIN_KEY configured (bootstrap credential)");
return;
}
match token_manager.has_active_admin_token() {
Ok(true) => {
tracing::info!("Admin access: existing admin token found in the token store");
return;
}
Ok(false) => {}
Err(e) => {
tracing::warn!("could not inspect the token store for admin tokens: {e}");
return;
}
}
match token_manager.issue_admin_token(BOOTSTRAP_ADMIN_TTL_HOURS, BOOTSTRAP_ADMIN_LABEL) {
Ok(token) => {
println!("─────────────────────────────────────────────────────────────");
println!("Admin token (shown once, store it now): {token}");
println!("Use it as: Authorization: Bearer <token>");
println!("Rotate it with: link-assistant-router tokens rotate <id>");
println!("─────────────────────────────────────────────────────────────");
tracing::info!("Generated a bootstrap admin token; admin endpoints are closed");
}
Err(e) => tracing::error!("failed to generate a bootstrap admin token: {e}"),
}
}
async fn run_server(
config: Config,
logger: LogLazy,
request_log: Option<&std::path::Path>,
request_log_max_bytes: u64,
request_log_max_total_bytes: u64,
) -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("Upstream: {}", config.upstream_base_url);
tracing::info!("Upstream provider: {:?}", config.upstream_provider);
let (subscription_provider, subscription_home) = config.subscription_pool();
tracing::info!(
"Subscription home ({subscription_provider}): {}",
subscription_home.display()
);
tracing::info!("Routing mode: {:?}", config.routing_mode);
tracing::info!("Storage policy: {:?}", config.storage_policy);
if config.routing_mode == RoutingMode::Cli || config.routing_mode == RoutingMode::Hybrid {
tracing::warn!(
"RoutingMode::{:?} is configured but the CLI backend is not yet wired; falling back to direct.",
config.routing_mode
);
}
let (store, account_router) = build_shared_state(&config)?;
if let Some(router) = account_router.as_ref() {
tracing::info!("Multi-account routing enabled ({} accounts)", router.len());
}
let token_manager = TokenManager::with_store(&config.token_secret, store);
match token_manager.release_stale_reservations() {
Ok(0) => {}
Ok(cleared) => tracing::info!("released {cleared} stale token spend reservation(s)"),
Err(error) => tracing::warn!("failed to release stale token reservations: {error}"),
}
announce_admin_access(&config, &token_manager);
let oauth_provider = OAuthProvider::new(&config.claude_code_home);
let metrics = Arc::new(Metrics::default());
let provider_store = ProviderStore::open(&config.data_dir, &config.token_secret)?;
provider_store
.set_subscription_entitlement_policy(config.subscription_entitlement_policy.clone())?;
for accepted in config.subscription_entitlement_policy.overrides() {
tracing::warn!(
"consumer-subscription bridge override enabled for exact cell {accepted}; operator accepted intermediary and provider-terms risk"
);
}
for client in config.subscription_entitlement_policy.proxied_clients() {
tracing::warn!(
client = client.canonical_name(),
"proxied-client request evidence enabled; a trusted intermediary may use this native client identity only on its reviewed canonical routes"
);
}
let client = link_assistant_router::upstream_client::build_upstream_client()?;
let crater_provider =
if config.upstream_provider == link_assistant_router::config::UpstreamProvider::Crater {
Some(Arc::new(ForgeFedTaskProvider::new(
client.clone(),
config.crater.clone(),
)) as Arc<dyn TaskProvider>)
} else {
None
};
let subscription_readers = config.subscription_readers();
for reader in &subscription_readers {
tracing::info!(
"Subscription provider {}: reading credentials from {}",
reader.provider(),
reader.home().display()
);
}
let subscription_reader = link_assistant_router::subscription::active_subscription_reader(
config.upstream_provider,
&subscription_readers,
);
let model_catalogs = Arc::new(
link_assistant_router::model_catalog::ModelCatalogCache::persistent(&config.data_dir),
);
let admin_claim = Arc::new(
link_assistant_router::admin::AdminClaim::load(
config.admin_key.clone(),
&config.data_dir,
config.admin_ui.candidate_ttl,
)
.with_token_manager(token_manager.clone()),
);
let state = AppState {
client,
token_manager,
oauth_provider,
account_router,
subscription_reader,
subscription_base_url: None,
subscription_readers,
model_catalogs: Arc::clone(&model_catalogs),
subscription_cache: Arc::new(link_assistant_router::refresh::TokenCache::new()),
upstream_base_url: config.upstream_base_url.clone(),
upstream_provider: config.upstream_provider,
gonka: link_assistant_router::gonka::GonkaConfig::new(
config.gonka_api_key.clone(),
config.gonka_source_url.as_deref(),
config.gonka_model.clone(),
),
bridge_model: config.bridge_model.clone(),
bridge_model_policy: config.bridge_model_policy,
audit: std::sync::Arc::new(link_assistant_router::audit::AuditLog::to_path(
config.audit_log.as_deref(),
)),
request_log: link_assistant_router::logging::request_log(
&config.data_dir,
request_log,
request_log_max_bytes,
request_log_max_total_bytes,
),
crater: crater_provider,
openai_compatible: config.openai_compatible.clone(),
provider_store,
logger,
max_proxy_request_bytes: config.max_proxy_request_bytes,
admin: Arc::clone(&admin_claim),
admin_key: config.admin_key.clone(),
allow_anonymous_admin: config.allow_anonymous_admin,
metrics: Arc::clone(&metrics),
activitypub_actor_base_url: config.activitypub_actor_base_url.clone(),
activitypub_public_key_pem: config.activitypub_public_key_pem.clone(),
mpp: config.mpp.clone(),
login_manager: LoginManager::new_with_data_dir(
config.login.clone(),
config.data_dir.clone(),
),
github: link_assistant_router::github_proxy::GitHubProxyConfig::from_env_with_data_dir(
Some(config.data_dir.as_path()),
)
.map_err(std::io::Error::other)?,
};
state.register_credential_recovery_in(
&config.data_dir,
&link_assistant_router::app_state::VendorClis {
claude: config.claude_cli_bin.as_deref(),
codex: config.codex_cli_bin.as_deref(),
},
);
state
.subscription_cache
.persist_rejections_in(&config.data_dir);
let mut catalog_readers = state
.subscription_readers
.iter()
.filter(|reader| {
state
.account_router
.as_ref()
.is_none_or(|router| router.provider() != reader.provider())
})
.cloned()
.map(|reader| {
(
link_assistant_router::credential_recovery_store::PRIMARY_ACCOUNT.to_string(),
reader,
)
})
.collect::<Vec<_>>();
if let Some(router) = state.account_router.as_ref() {
catalog_readers.extend(router.subscription_readers());
}
let catalog_refresh = tokio::spawn(
link_assistant_router::model_catalog::refresh_catalogs_for_accounts_forever(
state.client.clone(),
catalog_readers,
Arc::clone(&state.subscription_cache),
Arc::clone(&state.model_catalogs),
),
);
let listener_kind = if config.inference_only {
link_assistant_router::route_contract::ListenerKind::InferenceOnly
} else {
link_assistant_router::route_contract::ListenerKind::Combined
};
let app = link_assistant_router::server_router::router_for_listener(
state.clone(),
&config,
listener_kind,
)
.layer(from_fn_with_state(
state.clone(),
link_assistant_router::request_log::log_http_exchange,
))
.layer(TraceLayer::new_for_http().make_span_with(
|request: &axum::http::Request<axum::body::Body>| {
let uri =
link_assistant_router::request_log::safe_http_uri(request.method(), request.uri());
tracing::debug_span!(
"http request",
method = %request.method(),
uri = %uri,
version = ?request.version()
)
},
));
let shutdown = shutdown::Shutdown::listening();
let admin_server = if config.admin_ui.enabled {
let admin_addr = config.admin_ui.listen_addr;
let admin_app =
link_assistant_router::admin_api::router_with_config(state.clone(), &config)
.layer(from_fn_with_state(
state.clone(),
link_assistant_router::request_log::log_http_exchange,
))
.layer(TraceLayer::new_for_http().make_span_with(
|request: &axum::http::Request<axum::body::Body>| {
let uri = link_assistant_router::request_log::safe_http_uri(
request.method(),
request.uri(),
);
tracing::debug_span!(
"http request",
method = %request.method(),
uri = %uri,
version = ?request.version()
)
},
));
let admin_shutdown = shutdown.notified();
let admin_listener = tokio::net::TcpListener::bind(admin_addr).await?;
tracing::info!("Admin UI listening on {admin_addr}");
if admin_claim.is_claimed() {
tracing::info!("Admin credential present; bootstrap is closed");
} else {
tracing::warn!(
"Admin is unclaimed: the first visitor to {admin_addr} that confirms a claim becomes admin"
);
}
Some(tokio::spawn(async move {
if let Err(e) = axum::serve(admin_listener, admin_app)
.with_graceful_shutdown(admin_shutdown)
.await
{
tracing::error!("admin UI server error: {e}");
}
}))
} else {
tracing::info!("Admin UI disabled (set --admin-port / ADMIN_PORT to enable)");
None
};
let chat_channels = spawn_chat_channels(&config, &state, Arc::clone(&admin_claim));
#[cfg(unix)]
let socket_server = link_assistant_router::unix_listener::serve_configured(
link_assistant_router::server_router::github_adapter_router(state.clone()),
shutdown.notified(),
)
.await?;
#[cfg(not(unix))]
let socket_server: Option<tokio::task::JoinHandle<()>> = None;
match link_assistant_router::tls::from_env(std::path::Path::new(&config.data_dir)) {
Ok(link_assistant_router::tls::TlsSetup::Enabled { cert, key }) => {
let serve = link_assistant_router::tls::serve_https(
config.listen_addr,
app,
cert,
key,
shutdown.notified(),
);
Box::pin(serve)
.await
.map_err(|error| -> AnyError { error.to_string().into() })?;
}
Ok(link_assistant_router::tls::TlsSetup::Disabled) => {
let listener = tokio::net::TcpListener::bind(config.listen_addr).await?;
tracing::info!("Listening on http://{}", listener.local_addr()?);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown.notified())
.await?;
}
Err(error) => return Err(error.into()),
}
if let Some(handle) = socket_server {
handle.abort();
}
if let Some(handle) = admin_server {
handle.abort();
}
for handle in chat_channels {
handle.abort();
}
catalog_refresh.abort();
Ok(())
}
fn spawn_chat_channels(
config: &Config,
state: &AppState,
admin_claim: Arc<link_assistant_router::admin::AdminClaim>,
) -> Vec<tokio::task::JoinHandle<()>> {
let chat_config = config.chat_admin.clone();
if !chat_config.telegram_enabled() && !chat_config.vk_enabled() {
tracing::info!(
"Chat admin channels disabled (set TELEGRAM_BOT_TOKEN and/or VK_BOT_TOKEN to enable)"
);
return Vec::new();
}
let chat = Arc::new(
link_assistant_router::chat_admin::ChatAdmin::new(
admin_claim,
state.token_manager.clone(),
config.admin_key.clone(),
chat_config.clone(),
)
.with_status(Arc::new(state.clone())),
);
let mut handles = Vec::new();
if chat_config.telegram_enabled() {
let chat = Arc::clone(&chat);
let client = state.client.clone();
handles.push(tokio::spawn(async move {
link_assistant_router::telegram::run(chat, client).await;
}));
}
if chat_config.vk_enabled() {
let chat = Arc::clone(&chat);
let client = state.client.clone();
handles.push(tokio::spawn(async move {
link_assistant_router::vk::run(chat, client).await;
}));
}
if chat.admin_claim().is_claimed() {
tracing::info!("Chat admin: a credential exists; /start will ask for one");
} else {
tracing::warn!(
"Chat admin: unclaimed — the first private-chat user to confirm a /start becomes admin"
);
}
handles
}
async fn run_remote_command(
server: &link_assistant_router::managed_server::ResolvedServer,
command: &Command,
) -> ExitCode {
use link_assistant_router::remote_command::{no_remote_form, refuse};
match command {
Command::Tokens { op } => link_assistant_router::tokens_remote::run(server, op).await,
Command::Accounts { .. } => link_assistant_router::auth_remote::accounts(server).await,
Command::Providers { op } => {
link_assistant_router::providers_cli::run_remote(server, op).await
}
Command::Usage { provider, json, .. } => {
link_assistant_router::subscription_usage_cli::run(
&server.base_url,
server.token.as_deref(),
*provider,
*json,
)
.await
}
Command::Logs { .. } => refuse(no_remote_form(
"logs",
server,
"the request log lives on that deployment's disk and no endpoint serves it; \
run `router logs` there",
)),
Command::Doctor { .. } => refuse(no_remote_form(
"doctor",
server,
"run `router doctor` on that deployment; `router auth status` reports its \
credentials from here",
)),
Command::Tls { .. } => refuse(no_remote_form(
"tls",
server,
"the certificate is generated on the deployment that serves it; run `router tls` \
there and distribute the PEM it prints",
)),
_ => ExitCode::from(1),
}
}
fn run_tokens(config: &Config, op: &TokenOp) -> ExitCode {
let (store, _account_router) = match build_shared_state(config) {
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(1);
}
};
if matches!(op, TokenOp::Issue { .. } | TokenOp::Rotate { .. })
&& let Err(error) = link_assistant_router::token_secret::ensure_real(&config.token_secret)
{
eprintln!("error: {error}");
return ExitCode::from(2);
}
let mgr = TokenManager::with_store(&config.token_secret, store);
match op {
TokenOp::Issue {
ttl_hours,
label,
account,
max_requests,
max_tokens,
rate_limit_per_minute,
admin,
github_repo,
..
} => {
let request = IssueRequest {
ttl_hours: *ttl_hours,
label,
account: account.as_deref(),
max_requests: *max_requests,
max_tokens: *max_tokens,
rate_limit_per_minute: *rate_limit_per_minute,
scope: if *admin { ADMIN_SCOPE } else { "" },
github_repos: github_repo.clone(),
sliding_window_seconds: None,
client_kind: None,
principal_id: None,
};
if let Err(message) = request.validate() {
eprintln!("error: {message}");
return ExitCode::from(2);
}
match mgr.issue(&request) {
Ok(t) => {
println!("{t}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}
TokenOp::Rotate {
id,
ttl_hours,
label,
max_requests,
max_tokens,
rate_limit_per_minute,
account,
..
} => match mgr.rotate_token_with(
id,
&link_assistant_router::token::RotateOverrides {
label: (!label.is_empty()).then_some(label.as_str()),
ttl_hours: Some(*ttl_hours),
max_requests: *max_requests,
max_tokens: *max_tokens,
rate_limit_per_minute: *rate_limit_per_minute,
account: account.as_deref(),
},
) {
Ok(t) => {
println!("{t}");
eprintln!("revoked {id}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
TokenOp::List { json, .. } => match mgr.list_tokens() {
Ok(records) => {
let rows: Vec<serde_json::Value> = records
.into_iter()
.map(|record| serde_json::to_value(record).unwrap_or_default())
.collect();
if *json {
println!(
"{}",
serde_json::to_string_pretty(&rows).unwrap_or_else(|_| "[]".to_string())
);
} else {
link_assistant_router::token_report::print_table(&rows);
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
TokenOp::Revoke { id, .. } | TokenOp::Expire { id, .. } => match mgr.revoke_token(id) {
Ok(()) => {
println!("revoked {id}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
TokenOp::Show { id, .. } => match mgr.list_tokens() {
Ok(records) => records.into_iter().find(|r| r.id == *id).map_or_else(
|| {
eprintln!("not found: {id}");
ExitCode::from(2)
},
|r| {
println!("{}", serde_json::to_string_pretty(&r).unwrap_or_default());
ExitCode::SUCCESS
},
),
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
}
}
fn run_accounts(config: &Config, op: &AccountOp) -> ExitCode {
let router = match build_shared_state(config) {
Ok((_, Some(r))) => r,
Ok((_, None)) => {
let (provider, primary) = config.subscription_pool();
AccountRouter::new_for_provider(
primary,
&[],
provider,
AccountRouterOptions {
strategy: config.account_routing_strategy,
cooldown: Duration::from_secs(config.account_cooldown_secs),
session_affinity_ttl: Duration::from_secs(config.session_affinity_ttl_secs),
request_limits: config
.account_request_limits
.iter()
.map(|limit| (*limit != 0).then_some(*limit))
.collect(),
},
)
}
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(1);
}
};
let refreshes = link_assistant_router::refresh::TokenCache::new();
refreshes.persist_rejections_in(&config.data_dir);
link_assistant_router::accounts_cli::run(&router, Some(&refreshes), op)
}