fix(mcp): reuse CLI auth and run overrides

This commit is contained in:
Bryan Helmkamp 2026-05-11 10:24:32 -04:00
parent 81365a1cbf
commit 947001d719
No known key found for this signature in database
5 changed files with 426 additions and 141 deletions

View file

@ -2,14 +2,11 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use fabro_config::{
CliLayer, CliOutputLayer, ReplaceMap, RunExecutionLayer, RunGoalLayer, RunLayer, RunModelLayer,
RunSandboxLayer, parse_input_overrides,
};
use fabro_config::{CliLayer, CliOutputLayer, RunGoalLayer, RunLayer, parse_input_overrides};
use fabro_manifest::{RunOverrideInput, build_run_overrides};
use fabro_sandbox::SandboxProvider;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::interp::InterpString;
use fabro_types::settings::run::{ApprovalMode, RunMode};
use crate::args::{PreflightArgs, RunArgs};
@ -32,47 +29,6 @@ pub(crate) fn parse_labels(labels: &[String]) -> HashMap<String, String> {
.collect()
}
fn model_from_args(model: Option<&str>, provider: Option<&str>) -> Option<RunModelLayer> {
if model.is_none() && provider.is_none() {
return None;
}
Some(RunModelLayer {
provider: provider.map(InterpString::parse),
name: model.map(InterpString::parse),
fallbacks: Vec::new(),
})
}
fn sandbox_layer(
sandbox: Option<SandboxProvider>,
preserve: Option<bool>,
) -> Option<RunSandboxLayer> {
if sandbox.is_none() && preserve.is_none() {
return None;
}
Some(RunSandboxLayer {
provider: sandbox.map(|p| p.to_string()),
preserve,
..RunSandboxLayer::default()
})
}
fn execution_layer(dry_run: Option<bool>, auto_approve: Option<bool>) -> Option<RunExecutionLayer> {
if dry_run.is_none() && auto_approve.is_none() {
return None;
}
Some(RunExecutionLayer {
mode: dry_run.map(|d| if d { RunMode::DryRun } else { RunMode::Normal }),
approval: auto_approve.map(|a| {
if a {
ApprovalMode::Auto
} else {
ApprovalMode::Prompt
}
}),
})
}
fn cli_layer_for_verbose(verbose: bool) -> Option<CliLayer> {
verbose.then(|| CliLayer {
output: Some(CliOutputLayer {
@ -119,24 +75,21 @@ fn current_dir_or_dot() -> PathBuf {
}
pub(crate) fn run_args_overrides(args: &RunArgs) -> Result<ManifestSettingsOverrides> {
let model = model_from_args(args.model.as_deref(), args.provider.as_deref());
let sandbox = sandbox_layer(
args.sandbox.map(Into::into),
sparse_flag(args.preserve_sandbox),
);
let execution = execution_layer(sparse_flag(args.dry_run), sparse_flag(args.auto_approve));
let cwd = current_dir_or_dot();
let goal = goal_layer_from_args(args.goal.as_deref(), args.goal_file.as_deref(), &cwd)?;
let run = RunLayer {
goal,
metadata: ReplaceMap::from(parse_labels(&args.label)),
model,
sandbox,
execution,
..RunLayer::default()
};
let sandbox = args.sandbox.map(SandboxProvider::from);
let sandbox_provider = sandbox.as_ref().map(ToString::to_string);
let mut run = build_run_overrides(RunOverrideInput {
goal: None,
model: args.model.as_deref(),
provider: args.provider.as_deref(),
sandbox: sandbox_provider.as_deref(),
preserve_sandbox: sparse_flag(args.preserve_sandbox),
dry_run: sparse_flag(args.dry_run),
auto_approve: sparse_flag(args.auto_approve),
labels: parse_labels(&args.label),
});
run.goal = goal;
Ok(ManifestSettingsOverrides {
run: Some(run),
@ -146,21 +99,22 @@ pub(crate) fn run_args_overrides(args: &RunArgs) -> Result<ManifestSettingsOverr
}
pub(crate) fn preflight_args_overrides(args: &PreflightArgs) -> Result<ManifestSettingsOverrides> {
let model = model_from_args(args.model.as_deref(), args.provider.as_deref());
let sandbox = args.sandbox.map(|s| RunSandboxLayer {
provider: Some(SandboxProvider::from(s).to_string()),
..RunSandboxLayer::default()
});
let cwd = current_dir_or_dot();
let goal = goal_layer_from_args(args.goal.as_deref(), args.goal_file.as_deref(), &cwd)?;
let run = RunLayer {
goal,
model,
sandbox,
..RunLayer::default()
};
let sandbox_provider = args
.sandbox
.map(|sandbox| SandboxProvider::from(sandbox).to_string());
let mut run = build_run_overrides(RunOverrideInput {
goal: None,
model: args.model.as_deref(),
provider: args.provider.as_deref(),
sandbox: sandbox_provider.as_deref(),
preserve_sandbox: None,
dry_run: None,
auto_approve: None,
labels: HashMap::new(),
});
run.goal = goal;
Ok(ManifestSettingsOverrides {
run: Some(run),

View file

@ -12,6 +12,8 @@ use std::io::{BufRead as _, Write as _};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use chrono::{Duration as ChronoDuration, Utc};
use fabro_client::{AuthEntry, AuthStore, OAuthEntry, StoredSubject};
use fabro_mcp::client::McpClient;
use fabro_mcp::config::{McpServerSettings, McpTransport};
use fabro_test::{fabro_json_snapshot, fabro_snapshot, test_context};
@ -659,6 +661,100 @@ async fn mcp_search_includes_archived_runs_by_default() {
.expect("MCP client should shut down");
}
#[tokio::test(flavor = "multi_thread")]
async fn mcp_search_refreshes_expired_oauth_token() {
let context = test_context!();
let server = MockServer::start();
let target_url = format!("{}/api/v1", server.base_url());
let target: fabro_client::ServerTarget = target_url.parse().unwrap();
seed_oauth_auth(
&context.home_dir,
&target,
"expired-access",
"refresh-octocat",
);
let run_id = unique_run_id();
let expired_access = server.mock(|when, then| {
when.method(GET)
.path("/api/v1/runs")
.header("authorization", "Bearer expired-access");
then.status(401)
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"errors": [{
"detail": "access token expired",
"code": "access_token_expired"
}]
}));
});
let refresh = server.mock(|when, then| {
when.method(POST)
.path("/auth/cli/refresh")
.header("authorization", "Bearer refresh-octocat");
then.status(200)
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"access_token": "fresh-access",
"access_token_expires_at": (Utc::now() + ChronoDuration::minutes(10)).to_rfc3339(),
"refresh_token": "fresh-refresh",
"refresh_token_expires_at": (Utc::now() + ChronoDuration::days(30)).to_rfc3339(),
"subject": {
"idp_issuer": "https://github.com",
"idp_subject": "12345",
"login": "octocat",
"name": "The Octocat",
"email": "octocat@example.com"
}
}));
});
let fresh_access = server.mock(|when, then| {
when.method(GET)
.path("/api/v1/runs")
.header("authorization", "Bearer fresh-access")
.query_param("include_archived", "true")
.query_param("page[limit]", "100")
.query_param("page[offset]", "0");
then.status(200)
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"data": [remote_run_summary_json(
&run_id,
"Simple",
"simple",
"OAuth refreshed",
&serde_json::json!({ "kind": "submitted" }),
"2026-04-05T12:00:00Z",
)],
"meta": { "has_more": false }
}));
});
let client = spawn_mcp_client(&context, &["--server", &target_url]).await;
let result = call_tool_json(
&client,
"fabro_run_search",
serde_json::json!({ "run_ids": [run_id], "first": 1 }),
)
.await;
assert_eq!(result["runs"][0]["run_id"], run_id);
expired_access.assert();
refresh.assert();
fresh_access.assert();
let stored = AuthStore::new(context.home_dir.join(".fabro/auth.json"))
.get(&target)
.unwrap()
.unwrap();
let AuthEntry::OAuth(stored) = stored else {
panic!("expected refreshed OAuth entry");
};
assert_eq!(stored.access_token, "fresh-access");
client
.shutdown()
.await
.expect("MCP client should shut down");
}
#[tokio::test(flavor = "multi_thread")]
async fn mcp_search_orders_by_started_timestamp_before_created_timestamp() {
let context = test_context!();
@ -1496,6 +1592,7 @@ async fn mcp_events_desc_after_offset_and_limit_page_over_requested_order() {
.await;
assert_eq!(desc["events"][0]["event_id"], "evt-5");
assert_eq!(desc["next_cursor"], 5);
assert_eq!(paged["events"][0]["event_id"], "evt-3");
assert_eq!(paged["events"][1]["event_id"], "evt-4");
assert_eq!(paged["next_cursor"], 5);
@ -1509,6 +1606,80 @@ async fn mcp_events_desc_after_offset_and_limit_page_over_requested_order() {
.expect("MCP client should shut down");
}
#[tokio::test(flavor = "multi_thread")]
async fn mcp_events_desc_cursor_continues_to_older_events() {
let context = test_context!();
let server = MockServer::start();
let target_url = format!("{}/api/v1", server.base_url());
let target: fabro_client::ServerTarget = target_url.parse().unwrap();
seed_dev_token_auth(&context.home_dir, &target, TEST_DEV_TOKEN);
let run_id = unique_run_id();
let resolve = mock_resolved_run(&server, "nightly", &run_id);
let events = (1..=5)
.map(|sequence| {
serde_json::json!({
"seq": sequence,
"id": format!("evt-{sequence}"),
"ts": format!("2026-04-05T12:00:0{sequence}Z"),
"run_id": run_id,
"event": "run.started",
"properties": { "name": format!("event {sequence}") },
"actor": null
})
})
.collect::<Vec<_>>();
let full_events = server.mock(|when, then| {
when.method(GET)
.path(format!("/api/v1/runs/{run_id}/events"))
.query_param_missing("limit")
.query_param_missing("since_seq");
then.status(200)
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"data": events,
"meta": { "has_more": false }
}));
});
let client = spawn_mcp_client(&context, &["--server", &target_url]).await;
let first_page = call_tool_json(
&client,
"fabro_run_events",
serde_json::json!({
"run_id": "nightly",
"action": "list",
"direction": "desc",
"first": 2
}),
)
.await;
let second_page = call_tool_json(
&client,
"fabro_run_events",
serde_json::json!({
"run_id": "nightly",
"action": "list",
"direction": "desc",
"after": first_page["next_cursor"],
"first": 2
}),
)
.await;
assert_eq!(first_page["events"][0]["event_id"], "evt-5");
assert_eq!(first_page["events"][1]["event_id"], "evt-4");
assert_eq!(first_page["next_cursor"], 4);
assert_eq!(second_page["events"][0]["event_id"], "evt-3");
assert_eq!(second_page["events"][1]["event_id"], "evt-2");
assert_eq!(second_page["next_cursor"], 2);
resolve.assert_calls(2);
full_events.assert_calls(2);
client
.shutdown()
.await
.expect("MCP client should shut down");
}
#[tokio::test(flavor = "multi_thread")]
async fn mcp_events_offset_beyond_fetch_cap_reaches_later_pages() {
let context = test_context!();
@ -1745,6 +1916,34 @@ async fn create_mcp_run(client: &McpClient, workflow: PathBuf, start: bool) -> S
.to_string()
}
fn seed_oauth_auth(
home_dir: &Path,
target: &fabro_client::ServerTarget,
access_token: &str,
refresh_token: &str,
) {
let now = Utc::now();
AuthStore::new(home_dir.join(".fabro/auth.json"))
.put(
target,
AuthEntry::OAuth(OAuthEntry {
access_token: access_token.to_string(),
access_token_expires_at: now - ChronoDuration::minutes(1),
refresh_token: refresh_token.to_string(),
refresh_token_expires_at: now + ChronoDuration::days(30),
subject: StoredSubject {
idp_issuer: "https://github.com".to_string(),
idp_subject: "12345".to_string(),
login: "octocat".to_string(),
name: "The Octocat".to_string(),
email: "octocat@example.com".to_string(),
},
logged_in_at: now,
}),
)
.unwrap_or_else(|err| panic!("failed to seed OAuth auth: {err}"));
}
fn normalize_run_search(mut value: serde_json::Value) -> serde_json::Value {
if let Some(runs) = value["runs"].as_array_mut() {
for run in runs {

View file

@ -10,11 +10,15 @@ use anyhow::{Context, Result, anyhow};
use fabro_api::types;
use fabro_config::project::{self, discover_project_config, resolve_workflow_path};
use fabro_config::run::{resolve_run_goal_from_layer, resolve_run_goal_from_namespace};
use fabro_config::{CliLayer, DaytonaDockerfileLayer, RunLayer, WorkflowSettingsBuilder};
use fabro_config::{
CliLayer, DaytonaDockerfileLayer, ReplaceMap, RunExecutionLayer, RunGoalLayer, RunLayer,
RunModelLayer, RunSandboxLayer, WorkflowSettingsBuilder,
};
use fabro_graphviz::graph::AttrValue;
use fabro_graphviz::parser;
use fabro_template::{TemplateContext, render as render_template};
use fabro_types::settings::run::{ResolvedGoalSource, ResolvedRunGoal};
use fabro_types::settings::interp::InterpString;
use fabro_types::settings::run::{ApprovalMode, ResolvedGoalSource, ResolvedRunGoal, RunMode};
use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunId, WorkflowSettings};
use fabro_workflow::ManifestPath;
use fabro_workflow::git::{
@ -41,6 +45,73 @@ pub struct BuiltManifest {
pub target_path: PathBuf,
}
#[derive(Debug, Default)]
pub struct RunOverrideInput<'a> {
pub goal: Option<&'a str>,
pub model: Option<&'a str>,
pub provider: Option<&'a str>,
pub sandbox: Option<&'a str>,
pub preserve_sandbox: Option<bool>,
pub dry_run: Option<bool>,
pub auto_approve: Option<bool>,
pub labels: HashMap<String, String>,
}
#[must_use]
pub fn build_run_overrides(input: RunOverrideInput<'_>) -> RunLayer {
let goal = input
.goal
.map(|goal| RunGoalLayer::Inline(InterpString::parse(goal)));
let model = (input.model.is_some() || input.provider.is_some()).then(|| RunModelLayer {
provider: input.provider.map(InterpString::parse),
name: input.model.map(InterpString::parse),
fallbacks: Vec::new(),
});
let sandbox =
(input.sandbox.is_some() || input.preserve_sandbox.is_some()).then(|| RunSandboxLayer {
provider: input.sandbox.map(ToOwned::to_owned),
preserve: input.preserve_sandbox,
..RunSandboxLayer::default()
});
let execution =
(input.dry_run.is_some() || input.auto_approve.is_some()).then(|| RunExecutionLayer {
mode: input.dry_run.map(|dry_run| {
if dry_run {
RunMode::DryRun
} else {
RunMode::Normal
}
}),
approval: input.auto_approve.map(|auto_approve| {
if auto_approve {
ApprovalMode::Auto
} else {
ApprovalMode::Prompt
}
}),
});
RunLayer {
goal,
metadata: ReplaceMap::from(input.labels),
model,
sandbox,
execution,
..RunLayer::default()
}
}
#[must_use]
pub fn build_sparse_run_overrides(input: RunOverrideInput<'_>) -> Option<RunLayer> {
let run = build_run_overrides(input);
(run.goal.is_some()
|| !run.metadata.is_empty()
|| run.model.is_some()
|| run.sandbox.is_some()
|| run.execution.is_some())
.then_some(run)
}
struct CollectContext<'a> {
cwd: &'a Path,
inputs: &'a HashMap<String, toml::Value>,
@ -629,6 +700,64 @@ pub fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool {
mod tests {
use super::*;
#[test]
fn build_run_overrides_sets_common_cli_and_mcp_layers() {
let overrides = build_run_overrides(RunOverrideInput {
goal: Some("ship it"),
model: Some("gpt-5.4-mini"),
provider: Some("openai"),
sandbox: Some("local"),
preserve_sandbox: Some(true),
dry_run: Some(true),
auto_approve: Some(false),
labels: [("source".to_string(), "mcp".to_string())]
.into_iter()
.collect(),
});
let goal = overrides.goal.expect("goal override");
assert!(matches!(goal, fabro_config::RunGoalLayer::Inline(_)));
assert_eq!(
overrides
.model
.as_ref()
.unwrap()
.name
.as_ref()
.unwrap()
.as_source(),
"gpt-5.4-mini"
);
assert_eq!(
overrides
.model
.as_ref()
.unwrap()
.provider
.as_ref()
.unwrap()
.as_source(),
"openai"
);
assert_eq!(
overrides.sandbox.as_ref().unwrap().provider.as_deref(),
Some("local")
);
assert_eq!(overrides.sandbox.as_ref().unwrap().preserve, Some(true));
assert_eq!(
overrides.execution.as_ref().unwrap().mode,
Some(RunMode::DryRun)
);
assert_eq!(
overrides.execution.as_ref().unwrap().approval,
Some(ApprovalMode::Prompt)
);
assert_eq!(
overrides.metadata.0.get("source").map(String::as_str),
Some("mcp")
);
}
#[test]
fn build_manifest_bundles_imports_prompts_and_children() {
let temp = tempfile::tempdir().unwrap();

View file

@ -11,13 +11,12 @@ use std::time::{Duration, Instant};
use chrono::{DateTime, NaiveDate, Utc};
use fabro_api::types;
use fabro_client::Client;
use fabro_config::{
CliLayer, ReplaceMap, RunExecutionLayer, RunGoalLayer, RunLayer, RunModelLayer, RunSandboxLayer,
use fabro_config::{CliLayer, RunLayer};
use fabro_manifest::{
ManifestBuildInput, RunOverrideInput, build_run_manifest as build_canonical_run_manifest,
build_sparse_run_overrides,
};
use fabro_manifest::{ManifestBuildInput, build_run_manifest as build_canonical_run_manifest};
use fabro_server::manifest_validation;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{ApprovalMode, RunMode};
use fabro_types::{EventEnvelope, Run, RunId, RunStatus};
use fabro_util::exit::{self, ExitClass};
use rmcp::model::{CallToolResult, Content};
@ -596,12 +595,19 @@ pub(crate) async fn run_events(
.await
.map_err(|err| ToolError::from_anyhow(&err))?
.id;
let descending = raw.direction.as_deref() == Some("desc");
let fetch_after = if descending { None } else { raw.after };
let mut events = client
.list_run_events(&run_id, raw.after, event_fetch_limit(&raw))
.list_run_events(&run_id, fetch_after, event_fetch_limit(&raw))
.await
.map_err(|err| ToolError::from_anyhow(&err))?;
if descending {
if let Some(after) = raw.after {
events.retain(|event| event.seq < after);
}
}
filter_events(&mut events, &raw)?;
if raw.direction.as_deref() == Some("desc") {
if descending {
events.reverse();
}
let offset = raw.offset.unwrap_or(0);
@ -616,7 +622,13 @@ pub(crate) async fn run_events(
.iter()
.map(|event| run_event_result(event, max_content_length))
.collect::<ToolResult<Vec<_>>>()?;
let next_cursor = page.last().map(|event| event.seq.saturating_add(1));
let next_cursor = page.last().map(|event| {
if descending {
event.seq
} else {
event.seq.saturating_add(1)
}
});
Ok(RunEventsResult {
run_id: run_id.to_string(),
@ -902,52 +914,16 @@ fn mcp_manifest_args(spec: &CreateRunSpec) -> Option<types::ManifestArgs> {
}
fn mcp_run_overrides(spec: &CreateRunSpec) -> Option<RunLayer> {
let goal = spec
.goal
.as_ref()
.map(|goal| RunGoalLayer::Inline(InterpString::parse(goal)));
let model = (spec.model.is_some() || spec.provider.is_some()).then(|| RunModelLayer {
provider: spec.provider.as_deref().map(InterpString::parse),
name: spec.model.as_deref().map(InterpString::parse),
fallbacks: Vec::new(),
});
let sandbox =
(spec.sandbox.is_some() || spec.preserve_sandbox.is_some()).then(|| RunSandboxLayer {
provider: spec.sandbox.clone(),
preserve: spec.preserve_sandbox,
..RunSandboxLayer::default()
});
let execution =
(spec.dry_run.is_some() || spec.auto_approve.is_some()).then(|| RunExecutionLayer {
mode: spec.dry_run.map(|dry_run| {
if dry_run {
RunMode::DryRun
} else {
RunMode::Normal
}
}),
approval: spec.auto_approve.map(|auto_approve| {
if auto_approve {
ApprovalMode::Auto
} else {
ApprovalMode::Prompt
}
}),
});
let run = RunLayer {
goal,
metadata: ReplaceMap::from(spec.labels.clone()),
model,
sandbox,
execution,
..RunLayer::default()
};
(run.goal.is_some()
|| !run.metadata.is_empty()
|| run.model.is_some()
|| run.sandbox.is_some()
|| run.execution.is_some())
.then_some(run)
build_sparse_run_overrides(RunOverrideInput {
goal: spec.goal.as_deref(),
model: spec.model.as_deref(),
provider: spec.provider.as_deref(),
sandbox: spec.sandbox.as_deref(),
preserve_sandbox: spec.preserve_sandbox,
dry_run: spec.dry_run,
auto_approve: spec.auto_approve,
labels: spec.labels.clone(),
})
}
fn mcp_manifest_args_is_empty(args: &types::ManifestArgs) -> bool {

View file

@ -4,7 +4,7 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use fabro_client::{
AuthEntry, AuthStore, Client, Credential, ServerTarget, TransportConnector,
AuthEntry, AuthStore, Client, Credential, OAuthSession, ServerTarget, TransportConnector,
apply_bearer_token_auth,
};
use fabro_config::bind::Bind;
@ -194,15 +194,15 @@ async fn client_from_settings(settings: &McpServerSettings) -> Result<Client> {
async fn connect_target(server: &str, settings: &McpServerSettings) -> Result<Client> {
let target: ServerTarget = server.parse()?;
let mut credential = AuthStore::new(settings.home_dir.join(".fabro").join("auth.json"))
.get(&target)?
.map(credential_from_auth_entry);
let auth_store = AuthStore::new(settings.home_dir.join(".fabro").join("auth.json"));
let mut credential = resolve_target_credential_with_store(&target, &auth_store)?;
if credential.is_none() && target.is_unix_socket() {
let runtime_token_path = Storage::new(&settings.storage_dir)
.runtime_directory()
.dev_token_path();
credential = dev_token::read_dev_token_file(&runtime_token_path).map(Credential::DevToken);
}
let oauth_session = refreshable_oauth(&target, &auth_store, credential.as_ref());
let mut builder = Client::builder()
.target(target.clone())
.transport_connector(target_transport_connector(target))
@ -210,6 +210,9 @@ async fn connect_target(server: &str, settings: &McpServerSettings) -> Result<Cl
if let Some(credential) = credential {
builder = builder.credential(credential);
}
if let Some(oauth_session) = oauth_session {
builder = builder.oauth_session(oauth_session);
}
builder
.connect()
.await
@ -235,9 +238,9 @@ async fn connect_local_server(settings: &McpServerSettings) -> Result<Client> {
}
Bind::Tcp(addr) => {
let target = ServerTarget::http_url(format!("http://{addr}"))?;
let credential = AuthStore::new(settings.home_dir.join(".fabro").join("auth.json"))
.get(&target)?
.map(credential_from_auth_entry);
let auth_store = AuthStore::new(settings.home_dir.join(".fabro").join("auth.json"));
let credential = resolve_target_credential_with_store(&target, &auth_store)?;
let oauth_session = refreshable_oauth(&target, &auth_store, credential.as_ref());
let mut builder = Client::builder()
.target(target.clone())
.transport_connector(target_transport_connector(target))
@ -245,6 +248,9 @@ async fn connect_local_server(settings: &McpServerSettings) -> Result<Client> {
if let Some(credential) = credential {
builder = builder.credential(credential);
}
if let Some(oauth_session) = oauth_session {
builder = builder.oauth_session(oauth_session);
}
builder.connect().await
}
}
@ -299,13 +305,34 @@ async fn wait_for_runtime_dev_token(path: &Path) -> Result<String> {
))
}
fn credential_from_auth_entry(entry: AuthEntry) -> Credential {
fn resolve_target_credential_with_store(
target: &ServerTarget,
store: &AuthStore,
) -> Result<Option<Credential>> {
let Some(entry) = store.get(target)? else {
return Ok(None);
};
let now = chrono::Utc::now();
match entry {
AuthEntry::OAuth(entry) => Credential::OAuth(entry),
AuthEntry::DevToken(entry) => Credential::DevToken(entry.token),
AuthEntry::DevToken(entry) => Ok(Some(Credential::DevToken(entry.token))),
AuthEntry::OAuth(entry)
if entry.access_token_expires_at > now || entry.refresh_token_expires_at > now =>
{
Ok(Some(Credential::OAuth(entry)))
}
AuthEntry::OAuth(_) => Ok(None),
}
}
fn refreshable_oauth(
target: &ServerTarget,
auth_store: &AuthStore,
credential: Option<&Credential>,
) -> Option<OAuthSession> {
matches!(credential, Some(Credential::OAuth(_)))
.then(|| OAuthSession::new(target.clone(), auth_store.clone()))
}
fn target_transport_connector(target: ServerTarget) -> TransportConnector {
TransportConnector::new(move |bearer_token| {
let target = target.clone();