From 6dfeeca45e1feb21a96e71174d19656bcc86b795 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 4 Aug 2026 15:01:19 -0400 Subject: [PATCH 01/63] perf(agent): skip Daytona folder request for edits --- lib/components/fabro-agent/src/tools.rs | 4 +- .../fabro-sandbox/src/daytona/mod.rs | 84 ++++++++++++++++--- lib/components/fabro-sandbox/src/sandbox.rs | 10 +++ .../fabro-sandbox/src/test_support.rs | 14 +++- 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index f892c971c..aad8e4e3d 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -226,7 +226,7 @@ pub fn make_edit_file_tool() -> RegisteredTool { }; ctx.env - .write_file(file_path, &new_content) + .write_existing_file(file_path, &new_content) .await .map_err(|e| e.display_with_causes())?; Ok(format!("Successfully edited {file_path}")) @@ -1002,6 +1002,7 @@ mod tests { ) .await; assert_eq!(result.unwrap(), "Successfully wrote to /out.txt"); + assert_eq!(env.existing_file_write_count(), 0); let written = env.written_files.lock().unwrap(); assert_eq!(written.len(), 1); assert_eq!(written[0].0, "/out.txt"); @@ -1036,6 +1037,7 @@ mod tests { ) .await; assert_eq!(result.unwrap(), "Successfully edited /f.txt"); + assert_eq!(env.existing_file_write_count(), 1); let written = env.written_files.lock().unwrap(); assert_eq!(written.len(), 1); assert_eq!(written[0].1, "goodbye world"); diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 6371f23d2..72b0d2fff 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -511,6 +511,19 @@ impl DaytonaSandbox { resolve_path(path, self.working_directory()) } + async fn upload_file_content(&self, resolved_path: &str, content: &str) -> crate::Result<()> { + let sandbox = self.sandbox()?; + let fs_svc = sandbox + .fs() + .await + .map_err(|e| crate::Error::context("Failed to get fs service", e))?; + + fs_svc + .upload_file_bytes(resolved_path, content.as_bytes()) + .await + .map_err(|e| crate::Error::context(format!("Failed to write file {resolved_path}"), e)) + } + /// Verify a Daytona sandbox evaluates commands as non-login Bash. /// /// Runs on a freshly created sandbox before any Fabro-owned setup, and @@ -1613,17 +1626,12 @@ impl Sandbox for DaytonaSandbox { } } - let fs_svc = sandbox - .fs() - .await - .map_err(|e| crate::Error::context("Failed to get fs service", e))?; + self.upload_file_content(&resolved, content).await + } - fs_svc - .upload_file_bytes(&resolved, content.as_bytes()) - .await - .map_err(|e| crate::Error::context(format!("Failed to write file {resolved}"), e))?; - - Ok(()) + async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> { + let resolved = self.resolve_path(path); + self.upload_file_content(&resolved, content).await } async fn delete_file(&self, path: &str) -> crate::Result<()> { @@ -3432,6 +3440,62 @@ mod tests { delete.assert_async().await; } + #[tokio::test] + async fn write_existing_file_skips_parent_directory_creation() { + let server = MockServer::start_async().await; + let server_url = server.base_url(); + let sandbox_response = server + .mock_async(|when, then| { + when.method(GET).path("/sandbox/sandbox-edit"); + then.status(200) + .header("content-type", "application/json") + .json_body(sandbox_body("sandbox-edit", SandboxState::Started)); + }) + .await; + let toolbox_response = server + .mock_async(|when, then| { + when.method(GET) + .path("/sandbox/sandbox-edit/toolbox-proxy-url"); + then.status(200) + .header("content-type", "application/json") + .json_body(serde_json::json!({"url": server_url})); + }) + .await; + let folder = server + .mock_async(|when, then| { + when.method(POST).path("/sandbox-edit/files/folder"); + then.status(200); + }) + .await; + let upload = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox-edit/files/upload") + .query_param("path", "/home/daytona/workspace/src/lib.rs") + .body_includes("updated contents"); + then.status(200); + }) + .await; + + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("sandbox-edit") + .await + .expect("get mock sandbox"); + assert!(sandbox.sandbox.set(sdk_sandbox).is_ok()); + + sandbox + .write_existing_file("src/lib.rs", "updated contents") + .await + .expect("write existing file"); + + sandbox_response.assert_async().await; + toolbox_response.assert_async().await; + upload.assert_async().await; + folder.assert_calls_async(0).await; + } + /// Recover the inner command a wrapper carries, proving it survives the /// base64 transport byte-for-byte. fn decode_wrapped_command(wrapped: &str) -> String { diff --git a/lib/components/fabro-sandbox/src/sandbox.rs b/lib/components/fabro-sandbox/src/sandbox.rs index 6e6e2b336..31a873300 100644 --- a/lib/components/fabro-sandbox/src/sandbox.rs +++ b/lib/components/fabro-sandbox/src/sandbox.rs @@ -1048,6 +1048,16 @@ pub trait Sandbox: Send + Sync { } async fn write_file(&self, path: &str, content: &str) -> crate::Result<()>; + + /// Write a file that the caller has already confirmed exists. + /// + /// Providers can override this method to skip setup that is only needed + /// when creating a new path. The default preserves the behavior of + /// [`Sandbox::write_file`]. + async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> { + self.write_file(path, content).await + } + async fn delete_file(&self, path: &str) -> crate::Result<()>; async fn file_exists(&self, path: &str) -> crate::Result; async fn list_directory( diff --git a/lib/components/fabro-sandbox/src/test_support.rs b/lib/components/fabro-sandbox/src/test_support.rs index 7364dc8b0..d25d9bbf4 100644 --- a/lib/components/fabro-sandbox/src/test_support.rs +++ b/lib/components/fabro-sandbox/src/test_support.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::Mutex; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; @@ -29,6 +29,8 @@ pub struct MockSandbox { pub os_version_str: String, /// Captures (path, content) pairs from `write_file` calls. pub written_files: Mutex>, + /// Counts calls to `write_existing_file`. + pub existing_file_writes: AtomicUsize, /// Captures the `timeout_ms` argument from `exec_command` calls. pub captured_timeout: Mutex>, /// Captures the `command` argument from `exec_command` calls (last only). @@ -104,6 +106,10 @@ impl MockSandbox { .expect("delete_calls lock poisoned") } + pub fn existing_file_write_count(&self) -> usize { + self.existing_file_writes.load(Ordering::Relaxed) + } + pub fn set_stdio_process(&self, process: MockStdioProcess) { *self .stdio_process @@ -156,6 +162,7 @@ impl Default for MockSandbox { platform_str: "darwin", os_version_str: "Darwin 24.0.0".into(), written_files: Mutex::new(Vec::new()), + existing_file_writes: AtomicUsize::new(0), captured_timeout: Mutex::new(None), captured_command: Mutex::new(None), captured_commands: Mutex::new(Vec::new()), @@ -250,6 +257,11 @@ impl Sandbox for MockSandbox { Ok(()) } + async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> { + self.existing_file_writes.fetch_add(1, Ordering::Relaxed); + self.write_file(path, content).await + } + async fn delete_file(&self, _path: &str) -> crate::Result<()> { Ok(()) } From e11d268e30f8ce9a161328b4bd4a4b5ad21d980b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 5 Aug 2026 08:43:41 -0400 Subject: [PATCH 02/63] Bound doctor diagnostics within client timeout --- docs/public/api-reference/fabro-api.yaml | 9 ++ lib/apps/fabro-server/src/diagnostics.rs | 118 +++++++++++++++--- .../fabro-server/src/server/handler/system.rs | 59 ++++++++- 3 files changed, 168 insertions(+), 18 deletions(-) diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 261badbbb..4382d19fa 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -594,6 +594,15 @@ paths: application/json: schema: $ref: "#/components/schemas/DiagnosticsReport" + "504": + description: Diagnostics operation timed out + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /api/v1/openapi.json: get: diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index 988f84404..86f625366 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -6,7 +6,7 @@ use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use fabro_auth::auth_issue_message; use fabro_llm::client::Client as LlmClient; -use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe}; +use fabro_llm::model_test::{ModelTestOutcome, ModelTestStatus, run_basic_model_probe}; use fabro_model::{Catalog, ProviderId}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; @@ -23,6 +23,9 @@ use tokio::time::timeout; use crate::server::AppState; +const EXTERNAL_SERVICE_PROBE_TIMEOUT: Duration = Duration::from_secs(15); +const DOCKER_PROBE_TIMEOUT: Duration = Duration::from_secs(5); + fn http_client_or_check( name: &str, status: CheckStatus, @@ -254,11 +257,38 @@ async fn probe_single_provider( }; let model_id = model.id.clone(); - let outcome = run_basic_model_probe(model_id.as_str(), &provider, client).await; + let outcome = run_basic_model_probe(model_id.as_str(), provider.clone(), client); + provider_probe_with_timeout( + provider, + model_id.to_string(), + outcome, + EXTERNAL_SERVICE_PROBE_TIMEOUT, + ) + .await +} + +async fn provider_probe_with_timeout( + provider: ProviderId, + model_id: String, + probe: F, + probe_timeout: Duration, +) -> ProviderProbeResult +where + F: Future, +{ + let Ok(outcome) = timeout(probe_timeout, probe).await else { + return provider_probe_error( + provider, + Some(model_id), + probe_timeout_message(probe_timeout), + None, + ); + }; + match outcome.status { ModelTestStatus::Ok => ProviderProbeResult { provider, - model_id: Some(model_id.to_string()), + model_id: Some(model_id), status: ProviderProbeStatus::Ok, error_message: None, diagnostic_detail: None, @@ -267,16 +297,19 @@ async fn probe_single_provider( let raw = outcome .error_message .unwrap_or_else(|| "provider probe failed".to_string()); - provider_probe_error( - provider, - Some(model_id.to_string()), - redact_string(&raw), - None, - ) + provider_probe_error(provider, Some(model_id), redact_string(&raw), None) } } } +fn probe_timeout_message(probe_timeout: Duration) -> String { + if probe_timeout.subsec_nanos() == 0 { + format!("timeout ({}s)", probe_timeout.as_secs()) + } else { + format!("timeout ({}ms)", probe_timeout.as_millis()) + } +} + fn provider_probe_error( provider: ProviderId, model_id: Option, @@ -388,7 +421,7 @@ async fn check_github_app(state: &AppState) -> CheckResult { Err(result) => return result, }; let probe = timeout( - Duration::from_secs(15), + EXTERNAL_SERVICE_PROBE_TIMEOUT, http.get(format!("{}/user", fabro_github::github_api_base_url())) .header("Authorization", format!("Bearer {token}")) .header("Accept", "application/vnd.github+json") @@ -538,7 +571,7 @@ async fn check_github_app(state: &AppState) -> CheckResult { Err(result) => return result, }; let auth_result = timeout( - Duration::from_secs(15), + EXTERNAL_SERVICE_PROBE_TIMEOUT, fabro_github::get_authenticated_app(&http, &jwt, &fabro_github::github_api_base_url()), ) .await; @@ -581,7 +614,7 @@ async fn check_docker_sandbox(state: &AppState) -> CheckResult { .await .map_err(|err| err.display_with_causes()) }, - Duration::from_secs(5), + DOCKER_PROBE_TIMEOUT, ) .await } @@ -656,7 +689,29 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { }; }; - match state.check_daytona_api_key(api_key).await { + check_cloud_sandbox_with_probe( + || state.check_daytona_api_key(api_key), + EXTERNAL_SERVICE_PROBE_TIMEOUT, + ) + .await +} + +async fn check_cloud_sandbox_with_probe(probe: F, probe_timeout: Duration) -> CheckResult +where + F: FnOnce() -> Fut, + Fut: Future>, +{ + let Ok(probe) = timeout(probe_timeout, probe()).await else { + return CheckResult { + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: probe_timeout_message(probe_timeout), + details: vec![CheckDetail::new("Daytona probe timed out".to_string())], + remediation: Some("Verify DAYTONA_API_KEY value and Daytona reachability".to_string()), + }; + }; + + match probe { Ok(check) if check.ok() => CheckResult { name: "Cloud Sandbox".to_string(), status: CheckStatus::Pass, @@ -750,7 +805,7 @@ async fn check_brave_search(state: &AppState) -> CheckResult { Err(result) => return result, }; - let probe = timeout(Duration::from_secs(15), async move { + let probe = timeout(EXTERNAL_SERVICE_PROBE_TIMEOUT, async move { http.get("https://api.search.brave.com/res/v1/web/search?q=test&count=1") .header("X-Subscription-Token", api_key) .send() @@ -1035,6 +1090,27 @@ mod tests { ); } + #[tokio::test] + async fn provider_probe_reports_provider_specific_timeout() { + assert_eq!( + probe_timeout_message(EXTERNAL_SERVICE_PROBE_TIMEOUT), + "timeout (15s)" + ); + + let result = provider_probe_with_timeout( + ProviderId::new("modal"), + "modal/test-model".to_string(), + std::future::pending::(), + Duration::from_millis(1), + ) + .await; + + assert_eq!(result.provider, ProviderId::new("modal")); + assert_eq!(result.model_id.as_deref(), Some("modal/test-model")); + assert_eq!(result.status, ProviderProbeStatus::Error); + assert_eq!(result.error_message.as_deref(), Some("timeout (1ms)")); + } + #[test] fn docker_sandbox_probe_passes_when_daemon_responds() { let result = docker_sandbox_probe_check(Ok(())); @@ -1154,6 +1230,20 @@ enabled = false ); } + #[tokio::test] + async fn check_cloud_sandbox_reports_timeout() { + let result = check_cloud_sandbox_with_probe( + std::future::pending::>, + Duration::from_millis(1), + ) + .await; + + assert_eq!(result.name, "Cloud Sandbox"); + assert_eq!(result.status, CheckStatus::Error); + assert_eq!(result.summary, "timeout (1ms)"); + assert_eq!(result.details[0].text, "Daytona probe timed out"); + } + #[tokio::test] async fn check_brave_search_ignores_env_backed_api_key() { let state = TestAppStateBuilder::new() diff --git a/lib/apps/fabro-server/src/server/handler/system.rs b/lib/apps/fabro-server/src/server/handler/system.rs index d26e445a6..a38fe33f5 100644 --- a/lib/apps/fabro-server/src/server/handler/system.rs +++ b/lib/apps/fabro-server/src/server/handler/system.rs @@ -1,5 +1,7 @@ use std::collections::BTreeMap; +use std::future::Future; use std::sync::Arc; +use std::time::Duration; use chrono::Utc; use fabro_slack::config::{ @@ -9,6 +11,7 @@ use fabro_slack::config::{ use fabro_static::EnvVars; use fabro_types::settings::server::GithubIntegrationSettings; use fabro_vault::Vault; +use tokio::time::timeout; use super::super::{ AggregateBilling, AggregateBillingTotals, ApiError, AppState, BilledTokenCounts, @@ -21,6 +24,8 @@ use super::super::{ resource_sampler, spawn_blocking, system_sandbox_provider, to_i64, }; +const SERVER_DIAGNOSTICS_TIMEOUT: Duration = Duration::from_secs(25); + pub(super) fn routes() -> Router> { Router::new() .route("/repos/github/{owner}/{name}", get(get_github_repo)) @@ -683,11 +688,34 @@ async fn get_github_repo( } async fn run_diagnostics(_auth: RequiredUser, State(state): State>) -> Response { - ( - StatusCode::OK, - Json(diagnostics::run_all(state.as_ref()).await), + diagnostics_response_with_timeout( + Box::pin(diagnostics::run_all(state.as_ref())), + SERVER_DIAGNOSTICS_TIMEOUT, ) - .into_response() + .await +} + +async fn diagnostics_response_with_timeout( + diagnostics: F, + operation_timeout: Duration, +) -> Response +where + F: Future, +{ + let Ok(report) = timeout(operation_timeout, diagnostics).await else { + tracing::warn!( + timeout_secs = operation_timeout.as_secs(), + "server diagnostics timed out" + ); + return ApiError::with_code( + StatusCode::GATEWAY_TIMEOUT, + "Server diagnostics timed out.", + "diagnostics_timeout", + ) + .into_response(); + }; + + (StatusCode::OK, Json(report)).into_response() } pub(in crate::server) async fn openapi_spec() -> Response { @@ -737,3 +765,26 @@ async fn get_aggregate_billing( }; (StatusCode::OK, Json(response)).into_response() } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn diagnostics_response_returns_gateway_timeout_before_client_deadline() { + let response = diagnostics_response_with_timeout( + std::future::pending::(), + Duration::from_millis(1), + ) + .await; + + assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .expect("diagnostics timeout response body should be readable"); + let body: serde_json::Value = serde_json::from_slice(&body) + .expect("diagnostics timeout response should contain JSON"); + assert_eq!(body["errors"][0]["code"], "diagnostics_timeout"); + assert_eq!(body["errors"][0]["detail"], "Server diagnostics timed out."); + } +} From f6932529faed7d758efd6d6bb8dbd08576223ffe Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 5 Aug 2026 21:01:53 -0400 Subject: [PATCH 03/63] Let a node execute max_visits times before the cycle guard fires The executor incremented a node's visit count on entry and refused the visit once the count reached the limit, so a node with max_visits=N executed at most N-1 times. The documented contract in stages-and-nodes.mdx is "Max times this node can execute in a run", and both published examples describe bounded retry loops under that reading. A graph with max_visits=2 on a designed one-correction loop therefore failed as "stuck in a cycle" before the correction could run. Check the completed-visit count before entry instead: a node with max_visits=N now executes exactly N times, and the refused entry is not reported as a visit, so the error's count names the executions that actually happened. Also correct the nlspec example prose, which claimed the workflow "moves on with the best result" at the limit; exceeding max_visits fails the run. Co-Authored-By: Claude Fable 5 --- docs/public/examples/nlspec-conformance.mdx | 2 +- lib/foundation/fabro-core/src/executor.rs | 26 ++++++++++++++++----- lib/foundation/fabro-core/src/state.rs | 4 ++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/public/examples/nlspec-conformance.mdx b/docs/public/examples/nlspec-conformance.mdx index cfeb30fec..6a1161769 100644 --- a/docs/public/examples/nlspec-conformance.mdx +++ b/docs/public/examples/nlspec-conformance.mdx @@ -123,7 +123,7 @@ Do not rewrite working code. Make targeted fixes to the specific failures. ### Max visits as a safety valve -`max_visits=5` on the `fix` node prevents infinite loops. If the agent can't pass in 5 iterations, the workflow moves on with the best result so far. Tune this based on spec complexity: a 30-line spec might need 2 iterations, a 2,000-line spec might need 10. +`max_visits=5` on the `fix` node prevents infinite loops. The node can execute up to 5 times; a sixth visit fails the run rather than looping forever. Tune this based on spec complexity: a 30-line spec might need 2 iterations, a 2,000-line spec might need 10. ### Goal gate on full conformance diff --git a/lib/foundation/fabro-core/src/executor.rs b/lib/foundation/fabro-core/src/executor.rs index 3a94eb332..f6e3a6aa3 100644 --- a/lib/foundation/fabro-core/src/executor.rs +++ b/lib/foundation/fabro-core/src/executor.rs @@ -179,8 +179,11 @@ impl Executor { } } - // Check visit limits (>= matches fabro-workflow semantics) - let visits = state.increment_visits(node.id()); + // Check visit limits before entry: a node with a limit of N may + // execute N times, matching the documented contract. The count + // covers completed entries only, so the refused visit is not + // reported as one. + let visits = state.visits(node.id()); if let Some(max) = node.max_visits() { if visits >= max { return Err(Error::VisitLimitExceeded { @@ -201,6 +204,7 @@ impl Executor { }); } } + state.increment_visits(node.id()); // before_node lifecycle let node_result = match self.lifecycle.before_node(&node, &state).await? { @@ -807,7 +811,8 @@ mod tests { #[tokio::test] async fn executor_visit_limit_per_node() { - // Node with max_visits=2, loops back — fails on 2nd visit (>= semantics) + // Node with max_visits=2, loops back — executes exactly twice, then + // the third entry is refused. The error reports completed visits. let g = TestGraph::new( vec![ TestNode::new("loop_node").with_max_visits(2), @@ -821,11 +826,20 @@ mod tests { "loop_node", ); let state = ExecutionState::new(&g).unwrap(); + let handler = Arc::new(CountingHandler::new(vec![])); let executor = - ExecutorBuilder::new(Arc::new(AlwaysSucceedHandler) as Arc>) - .build(); + ExecutorBuilder::new(Arc::clone(&handler) as Arc>).build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(Error::VisitLimitExceeded { .. }))); + match result { + Err(Error::VisitLimitExceeded { visits, limit, .. }) => { + assert_eq!(visits, 2); + assert_eq!(limit, 2); + } + Err(other) => panic!("expected VisitLimitExceeded, got {other:?}"), + Ok(_) => panic!("expected VisitLimitExceeded, got success"), + } + // Two full loop_node -> other iterations ran before the refusal. + assert_eq!(handler.calls(), 4); } #[tokio::test] diff --git a/lib/foundation/fabro-core/src/state.rs b/lib/foundation/fabro-core/src/state.rs index ca3b8e56b..c44eeeb7d 100644 --- a/lib/foundation/fabro-core/src/state.rs +++ b/lib/foundation/fabro-core/src/state.rs @@ -79,6 +79,10 @@ impl ExecutionState { graph.get_node(&self.current_node_id) } + pub fn visits(&self, node_id: &str) -> usize { + self.node_visits.get(node_id).copied().unwrap_or(0) + } + pub fn increment_visits(&mut self, node_id: &str) -> usize { let count = self.node_visits.entry(node_id.to_string()).or_insert(0); *count += 1; From a4db43a8892a38b625014fc642d9e9fc79a818f2 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 6 Aug 2026 11:54:32 -0400 Subject: [PATCH 04/63] Report live billing totals for in-progress runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run summaries previously populated billing only from the terminal conclusion event, so the web UI's size chip showed dollar amounts only after a run completed — even though the size letter was already derived from live per-stage usage. Derive billing from the same projected total the size uses. projected_billing already prefers the conclusion's billing once a run concludes, so completed runs still report the authoritative final total. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-store/src/run_state.rs | 26 +++++++++------------ 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index 5e4ab5e37..720a76f0e 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -1360,8 +1360,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { .conclusion .as_ref() .map(|conclusion| conclusion.timing); - let terminal_total = terminal_total_usd_micros(state); - let current_total = projected_billing(state).total_usd_micros; + let total_usd_micros = projected_billing(state).total_usd_micros; Run { id: *run_id, @@ -1405,10 +1404,10 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { completed_at, }, timing: run_timing, - billing: terminal_total.map(|total_usd_micros| RunBillingSummary { + billing: total_usd_micros.map(|total_usd_micros| RunBillingSummary { total_usd_micros: Some(total_usd_micros), }), - size: RunSize::from_total_usd_micros(current_total), + size: RunSize::from_total_usd_micros(total_usd_micros), ask_fabro: AskFabro::default(), diff: diff_summary, pull_request: state.pull_request.clone(), @@ -1421,14 +1420,6 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { } } -fn terminal_total_usd_micros(state: &RunProjection) -> Option { - state - .conclusion - .as_ref() - .and_then(|conclusion| conclusion.billing.as_ref()) - .and_then(|billing| billing.total_usd_micros) -} - pub(crate) fn projected_billing(state: &RunProjection) -> BilledTokenCounts { if let Some(billing) = state .conclusion @@ -1693,8 +1684,8 @@ mod tests { BilledTokenCounts, BlockedReason, Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory, FailureDetail, FailureReason, Graph, McpServerStatus, Node, Outcome, ParallelBranchId, PendingReason, PermissionLevel, PullRequestCreationStatus, - PullRequestLink, QuestionType, ReasoningEffort, RunApprovalState, RunBlobId, - RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, Speed, + PullRequestLink, QuestionType, ReasoningEffort, RunApprovalState, RunBillingSummary, + RunBlobId, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, Speed, StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning, StageHandler, StageModelUsage, StageOutcome, StageState, StageTiming, SubAgentStatus, @@ -5284,7 +5275,12 @@ mod tests { let summary = build_summary(&state, &fixtures::RUN_1); assert_eq!(summary.size, RunSize::S); - assert_eq!(summary.billing, None); + assert_eq!( + summary.billing, + Some(RunBillingSummary { + total_usd_micros: Some(20_000_001), + }) + ); } #[test] From 57547ed7b6384be9e2096ff4b4f4c213adb48e8d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 6 Aug 2026 11:54:38 -0400 Subject: [PATCH 05/63] Populate repo and workflow filters in runs list view The Repo and Workflow dropdowns on the runs page derived their options from the board query, which is disabled in list view. With ?view=list, the options were always empty even when runs were visible. Derive the options from whichever data source the current view loads: the board query in columns view, or the current page of the paginated list query in list view. Extract the option-building into an exported buildFilterOptions helper that also keeps the active selection in the options when no loaded run matches it, so the filter button never renders an undefined label while paginating. A future change will replace page-derived options with a facets endpoint plus server-side repo/workflow query params. Co-Authored-By: Claude Fable 5 --- apps/fabro-web/app/routes/runs.test.tsx | 34 +++++++++++++++++++++++++ apps/fabro-web/app/routes/runs.tsx | 34 +++++++++++++++++-------- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx index 52196878b..71f30dd43 100644 --- a/apps/fabro-web/app/routes/runs.test.tsx +++ b/apps/fabro-web/app/routes/runs.test.tsx @@ -3,6 +3,7 @@ import type { BoardColumn, Run } from "@qltysh/fabro-api-client"; import { buildBoardColumns, + buildFilterOptions, loadStoredRunsWorkspaceSearchParams, placeArchivedColumnLast, persistRunsWorkspacePreferences, @@ -11,6 +12,7 @@ import { shouldRefreshBoardForEvent, } from "./runs"; import { summarizeBatchLifecycleAction } from "../components/runs-list/batch-lifecycle"; +import { mapRunListItem } from "../data/runs"; import { TEST_PRINCIPAL } from "../lib/test-fixtures"; function boardRun(id: string, column: BoardColumn, questionText?: string): Run { @@ -217,6 +219,38 @@ describe("runs route board mapping", () => { }); }); +describe("runs route filter options", () => { + function runWith(id: string, repoName: string, workflowName: string): Run { + const run = boardRun(id, "running"); + return { + ...run, + repository: { ...run.repository, name: repoName }, + workflow: { ...run.workflow, name: workflowName }, + }; + } + + test("derives sorted unique options from run items", () => { + const items = [ + runWith("a", "qlty/beta", "release"), + runWith("b", "qlty/alpha", "hello"), + runWith("c", "qlty/beta", "release"), + ].map(mapRunListItem); + + expect(buildFilterOptions(items, (item) => item.repo, "all")).toEqual(["alpha", "beta"]); + expect(buildFilterOptions(items, (item) => item.workflow, "all")).toEqual([ + "hello", + "release", + ]); + }); + + test("keeps the active selection when no loaded run matches it", () => { + const items = [runWith("a", "qlty/beta", "release")].map(mapRunListItem); + + expect(buildFilterOptions(items, (item) => item.repo, "gamma")).toEqual(["beta", "gamma"]); + expect(buildFilterOptions([], (item) => item.workflow, "release")).toEqual(["release"]); + }); +}); + describe("runs route workspace preferences", () => { class MemoryStorage { values = new Map(); diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index b1e09bf67..9aeceb78c 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -140,6 +140,18 @@ export function buildBoardColumns( }); } +export function buildFilterOptions( + items: RunItem[], + pick: (item: RunItem) => string, + selected: string, +): string[] { + const values = new Set(items.map(pick)); + // Keep the active selection visible even when no loaded run matches it, + // e.g. a stored repo filter while paginating the list view. + if (selected !== "all") values.add(selected); + return Array.from(values).sort(); +} + export function placeArchivedColumnLast(columns: Column[], includeArchived: boolean): Column[] { if (!includeArchived) return columns; const archived = columns.find((column) => column.id === "archived"); @@ -771,18 +783,18 @@ export default function Runs() { ); const hasGitHubAuth = authConfig.data?.methods.includes("github") === true; const serverUrl = systemInfo.data?.server_url; - const allRepos = Array.from( - new Set( - initialColumns.flatMap((col: Column) => col.items.map((item: RunItem) => String(item.repo))), - ), + // Filter options come from the loaded runs: all runs in columns view, the + // current page in list view (until a facets endpoint provides the full set). + const filterSourceItems: RunItem[] = + view === "list" + ? (listRunsPage.data?.data ?? []).map(mapRunListItem) + : initialColumns.flatMap((col: Column) => col.items); + const allRepos = buildFilterOptions(filterSourceItems, (item) => item.repo, repoFilter); + const allWorkflows = buildFilterOptions( + filterSourceItems, + (item) => item.workflow, + workflowFilter, ); - allRepos.sort(); - const allWorkflows = Array.from( - new Set( - initialColumns.flatMap((col: Column) => col.items.map((item: RunItem) => String(item.workflow))), - ), - ); - allWorkflows.sort(); const [columnsState, setColumnsState] = useState(() => ({ base: initialColumns, columns: initialColumns, From 4e24dcb68a49037ee64c087514eca4e0524f7ebe Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 6 Aug 2026 21:10:49 -0400 Subject: [PATCH 06/63] Add failing tests for run-spec redaction corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entropy redactor rewrites NAME= assignment pairs to a bare REDACTED, and the worker rehydrates its executable RunSpec from the projection folded from redacted stored events. Together these broke Daytona snapshot builds for any run definition whose inline Dockerfile pins a git SHA: the spec came back as `ARG REDACTED`, the build died on the unset variable under `set -eu`, and the environment's snapshot identity silently changed. Pin the intended contracts with red tests: - fabro-redact: an assignment whose value alone is below the entropy threshold survives redaction (pure hex cannot exceed 4.0 bits; only the name+value charset merge crosses 4.5), and a genuinely high-entropy value is redacted without destroying the key name. - fabro-workflow: the spec that load_from_store rehydrates round-trips byte-identical through the store, including content that looks like a secret — event redaction must not reach execution. Co-Authored-By: Claude Fable 5 --- .../fabro-workflow/src/pipeline/persist.rs | 44 +++++++++++++++++++ lib/foundation/fabro-redact/src/lib.rs | 21 +++++++++ 2 files changed, 65 insertions(+) diff --git a/lib/components/fabro-workflow/src/pipeline/persist.rs b/lib/components/fabro-workflow/src/pipeline/persist.rs index cc12c3cba..846f9b853 100644 --- a/lib/components/fabro-workflow/src/pipeline/persist.rs +++ b/lib/components/fabro-workflow/src/pipeline/persist.rs @@ -272,6 +272,50 @@ mod tests { assert!(loaded.diagnostics().is_empty()); } + #[tokio::test] + async fn load_from_store_preserves_high_entropy_dockerfile_content() { + // The spec the worker executes must survive the store byte-identical. + // Event redaction is a storage/display concern; when it reaches the + // spec that `load_from_store` rehydrates, the sandbox builds a + // corrupted Dockerfile: `ARG NAME=` pairs come back as + // `ARG REDACTED`, the build's `set -eu` step fails on the unset + // variable, and the environment's snapshot identity silently changes. + let temp = tempfile::tempdir().unwrap(); + let run_dir = temp.path().join("run"); + std::fs::create_dir_all(&run_dir).unwrap(); + let (graph, source) = graph_and_source(); + + // Two shapes that must both survive: the hex pins that triggered the + // production failure, and a token high-entropy enough that any + // detector will keep flagging it in stored events. The second keeps + // this test red until execution stops reading redacted content, + // independent of how the entropy heuristic evolves. + let dockerfile = "FROM buildpack-deps:noble\n\ + ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf\n\ + ARG DOCKER_INSTALL_SHA256=b991f2806186f7287bb9e53362060c382e906d154599b2fb0982f34246bacfd4\n\ + ENV CACHE_SALT=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p\n\ + RUN install-docker \"${DOCKER_INSTALL_COMMIT}\" \"${DOCKER_INSTALL_SHA256}\"\n"; + + let mut record = sample_record(different_graph()); + record.graph = graph; + record.settings.run.environment.image.dockerfile = Some( + fabro_types::settings::run::DockerfileSource::Inline(dockerfile.to_string()), + ); + + let run_store = seeded_store(&record, Some(&source)).await; + let loaded = load_from_store(&run_store.clone().into(), &run_dir) + .await + .unwrap(); + + assert_eq!( + loaded.run_spec().settings.run.environment.image.dockerfile, + Some(fabro_types::settings::run::DockerfileSource::Inline( + dockerfile.to_string() + )), + "the executable run spec must round-trip through the store unredacted" + ); + } + #[test] fn persist_returns_error_on_io_failure() { let temp = tempfile::tempdir().unwrap(); diff --git a/lib/foundation/fabro-redact/src/lib.rs b/lib/foundation/fabro-redact/src/lib.rs index 8ed562e53..4a7efa45b 100644 --- a/lib/foundation/fabro-redact/src/lib.rs +++ b/lib/foundation/fabro-redact/src/lib.rs @@ -111,6 +111,27 @@ mod tests { assert_eq!(result, "key=REDACTED"); } + #[test] + fn redact_string_keeps_assignment_with_low_entropy_value() { + // A pinned git SHA is pure hex, so the value alone can never exceed + // 4.0 bits of entropy. Only the merged NAME=value token crosses the + // 4.5-bit threshold, because the uppercase name widens the charset. + // Measuring the name together with the value redacts innocuous + // pins; the pair must survive. + let input = "ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf"; + assert_eq!(redact_string(input), input); + } + + #[test] + fn redact_string_keeps_assignment_key_for_high_entropy_value() { + // The value alone is above the entropy threshold, so it is + // redacted either way — but the name says which setting was + // redacted and must survive, as the gitleaks layer already + // does for `key=REDACTED`. + let result = redact_string("BUILD_STAMP=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p"); + assert_eq!(result, "BUILD_STAMP=REDACTED"); + } + #[test] fn redact_string_overlapping_detections_produce_single_redacted() { // A high-entropy string that also matches a gitleaks pattern From 3421c4f06fb77af09cc33b33bb57cbfbd226c752 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 6 Aug 2026 21:44:46 -0400 Subject: [PATCH 07/63] Keep the executable run spec out of reach of event redaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root-cause fixes for the sandbox failure where an inline Dockerfile came back from the store as `ARG REDACTED` and the Daytona snapshot build died on the unset variable. Entropy redaction measures values, not assignment pairs. The detector matched `NAME=value` as one token, so an uppercase name merged its charset into a pure-hex value (which alone can never exceed 4.0 bits) and pushed the pair over the 4.5-bit threshold — then replaced the whole pair, destroying the name. `find_entropy_regions` now strips an identifier-shaped `NAME=` prefix before measuring and redacts only the value, matching the gitleaks layer's `key=REDACTED` shape. Execution no longer reads redacted content. Every stored event passes through the redaction sink, and `load_from_store` rehydrated the worker's RunSpec from the projection folded from those events — so a redactor false positive silently rewrote the spec the sandbox builds from (and changed its snapshot identity). The creation path now writes the exact spec bytes to the content-addressed blob store and records `spec_blob` on run.created; `load_from_store` loads the spec from the blob, keeping the event stream authoritative for run identity, provenance, and event-recorded blob ids. Retry and fork carry the source run's `spec_blob` forward, so derived runs stop inheriting the redacted copy. Runs created before the blob existed fall back to the folded spec. The projection and every API surface keep serving the redacted fold; blobs were already stored unredacted (the workflow bundle carries the same bytes), so this adds no new exposure at rest. Co-Authored-By: Claude Fable 5 --- docs/public/api-reference/fabro-api.yaml | 2 + lib/apps/fabro-cli/src/commands/run/attach.rs | 1 + lib/apps/fabro-cli/tests/it/cmd/attach.rs | 1 + lib/apps/fabro-cli/tests/it/support/mod.rs | 1 + lib/apps/fabro-server/src/run_files.rs | 1 + .../fabro-server/src/server/handler/events.rs | 1 + .../fabro-server/src/server/handler/pair.rs | 1 + .../src/server/handler/sessions.rs | 1 + lib/apps/fabro-server/src/server/tests.rs | 8 ++ .../fabro-server/tests/it/api/run_files.rs | 1 + lib/components/fabro-dump/src/lib.rs | 1 + lib/components/fabro-store/src/run_state.rs | 4 + .../fabro-store/src/run_summary_store.rs | 1 + lib/components/fabro-store/src/slate/mod.rs | 1 + .../tests/serializable_projection.rs | 1 + .../fabro-workflow/src/billing_rollup.rs | 1 + .../fabro-workflow/src/event/convert.rs | 3 + .../fabro-workflow/src/event/events.rs | 2 + .../fabro-workflow/src/event/sink.rs | 1 + lib/components/fabro-workflow/src/git.rs | 1 + .../fabro-workflow/src/handler/agent.rs | 1 + .../fabro-workflow/src/handler/command.rs | 2 + .../fabro-workflow/src/handler/parallel.rs | 1 + .../fabro-workflow/src/handler/prompt.rs | 1 + .../fabro-workflow/src/lifecycle/git.rs | 1 + .../fabro-workflow/src/operations/archive.rs | 1 + .../fabro-workflow/src/operations/create.rs | 8 ++ .../fabro-workflow/src/operations/fork.rs | 4 + .../fabro-workflow/src/operations/retry.rs | 5 + .../fabro-workflow/src/operations/timeline.rs | 1 + .../src/pipeline/execute/tests.rs | 2 + .../fabro-workflow/src/pipeline/finalize.rs | 2 + .../fabro-workflow/src/pipeline/initialize.rs | 2 + .../fabro-workflow/src/pipeline/persist.rs | 109 +++++++++++++++--- .../src/pipeline/pull_request.rs | 9 ++ .../fabro-workflow/src/run_lookup.rs | 2 + .../fabro-workflow/src/run_metadata.rs | 1 + .../fabro-workflow/src/runtime_store.rs | 2 + .../fabro-workflow/src/stage_execution.rs | 1 + .../fabro-workflow/src/test_support.rs | 1 + .../tests/run_projection_round_trip.rs | 1 + lib/foundation/fabro-redact/src/entropy.rs | 33 +++++- lib/foundation/fabro-redact/src/jsonl.rs | 4 +- lib/foundation/fabro-test/src/lib.rs | 4 + lib/foundation/fabro-types/src/run.rs | 5 + .../fabro-types/src/run_event/run.rs | 5 + .../fabro-types/src/run_projection.rs | 3 + .../fabro-types/tests/run_event_serde.rs | 2 + .../fabro-types/tests/run_spec_methods.rs | 1 + .../fabro-types/tests/run_spec_serde.rs | 1 + 50 files changed, 229 insertions(+), 20 deletions(-) diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 261badbbb..1875a9662 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -11304,6 +11304,8 @@ components: type: ["string", "null"] definition_blob: type: ["string", "null"] + spec_blob: + type: ["string", "null"] git: oneOf: - $ref: "#/components/schemas/GitContext" diff --git a/lib/apps/fabro-cli/src/commands/run/attach.rs b/lib/apps/fabro-cli/src/commands/run/attach.rs index cd356ffa0..7c1cf93b7 100644 --- a/lib/apps/fabro-cli/src/commands/run/attach.rs +++ b/lib/apps/fabro-cli/src/commands/run/attach.rs @@ -849,6 +849,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/apps/fabro-cli/tests/it/cmd/attach.rs b/lib/apps/fabro-cli/tests/it/cmd/attach.rs index 813bf7424..cc00e14a2 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/attach.rs @@ -1024,6 +1024,7 @@ fn attach_json_errors_without_prompting_for_human_input() { } }, "source_directory": "[TEMP_DIR]", + "spec_blob": "[BLOB_ID]", "title": "Wait for approval", "web_url": "http://localhost:3000/runs/[ULID]", "workflow_slug": "human-gate", diff --git a/lib/apps/fabro-cli/tests/it/support/mod.rs b/lib/apps/fabro-cli/tests/it/support/mod.rs index 2f3557044..57e4a98e5 100644 --- a/lib/apps/fabro-cli/tests/it/support/mod.rs +++ b/lib/apps/fabro-cli/tests/it/support/mod.rs @@ -53,6 +53,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/apps/fabro-server/src/run_files.rs b/lib/apps/fabro-server/src/run_files.rs index 920460f79..2343933b6 100644 --- a/lib/apps/fabro-server/src/run_files.rs +++ b/lib/apps/fabro-server/src/run_files.rs @@ -2387,6 +2387,7 @@ index 1111111..2222222 160000 provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/apps/fabro-server/src/server/handler/events.rs b/lib/apps/fabro-server/src/server/handler/events.rs index 1bfb7f889..5fd655f1d 100644 --- a/lib/apps/fabro-server/src/server/handler/events.rs +++ b/lib/apps/fabro-server/src/server/handler/events.rs @@ -627,6 +627,7 @@ mod stage_events_tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/apps/fabro-server/src/server/handler/pair.rs b/lib/apps/fabro-server/src/server/handler/pair.rs index ec31d2e54..6e244bcac 100644 --- a/lib/apps/fabro-server/src/server/handler/pair.rs +++ b/lib/apps/fabro-server/src/server/handler/pair.rs @@ -1027,6 +1027,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index c2ee94b32..e9bacdfff 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -1923,6 +1923,7 @@ reasoning = false provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index a05443a00..4278eeec5 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -4650,6 +4650,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -4701,6 +4702,7 @@ async fn create_slack_notification_run( automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -5774,6 +5776,7 @@ async fn list_run_stages_distinguishes_visits() { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -5910,6 +5913,7 @@ async fn list_run_stages_exposes_execution_identity_for_resumed_stage() { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -7097,6 +7101,7 @@ async fn create_completed_run_ready_for_pull_request( provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; @@ -7113,6 +7118,7 @@ async fn create_completed_run_ready_for_pull_request( automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git, fork_source_ref: None, retried_from: None, @@ -14082,6 +14088,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -14831,6 +14838,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/apps/fabro-server/tests/it/api/run_files.rs b/lib/apps/fabro-server/tests/it/api/run_files.rs index 6d286cbab..307c4a574 100644 --- a/lib/apps/fabro-server/tests/it/api/run_files.rs +++ b/lib/apps/fabro-server/tests/it/api/run_files.rs @@ -68,6 +68,7 @@ async fn append_completed_run_with_final_patch( automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-dump/src/lib.rs b/lib/components/fabro-dump/src/lib.rs index 1b3a10a4c..39c73e1d8 100644 --- a/lib/components/fabro-dump/src/lib.rs +++ b/lib/components/fabro-dump/src/lib.rs @@ -501,6 +501,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, } } diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index 5e4ab5e37..dedcb0ef5 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -1046,6 +1046,7 @@ fn projection_from_created(event: &EventEnvelope) -> Result { provenance: props.provenance.clone(), manifest_blob: props.manifest_blob, definition_blob: None, + spec_blob: props.spec_blob, git: props.git.clone(), fork_source_ref: props.fork_source_ref.clone(), }; @@ -2292,6 +2293,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, } @@ -4098,6 +4100,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; @@ -4124,6 +4127,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; diff --git a/lib/components/fabro-store/src/run_summary_store.rs b/lib/components/fabro-store/src/run_summary_store.rs index 5db1a49b1..dcbec847e 100644 --- a/lib/components/fabro-store/src/run_summary_store.rs +++ b/lib/components/fabro-store/src/run_summary_store.rs @@ -601,6 +601,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/components/fabro-store/src/slate/mod.rs b/lib/components/fabro-store/src/slate/mod.rs index 5d9ae3bd9..04be6ba1f 100644 --- a/lib/components/fabro-store/src/slate/mod.rs +++ b/lib/components/fabro-store/src/slate/mod.rs @@ -602,6 +602,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: Some(fabro_types::GitContext { origin_url: "https://github.com/fabro-sh/fabro".to_string(), branch: "main".to_string(), diff --git a/lib/components/fabro-store/tests/serializable_projection.rs b/lib/components/fabro-store/tests/serializable_projection.rs index ef0ed067b..4e03e1782 100644 --- a/lib/components/fabro-store/tests/serializable_projection.rs +++ b/lib/components/fabro-store/tests/serializable_projection.rs @@ -25,6 +25,7 @@ fn sample_run_spec() -> RunSpec { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: Some(fabro_types::GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), diff --git a/lib/components/fabro-workflow/src/billing_rollup.rs b/lib/components/fabro-workflow/src/billing_rollup.rs index 986b541d4..ed26de465 100644 --- a/lib/components/fabro-workflow/src/billing_rollup.rs +++ b/lib/components/fabro-workflow/src/billing_rollup.rs @@ -322,6 +322,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, } diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index 6403d8933..ef255d5b8 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -36,6 +36,7 @@ fn event_body_from_event(event: &Event) -> EventBody { automation, provenance, manifest_blob, + spec_blob, git, fork_source_ref, retried_from, @@ -54,6 +55,7 @@ fn event_body_from_event(event: &Event) -> EventBody { automation: automation.clone(), provenance: provenance.clone(), manifest_blob: *manifest_blob, + spec_blob: *spec_blob, git: git.clone(), fork_source_ref: fork_source_ref.clone(), retried_from: *retried_from, @@ -2669,6 +2671,7 @@ mod tests { automation: Some(automation.clone()), provenance, manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index f36d01a16..f465a5cfe 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -41,6 +41,8 @@ pub enum Event { #[serde(default, skip_serializing_if = "Option::is_none")] manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + spec_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] fork_source_ref: Option, diff --git a/lib/components/fabro-workflow/src/event/sink.rs b/lib/components/fabro-workflow/src/event/sink.rs index f6abb1724..150c69f72 100644 --- a/lib/components/fabro-workflow/src/event/sink.rs +++ b/lib/components/fabro-workflow/src/event/sink.rs @@ -290,6 +290,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/git.rs b/lib/components/fabro-workflow/src/git.rs index de144b6a1..e7ca4feda 100644 --- a/lib/components/fabro-workflow/src/git.rs +++ b/lib/components/fabro-workflow/src/git.rs @@ -364,6 +364,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/handler/agent.rs b/lib/components/fabro-workflow/src/handler/agent.rs index 48b234a8b..99d20431b 100644 --- a/lib/components/fabro-workflow/src/handler/agent.rs +++ b/lib/components/fabro-workflow/src/handler/agent.rs @@ -501,6 +501,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/handler/command.rs b/lib/components/fabro-workflow/src/handler/command.rs index 5dfc41e46..60f2b9a51 100644 --- a/lib/components/fabro-workflow/src/handler/command.rs +++ b/lib/components/fabro-workflow/src/handler/command.rs @@ -374,6 +374,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, @@ -471,6 +472,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/handler/parallel.rs b/lib/components/fabro-workflow/src/handler/parallel.rs index 62fc59630..44323be72 100644 --- a/lib/components/fabro-workflow/src/handler/parallel.rs +++ b/lib/components/fabro-workflow/src/handler/parallel.rs @@ -956,6 +956,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/handler/prompt.rs b/lib/components/fabro-workflow/src/handler/prompt.rs index d586ca6b7..630332878 100644 --- a/lib/components/fabro-workflow/src/handler/prompt.rs +++ b/lib/components/fabro-workflow/src/handler/prompt.rs @@ -279,6 +279,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/lifecycle/git.rs b/lib/components/fabro-workflow/src/lifecycle/git.rs index fce72da6e..6fa277b31 100644 --- a/lib/components/fabro-workflow/src/lifecycle/git.rs +++ b/lib/components/fabro-workflow/src/lifecycle/git.rs @@ -750,6 +750,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/operations/archive.rs b/lib/components/fabro-workflow/src/operations/archive.rs index c44ff0006..92d9124cf 100644 --- a/lib/components/fabro-workflow/src/operations/archive.rs +++ b/lib/components/fabro-workflow/src/operations/archive.rs @@ -227,6 +227,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/operations/create.rs b/lib/components/fabro-workflow/src/operations/create.rs index 59f424e2d..3f23acd5b 100644 --- a/lib/components/fabro-workflow/src/operations/create.rs +++ b/lib/components/fabro-workflow/src/operations/create.rs @@ -470,6 +470,7 @@ pub async fn persist_create_run( provenance, manifest_blob: None, definition_blob: None, + spec_blob: None, git, fork_source_ref, }; @@ -528,6 +529,12 @@ async fn persist_created_run( } None => None, }; + // The spec on the run.created event is subject to secret redaction in + // stored copies; the blob keeps the exact bytes execution needs. + let spec_blob = { + let bytes = serde_json::to_vec(record).map_err(|err| Error::engine(err.to_string()))?; + Some(run_store.write_blob(&bytes).await.map_err(store_error)?) + }; let title = explicit_title.unwrap_or_else(|| fabro_types::infer_run_title(record.graph.goal())); let stored = to_run_event_at( @@ -554,6 +561,7 @@ async fn persist_created_run( automation: record.automation.clone(), provenance: record.provenance.clone(), manifest_blob, + spec_blob, git: record.git.clone(), fork_source_ref: record.fork_source_ref.clone(), retried_from: None, diff --git a/lib/components/fabro-workflow/src/operations/fork.rs b/lib/components/fabro-workflow/src/operations/fork.rs index 7e5e65516..3f728211d 100644 --- a/lib/components/fabro-workflow/src/operations/fork.rs +++ b/lib/components/fabro-workflow/src/operations/fork.rs @@ -162,6 +162,9 @@ async fn persist_forked_run( automation: spec.automation.clone(), provenance: spec.provenance.clone(), manifest_blob: spec.manifest_blob, + // Content-addressed, so the forked run reads the source run's + // unredacted spec bytes through the same id. + spec_blob: spec.spec_blob, git: spec.git.clone(), fork_source_ref: spec.fork_source_ref.clone(), retried_from: None, @@ -381,6 +384,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: Some(fabro_types::GitContext { origin_url: "https://github.com/example/repo.git".to_string(), branch: "main".to_string(), diff --git a/lib/components/fabro-workflow/src/operations/retry.rs b/lib/components/fabro-workflow/src/operations/retry.rs index 27a9df68a..49b5861f4 100644 --- a/lib/components/fabro-workflow/src/operations/retry.rs +++ b/lib/components/fabro-workflow/src/operations/retry.rs @@ -54,6 +54,7 @@ pub async fn retry_run( provenance: _, manifest_blob, definition_blob, + spec_blob, git, fork_source_ref, } = source.spec; @@ -78,6 +79,9 @@ pub async fn retry_run( automation, provenance: input.provenance.clone(), manifest_blob, + // Blobs are content-addressed, so the retried run reads the source + // run's unredacted spec bytes through the same id. + spec_blob, git, fork_source_ref, retried_from: Some(source_run_id), @@ -185,6 +189,7 @@ mod tests { automation: None, provenance: provenance("source-user"), manifest_blob, + spec_blob: None, git: Some(git_context()), fork_source_ref, retried_from: None, diff --git a/lib/components/fabro-workflow/src/operations/timeline.rs b/lib/components/fabro-workflow/src/operations/timeline.rs index b96d10aa4..83319745e 100644 --- a/lib/components/fabro-workflow/src/operations/timeline.rs +++ b/lib/components/fabro-workflow/src/operations/timeline.rs @@ -252,6 +252,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs index 118767be8..e69d19f29 100644 --- a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs @@ -173,6 +173,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }, ) @@ -218,6 +219,7 @@ async fn seed_created_and_starting( automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: run_options.pre_run_git.clone(), fork_source_ref: run_options.fork_source_ref.clone(), retried_from: None, diff --git a/lib/components/fabro-workflow/src/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index 8c497175d..74b1e9579 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -788,6 +788,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -906,6 +907,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 5a7dd6069..49c32de19 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -870,6 +870,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref, }, ) @@ -1053,6 +1054,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: run_options.fork_source_ref.clone(), retried_from: None, diff --git a/lib/components/fabro-workflow/src/pipeline/persist.rs b/lib/components/fabro-workflow/src/pipeline/persist.rs index 846f9b853..8c224c700 100644 --- a/lib/components/fabro-workflow/src/pipeline/persist.rs +++ b/lib/components/fabro-workflow/src/pipeline/persist.rs @@ -2,6 +2,7 @@ use std::path::Path; use super::types::{PersistOptions, Persisted, Validated}; use crate::error::Error; +use crate::records::RunSpec; use crate::runtime_store::RunStoreHandle; /// PERSIST phase: create the run directory and return durable metadata for @@ -37,7 +38,7 @@ pub(crate) async fn load_from_store( .state() .await .map_err(|err| Error::engine(err.to_string()))?; - let run_spec = state.spec; + let run_spec = executable_run_spec(run_store, state.spec).await?; let graph = run_spec.graph.clone(); let source = run_spec.graph_source.clone().unwrap_or_default(); @@ -50,6 +51,40 @@ pub(crate) async fn load_from_store( )) } +/// Replace the event-folded spec content with the exact bytes from the spec +/// blob. Stored events pass through secret redaction, so the folded spec is +/// display data; the blob written at creation is what execution must see. +/// Runs created before the blob existed fall back to the folded spec. +async fn executable_run_spec( + run_store: &RunStoreHandle, + folded: RunSpec, +) -> Result { + let Some(blob_id) = folded.spec_blob else { + return Ok(folded); + }; + let bytes = run_store + .read_blob(&blob_id) + .await + .map_err(|err| Error::engine(err.to_string()))? + .ok_or_else(|| { + Error::engine(format!( + "run spec blob is missing from the run store: {blob_id}" + )) + })?; + let mut spec: RunSpec = + serde_json::from_slice(&bytes).map_err(|err| Error::Parse(err.to_string()))?; + // The event stream stays authoritative for run identity, for provenance + // (a retry rewrites it), for blob ids recorded on events after the spec + // blob was written, and for a graph source the blob does not carry. + spec.run_id = folded.run_id; + spec.provenance = folded.provenance; + spec.manifest_blob = folded.manifest_blob; + spec.definition_blob = folded.definition_blob; + spec.spec_blob = folded.spec_blob; + spec.graph_source = spec.graph_source.or(folded.graph_source); + Ok(spec) +} + #[cfg(test)] #[expect(clippy::disallowed_methods, reason = "tests stage pipeline fixtures")] mod tests { @@ -150,30 +185,52 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, } } async fn seeded_store(record: &RunSpec, source: Option<&str>) -> RunDatabase { + seeded_store_with(record, source, true).await + } + + async fn seeded_store_with( + record: &RunSpec, + source: Option<&str>, + write_spec_blob: bool, + ) -> RunDatabase { let store = memory_store(); let run_store = store.create_run(&record.run_id).await.unwrap(); + // Mirror the production producer: the unredacted spec rides a blob + // and the redacted event carries its id. + let spec_blob = if write_spec_blob { + Some( + run_store + .write_blob(&serde_json::to_vec(record).unwrap()) + .await + .unwrap(), + ) + } else { + None + }; append_event(&run_store, &record.run_id, &Event::RunCreated { - run_id: record.run_id, - title: None, - settings: serde_json::to_value(&record.settings).unwrap(), - graph: serde_json::to_value(&record.graph).unwrap(), - workflow_source: source.map(ToOwned::to_owned), - labels: record.labels.clone().into_iter().collect(), + run_id: record.run_id, + title: None, + settings: serde_json::to_value(&record.settings).unwrap(), + graph: serde_json::to_value(&record.graph).unwrap(), + workflow_source: source.map(ToOwned::to_owned), + labels: record.labels.clone().into_iter().collect(), source_directory: record.source_directory.clone(), - workflow_slug: record.workflow_slug.clone(), - automation: record.automation.clone(), - provenance: record.provenance.clone(), - manifest_blob: None, - git: record.git.clone(), - fork_source_ref: record.fork_source_ref.clone(), - retried_from: None, - parent_id: None, - web_url: None, + workflow_slug: record.workflow_slug.clone(), + automation: record.automation.clone(), + provenance: record.provenance.clone(), + manifest_blob: None, + spec_blob, + git: record.git.clone(), + fork_source_ref: record.fork_source_ref.clone(), + retried_from: None, + parent_id: None, + web_url: None, }) .await .unwrap(); @@ -316,6 +373,26 @@ mod tests { ); } + #[tokio::test] + async fn load_from_store_falls_back_to_folded_spec_without_spec_blob() { + // Runs created before the spec blob existed carry no spec_blob on + // run.created; the folded spec is their only copy. + let temp = tempfile::tempdir().unwrap(); + let run_dir = temp.path().join("run"); + std::fs::create_dir_all(&run_dir).unwrap(); + let (graph, source) = graph_and_source(); + let mut record = sample_record(different_graph()); + record.graph = graph; + + let run_store = seeded_store_with(&record, Some(&source), false).await; + let loaded = load_from_store(&run_store.clone().into(), &run_dir) + .await + .unwrap(); + + assert_eq!(loaded.run_spec().settings, record.settings); + assert_eq!(loaded.run_spec().spec_blob, None); + } + #[test] fn persist_returns_error_on_io_failure() { let temp = tempfile::tempdir().unwrap(); diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index 1ab76a24b..f3f7e4a78 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -830,6 +830,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, @@ -1112,6 +1113,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1126,6 +1128,7 @@ mod tests { automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git: run_spec.git.clone(), fork_source_ref: None, retried_from: None, @@ -1179,6 +1182,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1193,6 +1197,7 @@ mod tests { automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git: run_spec.git.clone(), fork_source_ref: None, retried_from: None, @@ -1596,6 +1601,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1610,6 +1616,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -1813,6 +1820,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1827,6 +1835,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/run_lookup.rs b/lib/components/fabro-workflow/src/run_lookup.rs index 99e9825fe..e70caf216 100644 --- a/lib/components/fabro-workflow/src/run_lookup.rs +++ b/lib/components/fabro-workflow/src/run_lookup.rs @@ -487,6 +487,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, } } @@ -512,6 +513,7 @@ mod tests { automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git: run_spec.git.clone(), fork_source_ref: run_spec.fork_source_ref.clone(), retried_from: None, diff --git a/lib/components/fabro-workflow/src/run_metadata.rs b/lib/components/fabro-workflow/src/run_metadata.rs index 74be989df..754bb5dfa 100644 --- a/lib/components/fabro-workflow/src/run_metadata.rs +++ b/lib/components/fabro-workflow/src/run_metadata.rs @@ -641,6 +641,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }, chrono::Utc::now(), diff --git a/lib/components/fabro-workflow/src/runtime_store.rs b/lib/components/fabro-workflow/src/runtime_store.rs index c376c47e7..8f43ef647 100644 --- a/lib/components/fabro-workflow/src/runtime_store.rs +++ b/lib/components/fabro-workflow/src/runtime_store.rs @@ -151,6 +151,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, } } @@ -169,6 +170,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/stage_execution.rs b/lib/components/fabro-workflow/src/stage_execution.rs index c05a3547e..ec755127f 100644 --- a/lib/components/fabro-workflow/src/stage_execution.rs +++ b/lib/components/fabro-workflow/src/stage_execution.rs @@ -209,6 +209,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/components/fabro-workflow/src/test_support.rs b/lib/components/fabro-workflow/src/test_support.rs index 19390762d..c7407b7b9 100644 --- a/lib/components/fabro-workflow/src/test_support.rs +++ b/lib/components/fabro-workflow/src/test_support.rs @@ -204,6 +204,7 @@ async fn initialized( }, }, manifest_blob: None, + spec_blob: None, git: run_options.pre_run_git.clone(), fork_source_ref: run_options.fork_source_ref.clone(), retried_from: None, diff --git a/lib/foundation/fabro-api/tests/run_projection_round_trip.rs b/lib/foundation/fabro-api/tests/run_projection_round_trip.rs index 77d28178b..cbe1c6239 100644 --- a/lib/foundation/fabro-api/tests/run_projection_round_trip.rs +++ b/lib/foundation/fabro-api/tests/run_projection_round_trip.rs @@ -140,6 +140,7 @@ fn run_spec_json() -> serde_json::Value { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }) diff --git a/lib/foundation/fabro-redact/src/entropy.rs b/lib/foundation/fabro-redact/src/entropy.rs index dc744cf39..884aa3713 100644 --- a/lib/foundation/fabro-redact/src/entropy.rs +++ b/lib/foundation/fabro-redact/src/entropy.rs @@ -36,6 +36,12 @@ pub(super) fn shannon_entropy(s: &str) -> f64 { /// Returns regions where tokens match `[A-Za-z0-9+_=-]{10,}` and have /// Shannon entropy above the threshold (4.5 bits). Protects against /// consuming characters from JSON escape sequences. +/// +/// An assignment token (`NAME=value`) is measured and redacted by its +/// value alone. Measuring the pair merges the name's charset into the +/// value's and pushes innocuous values (a pure-hex git SHA can never +/// exceed 4.0 bits by itself) over the threshold, and redacting the +/// pair destroys the name that says what was redacted. pub(super) fn find_entropy_regions(s: &str) -> Vec { let mut regions = Vec::new(); for m in SECRET_PATTERN.find_iter(s) { @@ -58,6 +64,10 @@ pub(super) fn find_entropy_regions(s: &str) -> Vec { } } + if let Some(offset) = assignment_value_offset(&s[start..end]) { + start += offset; + } + if shannon_entropy(&s[start..end]) > ENTROPY_THRESHOLD { regions.push(Region { start, end }); } @@ -65,6 +75,24 @@ pub(super) fn find_entropy_regions(s: &str) -> Vec { regions } +/// For an assignment token (`NAME=value` with an identifier-shaped name), +/// return the byte offset where the value begins. Entropy above the 4.5-bit +/// threshold needs at least 23 distinct characters, so a value too short to +/// qualify simply measures under the threshold; no length guard is needed. +fn assignment_value_offset(token: &str) -> Option { + let eq = token.find('=')?; + let name = &token[..eq]; + let mut chars = name.chars(); + let first = chars.next()?; + if !(first.is_ascii_alphabetic() || first == '_') { + return None; + } + if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') { + return None; + } + Some(eq + 1) +} + #[cfg(test)] mod tests { use super::*; @@ -98,11 +126,12 @@ mod tests { #[test] fn regions_finds_high_entropy_token() { - // `=` is in the regex pattern, so "key=xK9..." matches as one token + // "key=xK9..." matches as one token, but only the value is + // measured and flagged; the name survives redaction. let input = "key=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p"; let regions = find_entropy_regions(input); assert_eq!(regions.len(), 1); - assert_eq!(regions[0].start, 0); + assert_eq!(regions[0].start, "key=".len()); assert_eq!(regions[0].end, input.len()); } diff --git a/lib/foundation/fabro-redact/src/jsonl.rs b/lib/foundation/fabro-redact/src/jsonl.rs index e466f7002..f0f50f6fa 100644 --- a/lib/foundation/fabro-redact/src/jsonl.rs +++ b/lib/foundation/fabro-redact/src/jsonl.rs @@ -209,7 +209,7 @@ mod tests { let redacted = redact_json_value(input); assert_eq!(redacted["name"], "fabro-01KQR3V9D4VPFFWMNTVH09J48G"); - assert_eq!(redacted["content"], "REDACTED"); + assert_eq!(redacted["content"], "token=REDACTED"); } #[test] @@ -262,7 +262,7 @@ mod tests { let redacted = redact_json_value(input); - assert_eq!(redacted["content"], "REDACTED"); + assert_eq!(redacted["content"], "key=REDACTED"); assert_eq!(redacted["session_id"], HIGH_ENTROPY_SECRET); } diff --git a/lib/foundation/fabro-test/src/lib.rs b/lib/foundation/fabro-test/src/lib.rs index 74a5f6727..eec273d32 100644 --- a/lib/foundation/fabro-test/src/lib.rs +++ b/lib/foundation/fabro-test/src/lib.rs @@ -1963,6 +1963,10 @@ pub fn json_snapshot_filters(mut filters: Vec<(String, String)>) -> Vec<(String, r#""definition_blob":\s*"[0-9a-f]{64}""#.to_string(), r#""definition_blob": "[BLOB_ID]""#.to_string(), )); + filters.push(( + r#""spec_blob":\s*"[0-9a-f]{64}""#.to_string(), + r#""spec_blob": "[BLOB_ID]""#.to_string(), + )); filters.push(( r#""run_dir":\s*"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]""#.to_string(), r#""run_dir": "[RUN_DIR]""#.to_string(), diff --git a/lib/foundation/fabro-types/src/run.rs b/lib/foundation/fabro-types/src/run.rs index cf0fbe1ff..0c79bb23d 100644 --- a/lib/foundation/fabro-types/src/run.rs +++ b/lib/foundation/fabro-types/src/run.rs @@ -76,6 +76,11 @@ pub struct RunSpec { pub manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub definition_blob: Option, + /// Unredacted copy of this spec in the blob store. Stored events pass + /// through secret redaction, so the spec folded from them is display + /// data; execution must load the spec from this blob. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub spec_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/foundation/fabro-types/src/run_event/run.rs b/lib/foundation/fabro-types/src/run_event/run.rs index d070d8aef..b8c7fa34a 100644 --- a/lib/foundation/fabro-types/src/run_event/run.rs +++ b/lib/foundation/fabro-types/src/run_event/run.rs @@ -28,6 +28,11 @@ pub struct RunCreatedProps { pub provenance: RunProvenance, #[serde(default, skip_serializing_if = "Option::is_none")] pub manifest_blob: Option, + /// Unredacted copy of the run spec in the blob store. The settings and + /// graph on this event are redacted at the sink; execution loads the + /// spec from this blob instead. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub spec_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index 3a6be70d4..3616aa33c 100644 --- a/lib/foundation/fabro-types/src/run_projection.rs +++ b/lib/foundation/fabro-types/src/run_projection.rs @@ -1087,6 +1087,7 @@ mod title_tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; @@ -1161,6 +1162,7 @@ mod iter_stages_tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, @@ -1365,6 +1367,7 @@ mod live_timing_tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/foundation/fabro-types/tests/run_event_serde.rs b/lib/foundation/fabro-types/tests/run_event_serde.rs index 633abab04..f287a6acc 100644 --- a/lib/foundation/fabro-types/tests/run_event_serde.rs +++ b/lib/foundation/fabro-types/tests/run_event_serde.rs @@ -32,6 +32,7 @@ fn run_created_props_round_trip_templated_settings() { }), provenance: test_run_provenance(), manifest_blob: None, + spec_blob: None, git: Some(GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), @@ -93,6 +94,7 @@ fn run_created_props_omits_web_url_when_absent() { automation: None, provenance: test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/foundation/fabro-types/tests/run_spec_methods.rs b/lib/foundation/fabro-types/tests/run_spec_methods.rs index f6f76fecf..50dff20b0 100644 --- a/lib/foundation/fabro-types/tests/run_spec_methods.rs +++ b/lib/foundation/fabro-types/tests/run_spec_methods.rs @@ -31,6 +31,7 @@ fn sample_run_spec() -> RunSpec { provenance: test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: Some(GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), diff --git a/lib/foundation/fabro-types/tests/run_spec_serde.rs b/lib/foundation/fabro-types/tests/run_spec_serde.rs index 97529bc93..ec7a7ef40 100644 --- a/lib/foundation/fabro-types/tests/run_spec_serde.rs +++ b/lib/foundation/fabro-types/tests/run_spec_serde.rs @@ -31,6 +31,7 @@ fn run_spec_round_trips_templated_settings() { provenance: test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: Some(GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), From a045ea4cb02635b87184cac20819f49c39b75fd5 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 12:02:10 -0400 Subject: [PATCH 08/63] Remove the unused list_blobs API from fabro-store RunDatabase::list_blobs and BlobStore::list have had no production callers since the store-dump export switched from enumerating the whole blob namespace to hydrating only referenced blob refs. The semantics have also gone stale: blobs now live in one content-addressed store shared across run handles, so list_blobs on a per-run handle returned every blob from every run, inviting exactly the per-run-enumeration misuse the old dump loop would be today. Co-Authored-By: Claude Fable 5 --- .../fabro-store/src/slate/blob_store.rs | 49 +------------------ lib/components/fabro-store/src/slate/mod.rs | 2 - .../fabro-store/src/slate/run_store.rs | 21 -------- 3 files changed, 1 insertion(+), 71 deletions(-) diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs index 68c6a9a54..cb168cd2b 100644 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ b/lib/components/fabro-store/src/slate/blob_store.rs @@ -2,11 +2,9 @@ use std::sync::Arc; use bytes::Bytes; use fabro_types::BlobHash; -use futures::StreamExt; -use tracing::warn; +use crate::Result; use crate::record::{RawBytesCodec, Record, Repository}; -use crate::{Error, Result}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Blob(pub Bytes); @@ -65,22 +63,6 @@ impl BlobStore { pub async fn exists(&self, id: &BlobHash) -> Result { self.repo.exists(id).await } - - pub(crate) async fn list(&self) -> Result> { - let mut stream = self.repo.scan_ids_stream(); - let mut ids = Vec::new(); - while let Some(result) = stream.next().await { - match result { - Ok(id) => ids.push(id), - Err(Error::KeyParse(err)) => { - warn!(error = %err, "Skipping malformed blob key during listing"); - } - Err(err) => return Err(err), - } - } - ids.sort(); - Ok(ids) - } } #[cfg(test)] @@ -139,35 +121,6 @@ mod tests { assert_eq!(store.read(&id).await.unwrap(), Some(Bytes::new())); } - #[tokio::test] - async fn list_returns_sorted_ids_and_handles_empty_store() { - let store = store().await; - assert!(store.list().await.unwrap().is_empty()); - - let first_id = store.write(br#"{"z":1}"#).await.unwrap(); - let second_id = store.write(br#"{"a":1}"#).await.unwrap(); - let mut expected = vec![first_id, second_id]; - expected.sort(); - - assert_eq!(store.list().await.unwrap(), expected); - } - - #[tokio::test] - async fn list_skips_malformed_blob_ids() { - let (raw_db, store) = raw_store("blob-store-list-tests").await; - let id = store.write(b"valid").await.unwrap(); - - raw_db - .put( - SlateKey::new("blobs").with("sha256").with("not-a-blob-id"), - b"malformed", - ) - .await - .unwrap(); - - assert_eq!(store.list().await.unwrap(), vec![id]); - } - #[tokio::test] async fn raw_db_reads_exact_blob_bytes() { let (raw_db, store) = raw_store("blob-store-tests").await; diff --git a/lib/components/fabro-store/src/slate/mod.rs b/lib/components/fabro-store/src/slate/mod.rs index 8f9c16ac7..21e410c71 100644 --- a/lib/components/fabro-store/src/slate/mod.rs +++ b/lib/components/fabro-store/src/slate/mod.rs @@ -862,8 +862,6 @@ mod tests { reader.read_blob(&blob_id).await.unwrap().as_deref(), Some(blob.as_slice()) ); - assert_eq!(reader.list_blobs().await.unwrap(), vec![blob_id]); - let err = reader.write_blob(b"blocked").await.unwrap_err(); assert!(matches!(err, Error::ReadOnly)); diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index 646b636ab..9148ede95 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -565,10 +565,6 @@ impl RunDatabase { self.inner.blob_store.read(id).await } - pub async fn list_blobs(&self) -> Result> { - self.inner.blob_store.list().await - } - pub async fn state(&self) -> Result { Ok(Arc::unwrap_or_clone(self.projected_state().await?)) } @@ -896,23 +892,6 @@ mod tests { use crate::{Database, Error, EventPayload, keys}; - #[tokio::test] - async fn list_blobs_reads_global_cas_namespace() { - let object_store = Arc::new(InMemory::new()); - let store = Database::new(object_store, "", Duration::from_millis(1), None); - let run_id = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); - let run = store.create_run(&run_id).await.unwrap(); - let first_blob = br#"{"a":1}"#; - let second_blob = br#"{"b":2}"#; - - let first_id = run.write_blob(first_blob).await.unwrap(); - let second_id = run.write_blob(second_blob).await.unwrap(); - let mut blob_ids = run.list_blobs().await.unwrap(); - blob_ids.sort(); - - assert_eq!(blob_ids, vec![first_id, second_id]); - } - fn stage_prompt_payload(run_id: &RunId, idx: u32, node_id: Option<&str>) -> EventPayload { stage_prompt_payload_for_stage(run_id, idx, node_id, None) } From bf4265e1b81a58bce1aff334092b902154a0bba1 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Fri, 14 Aug 2026 11:34:12 -0400 Subject: [PATCH 09/63] Unify blob hash vocabulary --- docs/internal/events.md | 2 +- docs/public/agents/outputs.mdx | 6 +- docs/public/api-reference/fabro-api.yaml | 24 +++--- docs/public/execution/context.mdx | 4 +- lib/apps/fabro-cli/src/commands/dump.rs | 4 +- lib/apps/fabro-cli/src/commands/run/output.rs | 6 +- lib/apps/fabro-cli/src/commands/run/runner.rs | 4 +- lib/apps/fabro-cli/tests/it/cmd/attach.rs | 8 +- .../fabro-server/src/principal_middleware.rs | 8 +- lib/apps/fabro-server/src/server.rs | 8 +- .../src/server/handler/artifacts.rs | 10 +-- .../fabro-server/src/server/handler/mod.rs | 2 +- lib/apps/fabro-server/src/server/tests.rs | 12 +-- lib/components/fabro-dump/src/lib.rs | 51 ++++++------ lib/components/fabro-store/src/slate/mod.rs | 8 +- .../fabro-workflow-version/src/store.rs | 11 ++- lib/components/fabro-workflow/src/artifact.rs | 80 +++++++++---------- .../fabro-workflow/src/command_log.rs | 10 +-- .../fabro-workflow/src/handler/command.rs | 6 +- .../fabro-workflow/src/handler/parallel.rs | 4 +- .../fabro-workflow/src/operations/start.rs | 10 +-- .../src/pipeline/execute/tests.rs | 4 +- .../fabro-workflow/src/runtime_store.rs | 4 +- .../tests/it/daytona_integration.rs | 4 +- .../fabro-workflow/tests/it/integration.rs | 12 +-- lib/foundation/fabro-client/src/client.rs | 12 ++- lib/foundation/fabro-test/src/lib.rs | 8 +- .../fabro-types/src/workflow_version_id.rs | 8 +- .../src/api/run-internals-api.ts | 46 +++++------ .../src/models/write-blob-response.ts | 6 +- 30 files changed, 194 insertions(+), 188 deletions(-) diff --git a/docs/internal/events.md b/docs/internal/events.md index 2920159e0..dfc7403b4 100644 --- a/docs/internal/events.md +++ b/docs/internal/events.md @@ -77,7 +77,7 @@ Emitted when the run record is created. | `source_directory` | string? | Submitter-side source directory | | `workflow_slug` | string? | Workflow slug | | `provenance` | object | Actor and request provenance | -| `manifest_blob` | string? | Blob id for the submitted manifest | +| `manifest_blob` | string? | Blob hash for the submitted manifest | | `git` | object? | Git provenance observed before the run: normalized `origin_url`, `branch`, optional `sha`, and `dirty` status | | `fork_source_ref` | object? | Source run/checkpoint reference when this run was forked | | `in_place` | boolean | Whether the run was created with `--in-place` (no git checkpoints) | diff --git a/docs/public/agents/outputs.mdx b/docs/public/agents/outputs.mdx index b36259b25..004c4f0e8 100644 --- a/docs/public/agents/outputs.mdx +++ b/docs/public/agents/outputs.mdx @@ -219,10 +219,10 @@ When Fabro builds a [preamble](/execution/context#preamble-construction) for a d - **plan**: success - Model: claude-sonnet-4-5, 12.4k tokens in / 3.2k out - Files: src/main.rs, tests/api_test.rs - - Response: See: /path/to/runtime/blobs/.json + - Response: See: /path/to/runtime/blobs/.json - **test**: success - Script: `cargo test 2>&1 || true` - - Stdout: See: /path/to/runtime/blobs/.json + - Stdout: See: /path/to/runtime/blobs/.json ``` This keeps preambles concise while still giving agents a path to read the full output if needed. @@ -237,7 +237,7 @@ Captured stage artifacts such as screenshots, videos, reports, and traces still For remote sandboxes (Docker, Daytona), execution-time file access happens inside the sandbox filesystem. -- Blob refs are materialized into `{working_directory}/.fabro/blobs/{blob_id}.json` +- Blob refs are materialized into `{working_directory}/.fabro/blobs/{blob_hash}.json` - Explicit non-blob `file://` refs keep the existing copy-on-demand behavior and are copied into `{working_directory}/.fabro/artifacts/{filename}` when needed In both cases, downstream handlers and agents continue to consume ordinary `file://` pointers during execution. diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 274282e72..b788fd713 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -3092,7 +3092,7 @@ paths: operationId: writeRunBlob tags: [Run Internals] summary: Write Run Blob - description: Writes an opaque binary blob and returns its content-addressed blob identifier. + description: Writes an opaque binary blob and returns its content-addressed blob hash. parameters: - $ref: "#/components/parameters/RunId" requestBody: @@ -3137,15 +3137,15 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" - /api/v1/runs/{id}/blobs/{blobId}: + /api/v1/runs/{id}/blobs/{blobHash}: get: operationId: readRunBlob tags: [Run Internals] summary: Read Run Blob - description: Reads a previously stored blob by identifier. + description: Reads a previously stored blob by hash. parameters: - $ref: "#/components/parameters/RunId" - - $ref: "#/components/parameters/BlobId" + - $ref: "#/components/parameters/BlobHash" responses: "200": description: Blob contents @@ -5974,11 +5974,11 @@ components: default: 65536 example: 65536 - BlobId: - name: blobId + BlobHash: + name: blobHash in: path required: true - description: Content-addressed blob identifier. + description: Content-addressed blob hash. schema: type: string pattern: '^[0-9a-f]{64}$' @@ -10284,15 +10284,15 @@ components: example: 42 WriteBlobResponse: - description: Content-addressed identifier for a stored blob. + description: Content-addressed hash of a stored blob. type: object required: - - id + - hash properties: - id: + hash: type: string - description: Blob identifier. - example: 550e8400-e29b-41d4-a716-446655440000 + description: Content-addressed hash of the stored blob. + example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 CommandTermination: description: Terminal state for a command execution. diff --git a/docs/public/execution/context.mdx b/docs/public/execution/context.mdx index 2ef5b0bbb..19f9c1d2c 100644 --- a/docs/public/execution/context.mdx +++ b/docs/public/execution/context.mdx @@ -243,8 +243,8 @@ Checkpoints and checkpoint-completed events persist these `blob://` refs, not ho Before Fabro builds a preamble or starts the next stage, it resolves any blob refs into execution-local files so handlers and agents still see normal `file://` references: -- Local execution materializes blobs under `{run_dir}/runtime/blobs/{blob_id}.json` -- Remote sandboxes materialize blobs under `{working_directory}/.fabro/blobs/{blob_id}.json` +- Local execution materializes blobs under `{run_dir}/runtime/blobs/{blob_hash}.json` +- Remote sandboxes materialize blobs under `{working_directory}/.fabro/blobs/{blob_hash}.json` These materialized `file://` paths are runtime-only. They are not written back into durable context snapshots. diff --git a/lib/apps/fabro-cli/src/commands/dump.rs b/lib/apps/fabro-cli/src/commands/dump.rs index b3938e12a..9c0454da8 100644 --- a/lib/apps/fabro-cli/src/commands/dump.rs +++ b/lib/apps/fabro-cli/src/commands/dump.rs @@ -86,8 +86,8 @@ async fn write_run_dump( dump.add_file_bytes("run.log", log); } - dump.hydrate_referenced_blobs_with_reader(|blob_id| { - Box::pin(async move { client.read_run_blob(run_id, &blob_id).await }) + dump.hydrate_referenced_blobs_with_reader(|blob_hash| { + Box::pin(async move { client.read_run_blob(run_id, &blob_hash).await }) }) .await?; diff --git a/lib/apps/fabro-cli/src/commands/run/output.rs b/lib/apps/fabro-cli/src/commands/run/output.rs index 0e5ba2d42..14d0e7c6f 100644 --- a/lib/apps/fabro-cli/src/commands/run/output.rs +++ b/lib/apps/fabro-cli/src/commands/run/output.rs @@ -325,11 +325,11 @@ async fn resolve_response_string( run_id: &RunId, response: &str, ) -> Result> { - let Some(blob_id) = blob_id_from_response(response) else { + let Some(blob_hash) = blob_hash_from_response(response) else { return Ok(Some(response.to_string())); }; - let Some(bytes) = client.read_run_blob(run_id, &blob_id).await? else { + let Some(bytes) = client.read_run_blob(run_id, &blob_hash).await? else { return Ok(None); }; let value: serde_json::Value = @@ -341,7 +341,7 @@ async fn resolve_response_string( })) } -fn blob_id_from_response(response: &str) -> Option { +fn blob_hash_from_response(response: &str) -> Option { parse_blob_ref(response) } diff --git a/lib/apps/fabro-cli/src/commands/run/runner.rs b/lib/apps/fabro-cli/src/commands/run/runner.rs index 446888e7e..72044458d 100644 --- a/lib/apps/fabro-cli/src/commands/run/runner.rs +++ b/lib/apps/fabro-cli/src/commands/run/runner.rs @@ -1022,8 +1022,8 @@ impl RunStoreBackend for HttpRunStore { self.with_retries("read run blob", || { let client = self.client.clone_for_reuse(); let run_id = self.run_id; - let blob_id = *id; - async move { client.read_run_blob(&run_id, &blob_id).await } + let blob_hash = *id; + async move { client.read_run_blob(&run_id, &blob_hash).await } }) .await } diff --git a/lib/apps/fabro-cli/tests/it/cmd/attach.rs b/lib/apps/fabro-cli/tests/it/cmd/attach.rs index 813bf7424..c2434f79d 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/attach.rs @@ -70,13 +70,13 @@ fn normalize_attach_json_progress_event(mut event: Value) -> Value { if properties.contains_key("manifest_blob") { properties.insert( "manifest_blob".to_string(), - Value::String("[BLOB_ID]".to_string()), + Value::String("[BLOB_HASH]".to_string()), ); } if properties.contains_key("definition_blob") { properties.insert( "definition_blob".to_string(), - Value::String("[BLOB_ID]".to_string()), + Value::String("[BLOB_HASH]".to_string()), ); } } @@ -896,7 +896,7 @@ fn attach_json_errors_without_prompting_for_human_input() { } } }, - "manifest_blob": "[BLOB_ID]", + "manifest_blob": "[BLOB_HASH]", "provenance": { "client": { "name": "fabro-cli", @@ -1036,7 +1036,7 @@ fn attach_json_errors_without_prompting_for_human_input() { "event": "run.submitted", "id": "[EVENT_ID]", "properties": { - "definition_blob": "[BLOB_ID]" + "definition_blob": "[BLOB_HASH]" }, "run_id": "[ULID]", "ts": "[TIMESTAMP]" diff --git a/lib/apps/fabro-server/src/principal_middleware.rs b/lib/apps/fabro-server/src/principal_middleware.rs index 2db165547..3d9bb0542 100644 --- a/lib/apps/fabro-server/src/principal_middleware.rs +++ b/lib/apps/fabro-server/src/principal_middleware.rs @@ -14,7 +14,7 @@ use strum::IntoStaticStr; use crate::auth::{AuthErrorCode, JwtError, REFRESH_TOKEN_PREFIX}; use crate::error::ApiError; use crate::jwt_auth::{self, AuthMode, ConfiguredAuth}; -use crate::server::{AppState, parse_blob_id_path, parse_run_id_path, parse_stage_id_path}; +use crate::server::{AppState, parse_blob_hash_path, parse_run_id_path, parse_stage_id_path}; use crate::worker_token::{self, WORKER_TOKEN_KID, WorkerScopeSet}; #[derive(Clone, Debug)] @@ -295,14 +295,14 @@ impl FromRequestParts> for RequireRunBlob { parts: &mut Parts, state: &Arc, ) -> Result { - let Path((id, blob_id)): Path<(String, String)> = Path::from_request_parts(parts, state) + let Path((id, blob_hash)): Path<(String, String)> = Path::from_request_parts(parts, state) .await .map_err(IntoResponse::into_response)?; let run_id = parse_run_id_path(&id)?; - let blob_id = parse_blob_id_path(&blob_id)?; + let blob_hash = parse_blob_hash_path(&blob_hash)?; require_worker_or_user_for_run(&auth_slot_from_parts(parts), &run_id) .map_err(IntoResponse::into_response)?; - Ok(Self(run_id, blob_id)) + Ok(Self(run_id, blob_hash)) } } diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index fa8af18c8..67ae1f32f 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -2889,11 +2889,11 @@ pub(crate) fn parse_stage_id_path(stage_id: &str) -> Result { #[allow( clippy::result_large_err, - reason = "Blob ID parsing returns HTTP 400 responses directly." + reason = "Blob hash parsing returns HTTP 400 responses directly." )] -pub(crate) fn parse_blob_id_path(blob_id: &str) -> Result { - BlobHash::from_str(blob_id) - .map_err(|_| ApiError::bad_request("Invalid blob ID.").into_response()) +pub(crate) fn parse_blob_hash_path(blob_hash: &str) -> Result { + BlobHash::from_str(blob_hash) + .map_err(|_| ApiError::bad_request("Invalid blob hash.").into_response()) } #[allow( diff --git a/lib/apps/fabro-server/src/server/handler/artifacts.rs b/lib/apps/fabro-server/src/server/handler/artifacts.rs index f426a0007..c203bdec0 100644 --- a/lib/apps/fabro-server/src/server/handler/artifacts.rs +++ b/lib/apps/fabro-server/src/server/handler/artifacts.rs @@ -32,7 +32,7 @@ pub(super) fn routes() -> Router> { Router::new() .route("/runs/{id}/checkpoint", get(get_checkpoint)) .route("/runs/{id}/blobs", post(write_run_blob)) - .route("/runs/{id}/blobs/{blobId}", get(read_run_blob)) + .route("/runs/{id}/blobs/{blobHash}", get(read_run_blob)) .route("/runs/{id}/artifacts", get(list_run_artifacts)) .route("/runs/{id}/artifacts/download", get(download_run_artifacts)) .route( @@ -105,8 +105,8 @@ async fn write_run_blob( } match state.stores.runs.open_run(&id).await { Ok(run_store) => match run_store.write_blob(&body).await { - Ok(blob_id) => Json(WriteBlobResponse { - id: blob_id.to_string(), + Ok(blob_hash) => Json(WriteBlobResponse { + hash: blob_hash.to_string(), }) .into_response(), Err(err) => { @@ -118,11 +118,11 @@ async fn write_run_blob( } async fn read_run_blob( - RequireRunBlob(id, blob_id): RequireRunBlob, + RequireRunBlob(id, blob_hash): RequireRunBlob, State(state): State>, ) -> Response { match state.stores.runs.open_run_reader(&id).await { - Ok(run_store) => match run_store.read_blob(&blob_id).await { + Ok(run_store) => match run_store.read_blob(&blob_hash).await { Ok(Some(bytes)) => octet_stream_response(bytes), Ok(None) => ApiError::not_found("Blob not found.").into_response(), Err(err) => { diff --git a/lib/apps/fabro-server/src/server/handler/mod.rs b/lib/apps/fabro-server/src/server/handler/mod.rs index bcb7f9ef1..d42999c74 100644 --- a/lib/apps/fabro-server/src/server/handler/mod.rs +++ b/lib/apps/fabro-server/src/server/handler/mod.rs @@ -101,7 +101,7 @@ pub(super) fn demo_routes() -> Router> { ) .route("/runs/{id}/attach", get(demo::run_events_stub)) .route("/runs/{id}/blobs", post(not_implemented)) - .route("/runs/{id}/blobs/{blobId}", get(not_implemented)) + .route("/runs/{id}/blobs/{blobHash}", get(not_implemented)) .route( "/runs/{id}/stages/{stageId}/logs/output", get(not_implemented), diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 2ffec61de..f97892272 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -11057,11 +11057,11 @@ async fn write_and_read_run_blob_round_trip() { .unwrap(); let response = app.clone().oneshot(req).await.unwrap(); let body = response_json!(response, StatusCode::OK).await; - let blob_id = body["id"].as_str().unwrap(); + let blob_hash = body["hash"].as_str().unwrap(); let req = Request::builder() .method("GET") - .uri(api(&format!("/runs/{run_id}/blobs/{blob_id}"))) + .uri(api(&format!("/runs/{run_id}/blobs/{blob_hash}"))) .body(Body::empty()) .unwrap(); let response = app.oneshot(req).await.unwrap(); @@ -11459,7 +11459,7 @@ async fn worker_token_accepts_run_scoped_routes_and_falls_back_to_user_jwt() { let worker_token = issue_test_worker_token(&run_id); let other_run_id = create_run_with_bearer(&app, &user_jwt).await; let other_worker_token = issue_test_worker_token(&other_run_id); - let blob_id = state + let blob_hash = state .stores .runs .open_run(&run_id) @@ -11553,7 +11553,7 @@ async fn worker_token_accepts_run_scoped_routes_and_falls_back_to_user_jwt() { .clone() .oneshot(bearer_request( Method::GET, - &format!("/runs/{run_id}/blobs/{blob_id}"), + &format!("/runs/{run_id}/blobs/{blob_hash}"), &worker_token, Body::empty(), )) @@ -12058,7 +12058,7 @@ async fn worker_token_is_rejected_on_user_only_routes() { let user_jwt = issue_test_user_jwt(); let run_id = create_run_with_bearer(&app, &user_jwt).await; let worker_token = issue_test_worker_token(&run_id); - let blob_id = BlobHash::new(b"blob"); + let blob_hash = BlobHash::new(b"blob"); let user_only_routes = vec![ (Method::GET, "/runs".to_string()), (Method::POST, "/runs".to_string()), @@ -12121,7 +12121,7 @@ async fn worker_token_is_rejected_on_user_only_routes() { .clone() .oneshot(bearer_request( Method::GET, - &format!("/runs/{run_id}/blobs/{blob_id}"), + &format!("/runs/{run_id}/blobs/{blob_hash}"), &worker_token, Body::empty(), )) diff --git a/lib/components/fabro-dump/src/lib.rs b/lib/components/fabro-dump/src/lib.rs index 1028408e0..9210a695d 100644 --- a/lib/components/fabro-dump/src/lib.rs +++ b/lib/components/fabro-dump/src/lib.rs @@ -214,30 +214,30 @@ impl RunDump { for entry in &mut self.entries { match &mut entry.contents { RunDumpContents::Json(value) => { - let mut blob_ids = Vec::new(); - collect_blob_refs_in_value(value, &mut blob_ids); - for blob_id in blob_ids { - if cache.contains_key(&blob_id) { + let mut blob_hashes = Vec::new(); + collect_blob_refs_in_value(value, &mut blob_hashes); + for blob_hash in blob_hashes { + if cache.contains_key(&blob_hash) { continue; } - let blob = read_blob(blob_id).await?.with_context(|| { - format!("blob {blob_id:?} is missing from the store") + let blob = read_blob(blob_hash).await?.with_context(|| { + format!("blob {blob_hash:?} is missing from the store") })?; let hydrated: serde_json::Value = serde_json::from_slice(&blob) - .with_context(|| format!("blob {blob_id:?} is not valid JSON"))?; - cache.insert(blob_id, hydrated); + .with_context(|| format!("blob {blob_hash:?} is not valid JSON"))?; + cache.insert(blob_hash, hydrated); } replace_blob_refs_in_value(value, &cache)?; } RunDumpContents::Text(text) => { - let Some(blob_id) = parse_blob_ref(text) else { + let Some(blob_hash) = parse_blob_ref(text) else { continue; }; - let blob = read_blob(blob_id) + let blob = read_blob(blob_hash) .await? - .with_context(|| format!("blob {blob_id:?} is missing from the store"))?; + .with_context(|| format!("blob {blob_hash:?} is missing from the store"))?; *text = serde_json::from_slice::(&blob).with_context(|| { - format!("blob {blob_id:?} is not a JSON string text log") + format!("blob {blob_hash:?} is not a JSON string text log") })?; } RunDumpContents::Bytes(_) => {} @@ -386,21 +386,21 @@ fn validate_relative_path(kind: &str, value: &str) -> Result { Ok(normalized) } -fn collect_blob_refs_in_value(value: &serde_json::Value, blob_ids: &mut Vec) { +fn collect_blob_refs_in_value(value: &serde_json::Value, blob_hashes: &mut Vec) { match value { serde_json::Value::String(current) => { - if let Some(blob_id) = parse_blob_ref(current) { - blob_ids.push(blob_id); + if let Some(blob_hash) = parse_blob_ref(current) { + blob_hashes.push(blob_hash); } } serde_json::Value::Array(items) => { for item in items { - collect_blob_refs_in_value(item, blob_ids); + collect_blob_refs_in_value(item, blob_hashes); } } serde_json::Value::Object(map) => { for item in map.values() { - collect_blob_refs_in_value(item, blob_ids); + collect_blob_refs_in_value(item, blob_hashes); } } serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {} @@ -413,13 +413,12 @@ fn replace_blob_refs_in_value( ) -> Result<()> { match value { serde_json::Value::String(current) => { - let Some(blob_id) = parse_blob_ref(current) else { + let Some(blob_hash) = parse_blob_ref(current) else { return Ok(()); }; - let hydrated = cache - .get(&blob_id) - .cloned() - .with_context(|| format!("blob {blob_id:?} is missing from the hydration cache"))?; + let hydrated = cache.get(&blob_hash).cloned().with_context(|| { + format!("blob {blob_hash:?} is missing from the hydration cache") + })?; *value = hydrated; } serde_json::Value::Array(items) => { @@ -724,8 +723,8 @@ mod tests { #[test] fn hydrate_referenced_blobs_ignores_legacy_artifact_file_refs() { let blob = serde_json::to_vec("hydrated legacy text").unwrap(); - let blob_id = fabro_types::BlobHash::new(&blob); - let legacy_ref = format!("file:///sandbox/.fabro/artifacts/{blob_id}.json"); + let blob_hash = fabro_types::BlobHash::new(&blob); + let legacy_ref = format!("file:///sandbox/.fabro/artifacts/{blob_hash}.json"); let mut dump = RunDump { entries: vec![RunDumpEntry::json( "run.json", @@ -736,10 +735,10 @@ mod tests { }; executor::block_on(async { - dump.hydrate_referenced_blobs_with_reader(|read_blob_id| { + dump.hydrate_referenced_blobs_with_reader(|read_blob_hash| { let blob = blob.clone(); Box::pin(async move { - assert_eq!(read_blob_id, blob_id); + assert_eq!(read_blob_hash, blob_hash); Ok(Some(bytes::Bytes::from(blob))) }) }) diff --git a/lib/components/fabro-store/src/slate/mod.rs b/lib/components/fabro-store/src/slate/mod.rs index 21e410c71..e0b66d6e8 100644 --- a/lib/components/fabro-store/src/slate/mod.rs +++ b/lib/components/fabro-store/src/slate/mod.rs @@ -836,12 +836,12 @@ mod tests { append_created(&run_2, "run-2", dt("2026-03-27T12:00:10Z")).await; let shared_blob = br#"{"summary":"shared"}"#; - let shared_blob_id = run_1.write_blob(shared_blob).await.unwrap(); + let shared_blob_hash = run_1.write_blob(shared_blob).await.unwrap(); store.delete_run(&test_run_id("run-1")).await.unwrap(); let reopened = store.open_run(&test_run_id("run-2")).await.unwrap(); - let read = reopened.read_blob(&shared_blob_id).await.unwrap(); + let read = reopened.read_blob(&shared_blob_hash).await.unwrap(); assert_eq!(read.as_deref(), Some(shared_blob.as_slice())); } @@ -851,7 +851,7 @@ mod tests { let run = store.create_run(&test_run_id("run-1")).await.unwrap(); append_created(&run, "run-1", dt("2026-03-27T12:00:00Z")).await; let blob = br#"{"summary":"readable"}"#; - let blob_id = run.write_blob(blob).await.unwrap(); + let blob_hash = run.write_blob(blob).await.unwrap(); // Evict the cached writer so the reader is built through the real // `open_run_reader` construction path, not a clone of the writer. @@ -859,7 +859,7 @@ mod tests { let reader = store.open_run_reader(&test_run_id("run-1")).await.unwrap(); assert_eq!( - reader.read_blob(&blob_id).await.unwrap().as_deref(), + reader.read_blob(&blob_hash).await.unwrap().as_deref(), Some(blob.as_slice()) ); let err = reader.write_blob(b"blocked").await.unwrap_err(); diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs index 80dcebd53..47a8c6299 100644 --- a/lib/components/fabro-workflow-version/src/store.rs +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -86,10 +86,10 @@ impl WorkflowVersionStore { &self, id: &WorkflowVersionId, ) -> Result, WorkflowVersionStoreError> { - let blob_id = (*id).into(); + let blob_hash = (*id).into(); let Some(bytes) = self .blobs - .read(&blob_id) + .read(&blob_hash) .await .map_err(|source| WorkflowVersionStoreError::Storage { source })? else { @@ -201,8 +201,11 @@ mod tests { let id = store.put(&version).await.unwrap(); assert_eq!(id, expected_id); - let blob_id = id.into(); - assert_eq!(blobs.read(&blob_id).await.unwrap().unwrap(), expected_bytes); + let blob_hash = id.into(); + assert_eq!( + blobs.read(&blob_hash).await.unwrap().unwrap(), + expected_bytes + ); assert_eq!(store.get(&id).await.unwrap(), Some(version)); } diff --git a/lib/components/fabro-workflow/src/artifact.rs b/lib/components/fabro-workflow/src/artifact.rs index ef892b442..a35714064 100644 --- a/lib/components/fabro-workflow/src/artifact.rs +++ b/lib/components/fabro-workflow/src/artifact.rs @@ -26,7 +26,7 @@ const ARTIFACT_POINTER_PREFIX: &str = "file://"; /// /// For each entry in `updates` whose serialized JSON exceeds /// `BLOB_OFFLOAD_THRESHOLD`, the value is persisted as a blob in `run_store` -/// and replaced with a `"blob://sha256/{blob_id}"` reference. +/// and replaced with a `"blob://sha256/{blob_hash}"` reference. /// Small values are left untouched. /// /// `parallel.results` is offloaded at each branch context-update boundary @@ -102,11 +102,11 @@ async fn offload_value(value: &mut Value, run_store: &RunStoreHandle) -> Result< .map_err(|e| Error::engine_with_source("artifact serialize failed", e))?; if bytes.len() > BLOB_OFFLOAD_THRESHOLD { - let blob_id = run_store + let blob_hash = run_store .write_blob(&bytes) .await .map_err(|e| Error::engine_with_anyhow("artifact blob write failed", e))?; - *value = Value::String(format_blob_ref(&blob_id)); + *value = Value::String(format_blob_ref(&blob_hash)); } Ok(()) } @@ -232,17 +232,17 @@ pub async fn resolve_text_or_blob_ref(value: &Value, run_store: &RunStoreHandle) /// blob reference. /// /// Managed `file://` references are normalized through their content-addressed -/// blob id instead of reading an execution-local path. Ordinary strings and +/// blob hash instead of reading an execution-local path. Ordinary strings and /// ordinary file references remain unchanged for the caller to validate. pub(crate) async fn resolve_json_value(value: Value, run_store: &RunStoreHandle) -> Result { - let blob_id = value.as_str().and_then(|reference| { + let blob_hash = value.as_str().and_then(|reference| { parse_blob_ref(reference).or_else(|| parse_managed_blob_file_ref(reference)) }); - let Some(blob_id) = blob_id else { + let Some(blob_hash) = blob_hash else { return Ok(value); }; - let bytes = read_required_blob(&blob_id, run_store).await?; + let bytes = read_required_blob(&blob_hash, run_store).await?; serde_json::from_slice(&bytes) .map_err(|err| Error::engine_with_source("artifact blob was not valid JSON", err)) } @@ -267,14 +267,14 @@ pub async fn resolve_text_or_blob_ref_str( current: &str, run_store: &RunStoreHandle, ) -> Result { - let Some(blob_id) = parse_blob_ref(current) else { + let Some(blob_hash) = parse_blob_ref(current) else { return Ok(current.to_string()); }; let bytes = run_store - .read_blob(&blob_id) + .read_blob(&blob_hash) .await .map_err(|e| Error::engine_with_anyhow("text blob read failed", e))? - .ok_or_else(|| Error::engine(format!("text blob missing: {blob_id}")))?; + .ok_or_else(|| Error::engine(format!("text blob missing: {blob_hash}")))?; serde_json::from_slice::(&bytes) .map_err(|e| Error::engine_with_source("text blob was not a JSON string", e)) } @@ -334,8 +334,8 @@ pub async fn sync_artifacts_to_env( fn normalize_durable_value(value: &mut Value) { match value { Value::String(current) => { - if let Some(blob_id) = parse_managed_blob_file_ref(current) { - *current = format_blob_ref(&blob_id); + if let Some(blob_hash) = parse_managed_blob_file_ref(current) { + *current = format_blob_ref(&blob_hash); } } Value::Array(items) => { @@ -382,8 +382,8 @@ fn resolve_execution_value<'a>( Value::String(current) => { if key.is_some_and(is_text_context_key) { *current = resolve_text_or_blob_ref_str(current, run_store).await?; - } else if let Some(blob_id) = parse_blob_ref(current) { - *current = materialize_blob_ref(&blob_id, run_store, env, run_dir).await?; + } else if let Some(blob_hash) = parse_blob_ref(current) { + *current = materialize_blob_ref(&blob_hash, run_store, env, run_dir).await?; } else if current.starts_with(ARTIFACT_POINTER_PREFIX) && parse_managed_blob_file_ref(current).is_none() { @@ -413,7 +413,7 @@ fn resolve_execution_value<'a>( } async fn materialize_blob_ref( - blob_id: &BlobHash, + blob_hash: &BlobHash, run_store: &RunStoreHandle, env: &dyn Sandbox, run_dir: &Path, @@ -421,9 +421,9 @@ async fn materialize_blob_ref( // Blobs are content-addressed, so an existing materialized file is always // current — check before paying for the store read. if is_local_execution(env, run_dir).await? { - let path = local_materialized_blob_path(run_dir, blob_id); + let path = local_materialized_blob_path(run_dir, blob_hash); if !path.exists() { - let bytes = read_required_blob(blob_id, run_store).await?; + let bytes = read_required_blob(blob_hash, run_store).await?; if let Some(parent) = path.parent() { fs::create_dir_all(parent).await.map_err(|err| { Error::Io(format!( @@ -439,13 +439,13 @@ async fn materialize_blob_ref( return Ok(format!("{ARTIFACT_POINTER_PREFIX}{}", path.display())); } - let remote_path = format!("{}/.fabro/blobs/{blob_id}.json", env.working_directory()); + let remote_path = format!("{}/.fabro/blobs/{blob_hash}.json", env.working_directory()); if !env .file_exists(&remote_path) .await .map_err(|e| Error::engine_with_source("failed to check blob existence", e))? { - let bytes = read_required_blob(blob_id, run_store).await?; + let bytes = read_required_blob(blob_hash, run_store).await?; let content = String::from_utf8(bytes.to_vec()) .map_err(|e| Error::engine_with_source("artifact blob was not valid UTF-8 JSON", e))?; env.write_file(&remote_path, &content).await.map_err(|e| { @@ -457,14 +457,14 @@ async fn materialize_blob_ref( } async fn read_required_blob( - blob_id: &BlobHash, + blob_hash: &BlobHash, run_store: &RunStoreHandle, ) -> Result { run_store - .read_blob(blob_id) + .read_blob(blob_hash) .await .map_err(|e| Error::engine_with_anyhow("artifact blob read failed", e))? - .ok_or_else(|| Error::engine(format!("artifact blob missing: {blob_id}"))) + .ok_or_else(|| Error::engine(format!("artifact blob missing: {blob_hash}"))) } async fn resolve_explicit_file_ref(value: &str, env: &dyn Sandbox) -> Result { @@ -508,11 +508,11 @@ async fn is_local_execution(env: &dyn Sandbox, run_dir: &Path) -> Result { .map_err(|e| Error::engine_with_source("failed to inspect sandbox locality", e)) } -fn local_materialized_blob_path(run_dir: &Path, blob_id: &BlobHash) -> PathBuf { +fn local_materialized_blob_path(run_dir: &Path, blob_hash: &BlobHash) -> PathBuf { RunScratch::new(run_dir) .runtime_dir() .join("blobs") - .join(format!("{blob_id}.json")) + .join(format!("{blob_hash}.json")) } #[cfg(test)] @@ -549,7 +549,7 @@ mod tests { let large_string = "x".repeat(BLOB_OFFLOAD_THRESHOLD + 1); let serialized = serde_json::to_vec(&serde_json::json!(large_string.clone())).unwrap(); - let expected_blob_id = fabro_types::BlobHash::new(&serialized); + let expected_blob_hash = fabro_types::BlobHash::new(&serialized); let mut updates = HashMap::new(); updates.insert("response.plan".to_string(), serde_json::json!(large_string)); @@ -561,11 +561,11 @@ mod tests { let pointer = updates.get("response.plan").unwrap(); assert_eq!( pointer, - &serde_json::json!(fabro_types::format_blob_ref(&expected_blob_id)) + &serde_json::json!(fabro_types::format_blob_ref(&expected_blob_hash)) ); let blob = run_store - .read_blob(&expected_blob_id) + .read_blob(&expected_blob_hash) .await .unwrap() .expect("blob should exist"); @@ -591,21 +591,21 @@ mod tests { async fn resolve_json_value_hydrates_blob_and_managed_file_references() { let run_store = make_run_store("structured-json-resolution").await; let value = serde_json::json!([{"name": "api"}, {"name": "web"}]); - let blob_id = run_store + let blob_hash = run_store .write_blob(&serde_json::to_vec(&value).unwrap()) .await .unwrap(); let handle = run_store.clone().into(); assert_eq!( - resolve_json_value(serde_json::json!(format_blob_ref(&blob_id)), &handle) + resolve_json_value(serde_json::json!(format_blob_ref(&blob_hash)), &handle) .await .unwrap(), value ); assert_eq!( resolve_json_value( - serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_id}.json")), + serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_hash}.json")), &handle, ) .await @@ -789,13 +789,13 @@ mod tests { #[test] fn normalize_durable_updates_rewrites_managed_blob_file_refs_recursively() { - let blob_id = fabro_types::BlobHash::new(b"hello"); + let blob_hash = fabro_types::BlobHash::new(b"hello"); let mut updates = HashMap::from([( "nested".to_string(), serde_json::json!({ "items": [ - format!("file:///tmp/run/runtime/blobs/{blob_id}.json"), - format!("file:///sandbox/.fabro/blobs/{blob_id}.json"), + format!("file:///tmp/run/runtime/blobs/{blob_hash}.json"), + format!("file:///sandbox/.fabro/blobs/{blob_hash}.json"), "file:///tmp/report.json", ] }), @@ -807,8 +807,8 @@ mod tests { updates["nested"], serde_json::json!({ "items": [ - fabro_types::format_blob_ref(&blob_id), - fabro_types::format_blob_ref(&blob_id), + fabro_types::format_blob_ref(&blob_hash), + fabro_types::format_blob_ref(&blob_hash), "file:///tmp/report.json", ] }) @@ -870,7 +870,7 @@ mod tests { #[test] fn normalize_checkpoint_for_resume_converts_managed_blob_file_refs_and_drops_preamble() { - let blob_id = fabro_types::BlobHash::new(b"managed"); + let blob_hash = fabro_types::BlobHash::new(b"managed"); let mut checkpoint = crate::records::Checkpoint { timestamp: chrono::Utc::now(), current_node: "work".to_string(), @@ -883,7 +883,7 @@ mod tests { ), ( "response.work".to_string(), - serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_id}.json")), + serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_hash}.json")), ), ]), node_outcomes: HashMap::from([( @@ -891,7 +891,7 @@ mod tests { crate::outcome::Outcome { context_updates: HashMap::from([( "response.work".to_string(), - serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_id}.json")), + serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_hash}.json")), )]), ..crate::outcome::Outcome::success() }, @@ -912,14 +912,14 @@ mod tests { ); assert_eq!( checkpoint.context_values.get("response.work"), - Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_id))) + Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_hash))) ); assert_eq!( checkpoint .node_outcomes .get("work") .and_then(|outcome| outcome.context_updates.get("response.work")), - Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_id))) + Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_hash))) ); } diff --git a/lib/components/fabro-workflow/src/command_log.rs b/lib/components/fabro-workflow/src/command_log.rs index 67c4373e1..ed998d208 100644 --- a/lib/components/fabro-workflow/src/command_log.rs +++ b/lib/components/fabro-workflow/src/command_log.rs @@ -109,14 +109,14 @@ pub async fn read_json_string_blob( run_store: &RunStoreHandle, blob_ref: &str, ) -> Result> { - let Some(blob_id) = fabro_types::parse_blob_ref(blob_ref) else { + let Some(blob_hash) = fabro_types::parse_blob_ref(blob_ref) else { return Ok(None); }; let bytes = run_store - .read_blob(&blob_id) + .read_blob(&blob_hash) .await .map_err(|err| Error::engine_with_anyhow("command log blob read failed", err))? - .ok_or_else(|| Error::engine(format!("command log blob missing: {blob_id}")))?; + .ok_or_else(|| Error::engine(format!("command log blob missing: {blob_hash}")))?; let text = serde_json::from_slice::(&bytes) .map_err(|err| Error::engine_with_source("command log blob was not a JSON string", err))?; Ok(Some(text)) @@ -155,9 +155,9 @@ async fn write_json_string_blob(run_store: &RunStoreHandle, text: &str) -> Resul let value = Value::String(text.to_string()); let bytes = serde_json::to_vec(&value) .map_err(|err| Error::engine_with_source("command log JSON serialization failed", err))?; - let blob_id = run_store + let blob_hash = run_store .write_blob(&bytes) .await .map_err(|err| Error::engine_with_anyhow("command log blob write failed", err))?; - Ok(format_blob_ref(&blob_id)) + Ok(format_blob_ref(&blob_hash)) } diff --git a/lib/components/fabro-workflow/src/handler/command.rs b/lib/components/fabro-workflow/src/handler/command.rs index f4fef254c..828a56101 100644 --- a/lib/components/fabro-workflow/src/handler/command.rs +++ b/lib/components/fabro-workflow/src/handler/command.rs @@ -390,12 +390,12 @@ mod tests { } async fn write_blob(&self, data: &[u8]) -> anyhow::Result { - let blob_id = fabro_types::BlobHash::new(data); + let blob_hash = fabro_types::BlobHash::new(data); self.blobs .lock() .await - .insert(blob_id, Bytes::copy_from_slice(data)); - Ok(blob_id) + .insert(blob_hash, Bytes::copy_from_slice(data)); + Ok(blob_hash) } async fn read_blob(&self, id: &fabro_types::BlobHash) -> anyhow::Result> { diff --git a/lib/components/fabro-workflow/src/handler/parallel.rs b/lib/components/fabro-workflow/src/handler/parallel.rs index f20d5d5f5..9da654d1d 100644 --- a/lib/components/fabro-workflow/src/handler/parallel.rs +++ b/lib/components/fabro-workflow/src/handler/parallel.rs @@ -1917,7 +1917,7 @@ mod tests { "name": "large-item", "body": "x".repeat(101 * 1024) }]); - let blob_id = run_store + let blob_hash = run_store .write_blob(&serde_json::to_vec(&items).unwrap()) .await .unwrap(); @@ -1933,7 +1933,7 @@ mod tests { ))); let (node, graph) = for_each_graph("items", 1); let context = test_context(); - context.set("items", serde_json::json!(format_blob_ref(&blob_id))); + context.set("items", serde_json::json!(format_blob_ref(&blob_hash))); let outcome = ParallelHandler .execute(&node, &context, &graph, sandbox_dir.path(), &services) diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index adbaab3f9..c295ae145 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -359,8 +359,8 @@ impl RunSession { let git = git_checkpoint_options_from_start(settings, &record.run_id, state.start); let definition_blob = state.spec.definition_blob; let accepted_definition = match definition_blob { - Some(blob_id) => { - Some(load_accepted_run_definition(&services.run_store, blob_id).await?) + Some(blob_hash) => { + Some(load_accepted_run_definition(&services.run_store, blob_hash).await?) } None => None, }; @@ -570,15 +570,15 @@ fn vault_token_lookup(vault: &Vault, name: &str) -> Option { async fn load_accepted_run_definition( run_store: &RunStoreHandle, - blob_id: fabro_types::BlobHash, + blob_hash: fabro_types::BlobHash, ) -> Result { let bytes = run_store - .read_blob(&blob_id) + .read_blob(&blob_hash) .await .map_err(|err| Error::engine(err.to_string()))? .ok_or_else(|| { Error::engine(format!( - "run definition blob is missing from the run store: {blob_id}" + "run definition blob is missing from the run store: {blob_hash}" )) })?; serde_json::from_slice(&bytes).map_err(|err| Error::Parse(err.to_string())) diff --git a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs index 118767be8..5aadd6018 100644 --- a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs @@ -751,11 +751,11 @@ impl HandlerTrait for BlobCommandOutputHandler { services: &crate::handler::EngineServices, ) -> std::result::Result { let blob = serde_json::to_vec("routed-ok").unwrap(); - let blob_id = services.run.run_store.write_blob(&blob).await.unwrap(); + let blob_hash = services.run.run_store.write_blob(&blob).await.unwrap(); let mut outcome = Outcome::success(); outcome.context_updates.insert( context::keys::COMMAND_OUTPUT.to_string(), - serde_json::json!(format_blob_ref(&blob_id)), + serde_json::json!(format_blob_ref(&blob_hash)), ); Ok(outcome) } diff --git a/lib/components/fabro-workflow/src/runtime_store.rs b/lib/components/fabro-workflow/src/runtime_store.rs index 45252d5d3..a12a7c85a 100644 --- a/lib/components/fabro-workflow/src/runtime_store.rs +++ b/lib/components/fabro-workflow/src/runtime_store.rs @@ -217,8 +217,8 @@ mod tests { }; handle.append_run_event(&event).await.unwrap(); - let blob_id = handle.write_blob(br#"{"ok":true}"#).await.unwrap(); - let blob = handle.read_blob(&blob_id).await.unwrap().unwrap(); + let blob_hash = handle.write_blob(br#"{"ok":true}"#).await.unwrap(); + let blob = handle.read_blob(&blob_hash).await.unwrap().unwrap(); let events = handle.list_events().await.unwrap(); assert_eq!(events.len(), 2); diff --git a/lib/components/fabro-workflow/tests/it/daytona_integration.rs b/lib/components/fabro-workflow/tests/it/daytona_integration.rs index 021eecf11..0acfb0f60 100644 --- a/lib/components/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/components/fabro-workflow/tests/it/daytona_integration.rs @@ -544,13 +544,13 @@ async fn daytona_pipeline_artifact_offload_and_sync() { .get("response.big_output") .expect("context should have response.big_output"); let pointer_str = pointer_value.as_str().expect("pointer should be a string"); - let expected_blob_id = fabro_types::BlobHash::new( + let expected_blob_hash = fabro_types::BlobHash::new( &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) .expect("large value should serialize"), ); assert_eq!( pointer_str, - fabro_types::format_blob_ref(&expected_blob_id), + fabro_types::format_blob_ref(&expected_blob_hash), "checkpoint should persist a blob ref" ); diff --git a/lib/components/fabro-workflow/tests/it/integration.rs b/lib/components/fabro-workflow/tests/it/integration.rs index 0206d2c38..02e0e74fb 100644 --- a/lib/components/fabro-workflow/tests/it/integration.rs +++ b/lib/components/fabro-workflow/tests/it/integration.rs @@ -233,7 +233,7 @@ fn resolve_checkpoint_text( let Some(current) = value.as_str() else { return Ok(value.to_string()); }; - let Some(blob_id) = parse_blob_ref(current) else { + let Some(blob_hash) = parse_blob_ref(current) else { return Ok(current.to_string()); }; @@ -272,7 +272,7 @@ fn resolve_checkpoint_text( }; let run = runtime.block_on(store.open_run_reader(&run_id))?; let bytes = runtime - .block_on(run.read_blob(&blob_id))? + .block_on(run.read_blob(&blob_hash))? .ok_or("checkpoint blob should exist")?; Ok(serde_json::from_slice::(&bytes)?) }, @@ -10059,13 +10059,13 @@ async fn large_context_values_are_offloaded_to_artifact_store() { .expect("context should have response.big_output"); let pointer_str = pointer_value.as_str().expect("pointer should be a string"); - let expected_blob_id = fabro_types::BlobHash::new( + let expected_blob_hash = fabro_types::BlobHash::new( &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) .expect("large value should serialize"), ); assert_eq!( pointer_str, - fabro_types::format_blob_ref(&expected_blob_id), + fabro_types::format_blob_ref(&expected_blob_hash), "value should be a durable blob ref" ); @@ -10258,13 +10258,13 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { .get("response.big_output") .expect("context should have response.big_output"); let pointer_str = pointer_value.as_str().expect("pointer should be a string"); - let expected_blob_id = fabro_types::BlobHash::new( + let expected_blob_hash = fabro_types::BlobHash::new( &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) .expect("large value should serialize"), ); assert_eq!( pointer_str, - fabro_types::format_blob_ref(&expected_blob_id), + fabro_types::format_blob_ref(&expected_blob_hash), "checkpoint should persist a blob ref" ); diff --git a/lib/foundation/fabro-client/src/client.rs b/lib/foundation/fabro-client/src/client.rs index 4ece0a9de..8e1c3a1b2 100644 --- a/lib/foundation/fabro-client/src/client.rs +++ b/lib/foundation/fabro-client/src/client.rs @@ -1841,18 +1841,22 @@ impl Client { .await?; response .into_inner() - .id + .hash .parse() - .context("write_run_blob returned invalid blob id") + .context("write_run_blob returned invalid blob hash") } - pub async fn read_run_blob(&self, run_id: &RunId, blob_id: &BlobHash) -> Result> { + pub async fn read_run_blob( + &self, + run_id: &RunId, + blob_hash: &BlobHash, + ) -> Result> { let response = self .current_state() .client .read_run_blob() .id(run_id.to_string()) - .blob_id(blob_id.to_string()) + .blob_hash(blob_hash.to_string()) .send() .await; match response { diff --git a/lib/foundation/fabro-test/src/lib.rs b/lib/foundation/fabro-test/src/lib.rs index 74a5f6727..cf0cc669a 100644 --- a/lib/foundation/fabro-test/src/lib.rs +++ b/lib/foundation/fabro-test/src/lib.rs @@ -1957,11 +1957,11 @@ pub fn json_snapshot_filters(mut filters: Vec<(String, String)>) -> Vec<(String, filters = json_elapsed_ms_snapshot_filters(filters); filters.push(( r#""manifest_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""manifest_blob": "[BLOB_ID]""#.to_string(), + r#""manifest_blob": "[BLOB_HASH]""#.to_string(), )); filters.push(( r#""definition_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""definition_blob": "[BLOB_ID]""#.to_string(), + r#""definition_blob": "[BLOB_HASH]""#.to_string(), )); filters.push(( r#""run_dir":\s*"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]""#.to_string(), @@ -2562,8 +2562,8 @@ mod tests { "inference_time_ms": "[INFERENCE_TIME_MS]", "tool_time_ms": "[TOOL_TIME_MS]", "active_time_ms": "[ACTIVE_TIME_MS]", - "manifest_blob": "[BLOB_ID]", - "definition_blob": "[BLOB_ID]", + "manifest_blob": "[BLOB_HASH]", + "definition_blob": "[BLOB_HASH]", "run_dir": "[RUN_DIR]", "message": "[CUSTOM]" }"# diff --git a/lib/foundation/fabro-types/src/workflow_version_id.rs b/lib/foundation/fabro-types/src/workflow_version_id.rs index 4bf597f47..7e19f5333 100644 --- a/lib/foundation/fabro-types/src/workflow_version_id.rs +++ b/lib/foundation/fabro-types/src/workflow_version_id.rs @@ -63,10 +63,10 @@ mod tests { #[test] fn conversion_preserves_digest_and_display() { - let blob_id = BlobHash::new(b"workflow"); - let version_id = WorkflowVersionId::from(blob_id); - assert_eq!(version_id.to_string(), blob_id.to_string()); - assert_eq!(BlobHash::from(version_id), blob_id); + let blob_hash = BlobHash::new(b"workflow"); + let version_id = WorkflowVersionId::from(blob_hash); + assert_eq!(version_id.to_string(), blob_hash.to_string()); + assert_eq!(BlobHash::from(version_id), blob_hash); } #[test] diff --git a/lib/packages/fabro-api-client/src/api/run-internals-api.ts b/lib/packages/fabro-api-client/src/api/run-internals-api.ts index 403e2eb05..668200fbe 100644 --- a/lib/packages/fabro-api-client/src/api/run-internals-api.ts +++ b/lib/packages/fabro-api-client/src/api/run-internals-api.ts @@ -781,21 +781,21 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config }; }, /** - * Reads a previously stored blob by identifier. + * Reads a previously stored blob by hash. * @summary Read Run Blob * @param {string} id Unique run identifier (ULID). - * @param {string} blobId Content-addressed blob identifier. + * @param {string} blobHash Content-addressed blob hash. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - readRunBlob: async (id: string, blobId: string, options: RawAxiosRequestConfig = {}): Promise => { + readRunBlob: async (id: string, blobHash: string, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('readRunBlob', 'id', id) - // verify required parameter 'blobId' is not null or undefined - assertParamExists('readRunBlob', 'blobId', blobId) - const localVarPath = `/api/v1/runs/{id}/blobs/{blobId}` + // verify required parameter 'blobHash' is not null or undefined + assertParamExists('readRunBlob', 'blobHash', blobHash) + const localVarPath = `/api/v1/runs/{id}/blobs/{blobHash}` .replace(`{${"id"}}`, encodeURIComponent(String(id))) - .replace(`{${"blobId"}}`, encodeURIComponent(String(blobId))); + .replace(`{${"blobHash"}}`, encodeURIComponent(String(blobHash))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -905,7 +905,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config }; }, /** - * Writes an opaque binary blob and returns its content-addressed blob identifier. + * Writes an opaque binary blob and returns its content-addressed blob hash. * @summary Write Run Blob * @param {string} id Unique run identifier (ULID). * @param {File} body @@ -1179,15 +1179,15 @@ export const RunInternalsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Reads a previously stored blob by identifier. + * Reads a previously stored blob by hash. * @summary Read Run Blob * @param {string} id Unique run identifier (ULID). - * @param {string} blobId Content-addressed blob identifier. + * @param {string} blobHash Content-addressed blob hash. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async readRunBlob(id: string, blobId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.readRunBlob(id, blobId, options); + async readRunBlob(id: string, blobHash: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.readRunBlob(id, blobHash, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.readRunBlob']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -1219,7 +1219,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Writes an opaque binary blob and returns its content-addressed blob identifier. + * Writes an opaque binary blob and returns its content-addressed blob hash. * @summary Write Run Blob * @param {string} id Unique run identifier (ULID). * @param {File} body @@ -1417,15 +1417,15 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b return localVarFp.putStageArtifact(id, stageId, retry, body, filename, options).then((request) => request(axios, basePath)); }, /** - * Reads a previously stored blob by identifier. + * Reads a previously stored blob by hash. * @summary Read Run Blob * @param {string} id Unique run identifier (ULID). - * @param {string} blobId Content-addressed blob identifier. + * @param {string} blobHash Content-addressed blob hash. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - readRunBlob(id: string, blobId: string, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.readRunBlob(id, blobId, options).then((request) => request(axios, basePath)); + readRunBlob(id: string, blobHash: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.readRunBlob(id, blobHash, options).then((request) => request(axios, basePath)); }, /** * Returns the latest checkpoint data for a run, or null if no checkpoint has been recorded yet. @@ -1448,7 +1448,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b return localVarFp.retrieveRunSettings(id, options).then((request) => request(axios, basePath)); }, /** - * Writes an opaque binary blob and returns its content-addressed blob identifier. + * Writes an opaque binary blob and returns its content-addressed blob hash. * @summary Write Run Blob * @param {string} id Unique run identifier (ULID). * @param {File} body @@ -1656,15 +1656,15 @@ export class RunInternalsApi extends BaseAPI { } /** - * Reads a previously stored blob by identifier. + * Reads a previously stored blob by hash. * @summary Read Run Blob * @param {string} id Unique run identifier (ULID). - * @param {string} blobId Content-addressed blob identifier. + * @param {string} blobHash Content-addressed blob hash. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public readRunBlob(id: string, blobId: string, options?: RawAxiosRequestConfig) { - return RunInternalsApiFp(this.configuration).readRunBlob(id, blobId, options).then((request) => request(this.axios, this.basePath)); + public readRunBlob(id: string, blobHash: string, options?: RawAxiosRequestConfig) { + return RunInternalsApiFp(this.configuration).readRunBlob(id, blobHash, options).then((request) => request(this.axios, this.basePath)); } /** @@ -1690,7 +1690,7 @@ export class RunInternalsApi extends BaseAPI { } /** - * Writes an opaque binary blob and returns its content-addressed blob identifier. + * Writes an opaque binary blob and returns its content-addressed blob hash. * @summary Write Run Blob * @param {string} id Unique run identifier (ULID). * @param {File} body diff --git a/lib/packages/fabro-api-client/src/models/write-blob-response.ts b/lib/packages/fabro-api-client/src/models/write-blob-response.ts index 295ff9874..17a0ecad9 100644 --- a/lib/packages/fabro-api-client/src/models/write-blob-response.ts +++ b/lib/packages/fabro-api-client/src/models/write-blob-response.ts @@ -15,11 +15,11 @@ /** - * Content-addressed identifier for a stored blob. + * Content-addressed hash of a stored blob. */ export interface WriteBlobResponse { /** - * Blob identifier. + * Content-addressed hash of the stored blob. */ - 'id': string; + 'hash': string; } From a52e2c3334bfbd6818f5df898e313914575356c4 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:22:05 -0400 Subject: [PATCH 10/63] Bump API spec version to 0.2.0 for the blob-write wire break The WriteBlobResponse field rename (id -> hash) is a breaking change to the wire contract with no compatibility shim, so signal it in the spec version. There is no runtime version handshake; clients generated from the older spec fail on the missing field until rebuilt. Co-Authored-By: Claude Fable 5 --- docs/public/api-reference/fabro-api.yaml | 2 +- lib/packages/fabro-api-client/src/api.ts | 2 +- lib/packages/fabro-api-client/src/api/auth-api.ts | 2 +- lib/packages/fabro-api-client/src/api/automations-api.ts | 2 +- lib/packages/fabro-api-client/src/api/billing-api.ts | 2 +- lib/packages/fabro-api-client/src/api/completions-api.ts | 2 +- lib/packages/fabro-api-client/src/api/discovery-api.ts | 2 +- lib/packages/fabro-api-client/src/api/environments-api.ts | 2 +- lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts | 2 +- lib/packages/fabro-api-client/src/api/insights-api.ts | 2 +- lib/packages/fabro-api-client/src/api/install-api.ts | 2 +- lib/packages/fabro-api-client/src/api/integrations-api.ts | 2 +- lib/packages/fabro-api-client/src/api/mcpservers-api.ts | 2 +- lib/packages/fabro-api-client/src/api/models-api.ts | 2 +- lib/packages/fabro-api-client/src/api/playground-api.ts | 2 +- lib/packages/fabro-api-client/src/api/repos-api.ts | 2 +- lib/packages/fabro-api-client/src/api/run-internals-api.ts | 2 +- lib/packages/fabro-api-client/src/api/run-outputs-api.ts | 2 +- lib/packages/fabro-api-client/src/api/runs-api.ts | 2 +- lib/packages/fabro-api-client/src/api/sandboxes-api.ts | 2 +- lib/packages/fabro-api-client/src/api/secrets-api.ts | 2 +- lib/packages/fabro-api-client/src/api/sessions-api.ts | 2 +- lib/packages/fabro-api-client/src/api/settings-api.ts | 2 +- lib/packages/fabro-api-client/src/api/system-api.ts | 2 +- lib/packages/fabro-api-client/src/api/variables-api.ts | 2 +- lib/packages/fabro-api-client/src/api/workflow-versions-api.ts | 2 +- lib/packages/fabro-api-client/src/api/workflows-api.ts | 2 +- lib/packages/fabro-api-client/src/base.ts | 2 +- lib/packages/fabro-api-client/src/common.ts | 2 +- lib/packages/fabro-api-client/src/configuration.ts | 2 +- lib/packages/fabro-api-client/src/index.ts | 2 +- lib/packages/fabro-api-client/src/models/activated-skill.ts | 2 +- lib/packages/fabro-api-client/src/models/agent-control-state.ts | 2 +- .../fabro-api-client/src/models/agent-mcp-tool-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/agent-message-props.ts | 2 +- .../src/models/agent-session-activated-props.ts | 2 +- .../src/models/agent-skill-activation-source.ts | 2 +- lib/packages/fabro-api-client/src/models/agent-skill-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/agent-tool-category.ts | 2 +- .../fabro-api-client/src/models/agent-tool-source-mcp.ts | 2 +- .../fabro-api-client/src/models/agent-tool-source-native.ts | 2 +- .../fabro-api-client/src/models/agent-tool-source-skill.ts | 2 +- lib/packages/fabro-api-client/src/models/agent-tool-source.ts | 2 +- lib/packages/fabro-api-client/src/models/agent-tool-summary.ts | 2 +- .../fabro-api-client/src/models/agent-tools-available-props.ts | 2 +- .../fabro-api-client/src/models/aggregate-billing-totals.ts | 2 +- lib/packages/fabro-api-client/src/models/aggregate-billing.ts | 2 +- lib/packages/fabro-api-client/src/models/api-question.ts | 2 +- .../fabro-api-client/src/models/append-event-response.ts | 2 +- lib/packages/fabro-api-client/src/models/approval-mode.ts | 2 +- .../fabro-api-client/src/models/artifact-batch-upload-entry.ts | 2 +- .../src/models/artifact-batch-upload-manifest.ts | 2 +- lib/packages/fabro-api-client/src/models/artifact-entry.ts | 2 +- .../fabro-api-client/src/models/artifact-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/artifacts-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/ask-fabro.ts | 2 +- .../fabro-api-client/src/models/auth-config-response.ts | 2 +- lib/packages/fabro-api-client/src/models/auth-me-response.ts | 2 +- lib/packages/fabro-api-client/src/models/auth-method.ts | 2 +- lib/packages/fabro-api-client/src/models/auth-session-user.ts | 2 +- lib/packages/fabro-api-client/src/models/auth-session.ts | 2 +- .../fabro-api-client/src/models/auth-sessions-response.ts | 2 +- .../fabro-api-client/src/models/automation-api-trigger.ts | 2 +- .../fabro-api-client/src/models/automation-list-meta.ts | 2 +- .../fabro-api-client/src/models/automation-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/automation-ref.ts | 2 +- .../fabro-api-client/src/models/automation-schedule-trigger.ts | 2 +- lib/packages/fabro-api-client/src/models/automation-target.ts | 2 +- lib/packages/fabro-api-client/src/models/automation-trigger.ts | 2 +- lib/packages/fabro-api-client/src/models/automation.ts | 2 +- .../fabro-api-client/src/models/batch-delete-runs-request.ts | 2 +- .../fabro-api-client/src/models/batch-delete-runs-response.ts | 2 +- .../fabro-api-client/src/models/batch-delete-runs-result.ts | 2 +- .../fabro-api-client/src/models/batch-delete-runs-summary.ts | 2 +- .../fabro-api-client/src/models/batch-run-lifecycle-request.ts | 2 +- .../fabro-api-client/src/models/batch-run-lifecycle-response.ts | 2 +- .../fabro-api-client/src/models/batch-run-lifecycle-result.ts | 2 +- .../fabro-api-client/src/models/batch-run-lifecycle-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/billed-token-counts.ts | 2 +- lib/packages/fabro-api-client/src/models/billing-by-model.ts | 2 +- lib/packages/fabro-api-client/src/models/billing-model-ref.ts | 2 +- lib/packages/fabro-api-client/src/models/billing-speed.ts | 2 +- lib/packages/fabro-api-client/src/models/billing-stage-ref.ts | 2 +- lib/packages/fabro-api-client/src/models/blocked-reason.ts | 2 +- lib/packages/fabro-api-client/src/models/board-column.ts | 2 +- lib/packages/fabro-api-client/src/models/check-run-status.ts | 2 +- lib/packages/fabro-api-client/src/models/check-run.ts | 2 +- lib/packages/fabro-api-client/src/models/checkpoint-record.ts | 2 +- .../src/models/close-run-pull-request-response.ts | 2 +- lib/packages/fabro-api-client/src/models/code-location.ts | 2 +- .../fabro-api-client/src/models/command-log-response.ts | 2 +- lib/packages/fabro-api-client/src/models/command-termination.ts | 2 +- .../fabro-api-client/src/models/completion-content-part.ts | 2 +- lib/packages/fabro-api-client/src/models/completion-message.ts | 2 +- lib/packages/fabro-api-client/src/models/completion-response.ts | 2 +- .../fabro-api-client/src/models/completion-tool-choice.ts | 2 +- .../fabro-api-client/src/models/completion-tool-definition.ts | 2 +- lib/packages/fabro-api-client/src/models/completion-usage.ts | 2 +- lib/packages/fabro-api-client/src/models/conclusion.ts | 2 +- lib/packages/fabro-api-client/src/models/cost-source.ts | 2 +- .../fabro-api-client/src/models/create-automation-request.ts | 2 +- .../fabro-api-client/src/models/create-completion-request.ts | 2 +- .../fabro-api-client/src/models/create-environment-request.ts | 2 +- .../fabro-api-client/src/models/create-mcp-server-request.ts | 2 +- .../src/models/create-playground-chat-request.ts | 2 +- .../src/models/create-run-pull-request-request.ts | 2 +- .../fabro-api-client/src/models/create-run-session-request.ts | 2 +- .../fabro-api-client/src/models/create-secret-request.ts | 2 +- .../fabro-api-client/src/models/create-variable-request.ts | 2 +- .../src/models/create-workflow-version-response.ts | 2 +- lib/packages/fabro-api-client/src/models/delete-run-response.ts | 2 +- lib/packages/fabro-api-client/src/models/delete-run-sandbox.ts | 2 +- .../fabro-api-client/src/models/delete-secret-request.ts | 2 +- lib/packages/fabro-api-client/src/models/deny-run-request.ts | 2 +- .../fabro-api-client/src/models/dev-token-login-request.ts | 2 +- .../fabro-api-client/src/models/dev-token-login-response.ts | 2 +- lib/packages/fabro-api-client/src/models/diagnostics-check.ts | 2 +- lib/packages/fabro-api-client/src/models/diagnostics-detail.ts | 2 +- lib/packages/fabro-api-client/src/models/diagnostics-report.ts | 2 +- lib/packages/fabro-api-client/src/models/diagnostics-section.ts | 2 +- lib/packages/fabro-api-client/src/models/diff-file.ts | 2 +- lib/packages/fabro-api-client/src/models/diff-stats.ts | 2 +- lib/packages/fabro-api-client/src/models/diff-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/dirty-status.ts | 2 +- lib/packages/fabro-api-client/src/models/disk-usage-response.ts | 2 +- lib/packages/fabro-api-client/src/models/disk-usage-run-row.ts | 2 +- .../fabro-api-client/src/models/disk-usage-summary-row.ts | 2 +- .../fabro-api-client/src/models/dockerfile-source-inline.ts | 2 +- .../fabro-api-client/src/models/dockerfile-source-path.ts | 2 +- lib/packages/fabro-api-client/src/models/dockerfile-source.ts | 2 +- .../src/models/environment-api-dockerfile-source-inline.ts | 2 +- .../src/models/environment-api-image-settings.ts | 2 +- .../fabro-api-client/src/models/environment-image-settings.ts | 2 +- .../src/models/environment-lifecycle-settings.ts | 2 +- .../fabro-api-client/src/models/environment-list-meta.ts | 2 +- .../fabro-api-client/src/models/environment-list-response.ts | 2 +- .../fabro-api-client/src/models/environment-network-mode.ts | 2 +- .../fabro-api-client/src/models/environment-network-settings.ts | 2 +- .../fabro-api-client/src/models/environment-provider.ts | 2 +- .../src/models/environment-resources-settings.ts | 2 +- .../fabro-api-client/src/models/environment-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/environment.ts | 2 +- .../fabro-api-client/src/models/error-response-entry.ts | 2 +- lib/packages/fabro-api-client/src/models/error-response.ts | 2 +- lib/packages/fabro-api-client/src/models/event-envelope.ts | 2 +- lib/packages/fabro-api-client/src/models/event-seq.ts | 2 +- lib/packages/fabro-api-client/src/models/exec-output-tail.ts | 2 +- .../fabro-api-client/src/models/execute-query-request.ts | 2 +- .../src/models/execute-query-response-rows-inner-inner.ts | 2 +- .../fabro-api-client/src/models/execute-query-response.ts | 2 +- lib/packages/fabro-api-client/src/models/failure-category.ts | 2 +- lib/packages/fabro-api-client/src/models/failure-detail.ts | 2 +- lib/packages/fabro-api-client/src/models/failure-reason.ts | 2 +- lib/packages/fabro-api-client/src/models/file-checkpoint.ts | 2 +- lib/packages/fabro-api-client/src/models/file-diff.ts | 2 +- lib/packages/fabro-api-client/src/models/fork-request.ts | 2 +- lib/packages/fabro-api-client/src/models/fork-response.ts | 2 +- lib/packages/fabro-api-client/src/models/fork-source-ref.ts | 2 +- lib/packages/fabro-api-client/src/models/git-author-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/git-context.ts | 2 +- .../fabro-api-client/src/models/github-integration-settings.ts | 2 +- .../fabro-api-client/src/models/github-integration-strategy.ts | 2 +- lib/packages/fabro-api-client/src/models/health-response.ts | 2 +- lib/packages/fabro-api-client/src/models/history-entry.ts | 2 +- lib/packages/fabro-api-client/src/models/hook-definition.ts | 2 +- lib/packages/fabro-api-client/src/models/hook-event.ts | 2 +- lib/packages/fabro-api-client/src/models/idp-identity.ts | 2 +- .../fabro-api-client/src/models/install-finish-response.ts | 2 +- .../src/models/install-github-app-manifest-input.ts | 2 +- .../src/models/install-github-app-manifest-response.ts | 2 +- .../fabro-api-client/src/models/install-github-app-owner.ts | 2 +- .../fabro-api-client/src/models/install-github-summary.ts | 2 +- .../fabro-api-client/src/models/install-github-token-input.ts | 2 +- .../src/models/install-github-token-test-input.ts | 2 +- .../src/models/install-github-token-test-response.ts | 2 +- .../fabro-api-client/src/models/install-llm-provider-input.ts | 2 +- .../fabro-api-client/src/models/install-llm-providers-input.ts | 2 +- .../src/models/install-llm-summary-providers-inner.ts | 2 +- lib/packages/fabro-api-client/src/models/install-llm-summary.ts | 2 +- .../fabro-api-client/src/models/install-llm-test-input.ts | 2 +- .../src/models/install-llm-validation-response.ts | 2 +- .../fabro-api-client/src/models/install-object-store-input.ts | 2 +- .../fabro-api-client/src/models/install-object-store-summary.ts | 2 +- .../src/models/install-object-store-validation-response.ts | 2 +- lib/packages/fabro-api-client/src/models/install-prefill.ts | 2 +- .../fabro-api-client/src/models/install-sandbox-input.ts | 2 +- .../fabro-api-client/src/models/install-sandbox-summary.ts | 2 +- .../src/models/install-sandbox-validation-response.ts | 2 +- .../fabro-api-client/src/models/install-server-config-input.ts | 2 +- .../fabro-api-client/src/models/install-session-response.ts | 2 +- .../fabro-api-client/src/models/integration-connection-kind.ts | 2 +- .../fabro-api-client/src/models/integration-connection-state.ts | 2 +- .../src/models/integration-connection-status.ts | 2 +- .../fabro-api-client/src/models/integration-provider.ts | 2 +- lib/packages/fabro-api-client/src/models/integration-status.ts | 2 +- .../src/models/integration-webhooks-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/interview-option.ts | 2 +- .../fabro-api-client/src/models/interview-provider-settings.ts | 2 +- .../fabro-api-client/src/models/interview-question-record.ts | 2 +- .../src/models/link-run-pull-request-request.ts | 2 +- lib/packages/fabro-api-client/src/models/llm-output-kind.ts | 2 +- lib/packages/fabro-api-client/src/models/log-destination.ts | 2 +- lib/packages/fabro-api-client/src/models/manifest-args.ts | 2 +- lib/packages/fabro-api-client/src/models/manifest-config.ts | 2 +- lib/packages/fabro-api-client/src/models/manifest-file-entry.ts | 2 +- lib/packages/fabro-api-client/src/models/manifest-file-ref.ts | 2 +- lib/packages/fabro-api-client/src/models/manifest-goal.ts | 2 +- lib/packages/fabro-api-client/src/models/manifest-target.ts | 2 +- .../fabro-api-client/src/models/manifest-workflow-config.ts | 2 +- lib/packages/fabro-api-client/src/models/manifest-workflow.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts | 2 +- .../fabro-api-client/src/models/mcp-server-list-meta.ts | 2 +- .../fabro-api-client/src/models/mcp-server-list-response.ts | 2 +- .../fabro-api-client/src/models/mcp-server-projection.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-server-settings.ts | 2 +- .../fabro-api-client/src/models/mcp-server-status-failed.ts | 2 +- .../fabro-api-client/src/models/mcp-server-status-ready.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-server-status.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-server.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-transport-http.ts | 2 +- .../fabro-api-client/src/models/mcp-transport-sandbox.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts | 2 +- .../fabro-api-client/src/models/mcp-transport-view-http.ts | 2 +- .../fabro-api-client/src/models/mcp-transport-view-sandbox.ts | 2 +- .../fabro-api-client/src/models/mcp-transport-view-stdio.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-transport-view.ts | 2 +- lib/packages/fabro-api-client/src/models/mcp-transport.ts | 2 +- lib/packages/fabro-api-client/src/models/merge-method.ts | 2 +- .../src/models/merge-run-pull-request-request.ts | 2 +- .../src/models/merge-run-pull-request-response.ts | 2 +- lib/packages/fabro-api-client/src/models/model-controls.ts | 2 +- lib/packages/fabro-api-client/src/models/model-costs.ts | 2 +- lib/packages/fabro-api-client/src/models/model-features.ts | 2 +- lib/packages/fabro-api-client/src/models/model-limits.ts | 2 +- lib/packages/fabro-api-client/src/models/model-reference.ts | 2 +- lib/packages/fabro-api-client/src/models/model-test-mode.ts | 2 +- lib/packages/fabro-api-client/src/models/model-test-result.ts | 2 +- lib/packages/fabro-api-client/src/models/model.ts | 2 +- .../src/models/notification-provider-settings.ts | 2 +- .../fabro-api-client/src/models/notification-route-settings.ts | 2 +- .../fabro-api-client/src/models/object-store-local-settings.ts | 2 +- .../fabro-api-client/src/models/object-store-s3-settings.ts | 2 +- .../fabro-api-client/src/models/object-store-settings.ts | 2 +- .../fabro-api-client/src/models/paginated-api-question-list.ts | 2 +- .../fabro-api-client/src/models/paginated-event-list.ts | 2 +- .../fabro-api-client/src/models/paginated-history-entry-list.ts | 2 +- .../fabro-api-client/src/models/paginated-model-list.ts | 2 +- .../fabro-api-client/src/models/paginated-run-commit-list.ts | 2 +- .../fabro-api-client/src/models/paginated-run-file-list.ts | 2 +- lib/packages/fabro-api-client/src/models/paginated-run-list.ts | 2 +- .../fabro-api-client/src/models/paginated-run-stage-list.ts | 2 +- .../fabro-api-client/src/models/paginated-saved-query-list.ts | 2 +- .../fabro-api-client/src/models/paginated-session-list.ts | 2 +- .../src/models/paginated-workflow-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/pagination-meta.ts | 2 +- lib/packages/fabro-api-client/src/models/pair-message-record.ts | 2 +- .../fabro-api-client/src/models/pair-message-request.ts | 2 +- lib/packages/fabro-api-client/src/models/pair-record.ts | 2 +- lib/packages/fabro-api-client/src/models/pair-start-request.ts | 2 +- lib/packages/fabro-api-client/src/models/pair-status.ts | 2 +- lib/packages/fabro-api-client/src/models/pair-target.ts | 2 +- .../src/models/pair-transcript-assistant-message.ts | 2 +- .../fabro-api-client/src/models/pair-transcript-detail-ref.ts | 2 +- .../fabro-api-client/src/models/pair-transcript-entry.ts | 2 +- .../fabro-api-client/src/models/pair-transcript-error.ts | 2 +- .../src/models/pair-transcript-response-meta.ts | 2 +- .../fabro-api-client/src/models/pair-transcript-response.ts | 2 +- .../src/models/pair-transcript-system-message.ts | 2 +- .../fabro-api-client/src/models/pair-transcript-tool-call.ts | 2 +- .../fabro-api-client/src/models/pair-transcript-user-message.ts | 2 +- .../fabro-api-client/src/models/pair-transcript-warning.ts | 2 +- .../fabro-api-client/src/models/parallel-branch-result.ts | 2 +- .../fabro-api-client/src/models/pending-interview-record.ts | 2 +- lib/packages/fabro-api-client/src/models/pending-reason.ts | 2 +- lib/packages/fabro-api-client/src/models/permission-level.ts | 2 +- .../fabro-api-client/src/models/preflight-check-detail.ts | 2 +- .../fabro-api-client/src/models/preflight-check-report.ts | 2 +- .../fabro-api-client/src/models/preflight-check-result.ts | 2 +- .../fabro-api-client/src/models/preflight-check-section.ts | 2 +- lib/packages/fabro-api-client/src/models/preflight-response.ts | 2 +- .../fabro-api-client/src/models/preflight-workflow-summary.ts | 2 +- .../fabro-api-client/src/models/prepared-command-step.ts | 2 +- .../fabro-api-client/src/models/prepared-script-step.ts | 2 +- lib/packages/fabro-api-client/src/models/prepared-step.ts | 2 +- lib/packages/fabro-api-client/src/models/preview-url-request.ts | 2 +- .../fabro-api-client/src/models/preview-url-response.ts | 2 +- lib/packages/fabro-api-client/src/models/principal-agent.ts | 2 +- lib/packages/fabro-api-client/src/models/principal-slack.ts | 2 +- lib/packages/fabro-api-client/src/models/principal-system.ts | 2 +- lib/packages/fabro-api-client/src/models/principal-user.ts | 2 +- lib/packages/fabro-api-client/src/models/principal-webhook.ts | 2 +- lib/packages/fabro-api-client/src/models/principal-worker.ts | 2 +- lib/packages/fabro-api-client/src/models/principal.ts | 2 +- lib/packages/fabro-api-client/src/models/project-namespace.ts | 2 +- .../src/models/provider-credential-test-request.ts | 2 +- .../src/models/provider-credential-test-response.ts | 2 +- lib/packages/fabro-api-client/src/models/provider-list.ts | 2 +- lib/packages/fabro-api-client/src/models/provider-test-list.ts | 2 +- .../fabro-api-client/src/models/provider-test-result.ts | 2 +- .../fabro-api-client/src/models/provider-test-status.ts | 2 +- .../fabro-api-client/src/models/provider-test-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/provider.ts | 2 +- lib/packages/fabro-api-client/src/models/prune-run-entry.ts | 2 +- lib/packages/fabro-api-client/src/models/prune-runs-request.ts | 2 +- lib/packages/fabro-api-client/src/models/prune-runs-response.ts | 2 +- .../fabro-api-client/src/models/pull-request-creation-status.ts | 2 +- .../fabro-api-client/src/models/pull-request-creation.ts | 2 +- .../fabro-api-client/src/models/pull-request-details-status.ts | 2 +- .../src/models/pull-request-details-timestamps.ts | 2 +- .../src/models/pull-request-details-unavailable-reason.ts | 2 +- .../fabro-api-client/src/models/pull-request-details.ts | 2 +- lib/packages/fabro-api-client/src/models/pull-request-link.ts | 2 +- lib/packages/fabro-api-client/src/models/pull-request-meta.ts | 2 +- lib/packages/fabro-api-client/src/models/pull-request-ref.ts | 2 +- .../fabro-api-client/src/models/pull-request-response.ts | 2 +- .../fabro-api-client/src/models/pull-request-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/pull-request-user.ts | 2 +- lib/packages/fabro-api-client/src/models/pull-request.ts | 2 +- lib/packages/fabro-api-client/src/models/question-type.ts | 2 +- .../fabro-api-client/src/models/reasoning-effort-feature.ts | 2 +- lib/packages/fabro-api-client/src/models/reasoning-effort.ts | 2 +- .../fabro-api-client/src/models/reasoning-output-trace-only.ts | 2 +- .../src/models/reasoning-output-with-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/reasoning-output.ts | 2 +- .../fabro-api-client/src/models/related-workflow-diagnostic.ts | 2 +- .../src/models/render-workflow-graph-direction.ts | 2 +- .../fabro-api-client/src/models/render-workflow-graph-format.ts | 2 +- .../src/models/render-workflow-graph-request.ts | 2 +- .../fabro-api-client/src/models/replace-automation-request.ts | 2 +- .../fabro-api-client/src/models/replace-environment-request.ts | 2 +- .../fabro-api-client/src/models/replace-mcp-server-request.ts | 2 +- .../src/models/repo-check-response-permissions.ts | 2 +- lib/packages/fabro-api-client/src/models/repo-check-response.ts | 2 +- lib/packages/fabro-api-client/src/models/repository-ref.ts | 2 +- lib/packages/fabro-api-client/src/models/review-target-kind.ts | 2 +- lib/packages/fabro-api-client/src/models/review-target.ts | 2 +- lib/packages/fabro-api-client/src/models/rewind-request.ts | 2 +- lib/packages/fabro-api-client/src/models/rewind-response.ts | 2 +- lib/packages/fabro-api-client/src/models/root-response-urls.ts | 2 +- lib/packages/fabro-api-client/src/models/root-response.ts | 2 +- lib/packages/fabro-api-client/src/models/run-agent-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-approval-state.ts | 2 +- lib/packages/fabro-api-client/src/models/run-approval.ts | 2 +- lib/packages/fabro-api-client/src/models/run-artifact-entry.ts | 2 +- .../fabro-api-client/src/models/run-artifact-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/run-billing-stage.ts | 2 +- lib/packages/fabro-api-client/src/models/run-billing-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/run-billing-totals.ts | 2 +- lib/packages/fabro-api-client/src/models/run-billing.ts | 2 +- lib/packages/fabro-api-client/src/models/run-branch-settings.ts | 2 +- .../fabro-api-client/src/models/run-checkpoint-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-checkpoint.ts | 2 +- .../fabro-api-client/src/models/run-client-provenance.ts | 2 +- lib/packages/fabro-api-client/src/models/run-clone-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-commit-parent.ts | 2 +- lib/packages/fabro-api-client/src/models/run-commit-person.ts | 2 +- lib/packages/fabro-api-client/src/models/run-commit.ts | 2 +- lib/packages/fabro-api-client/src/models/run-commits-meta.ts | 2 +- lib/packages/fabro-api-client/src/models/run-control-action.ts | 2 +- lib/packages/fabro-api-client/src/models/run-diff.ts | 2 +- .../fabro-api-client/src/models/run-environment-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-error.ts | 2 +- .../src/models/run-event-detail-response-content.ts | 2 +- .../src/models/run-event-detail-response-event.ts | 2 +- .../fabro-api-client/src/models/run-event-detail-response.ts | 2 +- lib/packages/fabro-api-client/src/models/run-event.ts | 2 +- .../fabro-api-client/src/models/run-execution-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-failure.ts | 2 +- lib/packages/fabro-api-client/src/models/run-files-meta.ts | 2 +- lib/packages/fabro-api-client/src/models/run-git-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-goal-file.ts | 2 +- lib/packages/fabro-api-client/src/models/run-goal-inline.ts | 2 +- lib/packages/fabro-api-client/src/models/run-goal.ts | 2 +- .../src/models/run-integrations-github-settings.ts | 2 +- .../fabro-api-client/src/models/run-integrations-settings.ts | 2 +- .../fabro-api-client/src/models/run-interviews-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-lifecycle.ts | 2 +- lib/packages/fabro-api-client/src/models/run-links.ts | 2 +- lib/packages/fabro-api-client/src/models/run-manifest.ts | 2 +- .../fabro-api-client/src/models/run-meta-branch-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-mode.ts | 2 +- lib/packages/fabro-api-client/src/models/run-model-controls.ts | 2 +- lib/packages/fabro-api-client/src/models/run-model-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-model.ts | 2 +- lib/packages/fabro-api-client/src/models/run-namespace.ts | 2 +- lib/packages/fabro-api-client/src/models/run-origin.ts | 2 +- .../fabro-api-client/src/models/run-pair-status-response.ts | 2 +- .../fabro-api-client/src/models/run-prepare-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/run-projection.ts | 2 +- lib/packages/fabro-api-client/src/models/run-provenance.ts | 2 +- lib/packages/fabro-api-client/src/models/run-question.ts | 2 +- lib/packages/fabro-api-client/src/models/run-reference.ts | 2 +- lib/packages/fabro-api-client/src/models/run-runnable-source.ts | 2 +- lib/packages/fabro-api-client/src/models/run-sandbox-failure.ts | 2 +- .../fabro-api-client/src/models/run-sandbox-instance.ts | 2 +- lib/packages/fabro-api-client/src/models/run-sandbox-kind.ts | 2 +- lib/packages/fabro-api-client/src/models/run-sandbox-plan.ts | 2 +- lib/packages/fabro-api-client/src/models/run-sandbox-runtime.ts | 2 +- lib/packages/fabro-api-client/src/models/run-sandbox.ts | 2 +- lib/packages/fabro-api-client/src/models/run-scm-settings.ts | 2 +- .../fabro-api-client/src/models/run-server-provenance.ts | 2 +- lib/packages/fabro-api-client/src/models/run-size.ts | 2 +- lib/packages/fabro-api-client/src/models/run-spec.ts | 2 +- lib/packages/fabro-api-client/src/models/run-stage.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-blocked.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-dead.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-failed.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-paused.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-pending.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-removing.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-runnable.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-running.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status-starting.ts | 2 +- .../fabro-api-client/src/models/run-status-submitted.ts | 2 +- .../fabro-api-client/src/models/run-status-succeeded.ts | 2 +- lib/packages/fabro-api-client/src/models/run-status.ts | 2 +- .../fabro-api-client/src/models/run-superseded-by-props.ts | 2 +- lib/packages/fabro-api-client/src/models/run-timestamps.ts | 2 +- lib/packages/fabro-api-client/src/models/run-timing.ts | 2 +- lib/packages/fabro-api-client/src/models/run.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-details.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-file-entry.ts | 2 +- .../fabro-api-client/src/models/sandbox-file-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-info.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-list-meta.ts | 2 +- .../fabro-api-client/src/models/sandbox-list-response.ts | 2 +- .../fabro-api-client/src/models/sandbox-network-policy-mode.ts | 2 +- .../fabro-api-client/src/models/sandbox-network-policy.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-network.ts | 2 +- .../fabro-api-client/src/models/sandbox-provider-kind.ts | 2 +- .../src/models/sandbox-provider-lookup-error.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-resources.ts | 2 +- .../src/models/sandbox-service-discovery-source.ts | 2 +- .../fabro-api-client/src/models/sandbox-service-list-meta.ts | 2 +- .../src/models/sandbox-service-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-service.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-state.ts | 2 +- lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts | 2 +- lib/packages/fabro-api-client/src/models/save-query-request.ts | 2 +- lib/packages/fabro-api-client/src/models/saved-query.ts | 2 +- .../fabro-api-client/src/models/secret-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/secret-metadata.ts | 2 +- lib/packages/fabro-api-client/src/models/secret-type.ts | 2 +- lib/packages/fabro-api-client/src/models/server-api-settings.ts | 2 +- .../fabro-api-client/src/models/server-artifacts-settings.ts | 2 +- .../fabro-api-client/src/models/server-auth-github-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/server-auth-method.ts | 2 +- .../fabro-api-client/src/models/server-auth-settings.ts | 2 +- .../fabro-api-client/src/models/server-integrations-settings.ts | 2 +- .../fabro-api-client/src/models/server-listen-settings.ts | 2 +- .../fabro-api-client/src/models/server-listen-tcp-settings.ts | 2 +- .../fabro-api-client/src/models/server-listen-unix-settings.ts | 2 +- .../fabro-api-client/src/models/server-logging-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/server-namespace.ts | 2 +- .../src/models/server-sandbox-provider-settings.ts | 2 +- .../src/models/server-sandbox-providers-settings.ts | 2 +- .../fabro-api-client/src/models/server-sandbox-settings.ts | 2 +- .../fabro-api-client/src/models/server-scheduler-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/server-settings.ts | 2 +- .../fabro-api-client/src/models/server-slate-db-settings.ts | 2 +- .../fabro-api-client/src/models/server-storage-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/server-web-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/session-detail.ts | 2 +- lib/packages/fabro-api-client/src/models/session-message.ts | 2 +- lib/packages/fabro-api-client/src/models/session-record.ts | 2 +- lib/packages/fabro-api-client/src/models/session-status.ts | 2 +- lib/packages/fabro-api-client/src/models/session-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/session-turn.ts | 2 +- lib/packages/fabro-api-client/src/models/skills-projection.ts | 2 +- .../fabro-api-client/src/models/slack-integration-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/ssh-access-request.ts | 2 +- lib/packages/fabro-api-client/src/models/ssh-access-response.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-completion.ts | 2 +- .../src/models/stage-context-window-breakdown-item.ts | 2 +- .../src/models/stage-context-window-category.ts | 2 +- .../src/models/stage-context-window-count-method.ts | 2 +- .../src/models/stage-context-window-projection.ts | 2 +- .../src/models/stage-context-window-staleness.ts | 2 +- .../src/models/stage-context-window-unavailable-reason.ts | 2 +- .../fabro-api-client/src/models/stage-context-window-warning.ts | 2 +- .../fabro-api-client/src/models/stage-context-window.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-handler.ts | 2 +- .../fabro-api-client/src/models/stage-inference-projection.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-model-usage.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-outcome.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-projection.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-state.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/stage-timing.ts | 2 +- .../fabro-api-client/src/models/stage-tool-batch-projection.ts | 2 +- lib/packages/fabro-api-client/src/models/start-record.ts | 2 +- lib/packages/fabro-api-client/src/models/start-run-request.ts | 2 +- lib/packages/fabro-api-client/src/models/steer-run-request.ts | 2 +- .../fabro-api-client/src/models/sub-agent-projection.ts | 2 +- .../fabro-api-client/src/models/sub-agent-status-closed.ts | 2 +- .../fabro-api-client/src/models/sub-agent-status-completed.ts | 2 +- .../fabro-api-client/src/models/sub-agent-status-failed.ts | 2 +- .../fabro-api-client/src/models/sub-agent-status-running.ts | 2 +- lib/packages/fabro-api-client/src/models/sub-agent-status.ts | 2 +- .../src/models/submit-answer-multi-selected-request.ts | 2 +- .../fabro-api-client/src/models/submit-answer-no-request.ts | 2 +- .../fabro-api-client/src/models/submit-answer-request.ts | 2 +- .../src/models/submit-answer-selected-request.ts | 2 +- .../fabro-api-client/src/models/submit-answer-text-request.ts | 2 +- .../fabro-api-client/src/models/submit-answer-yes-request.ts | 2 +- lib/packages/fabro-api-client/src/models/submit-turn-request.ts | 2 +- lib/packages/fabro-api-client/src/models/success-reason.ts | 2 +- lib/packages/fabro-api-client/src/models/system-actor-kind.ts | 2 +- .../fabro-api-client/src/models/system-cpu-resource-scope.ts | 2 +- .../fabro-api-client/src/models/system-cpu-resources.ts | 2 +- .../fabro-api-client/src/models/system-disk-resource-scope.ts | 2 +- .../fabro-api-client/src/models/system-disk-resources.ts | 2 +- .../fabro-api-client/src/models/system-info-response.ts | 2 +- .../fabro-api-client/src/models/system-integration-status.ts | 2 +- .../fabro-api-client/src/models/system-integrations-response.ts | 2 +- .../fabro-api-client/src/models/system-memory-resource-scope.ts | 2 +- .../fabro-api-client/src/models/system-memory-resources.ts | 2 +- .../fabro-api-client/src/models/system-repair-run-issue.ts | 2 +- .../fabro-api-client/src/models/system-repair-runs-response.ts | 2 +- .../fabro-api-client/src/models/system-resources-response.ts | 2 +- lib/packages/fabro-api-client/src/models/system-run-counts.ts | 2 +- .../fabro-api-client/src/models/timeline-entry-response.ts | 2 +- lib/packages/fabro-api-client/src/models/tls-mode.ts | 2 +- lib/packages/fabro-api-client/src/models/todo-list-kind.ts | 2 +- .../fabro-api-client/src/models/todo-list-projection.ts | 2 +- lib/packages/fabro-api-client/src/models/todo-projection.ts | 2 +- lib/packages/fabro-api-client/src/models/todo-status.ts | 2 +- .../fabro-api-client/src/models/update-run-parent-request.ts | 2 +- lib/packages/fabro-api-client/src/models/update-run-request.ts | 2 +- .../fabro-api-client/src/models/update-variable-request.ts | 2 +- lib/packages/fabro-api-client/src/models/user-response.ts | 2 +- lib/packages/fabro-api-client/src/models/validate-response.ts | 2 +- .../fabro-api-client/src/models/variable-list-response.ts | 2 +- lib/packages/fabro-api-client/src/models/variable.ts | 2 +- .../fabro-api-client/src/models/vnc-preview-response.ts | 2 +- lib/packages/fabro-api-client/src/models/webhook-strategy.ts | 2 +- .../fabro-api-client/src/models/workflow-detail-response.ts | 2 +- lib/packages/fabro-api-client/src/models/workflow-diagnostic.ts | 2 +- .../fabro-api-client/src/models/workflow-last-run-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/workflow-list-item.ts | 2 +- lib/packages/fabro-api-client/src/models/workflow-namespace.ts | 2 +- lib/packages/fabro-api-client/src/models/workflow-ref.ts | 2 +- lib/packages/fabro-api-client/src/models/workflow-reference.ts | 2 +- .../fabro-api-client/src/models/workflow-schedule-summary.ts | 2 +- lib/packages/fabro-api-client/src/models/workflow-settings.ts | 2 +- lib/packages/fabro-api-client/src/models/workflow-version.ts | 2 +- lib/packages/fabro-api-client/src/models/write-blob-response.ts | 2 +- 547 files changed, 547 insertions(+), 547 deletions(-) diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index b788fd713..6038b9c34 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -1,7 +1,7 @@ openapi: "3.1.0" info: title: Fabro Run API - version: "0.1.0" + version: "0.2.0" description: HTTP API for managing Fabro workflow run executions. tags: diff --git a/lib/packages/fabro-api-client/src/api.ts b/lib/packages/fabro-api-client/src/api.ts index 3d4ddb3c7..b7f58f486 100644 --- a/lib/packages/fabro-api-client/src/api.ts +++ b/lib/packages/fabro-api-client/src/api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/auth-api.ts b/lib/packages/fabro-api-client/src/api/auth-api.ts index e0bd4a9c7..9cb22e4c9 100644 --- a/lib/packages/fabro-api-client/src/api/auth-api.ts +++ b/lib/packages/fabro-api-client/src/api/auth-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/automations-api.ts b/lib/packages/fabro-api-client/src/api/automations-api.ts index 832fa0665..686aef151 100644 --- a/lib/packages/fabro-api-client/src/api/automations-api.ts +++ b/lib/packages/fabro-api-client/src/api/automations-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/billing-api.ts b/lib/packages/fabro-api-client/src/api/billing-api.ts index 31e957bba..93893cf1e 100644 --- a/lib/packages/fabro-api-client/src/api/billing-api.ts +++ b/lib/packages/fabro-api-client/src/api/billing-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/completions-api.ts b/lib/packages/fabro-api-client/src/api/completions-api.ts index 677344d43..e4cb58fd4 100644 --- a/lib/packages/fabro-api-client/src/api/completions-api.ts +++ b/lib/packages/fabro-api-client/src/api/completions-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/discovery-api.ts b/lib/packages/fabro-api-client/src/api/discovery-api.ts index dab8fa6d9..f14061c06 100644 --- a/lib/packages/fabro-api-client/src/api/discovery-api.ts +++ b/lib/packages/fabro-api-client/src/api/discovery-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/environments-api.ts b/lib/packages/fabro-api-client/src/api/environments-api.ts index 6fbcd086e..b458e7501 100644 --- a/lib/packages/fabro-api-client/src/api/environments-api.ts +++ b/lib/packages/fabro-api-client/src/api/environments-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts b/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts index 189517deb..32b365ac2 100644 --- a/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts +++ b/lib/packages/fabro-api-client/src/api/human-in-the-loop-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/insights-api.ts b/lib/packages/fabro-api-client/src/api/insights-api.ts index 0361ca0f6..a4ef749dd 100644 --- a/lib/packages/fabro-api-client/src/api/insights-api.ts +++ b/lib/packages/fabro-api-client/src/api/insights-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/install-api.ts b/lib/packages/fabro-api-client/src/api/install-api.ts index 3ba0e29cb..6f1fb0d3a 100644 --- a/lib/packages/fabro-api-client/src/api/install-api.ts +++ b/lib/packages/fabro-api-client/src/api/install-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/integrations-api.ts b/lib/packages/fabro-api-client/src/api/integrations-api.ts index 311da8593..13421515a 100644 --- a/lib/packages/fabro-api-client/src/api/integrations-api.ts +++ b/lib/packages/fabro-api-client/src/api/integrations-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/mcpservers-api.ts b/lib/packages/fabro-api-client/src/api/mcpservers-api.ts index f72066148..23cb3d716 100644 --- a/lib/packages/fabro-api-client/src/api/mcpservers-api.ts +++ b/lib/packages/fabro-api-client/src/api/mcpservers-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/models-api.ts b/lib/packages/fabro-api-client/src/api/models-api.ts index 03a90fac4..f2662fe2b 100644 --- a/lib/packages/fabro-api-client/src/api/models-api.ts +++ b/lib/packages/fabro-api-client/src/api/models-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/playground-api.ts b/lib/packages/fabro-api-client/src/api/playground-api.ts index c6843ab4f..acd6dfb6b 100644 --- a/lib/packages/fabro-api-client/src/api/playground-api.ts +++ b/lib/packages/fabro-api-client/src/api/playground-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/repos-api.ts b/lib/packages/fabro-api-client/src/api/repos-api.ts index f56f69caf..2b95ee7e0 100644 --- a/lib/packages/fabro-api-client/src/api/repos-api.ts +++ b/lib/packages/fabro-api-client/src/api/repos-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/run-internals-api.ts b/lib/packages/fabro-api-client/src/api/run-internals-api.ts index 668200fbe..771501839 100644 --- a/lib/packages/fabro-api-client/src/api/run-internals-api.ts +++ b/lib/packages/fabro-api-client/src/api/run-internals-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/run-outputs-api.ts b/lib/packages/fabro-api-client/src/api/run-outputs-api.ts index ca9c328db..65014bf61 100644 --- a/lib/packages/fabro-api-client/src/api/run-outputs-api.ts +++ b/lib/packages/fabro-api-client/src/api/run-outputs-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/runs-api.ts b/lib/packages/fabro-api-client/src/api/runs-api.ts index e3d936afe..f61c0fe8f 100644 --- a/lib/packages/fabro-api-client/src/api/runs-api.ts +++ b/lib/packages/fabro-api-client/src/api/runs-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/sandboxes-api.ts b/lib/packages/fabro-api-client/src/api/sandboxes-api.ts index 87ddcd56a..35dd77d55 100644 --- a/lib/packages/fabro-api-client/src/api/sandboxes-api.ts +++ b/lib/packages/fabro-api-client/src/api/sandboxes-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/secrets-api.ts b/lib/packages/fabro-api-client/src/api/secrets-api.ts index a22887e1a..56df87983 100644 --- a/lib/packages/fabro-api-client/src/api/secrets-api.ts +++ b/lib/packages/fabro-api-client/src/api/secrets-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/sessions-api.ts b/lib/packages/fabro-api-client/src/api/sessions-api.ts index 904647177..8cf90dddc 100644 --- a/lib/packages/fabro-api-client/src/api/sessions-api.ts +++ b/lib/packages/fabro-api-client/src/api/sessions-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/settings-api.ts b/lib/packages/fabro-api-client/src/api/settings-api.ts index 90d83487f..de280fc1a 100644 --- a/lib/packages/fabro-api-client/src/api/settings-api.ts +++ b/lib/packages/fabro-api-client/src/api/settings-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/system-api.ts b/lib/packages/fabro-api-client/src/api/system-api.ts index 4b9efebd8..e6a51a4a1 100644 --- a/lib/packages/fabro-api-client/src/api/system-api.ts +++ b/lib/packages/fabro-api-client/src/api/system-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/variables-api.ts b/lib/packages/fabro-api-client/src/api/variables-api.ts index 0542e1110..319c75436 100644 --- a/lib/packages/fabro-api-client/src/api/variables-api.ts +++ b/lib/packages/fabro-api-client/src/api/variables-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/workflow-versions-api.ts b/lib/packages/fabro-api-client/src/api/workflow-versions-api.ts index edd6a2ccf..4285c1849 100644 --- a/lib/packages/fabro-api-client/src/api/workflow-versions-api.ts +++ b/lib/packages/fabro-api-client/src/api/workflow-versions-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/api/workflows-api.ts b/lib/packages/fabro-api-client/src/api/workflows-api.ts index 2b68507b4..0638ee85a 100644 --- a/lib/packages/fabro-api-client/src/api/workflows-api.ts +++ b/lib/packages/fabro-api-client/src/api/workflows-api.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/base.ts b/lib/packages/fabro-api-client/src/base.ts index f486d5de2..25e65dfe7 100644 --- a/lib/packages/fabro-api-client/src/base.ts +++ b/lib/packages/fabro-api-client/src/base.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/common.ts b/lib/packages/fabro-api-client/src/common.ts index 0a2fab196..2b5be404c 100644 --- a/lib/packages/fabro-api-client/src/common.ts +++ b/lib/packages/fabro-api-client/src/common.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/configuration.ts b/lib/packages/fabro-api-client/src/configuration.ts index b62a5fb52..07b7ae9d0 100644 --- a/lib/packages/fabro-api-client/src/configuration.ts +++ b/lib/packages/fabro-api-client/src/configuration.ts @@ -3,7 +3,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/index.ts b/lib/packages/fabro-api-client/src/index.ts index 64e64478e..c0adbf056 100644 --- a/lib/packages/fabro-api-client/src/index.ts +++ b/lib/packages/fabro-api-client/src/index.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/activated-skill.ts b/lib/packages/fabro-api-client/src/models/activated-skill.ts index 2a8cd0a07..5bc9d5c57 100644 --- a/lib/packages/fabro-api-client/src/models/activated-skill.ts +++ b/lib/packages/fabro-api-client/src/models/activated-skill.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-control-state.ts b/lib/packages/fabro-api-client/src/models/agent-control-state.ts index 821dc97af..91966dbd3 100644 --- a/lib/packages/fabro-api-client/src/models/agent-control-state.ts +++ b/lib/packages/fabro-api-client/src/models/agent-control-state.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts b/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts index 294ed6e81..e5510ca18 100644 --- a/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts +++ b/lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-message-props.ts b/lib/packages/fabro-api-client/src/models/agent-message-props.ts index bb98aeeeb..2e93de6e4 100644 --- a/lib/packages/fabro-api-client/src/models/agent-message-props.ts +++ b/lib/packages/fabro-api-client/src/models/agent-message-props.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts b/lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts index 77a3d829f..e187d0f78 100644 --- a/lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts +++ b/lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts b/lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts index 334638e4c..99da82a62 100644 --- a/lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts +++ b/lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-skill-summary.ts b/lib/packages/fabro-api-client/src/models/agent-skill-summary.ts index c9a3f101c..ff1db8811 100644 --- a/lib/packages/fabro-api-client/src/models/agent-skill-summary.ts +++ b/lib/packages/fabro-api-client/src/models/agent-skill-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-tool-category.ts b/lib/packages/fabro-api-client/src/models/agent-tool-category.ts index 8e1eda4b0..c5cd0f4b3 100644 --- a/lib/packages/fabro-api-client/src/models/agent-tool-category.ts +++ b/lib/packages/fabro-api-client/src/models/agent-tool-category.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source-mcp.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source-mcp.ts index 3f8089a92..0ca524dd3 100644 --- a/lib/packages/fabro-api-client/src/models/agent-tool-source-mcp.ts +++ b/lib/packages/fabro-api-client/src/models/agent-tool-source-mcp.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source-native.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source-native.ts index 4ea94bead..6f8c4909f 100644 --- a/lib/packages/fabro-api-client/src/models/agent-tool-source-native.ts +++ b/lib/packages/fabro-api-client/src/models/agent-tool-source-native.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source-skill.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source-skill.ts index e150e8591..e01673606 100644 --- a/lib/packages/fabro-api-client/src/models/agent-tool-source-skill.ts +++ b/lib/packages/fabro-api-client/src/models/agent-tool-source-skill.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-tool-source.ts b/lib/packages/fabro-api-client/src/models/agent-tool-source.ts index 391f72fd5..0e664b6cf 100644 --- a/lib/packages/fabro-api-client/src/models/agent-tool-source.ts +++ b/lib/packages/fabro-api-client/src/models/agent-tool-source.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-tool-summary.ts b/lib/packages/fabro-api-client/src/models/agent-tool-summary.ts index b52e220c0..9a234f340 100644 --- a/lib/packages/fabro-api-client/src/models/agent-tool-summary.ts +++ b/lib/packages/fabro-api-client/src/models/agent-tool-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/agent-tools-available-props.ts b/lib/packages/fabro-api-client/src/models/agent-tools-available-props.ts index 1231a8aa8..71125a016 100644 --- a/lib/packages/fabro-api-client/src/models/agent-tools-available-props.ts +++ b/lib/packages/fabro-api-client/src/models/agent-tools-available-props.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts b/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts index 505bdf57a..7616b4a60 100644 --- a/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts +++ b/lib/packages/fabro-api-client/src/models/aggregate-billing-totals.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/aggregate-billing.ts b/lib/packages/fabro-api-client/src/models/aggregate-billing.ts index 140eed45a..efce18df8 100644 --- a/lib/packages/fabro-api-client/src/models/aggregate-billing.ts +++ b/lib/packages/fabro-api-client/src/models/aggregate-billing.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/api-question.ts b/lib/packages/fabro-api-client/src/models/api-question.ts index 8b0330475..1894720e4 100644 --- a/lib/packages/fabro-api-client/src/models/api-question.ts +++ b/lib/packages/fabro-api-client/src/models/api-question.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/append-event-response.ts b/lib/packages/fabro-api-client/src/models/append-event-response.ts index 66412ef29..dc5f95688 100644 --- a/lib/packages/fabro-api-client/src/models/append-event-response.ts +++ b/lib/packages/fabro-api-client/src/models/append-event-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/approval-mode.ts b/lib/packages/fabro-api-client/src/models/approval-mode.ts index 7684e7600..9d7f3beec 100644 --- a/lib/packages/fabro-api-client/src/models/approval-mode.ts +++ b/lib/packages/fabro-api-client/src/models/approval-mode.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts b/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts index c4dfe1825..29d80b063 100644 --- a/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts +++ b/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/artifact-batch-upload-manifest.ts b/lib/packages/fabro-api-client/src/models/artifact-batch-upload-manifest.ts index ad483a824..080a3e575 100644 --- a/lib/packages/fabro-api-client/src/models/artifact-batch-upload-manifest.ts +++ b/lib/packages/fabro-api-client/src/models/artifact-batch-upload-manifest.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/artifact-entry.ts b/lib/packages/fabro-api-client/src/models/artifact-entry.ts index cee73730c..2bec55376 100644 --- a/lib/packages/fabro-api-client/src/models/artifact-entry.ts +++ b/lib/packages/fabro-api-client/src/models/artifact-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/artifact-list-response.ts b/lib/packages/fabro-api-client/src/models/artifact-list-response.ts index a57ba2d10..f30aade3e 100644 --- a/lib/packages/fabro-api-client/src/models/artifact-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/artifact-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/artifacts-settings.ts b/lib/packages/fabro-api-client/src/models/artifacts-settings.ts index d9e7798db..9d2ab9b02 100644 --- a/lib/packages/fabro-api-client/src/models/artifacts-settings.ts +++ b/lib/packages/fabro-api-client/src/models/artifacts-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/ask-fabro.ts b/lib/packages/fabro-api-client/src/models/ask-fabro.ts index 8d9c47e76..d99f6e9c8 100644 --- a/lib/packages/fabro-api-client/src/models/ask-fabro.ts +++ b/lib/packages/fabro-api-client/src/models/ask-fabro.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/auth-config-response.ts b/lib/packages/fabro-api-client/src/models/auth-config-response.ts index c6a219185..d28517e9e 100644 --- a/lib/packages/fabro-api-client/src/models/auth-config-response.ts +++ b/lib/packages/fabro-api-client/src/models/auth-config-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/auth-me-response.ts b/lib/packages/fabro-api-client/src/models/auth-me-response.ts index c04b83e4f..e64ec8ba0 100644 --- a/lib/packages/fabro-api-client/src/models/auth-me-response.ts +++ b/lib/packages/fabro-api-client/src/models/auth-me-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/auth-method.ts b/lib/packages/fabro-api-client/src/models/auth-method.ts index 80b643fef..8ba1ecc9b 100644 --- a/lib/packages/fabro-api-client/src/models/auth-method.ts +++ b/lib/packages/fabro-api-client/src/models/auth-method.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/auth-session-user.ts b/lib/packages/fabro-api-client/src/models/auth-session-user.ts index 6afa92a88..065cacfba 100644 --- a/lib/packages/fabro-api-client/src/models/auth-session-user.ts +++ b/lib/packages/fabro-api-client/src/models/auth-session-user.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/auth-session.ts b/lib/packages/fabro-api-client/src/models/auth-session.ts index d57f6df59..088879153 100644 --- a/lib/packages/fabro-api-client/src/models/auth-session.ts +++ b/lib/packages/fabro-api-client/src/models/auth-session.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/auth-sessions-response.ts b/lib/packages/fabro-api-client/src/models/auth-sessions-response.ts index 82c01fb1d..3d0685eef 100644 --- a/lib/packages/fabro-api-client/src/models/auth-sessions-response.ts +++ b/lib/packages/fabro-api-client/src/models/auth-sessions-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation-api-trigger.ts b/lib/packages/fabro-api-client/src/models/automation-api-trigger.ts index 12a830945..b0bf14572 100644 --- a/lib/packages/fabro-api-client/src/models/automation-api-trigger.ts +++ b/lib/packages/fabro-api-client/src/models/automation-api-trigger.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation-list-meta.ts b/lib/packages/fabro-api-client/src/models/automation-list-meta.ts index 12524dba9..b2a9fb140 100644 --- a/lib/packages/fabro-api-client/src/models/automation-list-meta.ts +++ b/lib/packages/fabro-api-client/src/models/automation-list-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation-list-response.ts b/lib/packages/fabro-api-client/src/models/automation-list-response.ts index 58258b67d..b194db4a1 100644 --- a/lib/packages/fabro-api-client/src/models/automation-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/automation-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation-ref.ts b/lib/packages/fabro-api-client/src/models/automation-ref.ts index d8055703b..a22d99199 100644 --- a/lib/packages/fabro-api-client/src/models/automation-ref.ts +++ b/lib/packages/fabro-api-client/src/models/automation-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation-schedule-trigger.ts b/lib/packages/fabro-api-client/src/models/automation-schedule-trigger.ts index 85b9631b2..d44d209d9 100644 --- a/lib/packages/fabro-api-client/src/models/automation-schedule-trigger.ts +++ b/lib/packages/fabro-api-client/src/models/automation-schedule-trigger.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation-target.ts b/lib/packages/fabro-api-client/src/models/automation-target.ts index 6eb3daa75..74e94c989 100644 --- a/lib/packages/fabro-api-client/src/models/automation-target.ts +++ b/lib/packages/fabro-api-client/src/models/automation-target.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation-trigger.ts b/lib/packages/fabro-api-client/src/models/automation-trigger.ts index 30e73dc81..ed204177b 100644 --- a/lib/packages/fabro-api-client/src/models/automation-trigger.ts +++ b/lib/packages/fabro-api-client/src/models/automation-trigger.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/automation.ts b/lib/packages/fabro-api-client/src/models/automation.ts index 5c1c20bb1..92cf402e2 100644 --- a/lib/packages/fabro-api-client/src/models/automation.ts +++ b/lib/packages/fabro-api-client/src/models/automation.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-delete-runs-request.ts b/lib/packages/fabro-api-client/src/models/batch-delete-runs-request.ts index 039b5ffbc..38fa3b995 100644 --- a/lib/packages/fabro-api-client/src/models/batch-delete-runs-request.ts +++ b/lib/packages/fabro-api-client/src/models/batch-delete-runs-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-delete-runs-response.ts b/lib/packages/fabro-api-client/src/models/batch-delete-runs-response.ts index cdc96e05f..1d9d3173e 100644 --- a/lib/packages/fabro-api-client/src/models/batch-delete-runs-response.ts +++ b/lib/packages/fabro-api-client/src/models/batch-delete-runs-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-delete-runs-result.ts b/lib/packages/fabro-api-client/src/models/batch-delete-runs-result.ts index e55be5fe5..764c19013 100644 --- a/lib/packages/fabro-api-client/src/models/batch-delete-runs-result.ts +++ b/lib/packages/fabro-api-client/src/models/batch-delete-runs-result.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-delete-runs-summary.ts b/lib/packages/fabro-api-client/src/models/batch-delete-runs-summary.ts index a335230b2..849dc7a29 100644 --- a/lib/packages/fabro-api-client/src/models/batch-delete-runs-summary.ts +++ b/lib/packages/fabro-api-client/src/models/batch-delete-runs-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-request.ts b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-request.ts index d93be0250..86a8b5ac9 100644 --- a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-request.ts +++ b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-response.ts b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-response.ts index 8dc5a80dd..b2bca1f44 100644 --- a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-response.ts +++ b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-result.ts b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-result.ts index d3559109f..851e4e7f3 100644 --- a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-result.ts +++ b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-result.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-summary.ts b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-summary.ts index e01f9815d..f5bf9cd2a 100644 --- a/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-summary.ts +++ b/lib/packages/fabro-api-client/src/models/batch-run-lifecycle-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/billed-token-counts.ts b/lib/packages/fabro-api-client/src/models/billed-token-counts.ts index 1c945f4d3..19413b7c0 100644 --- a/lib/packages/fabro-api-client/src/models/billed-token-counts.ts +++ b/lib/packages/fabro-api-client/src/models/billed-token-counts.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/billing-by-model.ts b/lib/packages/fabro-api-client/src/models/billing-by-model.ts index 0678eae9e..9c2bf6153 100644 --- a/lib/packages/fabro-api-client/src/models/billing-by-model.ts +++ b/lib/packages/fabro-api-client/src/models/billing-by-model.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/billing-model-ref.ts b/lib/packages/fabro-api-client/src/models/billing-model-ref.ts index 5b12fa237..b7f812bb1 100644 --- a/lib/packages/fabro-api-client/src/models/billing-model-ref.ts +++ b/lib/packages/fabro-api-client/src/models/billing-model-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/billing-speed.ts b/lib/packages/fabro-api-client/src/models/billing-speed.ts index 3cad0e643..80e070d52 100644 --- a/lib/packages/fabro-api-client/src/models/billing-speed.ts +++ b/lib/packages/fabro-api-client/src/models/billing-speed.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/billing-stage-ref.ts b/lib/packages/fabro-api-client/src/models/billing-stage-ref.ts index 8db422a67..ca95cb907 100644 --- a/lib/packages/fabro-api-client/src/models/billing-stage-ref.ts +++ b/lib/packages/fabro-api-client/src/models/billing-stage-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/blocked-reason.ts b/lib/packages/fabro-api-client/src/models/blocked-reason.ts index dda10abec..7567ffe5a 100644 --- a/lib/packages/fabro-api-client/src/models/blocked-reason.ts +++ b/lib/packages/fabro-api-client/src/models/blocked-reason.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/board-column.ts b/lib/packages/fabro-api-client/src/models/board-column.ts index 8dcd91a7f..ef67b0119 100644 --- a/lib/packages/fabro-api-client/src/models/board-column.ts +++ b/lib/packages/fabro-api-client/src/models/board-column.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/check-run-status.ts b/lib/packages/fabro-api-client/src/models/check-run-status.ts index c82703c8d..d3fbe1d5e 100644 --- a/lib/packages/fabro-api-client/src/models/check-run-status.ts +++ b/lib/packages/fabro-api-client/src/models/check-run-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/check-run.ts b/lib/packages/fabro-api-client/src/models/check-run.ts index b94d2b9e5..f00a1afa3 100644 --- a/lib/packages/fabro-api-client/src/models/check-run.ts +++ b/lib/packages/fabro-api-client/src/models/check-run.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/checkpoint-record.ts b/lib/packages/fabro-api-client/src/models/checkpoint-record.ts index 9177008eb..d3f7f4e34 100644 --- a/lib/packages/fabro-api-client/src/models/checkpoint-record.ts +++ b/lib/packages/fabro-api-client/src/models/checkpoint-record.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/close-run-pull-request-response.ts b/lib/packages/fabro-api-client/src/models/close-run-pull-request-response.ts index ba522a231..521e27156 100644 --- a/lib/packages/fabro-api-client/src/models/close-run-pull-request-response.ts +++ b/lib/packages/fabro-api-client/src/models/close-run-pull-request-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/code-location.ts b/lib/packages/fabro-api-client/src/models/code-location.ts index 42a1a3f82..52e29b6e4 100644 --- a/lib/packages/fabro-api-client/src/models/code-location.ts +++ b/lib/packages/fabro-api-client/src/models/code-location.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/command-log-response.ts b/lib/packages/fabro-api-client/src/models/command-log-response.ts index 6e475131c..b28515f6c 100644 --- a/lib/packages/fabro-api-client/src/models/command-log-response.ts +++ b/lib/packages/fabro-api-client/src/models/command-log-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/command-termination.ts b/lib/packages/fabro-api-client/src/models/command-termination.ts index 11323d767..30eb79b6f 100644 --- a/lib/packages/fabro-api-client/src/models/command-termination.ts +++ b/lib/packages/fabro-api-client/src/models/command-termination.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/completion-content-part.ts b/lib/packages/fabro-api-client/src/models/completion-content-part.ts index def5398f4..0f0d2d7e0 100644 --- a/lib/packages/fabro-api-client/src/models/completion-content-part.ts +++ b/lib/packages/fabro-api-client/src/models/completion-content-part.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/completion-message.ts b/lib/packages/fabro-api-client/src/models/completion-message.ts index da81f2603..60f264761 100644 --- a/lib/packages/fabro-api-client/src/models/completion-message.ts +++ b/lib/packages/fabro-api-client/src/models/completion-message.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/completion-response.ts b/lib/packages/fabro-api-client/src/models/completion-response.ts index 5b3d34e4b..555907f9c 100644 --- a/lib/packages/fabro-api-client/src/models/completion-response.ts +++ b/lib/packages/fabro-api-client/src/models/completion-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts b/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts index 97196c2e1..87b4ffdb2 100644 --- a/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts +++ b/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/completion-tool-definition.ts b/lib/packages/fabro-api-client/src/models/completion-tool-definition.ts index ba2b91b38..2cdfa60e7 100644 --- a/lib/packages/fabro-api-client/src/models/completion-tool-definition.ts +++ b/lib/packages/fabro-api-client/src/models/completion-tool-definition.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/completion-usage.ts b/lib/packages/fabro-api-client/src/models/completion-usage.ts index 056007b1b..6d2e8892d 100644 --- a/lib/packages/fabro-api-client/src/models/completion-usage.ts +++ b/lib/packages/fabro-api-client/src/models/completion-usage.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/conclusion.ts b/lib/packages/fabro-api-client/src/models/conclusion.ts index f687ddf24..e7a3c8da4 100644 --- a/lib/packages/fabro-api-client/src/models/conclusion.ts +++ b/lib/packages/fabro-api-client/src/models/conclusion.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/cost-source.ts b/lib/packages/fabro-api-client/src/models/cost-source.ts index 172115254..9e91b3d6f 100644 --- a/lib/packages/fabro-api-client/src/models/cost-source.ts +++ b/lib/packages/fabro-api-client/src/models/cost-source.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-automation-request.ts b/lib/packages/fabro-api-client/src/models/create-automation-request.ts index ce99d34db..47669ad5d 100644 --- a/lib/packages/fabro-api-client/src/models/create-automation-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-automation-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-completion-request.ts b/lib/packages/fabro-api-client/src/models/create-completion-request.ts index 836e20c0d..c59f7869d 100644 --- a/lib/packages/fabro-api-client/src/models/create-completion-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-completion-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-environment-request.ts b/lib/packages/fabro-api-client/src/models/create-environment-request.ts index fd4d48006..8ae7a1dd9 100644 --- a/lib/packages/fabro-api-client/src/models/create-environment-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-environment-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-mcp-server-request.ts b/lib/packages/fabro-api-client/src/models/create-mcp-server-request.ts index 058a3f7e3..bf8af246c 100644 --- a/lib/packages/fabro-api-client/src/models/create-mcp-server-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-mcp-server-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-playground-chat-request.ts b/lib/packages/fabro-api-client/src/models/create-playground-chat-request.ts index 1d1ddc216..7c17866ec 100644 --- a/lib/packages/fabro-api-client/src/models/create-playground-chat-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-playground-chat-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-run-pull-request-request.ts b/lib/packages/fabro-api-client/src/models/create-run-pull-request-request.ts index 7ec1a3020..5c0473b53 100644 --- a/lib/packages/fabro-api-client/src/models/create-run-pull-request-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-run-pull-request-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-run-session-request.ts b/lib/packages/fabro-api-client/src/models/create-run-session-request.ts index f079d1b01..2c2d7a939 100644 --- a/lib/packages/fabro-api-client/src/models/create-run-session-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-run-session-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-secret-request.ts b/lib/packages/fabro-api-client/src/models/create-secret-request.ts index 5061fd4a7..856e826a3 100644 --- a/lib/packages/fabro-api-client/src/models/create-secret-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-secret-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-variable-request.ts b/lib/packages/fabro-api-client/src/models/create-variable-request.ts index f33de18d1..5f5cef14d 100644 --- a/lib/packages/fabro-api-client/src/models/create-variable-request.ts +++ b/lib/packages/fabro-api-client/src/models/create-variable-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts b/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts index 79ede2d1b..284fda5a5 100644 --- a/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts +++ b/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/delete-run-response.ts b/lib/packages/fabro-api-client/src/models/delete-run-response.ts index 696f161ca..587f58ecf 100644 --- a/lib/packages/fabro-api-client/src/models/delete-run-response.ts +++ b/lib/packages/fabro-api-client/src/models/delete-run-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/delete-run-sandbox.ts b/lib/packages/fabro-api-client/src/models/delete-run-sandbox.ts index 349ce716d..bdcf34a0c 100644 --- a/lib/packages/fabro-api-client/src/models/delete-run-sandbox.ts +++ b/lib/packages/fabro-api-client/src/models/delete-run-sandbox.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/delete-secret-request.ts b/lib/packages/fabro-api-client/src/models/delete-secret-request.ts index 34e00310b..e7282b932 100644 --- a/lib/packages/fabro-api-client/src/models/delete-secret-request.ts +++ b/lib/packages/fabro-api-client/src/models/delete-secret-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/deny-run-request.ts b/lib/packages/fabro-api-client/src/models/deny-run-request.ts index b7ae55fb0..00055cdda 100644 --- a/lib/packages/fabro-api-client/src/models/deny-run-request.ts +++ b/lib/packages/fabro-api-client/src/models/deny-run-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/dev-token-login-request.ts b/lib/packages/fabro-api-client/src/models/dev-token-login-request.ts index 8dd541b95..2ca322d2c 100644 --- a/lib/packages/fabro-api-client/src/models/dev-token-login-request.ts +++ b/lib/packages/fabro-api-client/src/models/dev-token-login-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/dev-token-login-response.ts b/lib/packages/fabro-api-client/src/models/dev-token-login-response.ts index 8492cc69c..7ffa09c89 100644 --- a/lib/packages/fabro-api-client/src/models/dev-token-login-response.ts +++ b/lib/packages/fabro-api-client/src/models/dev-token-login-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/diagnostics-check.ts b/lib/packages/fabro-api-client/src/models/diagnostics-check.ts index efa40dc67..d9887001f 100644 --- a/lib/packages/fabro-api-client/src/models/diagnostics-check.ts +++ b/lib/packages/fabro-api-client/src/models/diagnostics-check.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/diagnostics-detail.ts b/lib/packages/fabro-api-client/src/models/diagnostics-detail.ts index 538f48eb8..6b8812eb4 100644 --- a/lib/packages/fabro-api-client/src/models/diagnostics-detail.ts +++ b/lib/packages/fabro-api-client/src/models/diagnostics-detail.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/diagnostics-report.ts b/lib/packages/fabro-api-client/src/models/diagnostics-report.ts index 55d32af76..36684c310 100644 --- a/lib/packages/fabro-api-client/src/models/diagnostics-report.ts +++ b/lib/packages/fabro-api-client/src/models/diagnostics-report.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/diagnostics-section.ts b/lib/packages/fabro-api-client/src/models/diagnostics-section.ts index d21f0c3ca..a0bba8977 100644 --- a/lib/packages/fabro-api-client/src/models/diagnostics-section.ts +++ b/lib/packages/fabro-api-client/src/models/diagnostics-section.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/diff-file.ts b/lib/packages/fabro-api-client/src/models/diff-file.ts index ca7126713..4bce821f6 100644 --- a/lib/packages/fabro-api-client/src/models/diff-file.ts +++ b/lib/packages/fabro-api-client/src/models/diff-file.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/diff-stats.ts b/lib/packages/fabro-api-client/src/models/diff-stats.ts index b86968c04..3c55280a9 100644 --- a/lib/packages/fabro-api-client/src/models/diff-stats.ts +++ b/lib/packages/fabro-api-client/src/models/diff-stats.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/diff-summary.ts b/lib/packages/fabro-api-client/src/models/diff-summary.ts index f5dbcea5e..fdbf5b6d3 100644 --- a/lib/packages/fabro-api-client/src/models/diff-summary.ts +++ b/lib/packages/fabro-api-client/src/models/diff-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/dirty-status.ts b/lib/packages/fabro-api-client/src/models/dirty-status.ts index 2a89a3c2a..9857b7061 100644 --- a/lib/packages/fabro-api-client/src/models/dirty-status.ts +++ b/lib/packages/fabro-api-client/src/models/dirty-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/disk-usage-response.ts b/lib/packages/fabro-api-client/src/models/disk-usage-response.ts index f9573e986..4410fe07c 100644 --- a/lib/packages/fabro-api-client/src/models/disk-usage-response.ts +++ b/lib/packages/fabro-api-client/src/models/disk-usage-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/disk-usage-run-row.ts b/lib/packages/fabro-api-client/src/models/disk-usage-run-row.ts index 13392839f..84697b185 100644 --- a/lib/packages/fabro-api-client/src/models/disk-usage-run-row.ts +++ b/lib/packages/fabro-api-client/src/models/disk-usage-run-row.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/disk-usage-summary-row.ts b/lib/packages/fabro-api-client/src/models/disk-usage-summary-row.ts index 3fe75b0fd..d99cda3c6 100644 --- a/lib/packages/fabro-api-client/src/models/disk-usage-summary-row.ts +++ b/lib/packages/fabro-api-client/src/models/disk-usage-summary-row.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/dockerfile-source-inline.ts b/lib/packages/fabro-api-client/src/models/dockerfile-source-inline.ts index 296eb3d94..7bc18793c 100644 --- a/lib/packages/fabro-api-client/src/models/dockerfile-source-inline.ts +++ b/lib/packages/fabro-api-client/src/models/dockerfile-source-inline.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/dockerfile-source-path.ts b/lib/packages/fabro-api-client/src/models/dockerfile-source-path.ts index 2510b3634..888514e9c 100644 --- a/lib/packages/fabro-api-client/src/models/dockerfile-source-path.ts +++ b/lib/packages/fabro-api-client/src/models/dockerfile-source-path.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/dockerfile-source.ts b/lib/packages/fabro-api-client/src/models/dockerfile-source.ts index dfb2ef280..4a397e53b 100644 --- a/lib/packages/fabro-api-client/src/models/dockerfile-source.ts +++ b/lib/packages/fabro-api-client/src/models/dockerfile-source.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-api-dockerfile-source-inline.ts b/lib/packages/fabro-api-client/src/models/environment-api-dockerfile-source-inline.ts index 35180b10a..83b3a1dbc 100644 --- a/lib/packages/fabro-api-client/src/models/environment-api-dockerfile-source-inline.ts +++ b/lib/packages/fabro-api-client/src/models/environment-api-dockerfile-source-inline.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-api-image-settings.ts b/lib/packages/fabro-api-client/src/models/environment-api-image-settings.ts index 54578b0cd..c41e8d3c0 100644 --- a/lib/packages/fabro-api-client/src/models/environment-api-image-settings.ts +++ b/lib/packages/fabro-api-client/src/models/environment-api-image-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-image-settings.ts b/lib/packages/fabro-api-client/src/models/environment-image-settings.ts index 997f1d64c..e2b4f60e6 100644 --- a/lib/packages/fabro-api-client/src/models/environment-image-settings.ts +++ b/lib/packages/fabro-api-client/src/models/environment-image-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-lifecycle-settings.ts b/lib/packages/fabro-api-client/src/models/environment-lifecycle-settings.ts index f9a2426a9..018fff3ff 100644 --- a/lib/packages/fabro-api-client/src/models/environment-lifecycle-settings.ts +++ b/lib/packages/fabro-api-client/src/models/environment-lifecycle-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-list-meta.ts b/lib/packages/fabro-api-client/src/models/environment-list-meta.ts index aa92bad87..d198d6a46 100644 --- a/lib/packages/fabro-api-client/src/models/environment-list-meta.ts +++ b/lib/packages/fabro-api-client/src/models/environment-list-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-list-response.ts b/lib/packages/fabro-api-client/src/models/environment-list-response.ts index cf25725b1..f4ef942cf 100644 --- a/lib/packages/fabro-api-client/src/models/environment-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/environment-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-network-mode.ts b/lib/packages/fabro-api-client/src/models/environment-network-mode.ts index ab4772098..6da5fec7c 100644 --- a/lib/packages/fabro-api-client/src/models/environment-network-mode.ts +++ b/lib/packages/fabro-api-client/src/models/environment-network-mode.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-network-settings.ts b/lib/packages/fabro-api-client/src/models/environment-network-settings.ts index 8bdf99fb6..50eff443d 100644 --- a/lib/packages/fabro-api-client/src/models/environment-network-settings.ts +++ b/lib/packages/fabro-api-client/src/models/environment-network-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-provider.ts b/lib/packages/fabro-api-client/src/models/environment-provider.ts index 0761c4b4c..bee7f45e0 100644 --- a/lib/packages/fabro-api-client/src/models/environment-provider.ts +++ b/lib/packages/fabro-api-client/src/models/environment-provider.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-resources-settings.ts b/lib/packages/fabro-api-client/src/models/environment-resources-settings.ts index c17caaf36..e83fbc46f 100644 --- a/lib/packages/fabro-api-client/src/models/environment-resources-settings.ts +++ b/lib/packages/fabro-api-client/src/models/environment-resources-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment-settings.ts b/lib/packages/fabro-api-client/src/models/environment-settings.ts index 7f5f9f007..01e501724 100644 --- a/lib/packages/fabro-api-client/src/models/environment-settings.ts +++ b/lib/packages/fabro-api-client/src/models/environment-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/environment.ts b/lib/packages/fabro-api-client/src/models/environment.ts index 28037d144..38b8847c0 100644 --- a/lib/packages/fabro-api-client/src/models/environment.ts +++ b/lib/packages/fabro-api-client/src/models/environment.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/error-response-entry.ts b/lib/packages/fabro-api-client/src/models/error-response-entry.ts index 8d2095c92..081416711 100644 --- a/lib/packages/fabro-api-client/src/models/error-response-entry.ts +++ b/lib/packages/fabro-api-client/src/models/error-response-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/error-response.ts b/lib/packages/fabro-api-client/src/models/error-response.ts index 95c8ef4d3..d66e866b5 100644 --- a/lib/packages/fabro-api-client/src/models/error-response.ts +++ b/lib/packages/fabro-api-client/src/models/error-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/event-envelope.ts b/lib/packages/fabro-api-client/src/models/event-envelope.ts index 8ed673472..1ff9fe51d 100644 --- a/lib/packages/fabro-api-client/src/models/event-envelope.ts +++ b/lib/packages/fabro-api-client/src/models/event-envelope.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/event-seq.ts b/lib/packages/fabro-api-client/src/models/event-seq.ts index d336fddf6..948345b27 100644 --- a/lib/packages/fabro-api-client/src/models/event-seq.ts +++ b/lib/packages/fabro-api-client/src/models/event-seq.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/exec-output-tail.ts b/lib/packages/fabro-api-client/src/models/exec-output-tail.ts index 1405c08c2..9ea534709 100644 --- a/lib/packages/fabro-api-client/src/models/exec-output-tail.ts +++ b/lib/packages/fabro-api-client/src/models/exec-output-tail.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/execute-query-request.ts b/lib/packages/fabro-api-client/src/models/execute-query-request.ts index f1c8041ec..3b0ceeba8 100644 --- a/lib/packages/fabro-api-client/src/models/execute-query-request.ts +++ b/lib/packages/fabro-api-client/src/models/execute-query-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/execute-query-response-rows-inner-inner.ts b/lib/packages/fabro-api-client/src/models/execute-query-response-rows-inner-inner.ts index 8201a9181..ab3f4b323 100644 --- a/lib/packages/fabro-api-client/src/models/execute-query-response-rows-inner-inner.ts +++ b/lib/packages/fabro-api-client/src/models/execute-query-response-rows-inner-inner.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/execute-query-response.ts b/lib/packages/fabro-api-client/src/models/execute-query-response.ts index d3bb5d984..eaf2aea4f 100644 --- a/lib/packages/fabro-api-client/src/models/execute-query-response.ts +++ b/lib/packages/fabro-api-client/src/models/execute-query-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/failure-category.ts b/lib/packages/fabro-api-client/src/models/failure-category.ts index c9460754e..cedcc3c27 100644 --- a/lib/packages/fabro-api-client/src/models/failure-category.ts +++ b/lib/packages/fabro-api-client/src/models/failure-category.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/failure-detail.ts b/lib/packages/fabro-api-client/src/models/failure-detail.ts index c50c2441a..f8c6beb33 100644 --- a/lib/packages/fabro-api-client/src/models/failure-detail.ts +++ b/lib/packages/fabro-api-client/src/models/failure-detail.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/failure-reason.ts b/lib/packages/fabro-api-client/src/models/failure-reason.ts index 79172e887..1f0e787e1 100644 --- a/lib/packages/fabro-api-client/src/models/failure-reason.ts +++ b/lib/packages/fabro-api-client/src/models/failure-reason.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/file-checkpoint.ts b/lib/packages/fabro-api-client/src/models/file-checkpoint.ts index b1c41271c..4d670d20a 100644 --- a/lib/packages/fabro-api-client/src/models/file-checkpoint.ts +++ b/lib/packages/fabro-api-client/src/models/file-checkpoint.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/file-diff.ts b/lib/packages/fabro-api-client/src/models/file-diff.ts index 1e16e718c..320a22c54 100644 --- a/lib/packages/fabro-api-client/src/models/file-diff.ts +++ b/lib/packages/fabro-api-client/src/models/file-diff.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/fork-request.ts b/lib/packages/fabro-api-client/src/models/fork-request.ts index 6b5e4fce8..4f68ca46f 100644 --- a/lib/packages/fabro-api-client/src/models/fork-request.ts +++ b/lib/packages/fabro-api-client/src/models/fork-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/fork-response.ts b/lib/packages/fabro-api-client/src/models/fork-response.ts index 3099927c0..c3cef2801 100644 --- a/lib/packages/fabro-api-client/src/models/fork-response.ts +++ b/lib/packages/fabro-api-client/src/models/fork-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/fork-source-ref.ts b/lib/packages/fabro-api-client/src/models/fork-source-ref.ts index 8ee8e63f2..7f03a2f3f 100644 --- a/lib/packages/fabro-api-client/src/models/fork-source-ref.ts +++ b/lib/packages/fabro-api-client/src/models/fork-source-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/git-author-settings.ts b/lib/packages/fabro-api-client/src/models/git-author-settings.ts index 619c72dda..55aa32893 100644 --- a/lib/packages/fabro-api-client/src/models/git-author-settings.ts +++ b/lib/packages/fabro-api-client/src/models/git-author-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/git-context.ts b/lib/packages/fabro-api-client/src/models/git-context.ts index 20c205643..61e8a94f2 100644 --- a/lib/packages/fabro-api-client/src/models/git-context.ts +++ b/lib/packages/fabro-api-client/src/models/git-context.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/github-integration-settings.ts b/lib/packages/fabro-api-client/src/models/github-integration-settings.ts index 6289d6143..fa6bdfcac 100644 --- a/lib/packages/fabro-api-client/src/models/github-integration-settings.ts +++ b/lib/packages/fabro-api-client/src/models/github-integration-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts b/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts index 9b1e1aa37..37e8d8134 100644 --- a/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts +++ b/lib/packages/fabro-api-client/src/models/github-integration-strategy.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/health-response.ts b/lib/packages/fabro-api-client/src/models/health-response.ts index 8dce26d2c..2dc9014b0 100644 --- a/lib/packages/fabro-api-client/src/models/health-response.ts +++ b/lib/packages/fabro-api-client/src/models/health-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/history-entry.ts b/lib/packages/fabro-api-client/src/models/history-entry.ts index 2c491cc36..fab843c64 100644 --- a/lib/packages/fabro-api-client/src/models/history-entry.ts +++ b/lib/packages/fabro-api-client/src/models/history-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/hook-definition.ts b/lib/packages/fabro-api-client/src/models/hook-definition.ts index a8b2100e8..83a009767 100644 --- a/lib/packages/fabro-api-client/src/models/hook-definition.ts +++ b/lib/packages/fabro-api-client/src/models/hook-definition.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/hook-event.ts b/lib/packages/fabro-api-client/src/models/hook-event.ts index aabc20ecd..ab3196dd4 100644 --- a/lib/packages/fabro-api-client/src/models/hook-event.ts +++ b/lib/packages/fabro-api-client/src/models/hook-event.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/idp-identity.ts b/lib/packages/fabro-api-client/src/models/idp-identity.ts index 6eb4dd167..6aefab89a 100644 --- a/lib/packages/fabro-api-client/src/models/idp-identity.ts +++ b/lib/packages/fabro-api-client/src/models/idp-identity.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-finish-response.ts b/lib/packages/fabro-api-client/src/models/install-finish-response.ts index 32fd470d9..3686a9175 100644 --- a/lib/packages/fabro-api-client/src/models/install-finish-response.ts +++ b/lib/packages/fabro-api-client/src/models/install-finish-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-github-app-manifest-input.ts b/lib/packages/fabro-api-client/src/models/install-github-app-manifest-input.ts index aaeddfb88..75d2d40f6 100644 --- a/lib/packages/fabro-api-client/src/models/install-github-app-manifest-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-github-app-manifest-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-github-app-manifest-response.ts b/lib/packages/fabro-api-client/src/models/install-github-app-manifest-response.ts index 12e1b8d64..92de33a62 100644 --- a/lib/packages/fabro-api-client/src/models/install-github-app-manifest-response.ts +++ b/lib/packages/fabro-api-client/src/models/install-github-app-manifest-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-github-app-owner.ts b/lib/packages/fabro-api-client/src/models/install-github-app-owner.ts index 93afa2404..d2b1ab048 100644 --- a/lib/packages/fabro-api-client/src/models/install-github-app-owner.ts +++ b/lib/packages/fabro-api-client/src/models/install-github-app-owner.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-github-summary.ts b/lib/packages/fabro-api-client/src/models/install-github-summary.ts index c9cc30b79..13b5ae416 100644 --- a/lib/packages/fabro-api-client/src/models/install-github-summary.ts +++ b/lib/packages/fabro-api-client/src/models/install-github-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-github-token-input.ts b/lib/packages/fabro-api-client/src/models/install-github-token-input.ts index c1dab44a5..bbc488c60 100644 --- a/lib/packages/fabro-api-client/src/models/install-github-token-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-github-token-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-github-token-test-input.ts b/lib/packages/fabro-api-client/src/models/install-github-token-test-input.ts index d147b0f5d..e970fe2c8 100644 --- a/lib/packages/fabro-api-client/src/models/install-github-token-test-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-github-token-test-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-github-token-test-response.ts b/lib/packages/fabro-api-client/src/models/install-github-token-test-response.ts index 1d8d74406..9f7b1b86b 100644 --- a/lib/packages/fabro-api-client/src/models/install-github-token-test-response.ts +++ b/lib/packages/fabro-api-client/src/models/install-github-token-test-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-llm-provider-input.ts b/lib/packages/fabro-api-client/src/models/install-llm-provider-input.ts index 3f192e9e6..79bc37e38 100644 --- a/lib/packages/fabro-api-client/src/models/install-llm-provider-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-llm-provider-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-llm-providers-input.ts b/lib/packages/fabro-api-client/src/models/install-llm-providers-input.ts index 7e83d5a13..6ea5a58f0 100644 --- a/lib/packages/fabro-api-client/src/models/install-llm-providers-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-llm-providers-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-llm-summary-providers-inner.ts b/lib/packages/fabro-api-client/src/models/install-llm-summary-providers-inner.ts index ff4eaffe7..001f84689 100644 --- a/lib/packages/fabro-api-client/src/models/install-llm-summary-providers-inner.ts +++ b/lib/packages/fabro-api-client/src/models/install-llm-summary-providers-inner.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-llm-summary.ts b/lib/packages/fabro-api-client/src/models/install-llm-summary.ts index 04042cd75..cee52f3bb 100644 --- a/lib/packages/fabro-api-client/src/models/install-llm-summary.ts +++ b/lib/packages/fabro-api-client/src/models/install-llm-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-llm-test-input.ts b/lib/packages/fabro-api-client/src/models/install-llm-test-input.ts index 45377472f..b0d33fe73 100644 --- a/lib/packages/fabro-api-client/src/models/install-llm-test-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-llm-test-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-llm-validation-response.ts b/lib/packages/fabro-api-client/src/models/install-llm-validation-response.ts index 22e904afd..bd4a4afce 100644 --- a/lib/packages/fabro-api-client/src/models/install-llm-validation-response.ts +++ b/lib/packages/fabro-api-client/src/models/install-llm-validation-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-object-store-input.ts b/lib/packages/fabro-api-client/src/models/install-object-store-input.ts index 8dd8f497d..7b611b8a9 100644 --- a/lib/packages/fabro-api-client/src/models/install-object-store-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-object-store-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts b/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts index 47c32b0ba..2dee0735f 100644 --- a/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts +++ b/lib/packages/fabro-api-client/src/models/install-object-store-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-object-store-validation-response.ts b/lib/packages/fabro-api-client/src/models/install-object-store-validation-response.ts index 6785e09b3..d0d83b92f 100644 --- a/lib/packages/fabro-api-client/src/models/install-object-store-validation-response.ts +++ b/lib/packages/fabro-api-client/src/models/install-object-store-validation-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-prefill.ts b/lib/packages/fabro-api-client/src/models/install-prefill.ts index 1c20eb029..6ddc45ff7 100644 --- a/lib/packages/fabro-api-client/src/models/install-prefill.ts +++ b/lib/packages/fabro-api-client/src/models/install-prefill.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-sandbox-input.ts b/lib/packages/fabro-api-client/src/models/install-sandbox-input.ts index b29a427f9..b806a7ac2 100644 --- a/lib/packages/fabro-api-client/src/models/install-sandbox-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-sandbox-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-sandbox-summary.ts b/lib/packages/fabro-api-client/src/models/install-sandbox-summary.ts index 6eb3a2422..4ed968718 100644 --- a/lib/packages/fabro-api-client/src/models/install-sandbox-summary.ts +++ b/lib/packages/fabro-api-client/src/models/install-sandbox-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-sandbox-validation-response.ts b/lib/packages/fabro-api-client/src/models/install-sandbox-validation-response.ts index 863e179a3..d8592b1f7 100644 --- a/lib/packages/fabro-api-client/src/models/install-sandbox-validation-response.ts +++ b/lib/packages/fabro-api-client/src/models/install-sandbox-validation-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-server-config-input.ts b/lib/packages/fabro-api-client/src/models/install-server-config-input.ts index 7e9c30a73..1418f78f2 100644 --- a/lib/packages/fabro-api-client/src/models/install-server-config-input.ts +++ b/lib/packages/fabro-api-client/src/models/install-server-config-input.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/install-session-response.ts b/lib/packages/fabro-api-client/src/models/install-session-response.ts index 9eecbb3b0..6ae325b1e 100644 --- a/lib/packages/fabro-api-client/src/models/install-session-response.ts +++ b/lib/packages/fabro-api-client/src/models/install-session-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/integration-connection-kind.ts b/lib/packages/fabro-api-client/src/models/integration-connection-kind.ts index 6d3b6fcfe..7d213d953 100644 --- a/lib/packages/fabro-api-client/src/models/integration-connection-kind.ts +++ b/lib/packages/fabro-api-client/src/models/integration-connection-kind.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/integration-connection-state.ts b/lib/packages/fabro-api-client/src/models/integration-connection-state.ts index ff9696c5e..d88ce1bbc 100644 --- a/lib/packages/fabro-api-client/src/models/integration-connection-state.ts +++ b/lib/packages/fabro-api-client/src/models/integration-connection-state.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/integration-connection-status.ts b/lib/packages/fabro-api-client/src/models/integration-connection-status.ts index 6916f4e79..985f05421 100644 --- a/lib/packages/fabro-api-client/src/models/integration-connection-status.ts +++ b/lib/packages/fabro-api-client/src/models/integration-connection-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/integration-provider.ts b/lib/packages/fabro-api-client/src/models/integration-provider.ts index 2c98029ba..ddffa9b53 100644 --- a/lib/packages/fabro-api-client/src/models/integration-provider.ts +++ b/lib/packages/fabro-api-client/src/models/integration-provider.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/integration-status.ts b/lib/packages/fabro-api-client/src/models/integration-status.ts index 680dd34eb..c7687b17b 100644 --- a/lib/packages/fabro-api-client/src/models/integration-status.ts +++ b/lib/packages/fabro-api-client/src/models/integration-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts index 367922a74..41833bf0b 100644 --- a/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts +++ b/lib/packages/fabro-api-client/src/models/integration-webhooks-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/interview-option.ts b/lib/packages/fabro-api-client/src/models/interview-option.ts index 16c536f80..40d83144e 100644 --- a/lib/packages/fabro-api-client/src/models/interview-option.ts +++ b/lib/packages/fabro-api-client/src/models/interview-option.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/interview-provider-settings.ts b/lib/packages/fabro-api-client/src/models/interview-provider-settings.ts index 34d8d5c0e..84afb95df 100644 --- a/lib/packages/fabro-api-client/src/models/interview-provider-settings.ts +++ b/lib/packages/fabro-api-client/src/models/interview-provider-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/interview-question-record.ts b/lib/packages/fabro-api-client/src/models/interview-question-record.ts index 2f2ecd2b8..1a7883615 100644 --- a/lib/packages/fabro-api-client/src/models/interview-question-record.ts +++ b/lib/packages/fabro-api-client/src/models/interview-question-record.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/link-run-pull-request-request.ts b/lib/packages/fabro-api-client/src/models/link-run-pull-request-request.ts index ad2f7e508..ff4ec1d89 100644 --- a/lib/packages/fabro-api-client/src/models/link-run-pull-request-request.ts +++ b/lib/packages/fabro-api-client/src/models/link-run-pull-request-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/llm-output-kind.ts b/lib/packages/fabro-api-client/src/models/llm-output-kind.ts index 1c9663257..4dee9f9ff 100644 --- a/lib/packages/fabro-api-client/src/models/llm-output-kind.ts +++ b/lib/packages/fabro-api-client/src/models/llm-output-kind.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/log-destination.ts b/lib/packages/fabro-api-client/src/models/log-destination.ts index 6e7caf66f..22ab4f10d 100644 --- a/lib/packages/fabro-api-client/src/models/log-destination.ts +++ b/lib/packages/fabro-api-client/src/models/log-destination.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-args.ts b/lib/packages/fabro-api-client/src/models/manifest-args.ts index c0ae3985e..1430fc35c 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-args.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-args.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-config.ts b/lib/packages/fabro-api-client/src/models/manifest-config.ts index c501c3094..e1493fb49 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-config.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-config.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-file-entry.ts b/lib/packages/fabro-api-client/src/models/manifest-file-entry.ts index 8f2cfe38f..1a6edae4d 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-file-entry.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-file-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-file-ref.ts b/lib/packages/fabro-api-client/src/models/manifest-file-ref.ts index e0a476878..5a966f00b 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-file-ref.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-file-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-goal.ts b/lib/packages/fabro-api-client/src/models/manifest-goal.ts index 377b6e3ac..4b0182f72 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-goal.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-goal.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-target.ts b/lib/packages/fabro-api-client/src/models/manifest-target.ts index c78249f32..527b51e3b 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-target.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-target.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-workflow-config.ts b/lib/packages/fabro-api-client/src/models/manifest-workflow-config.ts index 960745f81..9f1e4692b 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-workflow-config.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-workflow-config.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/manifest-workflow.ts b/lib/packages/fabro-api-client/src/models/manifest-workflow.ts index 222092b7a..db9db35bb 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-workflow.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-workflow.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts b/lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts index c03287667..b927e2418 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-list-meta.ts b/lib/packages/fabro-api-client/src/models/mcp-server-list-meta.ts index a90b708eb..8d1a82cbd 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-list-meta.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-list-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-list-response.ts b/lib/packages/fabro-api-client/src/models/mcp-server-list-response.ts index f1a6ffe39..85839b509 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts b/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts index 8b9f0e19e..6d6f01393 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-settings.ts b/lib/packages/fabro-api-client/src/models/mcp-server-settings.ts index b84ea5675..e4f8ed723 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-settings.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts index acee4291f..c644eba1d 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts index ab82276ed..58dbbd421 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-status.ts b/lib/packages/fabro-api-client/src/models/mcp-server-status.ts index 50af943f0..eb898822a 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server-status.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-server.ts b/lib/packages/fabro-api-client/src/models/mcp-server.ts index e791b7feb..163010400 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-server.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-server.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts index af9b8991c..61fafcc16 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts index e534632b9..e8f789f10 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts index 27a3d7270..6acdf9dde 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view-http.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view-http.ts index 724f99612..77739ba5d 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-view-http.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view-http.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view-sandbox.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view-sandbox.ts index ea01a6cab..d0be5f749 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-view-sandbox.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view-sandbox.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view-stdio.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view-stdio.ts index 03fd5665f..b48fb8e6d 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-view-stdio.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view-stdio.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view.ts index f9761b5a8..30dbd8ad7 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-view.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport.ts b/lib/packages/fabro-api-client/src/models/mcp-transport.ts index 611215bdd..cea33b117 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/merge-method.ts b/lib/packages/fabro-api-client/src/models/merge-method.ts index a5f0a71d7..425baf91e 100644 --- a/lib/packages/fabro-api-client/src/models/merge-method.ts +++ b/lib/packages/fabro-api-client/src/models/merge-method.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/merge-run-pull-request-request.ts b/lib/packages/fabro-api-client/src/models/merge-run-pull-request-request.ts index f86dea1ea..ac9f7d148 100644 --- a/lib/packages/fabro-api-client/src/models/merge-run-pull-request-request.ts +++ b/lib/packages/fabro-api-client/src/models/merge-run-pull-request-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/merge-run-pull-request-response.ts b/lib/packages/fabro-api-client/src/models/merge-run-pull-request-response.ts index 9dd748a21..eb473b1df 100644 --- a/lib/packages/fabro-api-client/src/models/merge-run-pull-request-response.ts +++ b/lib/packages/fabro-api-client/src/models/merge-run-pull-request-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model-controls.ts b/lib/packages/fabro-api-client/src/models/model-controls.ts index 59f22ce2a..55babaee6 100644 --- a/lib/packages/fabro-api-client/src/models/model-controls.ts +++ b/lib/packages/fabro-api-client/src/models/model-controls.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model-costs.ts b/lib/packages/fabro-api-client/src/models/model-costs.ts index 97ab64e48..511ee967f 100644 --- a/lib/packages/fabro-api-client/src/models/model-costs.ts +++ b/lib/packages/fabro-api-client/src/models/model-costs.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model-features.ts b/lib/packages/fabro-api-client/src/models/model-features.ts index 5f23381b3..42fcae9d6 100644 --- a/lib/packages/fabro-api-client/src/models/model-features.ts +++ b/lib/packages/fabro-api-client/src/models/model-features.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model-limits.ts b/lib/packages/fabro-api-client/src/models/model-limits.ts index c915d36b5..ed22b4b40 100644 --- a/lib/packages/fabro-api-client/src/models/model-limits.ts +++ b/lib/packages/fabro-api-client/src/models/model-limits.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model-reference.ts b/lib/packages/fabro-api-client/src/models/model-reference.ts index a8013e1e9..30a8eed69 100644 --- a/lib/packages/fabro-api-client/src/models/model-reference.ts +++ b/lib/packages/fabro-api-client/src/models/model-reference.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model-test-mode.ts b/lib/packages/fabro-api-client/src/models/model-test-mode.ts index 2aca92dda..4e83d41dd 100644 --- a/lib/packages/fabro-api-client/src/models/model-test-mode.ts +++ b/lib/packages/fabro-api-client/src/models/model-test-mode.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model-test-result.ts b/lib/packages/fabro-api-client/src/models/model-test-result.ts index cba04c056..8d6bbe596 100644 --- a/lib/packages/fabro-api-client/src/models/model-test-result.ts +++ b/lib/packages/fabro-api-client/src/models/model-test-result.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/model.ts b/lib/packages/fabro-api-client/src/models/model.ts index 0bf86bee5..0de1f0971 100644 --- a/lib/packages/fabro-api-client/src/models/model.ts +++ b/lib/packages/fabro-api-client/src/models/model.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/notification-provider-settings.ts b/lib/packages/fabro-api-client/src/models/notification-provider-settings.ts index 9149b9342..d430995d7 100644 --- a/lib/packages/fabro-api-client/src/models/notification-provider-settings.ts +++ b/lib/packages/fabro-api-client/src/models/notification-provider-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/notification-route-settings.ts b/lib/packages/fabro-api-client/src/models/notification-route-settings.ts index 3e80ebd8c..1312f85e1 100644 --- a/lib/packages/fabro-api-client/src/models/notification-route-settings.ts +++ b/lib/packages/fabro-api-client/src/models/notification-route-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts index b16c42346..c0f138e6b 100644 --- a/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts +++ b/lib/packages/fabro-api-client/src/models/object-store-local-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts index fb0550698..f82da9357 100644 --- a/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts +++ b/lib/packages/fabro-api-client/src/models/object-store-s3-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/object-store-settings.ts b/lib/packages/fabro-api-client/src/models/object-store-settings.ts index 2099b6752..c33939d8c 100644 --- a/lib/packages/fabro-api-client/src/models/object-store-settings.ts +++ b/lib/packages/fabro-api-client/src/models/object-store-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-api-question-list.ts b/lib/packages/fabro-api-client/src/models/paginated-api-question-list.ts index 8f5cb1a03..a246b2aa0 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-api-question-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-api-question-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-event-list.ts b/lib/packages/fabro-api-client/src/models/paginated-event-list.ts index cf2934f56..1d1dddd14 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-event-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-event-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-history-entry-list.ts b/lib/packages/fabro-api-client/src/models/paginated-history-entry-list.ts index c8c2661e7..3e1af07d1 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-history-entry-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-history-entry-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-model-list.ts b/lib/packages/fabro-api-client/src/models/paginated-model-list.ts index 2760f8335..abac7b698 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-model-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-model-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-run-commit-list.ts b/lib/packages/fabro-api-client/src/models/paginated-run-commit-list.ts index 1290d80d2..2ffb7f6d5 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-run-commit-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-run-commit-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts b/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts index 1d84791af..8c27bf6db 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-run-file-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-run-list.ts b/lib/packages/fabro-api-client/src/models/paginated-run-list.ts index 5aa510d61..df9965b8d 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-run-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-run-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-run-stage-list.ts b/lib/packages/fabro-api-client/src/models/paginated-run-stage-list.ts index 14862230b..4b6d2ff68 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-run-stage-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-run-stage-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-saved-query-list.ts b/lib/packages/fabro-api-client/src/models/paginated-saved-query-list.ts index 9af8533df..5d2df675a 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-saved-query-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-saved-query-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-session-list.ts b/lib/packages/fabro-api-client/src/models/paginated-session-list.ts index 25dc0cf9a..db90f4e6f 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-session-list.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-session-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/paginated-workflow-list-response.ts b/lib/packages/fabro-api-client/src/models/paginated-workflow-list-response.ts index 59e1a99c8..074575a22 100644 --- a/lib/packages/fabro-api-client/src/models/paginated-workflow-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/paginated-workflow-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pagination-meta.ts b/lib/packages/fabro-api-client/src/models/pagination-meta.ts index 963f781a4..c91ae9bb2 100644 --- a/lib/packages/fabro-api-client/src/models/pagination-meta.ts +++ b/lib/packages/fabro-api-client/src/models/pagination-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-message-record.ts b/lib/packages/fabro-api-client/src/models/pair-message-record.ts index d8d413209..1754e078f 100644 --- a/lib/packages/fabro-api-client/src/models/pair-message-record.ts +++ b/lib/packages/fabro-api-client/src/models/pair-message-record.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-message-request.ts b/lib/packages/fabro-api-client/src/models/pair-message-request.ts index 2a6607d94..435ec6dbf 100644 --- a/lib/packages/fabro-api-client/src/models/pair-message-request.ts +++ b/lib/packages/fabro-api-client/src/models/pair-message-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-record.ts b/lib/packages/fabro-api-client/src/models/pair-record.ts index 34b029029..aa34088d6 100644 --- a/lib/packages/fabro-api-client/src/models/pair-record.ts +++ b/lib/packages/fabro-api-client/src/models/pair-record.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-start-request.ts b/lib/packages/fabro-api-client/src/models/pair-start-request.ts index 73853b374..d5857d0c6 100644 --- a/lib/packages/fabro-api-client/src/models/pair-start-request.ts +++ b/lib/packages/fabro-api-client/src/models/pair-start-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-status.ts b/lib/packages/fabro-api-client/src/models/pair-status.ts index 8b64c2256..7b41c0481 100644 --- a/lib/packages/fabro-api-client/src/models/pair-status.ts +++ b/lib/packages/fabro-api-client/src/models/pair-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-target.ts b/lib/packages/fabro-api-client/src/models/pair-target.ts index 40513b26d..0ed0a7fa7 100644 --- a/lib/packages/fabro-api-client/src/models/pair-target.ts +++ b/lib/packages/fabro-api-client/src/models/pair-target.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message.ts index b3d603fa4..ecb97f5b3 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-detail-ref.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-detail-ref.ts index 7c168a332..87ce0be57 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-detail-ref.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-detail-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-entry.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-entry.ts index a6e3b8aa2..9ff0f4cf6 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-entry.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-error.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-error.ts index ccab55770..f89f5f4ee 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-error.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-error.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-response-meta.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-response-meta.ts index 6638978f6..6669da69d 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-response-meta.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-response-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-response.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-response.ts index dfd8b6168..b12476ccd 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-response.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-system-message.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-system-message.ts index 674db665a..fa861fedd 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-system-message.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-system-message.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-tool-call.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-tool-call.ts index 3c37513f1..69eca69f0 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-tool-call.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-tool-call.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-user-message.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-user-message.ts index 756bd0617..eb1050b5c 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-user-message.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-user-message.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pair-transcript-warning.ts b/lib/packages/fabro-api-client/src/models/pair-transcript-warning.ts index 7c8cfb8b1..a746927f8 100644 --- a/lib/packages/fabro-api-client/src/models/pair-transcript-warning.ts +++ b/lib/packages/fabro-api-client/src/models/pair-transcript-warning.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/parallel-branch-result.ts b/lib/packages/fabro-api-client/src/models/parallel-branch-result.ts index c3177f58a..7ac5e797f 100644 --- a/lib/packages/fabro-api-client/src/models/parallel-branch-result.ts +++ b/lib/packages/fabro-api-client/src/models/parallel-branch-result.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pending-interview-record.ts b/lib/packages/fabro-api-client/src/models/pending-interview-record.ts index 8d98b95b8..91af37120 100644 --- a/lib/packages/fabro-api-client/src/models/pending-interview-record.ts +++ b/lib/packages/fabro-api-client/src/models/pending-interview-record.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pending-reason.ts b/lib/packages/fabro-api-client/src/models/pending-reason.ts index 50fbc7c07..dd973b250 100644 --- a/lib/packages/fabro-api-client/src/models/pending-reason.ts +++ b/lib/packages/fabro-api-client/src/models/pending-reason.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/permission-level.ts b/lib/packages/fabro-api-client/src/models/permission-level.ts index e006d64f8..b42f3f188 100644 --- a/lib/packages/fabro-api-client/src/models/permission-level.ts +++ b/lib/packages/fabro-api-client/src/models/permission-level.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preflight-check-detail.ts b/lib/packages/fabro-api-client/src/models/preflight-check-detail.ts index 3d829c5f2..f4009ac51 100644 --- a/lib/packages/fabro-api-client/src/models/preflight-check-detail.ts +++ b/lib/packages/fabro-api-client/src/models/preflight-check-detail.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preflight-check-report.ts b/lib/packages/fabro-api-client/src/models/preflight-check-report.ts index 081285402..bb5183a4e 100644 --- a/lib/packages/fabro-api-client/src/models/preflight-check-report.ts +++ b/lib/packages/fabro-api-client/src/models/preflight-check-report.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preflight-check-result.ts b/lib/packages/fabro-api-client/src/models/preflight-check-result.ts index a359c849e..2fc996da8 100644 --- a/lib/packages/fabro-api-client/src/models/preflight-check-result.ts +++ b/lib/packages/fabro-api-client/src/models/preflight-check-result.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preflight-check-section.ts b/lib/packages/fabro-api-client/src/models/preflight-check-section.ts index db144176a..f071fd0de 100644 --- a/lib/packages/fabro-api-client/src/models/preflight-check-section.ts +++ b/lib/packages/fabro-api-client/src/models/preflight-check-section.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preflight-response.ts b/lib/packages/fabro-api-client/src/models/preflight-response.ts index 2819d5dcd..4a57a488f 100644 --- a/lib/packages/fabro-api-client/src/models/preflight-response.ts +++ b/lib/packages/fabro-api-client/src/models/preflight-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preflight-workflow-summary.ts b/lib/packages/fabro-api-client/src/models/preflight-workflow-summary.ts index a840541ea..73323eeaf 100644 --- a/lib/packages/fabro-api-client/src/models/preflight-workflow-summary.ts +++ b/lib/packages/fabro-api-client/src/models/preflight-workflow-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/prepared-command-step.ts b/lib/packages/fabro-api-client/src/models/prepared-command-step.ts index a707858dc..7ab394bbb 100644 --- a/lib/packages/fabro-api-client/src/models/prepared-command-step.ts +++ b/lib/packages/fabro-api-client/src/models/prepared-command-step.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/prepared-script-step.ts b/lib/packages/fabro-api-client/src/models/prepared-script-step.ts index 16a88d436..4c20de3be 100644 --- a/lib/packages/fabro-api-client/src/models/prepared-script-step.ts +++ b/lib/packages/fabro-api-client/src/models/prepared-script-step.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/prepared-step.ts b/lib/packages/fabro-api-client/src/models/prepared-step.ts index 21e5c57ec..a6e2b4572 100644 --- a/lib/packages/fabro-api-client/src/models/prepared-step.ts +++ b/lib/packages/fabro-api-client/src/models/prepared-step.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preview-url-request.ts b/lib/packages/fabro-api-client/src/models/preview-url-request.ts index 1f6f4a934..777822744 100644 --- a/lib/packages/fabro-api-client/src/models/preview-url-request.ts +++ b/lib/packages/fabro-api-client/src/models/preview-url-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/preview-url-response.ts b/lib/packages/fabro-api-client/src/models/preview-url-response.ts index 904f29090..79f4955ed 100644 --- a/lib/packages/fabro-api-client/src/models/preview-url-response.ts +++ b/lib/packages/fabro-api-client/src/models/preview-url-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/principal-agent.ts b/lib/packages/fabro-api-client/src/models/principal-agent.ts index 61264b88d..7798084a1 100644 --- a/lib/packages/fabro-api-client/src/models/principal-agent.ts +++ b/lib/packages/fabro-api-client/src/models/principal-agent.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/principal-slack.ts b/lib/packages/fabro-api-client/src/models/principal-slack.ts index 61ab4c2d3..400111544 100644 --- a/lib/packages/fabro-api-client/src/models/principal-slack.ts +++ b/lib/packages/fabro-api-client/src/models/principal-slack.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/principal-system.ts b/lib/packages/fabro-api-client/src/models/principal-system.ts index ec4e5562d..674d02929 100644 --- a/lib/packages/fabro-api-client/src/models/principal-system.ts +++ b/lib/packages/fabro-api-client/src/models/principal-system.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/principal-user.ts b/lib/packages/fabro-api-client/src/models/principal-user.ts index c92be9530..46f948ea9 100644 --- a/lib/packages/fabro-api-client/src/models/principal-user.ts +++ b/lib/packages/fabro-api-client/src/models/principal-user.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/principal-webhook.ts b/lib/packages/fabro-api-client/src/models/principal-webhook.ts index a2aae6f71..6f4203890 100644 --- a/lib/packages/fabro-api-client/src/models/principal-webhook.ts +++ b/lib/packages/fabro-api-client/src/models/principal-webhook.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/principal-worker.ts b/lib/packages/fabro-api-client/src/models/principal-worker.ts index feece9215..fd00c0792 100644 --- a/lib/packages/fabro-api-client/src/models/principal-worker.ts +++ b/lib/packages/fabro-api-client/src/models/principal-worker.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/principal.ts b/lib/packages/fabro-api-client/src/models/principal.ts index 08b5422df..cb55cb486 100644 --- a/lib/packages/fabro-api-client/src/models/principal.ts +++ b/lib/packages/fabro-api-client/src/models/principal.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/project-namespace.ts b/lib/packages/fabro-api-client/src/models/project-namespace.ts index 80081d94e..cd3de0053 100644 --- a/lib/packages/fabro-api-client/src/models/project-namespace.ts +++ b/lib/packages/fabro-api-client/src/models/project-namespace.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider-credential-test-request.ts b/lib/packages/fabro-api-client/src/models/provider-credential-test-request.ts index 390b00848..687a2afd7 100644 --- a/lib/packages/fabro-api-client/src/models/provider-credential-test-request.ts +++ b/lib/packages/fabro-api-client/src/models/provider-credential-test-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider-credential-test-response.ts b/lib/packages/fabro-api-client/src/models/provider-credential-test-response.ts index b74a688eb..48973ece1 100644 --- a/lib/packages/fabro-api-client/src/models/provider-credential-test-response.ts +++ b/lib/packages/fabro-api-client/src/models/provider-credential-test-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider-list.ts b/lib/packages/fabro-api-client/src/models/provider-list.ts index dd58863c5..335e7dc55 100644 --- a/lib/packages/fabro-api-client/src/models/provider-list.ts +++ b/lib/packages/fabro-api-client/src/models/provider-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider-test-list.ts b/lib/packages/fabro-api-client/src/models/provider-test-list.ts index 6235bbd5d..257a40ec7 100644 --- a/lib/packages/fabro-api-client/src/models/provider-test-list.ts +++ b/lib/packages/fabro-api-client/src/models/provider-test-list.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider-test-result.ts b/lib/packages/fabro-api-client/src/models/provider-test-result.ts index 0a34c894b..afe8b63a8 100644 --- a/lib/packages/fabro-api-client/src/models/provider-test-result.ts +++ b/lib/packages/fabro-api-client/src/models/provider-test-result.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider-test-status.ts b/lib/packages/fabro-api-client/src/models/provider-test-status.ts index fce18e021..866c2c509 100644 --- a/lib/packages/fabro-api-client/src/models/provider-test-status.ts +++ b/lib/packages/fabro-api-client/src/models/provider-test-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider-test-summary.ts b/lib/packages/fabro-api-client/src/models/provider-test-summary.ts index 0c3622fa5..93f635260 100644 --- a/lib/packages/fabro-api-client/src/models/provider-test-summary.ts +++ b/lib/packages/fabro-api-client/src/models/provider-test-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/provider.ts b/lib/packages/fabro-api-client/src/models/provider.ts index 901f1ca37..ae5be76c3 100644 --- a/lib/packages/fabro-api-client/src/models/provider.ts +++ b/lib/packages/fabro-api-client/src/models/provider.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/prune-run-entry.ts b/lib/packages/fabro-api-client/src/models/prune-run-entry.ts index ddcf261b9..c184b6267 100644 --- a/lib/packages/fabro-api-client/src/models/prune-run-entry.ts +++ b/lib/packages/fabro-api-client/src/models/prune-run-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/prune-runs-request.ts b/lib/packages/fabro-api-client/src/models/prune-runs-request.ts index 1d1a8f058..237af45eb 100644 --- a/lib/packages/fabro-api-client/src/models/prune-runs-request.ts +++ b/lib/packages/fabro-api-client/src/models/prune-runs-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/prune-runs-response.ts b/lib/packages/fabro-api-client/src/models/prune-runs-response.ts index ca62198ba..c8c9df486 100644 --- a/lib/packages/fabro-api-client/src/models/prune-runs-response.ts +++ b/lib/packages/fabro-api-client/src/models/prune-runs-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-creation-status.ts b/lib/packages/fabro-api-client/src/models/pull-request-creation-status.ts index f6e5a3941..3be08ee5e 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-creation-status.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-creation-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-creation.ts b/lib/packages/fabro-api-client/src/models/pull-request-creation.ts index fd1db4d0e..f36ddf38d 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-creation.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-creation.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-details-status.ts b/lib/packages/fabro-api-client/src/models/pull-request-details-status.ts index 03f4c1cdc..fc713327e 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-details-status.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-details-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-details-timestamps.ts b/lib/packages/fabro-api-client/src/models/pull-request-details-timestamps.ts index 79a699d36..19d6b9329 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-details-timestamps.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-details-timestamps.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-details-unavailable-reason.ts b/lib/packages/fabro-api-client/src/models/pull-request-details-unavailable-reason.ts index 8bb87c59e..166550715 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-details-unavailable-reason.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-details-unavailable-reason.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-details.ts b/lib/packages/fabro-api-client/src/models/pull-request-details.ts index e871d9563..f12541381 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-details.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-details.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-link.ts b/lib/packages/fabro-api-client/src/models/pull-request-link.ts index 8cf17711f..5fa46bcd5 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-link.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-link.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-meta.ts b/lib/packages/fabro-api-client/src/models/pull-request-meta.ts index 84bcf981f..8914d163a 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-meta.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-ref.ts b/lib/packages/fabro-api-client/src/models/pull-request-ref.ts index c96b40d42..976abe21f 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-ref.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-response.ts b/lib/packages/fabro-api-client/src/models/pull-request-response.ts index 9f6eff305..a710d1070 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-response.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-settings.ts b/lib/packages/fabro-api-client/src/models/pull-request-settings.ts index 55a41c8ba..279068caa 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-settings.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request-user.ts b/lib/packages/fabro-api-client/src/models/pull-request-user.ts index e50e4a631..460a268de 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-user.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-user.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/pull-request.ts b/lib/packages/fabro-api-client/src/models/pull-request.ts index a67061450..38dc8f0ad 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/question-type.ts b/lib/packages/fabro-api-client/src/models/question-type.ts index a40eda259..fc7caeedf 100644 --- a/lib/packages/fabro-api-client/src/models/question-type.ts +++ b/lib/packages/fabro-api-client/src/models/question-type.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/reasoning-effort-feature.ts b/lib/packages/fabro-api-client/src/models/reasoning-effort-feature.ts index 1aa9d6734..f7de9a438 100644 --- a/lib/packages/fabro-api-client/src/models/reasoning-effort-feature.ts +++ b/lib/packages/fabro-api-client/src/models/reasoning-effort-feature.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/reasoning-effort.ts b/lib/packages/fabro-api-client/src/models/reasoning-effort.ts index 66644a073..adb8c1398 100644 --- a/lib/packages/fabro-api-client/src/models/reasoning-effort.ts +++ b/lib/packages/fabro-api-client/src/models/reasoning-effort.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/reasoning-output-trace-only.ts b/lib/packages/fabro-api-client/src/models/reasoning-output-trace-only.ts index 8f757fddf..6199fc17a 100644 --- a/lib/packages/fabro-api-client/src/models/reasoning-output-trace-only.ts +++ b/lib/packages/fabro-api-client/src/models/reasoning-output-trace-only.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/reasoning-output-with-summary.ts b/lib/packages/fabro-api-client/src/models/reasoning-output-with-summary.ts index c93d38405..92f212bdd 100644 --- a/lib/packages/fabro-api-client/src/models/reasoning-output-with-summary.ts +++ b/lib/packages/fabro-api-client/src/models/reasoning-output-with-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/reasoning-output.ts b/lib/packages/fabro-api-client/src/models/reasoning-output.ts index b2c60fb2c..118a66d72 100644 --- a/lib/packages/fabro-api-client/src/models/reasoning-output.ts +++ b/lib/packages/fabro-api-client/src/models/reasoning-output.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/related-workflow-diagnostic.ts b/lib/packages/fabro-api-client/src/models/related-workflow-diagnostic.ts index 5e80bfb6b..dd4e79204 100644 --- a/lib/packages/fabro-api-client/src/models/related-workflow-diagnostic.ts +++ b/lib/packages/fabro-api-client/src/models/related-workflow-diagnostic.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/render-workflow-graph-direction.ts b/lib/packages/fabro-api-client/src/models/render-workflow-graph-direction.ts index 097c833ad..564a8fe17 100644 --- a/lib/packages/fabro-api-client/src/models/render-workflow-graph-direction.ts +++ b/lib/packages/fabro-api-client/src/models/render-workflow-graph-direction.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/render-workflow-graph-format.ts b/lib/packages/fabro-api-client/src/models/render-workflow-graph-format.ts index fffd9ab43..65f98e596 100644 --- a/lib/packages/fabro-api-client/src/models/render-workflow-graph-format.ts +++ b/lib/packages/fabro-api-client/src/models/render-workflow-graph-format.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/render-workflow-graph-request.ts b/lib/packages/fabro-api-client/src/models/render-workflow-graph-request.ts index f2064b906..5f16ac057 100644 --- a/lib/packages/fabro-api-client/src/models/render-workflow-graph-request.ts +++ b/lib/packages/fabro-api-client/src/models/render-workflow-graph-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/replace-automation-request.ts b/lib/packages/fabro-api-client/src/models/replace-automation-request.ts index 49a5533f4..4b3b02555 100644 --- a/lib/packages/fabro-api-client/src/models/replace-automation-request.ts +++ b/lib/packages/fabro-api-client/src/models/replace-automation-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/replace-environment-request.ts b/lib/packages/fabro-api-client/src/models/replace-environment-request.ts index bddac795e..5fb4554b1 100644 --- a/lib/packages/fabro-api-client/src/models/replace-environment-request.ts +++ b/lib/packages/fabro-api-client/src/models/replace-environment-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/replace-mcp-server-request.ts b/lib/packages/fabro-api-client/src/models/replace-mcp-server-request.ts index 3efb9cf8b..fc1b5c2d0 100644 --- a/lib/packages/fabro-api-client/src/models/replace-mcp-server-request.ts +++ b/lib/packages/fabro-api-client/src/models/replace-mcp-server-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/repo-check-response-permissions.ts b/lib/packages/fabro-api-client/src/models/repo-check-response-permissions.ts index 7ef36c796..55e5a8f7b 100644 --- a/lib/packages/fabro-api-client/src/models/repo-check-response-permissions.ts +++ b/lib/packages/fabro-api-client/src/models/repo-check-response-permissions.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/repo-check-response.ts b/lib/packages/fabro-api-client/src/models/repo-check-response.ts index e61d1558f..d36ecfbe2 100644 --- a/lib/packages/fabro-api-client/src/models/repo-check-response.ts +++ b/lib/packages/fabro-api-client/src/models/repo-check-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/repository-ref.ts b/lib/packages/fabro-api-client/src/models/repository-ref.ts index 1c9652e82..68ce062c6 100644 --- a/lib/packages/fabro-api-client/src/models/repository-ref.ts +++ b/lib/packages/fabro-api-client/src/models/repository-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/review-target-kind.ts b/lib/packages/fabro-api-client/src/models/review-target-kind.ts index e50c78a38..37cf9b3e3 100644 --- a/lib/packages/fabro-api-client/src/models/review-target-kind.ts +++ b/lib/packages/fabro-api-client/src/models/review-target-kind.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/review-target.ts b/lib/packages/fabro-api-client/src/models/review-target.ts index 61b476d21..0b4a883ff 100644 --- a/lib/packages/fabro-api-client/src/models/review-target.ts +++ b/lib/packages/fabro-api-client/src/models/review-target.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/rewind-request.ts b/lib/packages/fabro-api-client/src/models/rewind-request.ts index 50e6072f2..ade0f4f4c 100644 --- a/lib/packages/fabro-api-client/src/models/rewind-request.ts +++ b/lib/packages/fabro-api-client/src/models/rewind-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/rewind-response.ts b/lib/packages/fabro-api-client/src/models/rewind-response.ts index 527c0e62f..45a5b7179 100644 --- a/lib/packages/fabro-api-client/src/models/rewind-response.ts +++ b/lib/packages/fabro-api-client/src/models/rewind-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/root-response-urls.ts b/lib/packages/fabro-api-client/src/models/root-response-urls.ts index 7f09efd4d..0caa27dcd 100644 --- a/lib/packages/fabro-api-client/src/models/root-response-urls.ts +++ b/lib/packages/fabro-api-client/src/models/root-response-urls.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/root-response.ts b/lib/packages/fabro-api-client/src/models/root-response.ts index cc29060ab..6ffd8144f 100644 --- a/lib/packages/fabro-api-client/src/models/root-response.ts +++ b/lib/packages/fabro-api-client/src/models/root-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-agent-settings.ts b/lib/packages/fabro-api-client/src/models/run-agent-settings.ts index 044c251c8..be5774084 100644 --- a/lib/packages/fabro-api-client/src/models/run-agent-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-agent-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-approval-state.ts b/lib/packages/fabro-api-client/src/models/run-approval-state.ts index 4345fa4aa..c13c90e5e 100644 --- a/lib/packages/fabro-api-client/src/models/run-approval-state.ts +++ b/lib/packages/fabro-api-client/src/models/run-approval-state.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-approval.ts b/lib/packages/fabro-api-client/src/models/run-approval.ts index 639d6d4df..45fc161e4 100644 --- a/lib/packages/fabro-api-client/src/models/run-approval.ts +++ b/lib/packages/fabro-api-client/src/models/run-approval.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-artifact-entry.ts b/lib/packages/fabro-api-client/src/models/run-artifact-entry.ts index d88afb21a..f655c5dfc 100644 --- a/lib/packages/fabro-api-client/src/models/run-artifact-entry.ts +++ b/lib/packages/fabro-api-client/src/models/run-artifact-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-artifact-list-response.ts b/lib/packages/fabro-api-client/src/models/run-artifact-list-response.ts index 74ed37164..d8b0c7d01 100644 --- a/lib/packages/fabro-api-client/src/models/run-artifact-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/run-artifact-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-billing-stage.ts b/lib/packages/fabro-api-client/src/models/run-billing-stage.ts index 6238e97c8..5db2989dd 100644 --- a/lib/packages/fabro-api-client/src/models/run-billing-stage.ts +++ b/lib/packages/fabro-api-client/src/models/run-billing-stage.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-billing-summary.ts b/lib/packages/fabro-api-client/src/models/run-billing-summary.ts index 4e0cb5ee7..c573d568d 100644 --- a/lib/packages/fabro-api-client/src/models/run-billing-summary.ts +++ b/lib/packages/fabro-api-client/src/models/run-billing-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-billing-totals.ts b/lib/packages/fabro-api-client/src/models/run-billing-totals.ts index 68861f7f8..66e64f64a 100644 --- a/lib/packages/fabro-api-client/src/models/run-billing-totals.ts +++ b/lib/packages/fabro-api-client/src/models/run-billing-totals.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-billing.ts b/lib/packages/fabro-api-client/src/models/run-billing.ts index 7bf377d78..77cab4145 100644 --- a/lib/packages/fabro-api-client/src/models/run-billing.ts +++ b/lib/packages/fabro-api-client/src/models/run-billing.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-branch-settings.ts b/lib/packages/fabro-api-client/src/models/run-branch-settings.ts index 1805c3f2b..b1c62222c 100644 --- a/lib/packages/fabro-api-client/src/models/run-branch-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-branch-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts b/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts index 4af156da7..2342ac774 100644 --- a/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-checkpoint-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-checkpoint.ts b/lib/packages/fabro-api-client/src/models/run-checkpoint.ts index 3bc50540b..1d6481b7e 100644 --- a/lib/packages/fabro-api-client/src/models/run-checkpoint.ts +++ b/lib/packages/fabro-api-client/src/models/run-checkpoint.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-client-provenance.ts b/lib/packages/fabro-api-client/src/models/run-client-provenance.ts index f1645f81f..647aec1bf 100644 --- a/lib/packages/fabro-api-client/src/models/run-client-provenance.ts +++ b/lib/packages/fabro-api-client/src/models/run-client-provenance.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-clone-settings.ts b/lib/packages/fabro-api-client/src/models/run-clone-settings.ts index 4c4b6a657..7258d9d8d 100644 --- a/lib/packages/fabro-api-client/src/models/run-clone-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-clone-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-commit-parent.ts b/lib/packages/fabro-api-client/src/models/run-commit-parent.ts index 1cc1cea61..479b5232f 100644 --- a/lib/packages/fabro-api-client/src/models/run-commit-parent.ts +++ b/lib/packages/fabro-api-client/src/models/run-commit-parent.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-commit-person.ts b/lib/packages/fabro-api-client/src/models/run-commit-person.ts index 6a33f0ce6..1eabb61b6 100644 --- a/lib/packages/fabro-api-client/src/models/run-commit-person.ts +++ b/lib/packages/fabro-api-client/src/models/run-commit-person.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-commit.ts b/lib/packages/fabro-api-client/src/models/run-commit.ts index f0b033ffc..34ad5dad0 100644 --- a/lib/packages/fabro-api-client/src/models/run-commit.ts +++ b/lib/packages/fabro-api-client/src/models/run-commit.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-commits-meta.ts b/lib/packages/fabro-api-client/src/models/run-commits-meta.ts index 06730f348..7ecfcb7a5 100644 --- a/lib/packages/fabro-api-client/src/models/run-commits-meta.ts +++ b/lib/packages/fabro-api-client/src/models/run-commits-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-control-action.ts b/lib/packages/fabro-api-client/src/models/run-control-action.ts index c68e17dec..93434cbe2 100644 --- a/lib/packages/fabro-api-client/src/models/run-control-action.ts +++ b/lib/packages/fabro-api-client/src/models/run-control-action.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-diff.ts b/lib/packages/fabro-api-client/src/models/run-diff.ts index 6b740259f..b625efafa 100644 --- a/lib/packages/fabro-api-client/src/models/run-diff.ts +++ b/lib/packages/fabro-api-client/src/models/run-diff.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-environment-settings.ts b/lib/packages/fabro-api-client/src/models/run-environment-settings.ts index b5eafa3d2..5ee20ffc0 100644 --- a/lib/packages/fabro-api-client/src/models/run-environment-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-environment-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-error.ts b/lib/packages/fabro-api-client/src/models/run-error.ts index 3367acc39..7f08f6498 100644 --- a/lib/packages/fabro-api-client/src/models/run-error.ts +++ b/lib/packages/fabro-api-client/src/models/run-error.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-event-detail-response-content.ts b/lib/packages/fabro-api-client/src/models/run-event-detail-response-content.ts index 211dcc46e..cdbf7a8a3 100644 --- a/lib/packages/fabro-api-client/src/models/run-event-detail-response-content.ts +++ b/lib/packages/fabro-api-client/src/models/run-event-detail-response-content.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-event-detail-response-event.ts b/lib/packages/fabro-api-client/src/models/run-event-detail-response-event.ts index b5f059e07..0214b752b 100644 --- a/lib/packages/fabro-api-client/src/models/run-event-detail-response-event.ts +++ b/lib/packages/fabro-api-client/src/models/run-event-detail-response-event.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-event-detail-response.ts b/lib/packages/fabro-api-client/src/models/run-event-detail-response.ts index 3d50004b6..800c08039 100644 --- a/lib/packages/fabro-api-client/src/models/run-event-detail-response.ts +++ b/lib/packages/fabro-api-client/src/models/run-event-detail-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-event.ts b/lib/packages/fabro-api-client/src/models/run-event.ts index 073afd03b..a27df1702 100644 --- a/lib/packages/fabro-api-client/src/models/run-event.ts +++ b/lib/packages/fabro-api-client/src/models/run-event.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-execution-settings.ts b/lib/packages/fabro-api-client/src/models/run-execution-settings.ts index 0508d2646..e68d95c32 100644 --- a/lib/packages/fabro-api-client/src/models/run-execution-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-execution-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-failure.ts b/lib/packages/fabro-api-client/src/models/run-failure.ts index 114e172db..ceb7c8cf3 100644 --- a/lib/packages/fabro-api-client/src/models/run-failure.ts +++ b/lib/packages/fabro-api-client/src/models/run-failure.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-files-meta.ts b/lib/packages/fabro-api-client/src/models/run-files-meta.ts index 2e095d758..83d95b836 100644 --- a/lib/packages/fabro-api-client/src/models/run-files-meta.ts +++ b/lib/packages/fabro-api-client/src/models/run-files-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-git-settings.ts b/lib/packages/fabro-api-client/src/models/run-git-settings.ts index 2d390957b..5007209e3 100644 --- a/lib/packages/fabro-api-client/src/models/run-git-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-git-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-goal-file.ts b/lib/packages/fabro-api-client/src/models/run-goal-file.ts index a2b0052ef..64dc5c3f2 100644 --- a/lib/packages/fabro-api-client/src/models/run-goal-file.ts +++ b/lib/packages/fabro-api-client/src/models/run-goal-file.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-goal-inline.ts b/lib/packages/fabro-api-client/src/models/run-goal-inline.ts index d0afe3a11..9c2deb82b 100644 --- a/lib/packages/fabro-api-client/src/models/run-goal-inline.ts +++ b/lib/packages/fabro-api-client/src/models/run-goal-inline.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-goal.ts b/lib/packages/fabro-api-client/src/models/run-goal.ts index 560389840..7b28656f1 100644 --- a/lib/packages/fabro-api-client/src/models/run-goal.ts +++ b/lib/packages/fabro-api-client/src/models/run-goal.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-integrations-github-settings.ts b/lib/packages/fabro-api-client/src/models/run-integrations-github-settings.ts index 88df508a5..a28ae5b4b 100644 --- a/lib/packages/fabro-api-client/src/models/run-integrations-github-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-integrations-github-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-integrations-settings.ts b/lib/packages/fabro-api-client/src/models/run-integrations-settings.ts index 4bf715c94..28c4f5cc8 100644 --- a/lib/packages/fabro-api-client/src/models/run-integrations-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-integrations-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts b/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts index 5d7108e20..b01a6bbd9 100644 --- a/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-interviews-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-lifecycle.ts b/lib/packages/fabro-api-client/src/models/run-lifecycle.ts index ca44ba35e..13e2c22b6 100644 --- a/lib/packages/fabro-api-client/src/models/run-lifecycle.ts +++ b/lib/packages/fabro-api-client/src/models/run-lifecycle.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-links.ts b/lib/packages/fabro-api-client/src/models/run-links.ts index ac0336dd7..7c645794b 100644 --- a/lib/packages/fabro-api-client/src/models/run-links.ts +++ b/lib/packages/fabro-api-client/src/models/run-links.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-manifest.ts b/lib/packages/fabro-api-client/src/models/run-manifest.ts index 6c2813065..0ad1ff4d7 100644 --- a/lib/packages/fabro-api-client/src/models/run-manifest.ts +++ b/lib/packages/fabro-api-client/src/models/run-manifest.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-meta-branch-settings.ts b/lib/packages/fabro-api-client/src/models/run-meta-branch-settings.ts index 950734a44..ffb69731a 100644 --- a/lib/packages/fabro-api-client/src/models/run-meta-branch-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-meta-branch-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-mode.ts b/lib/packages/fabro-api-client/src/models/run-mode.ts index 4a8ab07e2..1a21817ae 100644 --- a/lib/packages/fabro-api-client/src/models/run-mode.ts +++ b/lib/packages/fabro-api-client/src/models/run-mode.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-model-controls.ts b/lib/packages/fabro-api-client/src/models/run-model-controls.ts index 5d60f0fa4..770bb3493 100644 --- a/lib/packages/fabro-api-client/src/models/run-model-controls.ts +++ b/lib/packages/fabro-api-client/src/models/run-model-controls.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-model-settings.ts b/lib/packages/fabro-api-client/src/models/run-model-settings.ts index 599b6a26a..82b7c0798 100644 --- a/lib/packages/fabro-api-client/src/models/run-model-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-model-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-model.ts b/lib/packages/fabro-api-client/src/models/run-model.ts index 261182e70..518f3bb71 100644 --- a/lib/packages/fabro-api-client/src/models/run-model.ts +++ b/lib/packages/fabro-api-client/src/models/run-model.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-namespace.ts b/lib/packages/fabro-api-client/src/models/run-namespace.ts index cea567a72..19a89a218 100644 --- a/lib/packages/fabro-api-client/src/models/run-namespace.ts +++ b/lib/packages/fabro-api-client/src/models/run-namespace.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-origin.ts b/lib/packages/fabro-api-client/src/models/run-origin.ts index fe4a05773..a03407dd7 100644 --- a/lib/packages/fabro-api-client/src/models/run-origin.ts +++ b/lib/packages/fabro-api-client/src/models/run-origin.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-pair-status-response.ts b/lib/packages/fabro-api-client/src/models/run-pair-status-response.ts index 5bf9a5adc..4d2d987a8 100644 --- a/lib/packages/fabro-api-client/src/models/run-pair-status-response.ts +++ b/lib/packages/fabro-api-client/src/models/run-pair-status-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-prepare-settings.ts b/lib/packages/fabro-api-client/src/models/run-prepare-settings.ts index 48a4b75fa..26900c950 100644 --- a/lib/packages/fabro-api-client/src/models/run-prepare-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-prepare-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-projection.ts b/lib/packages/fabro-api-client/src/models/run-projection.ts index bd527066f..2f679a012 100644 --- a/lib/packages/fabro-api-client/src/models/run-projection.ts +++ b/lib/packages/fabro-api-client/src/models/run-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-provenance.ts b/lib/packages/fabro-api-client/src/models/run-provenance.ts index 59fa7a063..525391cc5 100644 --- a/lib/packages/fabro-api-client/src/models/run-provenance.ts +++ b/lib/packages/fabro-api-client/src/models/run-provenance.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-question.ts b/lib/packages/fabro-api-client/src/models/run-question.ts index 9ad4b72fd..a4dc008e5 100644 --- a/lib/packages/fabro-api-client/src/models/run-question.ts +++ b/lib/packages/fabro-api-client/src/models/run-question.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-reference.ts b/lib/packages/fabro-api-client/src/models/run-reference.ts index 890d55119..2f1d853df 100644 --- a/lib/packages/fabro-api-client/src/models/run-reference.ts +++ b/lib/packages/fabro-api-client/src/models/run-reference.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-runnable-source.ts b/lib/packages/fabro-api-client/src/models/run-runnable-source.ts index 88d1bd649..35d0db75b 100644 --- a/lib/packages/fabro-api-client/src/models/run-runnable-source.ts +++ b/lib/packages/fabro-api-client/src/models/run-runnable-source.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox-failure.ts b/lib/packages/fabro-api-client/src/models/run-sandbox-failure.ts index c30582f99..a72e0117d 100644 --- a/lib/packages/fabro-api-client/src/models/run-sandbox-failure.ts +++ b/lib/packages/fabro-api-client/src/models/run-sandbox-failure.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox-instance.ts b/lib/packages/fabro-api-client/src/models/run-sandbox-instance.ts index 1fd11d41c..9b526b4bd 100644 --- a/lib/packages/fabro-api-client/src/models/run-sandbox-instance.ts +++ b/lib/packages/fabro-api-client/src/models/run-sandbox-instance.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox-kind.ts b/lib/packages/fabro-api-client/src/models/run-sandbox-kind.ts index 5839dafad..2dbcc46d0 100644 --- a/lib/packages/fabro-api-client/src/models/run-sandbox-kind.ts +++ b/lib/packages/fabro-api-client/src/models/run-sandbox-kind.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox-plan.ts b/lib/packages/fabro-api-client/src/models/run-sandbox-plan.ts index 6a7e2d032..9ea86deff 100644 --- a/lib/packages/fabro-api-client/src/models/run-sandbox-plan.ts +++ b/lib/packages/fabro-api-client/src/models/run-sandbox-plan.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox-runtime.ts b/lib/packages/fabro-api-client/src/models/run-sandbox-runtime.ts index 24807703a..003a25cea 100644 --- a/lib/packages/fabro-api-client/src/models/run-sandbox-runtime.ts +++ b/lib/packages/fabro-api-client/src/models/run-sandbox-runtime.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-sandbox.ts b/lib/packages/fabro-api-client/src/models/run-sandbox.ts index 12c15b34f..c6df6dd0f 100644 --- a/lib/packages/fabro-api-client/src/models/run-sandbox.ts +++ b/lib/packages/fabro-api-client/src/models/run-sandbox.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-scm-settings.ts b/lib/packages/fabro-api-client/src/models/run-scm-settings.ts index f0970dcec..65a2a364e 100644 --- a/lib/packages/fabro-api-client/src/models/run-scm-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-scm-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-server-provenance.ts b/lib/packages/fabro-api-client/src/models/run-server-provenance.ts index 8acad6eb4..df303739c 100644 --- a/lib/packages/fabro-api-client/src/models/run-server-provenance.ts +++ b/lib/packages/fabro-api-client/src/models/run-server-provenance.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-size.ts b/lib/packages/fabro-api-client/src/models/run-size.ts index e1a4b60f2..a1c40398f 100644 --- a/lib/packages/fabro-api-client/src/models/run-size.ts +++ b/lib/packages/fabro-api-client/src/models/run-size.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts index 6abad2884..4797dc2ad 100644 --- a/lib/packages/fabro-api-client/src/models/run-spec.ts +++ b/lib/packages/fabro-api-client/src/models/run-spec.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-stage.ts b/lib/packages/fabro-api-client/src/models/run-stage.ts index 442753331..cb976f5c0 100644 --- a/lib/packages/fabro-api-client/src/models/run-stage.ts +++ b/lib/packages/fabro-api-client/src/models/run-stage.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-blocked.ts b/lib/packages/fabro-api-client/src/models/run-status-blocked.ts index 659a28a05..e71c51ce8 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-blocked.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-blocked.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-dead.ts b/lib/packages/fabro-api-client/src/models/run-status-dead.ts index 422710ef5..a05b36986 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-dead.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-dead.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-failed.ts b/lib/packages/fabro-api-client/src/models/run-status-failed.ts index 36b9dbeec..6b7642e3a 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-failed.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-failed.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-paused.ts b/lib/packages/fabro-api-client/src/models/run-status-paused.ts index 4ee7870a9..c99d8b558 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-paused.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-paused.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-pending.ts b/lib/packages/fabro-api-client/src/models/run-status-pending.ts index f2e1e3f91..b442a13e4 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-pending.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-pending.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-removing.ts b/lib/packages/fabro-api-client/src/models/run-status-removing.ts index 94935af9b..bcddd38cc 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-removing.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-removing.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-runnable.ts b/lib/packages/fabro-api-client/src/models/run-status-runnable.ts index 08141c460..dfcef94c4 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-runnable.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-runnable.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-running.ts b/lib/packages/fabro-api-client/src/models/run-status-running.ts index b9347f7a1..608e9216f 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-running.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-running.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-starting.ts b/lib/packages/fabro-api-client/src/models/run-status-starting.ts index 85b5ae96b..a9d38b3ff 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-starting.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-starting.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-submitted.ts b/lib/packages/fabro-api-client/src/models/run-status-submitted.ts index fba39f37a..56903e07b 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-submitted.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-submitted.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status-succeeded.ts b/lib/packages/fabro-api-client/src/models/run-status-succeeded.ts index 6d2c1bced..bca6666af 100644 --- a/lib/packages/fabro-api-client/src/models/run-status-succeeded.ts +++ b/lib/packages/fabro-api-client/src/models/run-status-succeeded.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-status.ts b/lib/packages/fabro-api-client/src/models/run-status.ts index 75c85f149..f04558848 100644 --- a/lib/packages/fabro-api-client/src/models/run-status.ts +++ b/lib/packages/fabro-api-client/src/models/run-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts b/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts index 49cfa9fa6..f27d4a85f 100644 --- a/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts +++ b/lib/packages/fabro-api-client/src/models/run-superseded-by-props.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-timestamps.ts b/lib/packages/fabro-api-client/src/models/run-timestamps.ts index abb59f410..c6c0b5759 100644 --- a/lib/packages/fabro-api-client/src/models/run-timestamps.ts +++ b/lib/packages/fabro-api-client/src/models/run-timestamps.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run-timing.ts b/lib/packages/fabro-api-client/src/models/run-timing.ts index f0344e5d3..0608f7c5c 100644 --- a/lib/packages/fabro-api-client/src/models/run-timing.ts +++ b/lib/packages/fabro-api-client/src/models/run-timing.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts index a4ade38a3..5771f15c2 100644 --- a/lib/packages/fabro-api-client/src/models/run.ts +++ b/lib/packages/fabro-api-client/src/models/run.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-details.ts b/lib/packages/fabro-api-client/src/models/sandbox-details.ts index 016fd2661..63075bcc9 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-details.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-details.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-file-entry.ts b/lib/packages/fabro-api-client/src/models/sandbox-file-entry.ts index ccc44e7e9..6189c5e05 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-file-entry.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-file-entry.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-file-list-response.ts b/lib/packages/fabro-api-client/src/models/sandbox-file-list-response.ts index 21ce28514..5056f949d 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-file-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-file-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-info.ts b/lib/packages/fabro-api-client/src/models/sandbox-info.ts index 9f026d745..6f7a8b71c 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-info.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-info.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-list-meta.ts b/lib/packages/fabro-api-client/src/models/sandbox-list-meta.ts index a8258b994..fbaf87e58 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-list-meta.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-list-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-list-response.ts b/lib/packages/fabro-api-client/src/models/sandbox-list-response.ts index 7ac3f6824..b4c93f957 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts index 0579ee087..3a208c284 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-network-policy-mode.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts b/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts index da49f0397..b26016a62 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-network-policy.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-network.ts b/lib/packages/fabro-api-client/src/models/sandbox-network.ts index 91b5d8e1d..e378225e2 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-network.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-network.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-provider-kind.ts b/lib/packages/fabro-api-client/src/models/sandbox-provider-kind.ts index 57f0c222c..f92894dfd 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-provider-kind.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-provider-kind.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-provider-lookup-error.ts b/lib/packages/fabro-api-client/src/models/sandbox-provider-lookup-error.ts index d5fee5806..c7f8b4fe5 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-provider-lookup-error.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-provider-lookup-error.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-resources.ts b/lib/packages/fabro-api-client/src/models/sandbox-resources.ts index 662cdb3de..f23c87f44 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-resources.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-resources.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts index 442f173e0..1c6db31fe 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts index eeba349a3..c24d6c81d 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts index 1494ec5b7..a41676b3b 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service.ts b/lib/packages/fabro-api-client/src/models/sandbox-service.ts index fffdbd642..9e89797e7 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-state.ts b/lib/packages/fabro-api-client/src/models/sandbox-state.ts index 38f785751..c40419d2f 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-state.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-state.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts b/lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts index 83937e370..da431e4cb 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-timestamps.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/save-query-request.ts b/lib/packages/fabro-api-client/src/models/save-query-request.ts index 0b3753cfb..95dc328ca 100644 --- a/lib/packages/fabro-api-client/src/models/save-query-request.ts +++ b/lib/packages/fabro-api-client/src/models/save-query-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/saved-query.ts b/lib/packages/fabro-api-client/src/models/saved-query.ts index 21ae5bf6f..d907feed1 100644 --- a/lib/packages/fabro-api-client/src/models/saved-query.ts +++ b/lib/packages/fabro-api-client/src/models/saved-query.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/secret-list-response.ts b/lib/packages/fabro-api-client/src/models/secret-list-response.ts index f5b8354ac..424684530 100644 --- a/lib/packages/fabro-api-client/src/models/secret-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/secret-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/secret-metadata.ts b/lib/packages/fabro-api-client/src/models/secret-metadata.ts index 22cfc4497..7c2501029 100644 --- a/lib/packages/fabro-api-client/src/models/secret-metadata.ts +++ b/lib/packages/fabro-api-client/src/models/secret-metadata.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/secret-type.ts b/lib/packages/fabro-api-client/src/models/secret-type.ts index 1b8fc239c..8ae9c8e08 100644 --- a/lib/packages/fabro-api-client/src/models/secret-type.ts +++ b/lib/packages/fabro-api-client/src/models/secret-type.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-api-settings.ts b/lib/packages/fabro-api-client/src/models/server-api-settings.ts index 3392c3e71..9049d9f1b 100644 --- a/lib/packages/fabro-api-client/src/models/server-api-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-api-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts b/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts index 06131fc77..5452c2b94 100644 --- a/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-artifacts-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts b/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts index 0eadd9e5d..eda32135b 100644 --- a/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-auth-github-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-auth-method.ts b/lib/packages/fabro-api-client/src/models/server-auth-method.ts index 737fe1c2b..8de81bb47 100644 --- a/lib/packages/fabro-api-client/src/models/server-auth-method.ts +++ b/lib/packages/fabro-api-client/src/models/server-auth-method.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-auth-settings.ts b/lib/packages/fabro-api-client/src/models/server-auth-settings.ts index 650eecb9e..d1dbc18ed 100644 --- a/lib/packages/fabro-api-client/src/models/server-auth-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-auth-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts index b6e9eef93..88b8fdd8d 100644 --- a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-listen-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-settings.ts index d3b6586ff..6898ed62d 100644 --- a/lib/packages/fabro-api-client/src/models/server-listen-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-listen-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts index 87b0bd4e0..ae99043d0 100644 --- a/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-listen-tcp-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts b/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts index 643f0b727..ccf48d6ce 100644 --- a/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-listen-unix-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-logging-settings.ts b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts index 2fef328fd..994118bdc 100644 --- a/lib/packages/fabro-api-client/src/models/server-logging-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-namespace.ts b/lib/packages/fabro-api-client/src/models/server-namespace.ts index 153195ed8..5ff18c6d5 100644 --- a/lib/packages/fabro-api-client/src/models/server-namespace.ts +++ b/lib/packages/fabro-api-client/src/models/server-namespace.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts index c50d09f6d..96fa4f679 100644 --- a/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts index 9fa9a35a1..ffc563bb0 100644 --- a/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts index cb6a3b2d6..7fd2c4625 100644 --- a/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts b/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts index 32d67b631..ede8c6126 100644 --- a/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-scheduler-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-settings.ts b/lib/packages/fabro-api-client/src/models/server-settings.ts index e299e4dda..f6291f430 100644 --- a/lib/packages/fabro-api-client/src/models/server-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts b/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts index 0b7b8583d..69c684618 100644 --- a/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-slate-db-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-storage-settings.ts b/lib/packages/fabro-api-client/src/models/server-storage-settings.ts index 34c12ee29..8584ff40a 100644 --- a/lib/packages/fabro-api-client/src/models/server-storage-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-storage-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/server-web-settings.ts b/lib/packages/fabro-api-client/src/models/server-web-settings.ts index dd8ea8e0b..25b7e1d3b 100644 --- a/lib/packages/fabro-api-client/src/models/server-web-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-web-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/session-detail.ts b/lib/packages/fabro-api-client/src/models/session-detail.ts index e1dce8d26..0a2e726b1 100644 --- a/lib/packages/fabro-api-client/src/models/session-detail.ts +++ b/lib/packages/fabro-api-client/src/models/session-detail.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/session-message.ts b/lib/packages/fabro-api-client/src/models/session-message.ts index 6ac25cf10..5308f3332 100644 --- a/lib/packages/fabro-api-client/src/models/session-message.ts +++ b/lib/packages/fabro-api-client/src/models/session-message.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/session-record.ts b/lib/packages/fabro-api-client/src/models/session-record.ts index 5bb2ee06d..0c2ef4b79 100644 --- a/lib/packages/fabro-api-client/src/models/session-record.ts +++ b/lib/packages/fabro-api-client/src/models/session-record.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/session-status.ts b/lib/packages/fabro-api-client/src/models/session-status.ts index b0c6568ab..701bb97d7 100644 --- a/lib/packages/fabro-api-client/src/models/session-status.ts +++ b/lib/packages/fabro-api-client/src/models/session-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/session-summary.ts b/lib/packages/fabro-api-client/src/models/session-summary.ts index 6520d77c7..3aec2b2ff 100644 --- a/lib/packages/fabro-api-client/src/models/session-summary.ts +++ b/lib/packages/fabro-api-client/src/models/session-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/session-turn.ts b/lib/packages/fabro-api-client/src/models/session-turn.ts index 65950d477..ad83c28e2 100644 --- a/lib/packages/fabro-api-client/src/models/session-turn.ts +++ b/lib/packages/fabro-api-client/src/models/session-turn.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/skills-projection.ts b/lib/packages/fabro-api-client/src/models/skills-projection.ts index 897c4b4af..92f77b1a8 100644 --- a/lib/packages/fabro-api-client/src/models/skills-projection.ts +++ b/lib/packages/fabro-api-client/src/models/skills-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts b/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts index 4bdbd0687..cfe8a589b 100644 --- a/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts +++ b/lib/packages/fabro-api-client/src/models/slack-integration-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/ssh-access-request.ts b/lib/packages/fabro-api-client/src/models/ssh-access-request.ts index 9f75dc437..4e27da135 100644 --- a/lib/packages/fabro-api-client/src/models/ssh-access-request.ts +++ b/lib/packages/fabro-api-client/src/models/ssh-access-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/ssh-access-response.ts b/lib/packages/fabro-api-client/src/models/ssh-access-response.ts index 4cbe63f52..c59390e87 100644 --- a/lib/packages/fabro-api-client/src/models/ssh-access-response.ts +++ b/lib/packages/fabro-api-client/src/models/ssh-access-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-completion.ts b/lib/packages/fabro-api-client/src/models/stage-completion.ts index 142cf40a6..d6f8abea9 100644 --- a/lib/packages/fabro-api-client/src/models/stage-completion.ts +++ b/lib/packages/fabro-api-client/src/models/stage-completion.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window-breakdown-item.ts b/lib/packages/fabro-api-client/src/models/stage-context-window-breakdown-item.ts index cbcd88a4a..df350dc38 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window-breakdown-item.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window-breakdown-item.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window-category.ts b/lib/packages/fabro-api-client/src/models/stage-context-window-category.ts index cca48ef46..315fe5f3f 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window-category.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window-category.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window-count-method.ts b/lib/packages/fabro-api-client/src/models/stage-context-window-count-method.ts index aefb7a767..2b706d8b2 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window-count-method.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window-count-method.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window-projection.ts b/lib/packages/fabro-api-client/src/models/stage-context-window-projection.ts index 33f02bf19..6cf4d2f8d 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window-staleness.ts b/lib/packages/fabro-api-client/src/models/stage-context-window-staleness.ts index 60f0b777d..2163e7834 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window-staleness.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window-staleness.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window-unavailable-reason.ts b/lib/packages/fabro-api-client/src/models/stage-context-window-unavailable-reason.ts index 4420c4dbe..b22bb7392 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window-unavailable-reason.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window-unavailable-reason.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window-warning.ts b/lib/packages/fabro-api-client/src/models/stage-context-window-warning.ts index f10d15ad7..18c97d0df 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window-warning.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window-warning.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-context-window.ts b/lib/packages/fabro-api-client/src/models/stage-context-window.ts index ee63a7811..fe78e3f8f 100644 --- a/lib/packages/fabro-api-client/src/models/stage-context-window.ts +++ b/lib/packages/fabro-api-client/src/models/stage-context-window.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-handler.ts b/lib/packages/fabro-api-client/src/models/stage-handler.ts index 1398d3e34..6d8827ce0 100644 --- a/lib/packages/fabro-api-client/src/models/stage-handler.ts +++ b/lib/packages/fabro-api-client/src/models/stage-handler.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-inference-projection.ts b/lib/packages/fabro-api-client/src/models/stage-inference-projection.ts index 13a08869b..5a28adc18 100644 --- a/lib/packages/fabro-api-client/src/models/stage-inference-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-inference-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-model-usage.ts b/lib/packages/fabro-api-client/src/models/stage-model-usage.ts index 8ba389a8a..c2f89645c 100644 --- a/lib/packages/fabro-api-client/src/models/stage-model-usage.ts +++ b/lib/packages/fabro-api-client/src/models/stage-model-usage.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-outcome.ts b/lib/packages/fabro-api-client/src/models/stage-outcome.ts index 1295b7ec4..3fa8b5caa 100644 --- a/lib/packages/fabro-api-client/src/models/stage-outcome.ts +++ b/lib/packages/fabro-api-client/src/models/stage-outcome.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-projection.ts b/lib/packages/fabro-api-client/src/models/stage-projection.ts index 56a53f7c0..35cfdc492 100644 --- a/lib/packages/fabro-api-client/src/models/stage-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-state.ts b/lib/packages/fabro-api-client/src/models/stage-state.ts index 9443fdfad..650829f85 100644 --- a/lib/packages/fabro-api-client/src/models/stage-state.ts +++ b/lib/packages/fabro-api-client/src/models/stage-state.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-summary.ts b/lib/packages/fabro-api-client/src/models/stage-summary.ts index 011fcf555..bfd131b68 100644 --- a/lib/packages/fabro-api-client/src/models/stage-summary.ts +++ b/lib/packages/fabro-api-client/src/models/stage-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-timing.ts b/lib/packages/fabro-api-client/src/models/stage-timing.ts index 4b106816c..dd9d9d9d6 100644 --- a/lib/packages/fabro-api-client/src/models/stage-timing.ts +++ b/lib/packages/fabro-api-client/src/models/stage-timing.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/stage-tool-batch-projection.ts b/lib/packages/fabro-api-client/src/models/stage-tool-batch-projection.ts index 971fa7c59..2432c19d6 100644 --- a/lib/packages/fabro-api-client/src/models/stage-tool-batch-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-tool-batch-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/start-record.ts b/lib/packages/fabro-api-client/src/models/start-record.ts index 7882a9789..e361528a1 100644 --- a/lib/packages/fabro-api-client/src/models/start-record.ts +++ b/lib/packages/fabro-api-client/src/models/start-record.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/start-run-request.ts b/lib/packages/fabro-api-client/src/models/start-run-request.ts index baccb9915..d733c6fd8 100644 --- a/lib/packages/fabro-api-client/src/models/start-run-request.ts +++ b/lib/packages/fabro-api-client/src/models/start-run-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/steer-run-request.ts b/lib/packages/fabro-api-client/src/models/steer-run-request.ts index 4169fadc1..daa65e248 100644 --- a/lib/packages/fabro-api-client/src/models/steer-run-request.ts +++ b/lib/packages/fabro-api-client/src/models/steer-run-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts b/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts index c2576d069..4d1b22a0f 100644 --- a/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts +++ b/lib/packages/fabro-api-client/src/models/sub-agent-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts index d4c7bb5a6..114cb14ca 100644 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts index 2250ee02a..b5c186fb3 100644 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts index a9eed6831..cc08b4a3a 100644 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts index 79063cbe0..9f47c69d1 100644 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/sub-agent-status.ts b/lib/packages/fabro-api-client/src/models/sub-agent-status.ts index 630717c08..1d88f804b 100644 --- a/lib/packages/fabro-api-client/src/models/sub-agent-status.ts +++ b/lib/packages/fabro-api-client/src/models/sub-agent-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/submit-answer-multi-selected-request.ts b/lib/packages/fabro-api-client/src/models/submit-answer-multi-selected-request.ts index b3458cfce..1786ffd3e 100644 --- a/lib/packages/fabro-api-client/src/models/submit-answer-multi-selected-request.ts +++ b/lib/packages/fabro-api-client/src/models/submit-answer-multi-selected-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/submit-answer-no-request.ts b/lib/packages/fabro-api-client/src/models/submit-answer-no-request.ts index aef539155..f116ebd9b 100644 --- a/lib/packages/fabro-api-client/src/models/submit-answer-no-request.ts +++ b/lib/packages/fabro-api-client/src/models/submit-answer-no-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/submit-answer-request.ts b/lib/packages/fabro-api-client/src/models/submit-answer-request.ts index 04fb93ddb..0390a6a30 100644 --- a/lib/packages/fabro-api-client/src/models/submit-answer-request.ts +++ b/lib/packages/fabro-api-client/src/models/submit-answer-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/submit-answer-selected-request.ts b/lib/packages/fabro-api-client/src/models/submit-answer-selected-request.ts index 3c4c42b95..28ae12a47 100644 --- a/lib/packages/fabro-api-client/src/models/submit-answer-selected-request.ts +++ b/lib/packages/fabro-api-client/src/models/submit-answer-selected-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/submit-answer-text-request.ts b/lib/packages/fabro-api-client/src/models/submit-answer-text-request.ts index a1c419d3b..816571bb2 100644 --- a/lib/packages/fabro-api-client/src/models/submit-answer-text-request.ts +++ b/lib/packages/fabro-api-client/src/models/submit-answer-text-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/submit-answer-yes-request.ts b/lib/packages/fabro-api-client/src/models/submit-answer-yes-request.ts index 5a657bae4..aafdb10af 100644 --- a/lib/packages/fabro-api-client/src/models/submit-answer-yes-request.ts +++ b/lib/packages/fabro-api-client/src/models/submit-answer-yes-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/submit-turn-request.ts b/lib/packages/fabro-api-client/src/models/submit-turn-request.ts index 06375abf8..bac7445ff 100644 --- a/lib/packages/fabro-api-client/src/models/submit-turn-request.ts +++ b/lib/packages/fabro-api-client/src/models/submit-turn-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/success-reason.ts b/lib/packages/fabro-api-client/src/models/success-reason.ts index 011e1ca5e..a863a8a8a 100644 --- a/lib/packages/fabro-api-client/src/models/success-reason.ts +++ b/lib/packages/fabro-api-client/src/models/success-reason.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-actor-kind.ts b/lib/packages/fabro-api-client/src/models/system-actor-kind.ts index 9904e8b28..76eec34b9 100644 --- a/lib/packages/fabro-api-client/src/models/system-actor-kind.ts +++ b/lib/packages/fabro-api-client/src/models/system-actor-kind.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-cpu-resource-scope.ts b/lib/packages/fabro-api-client/src/models/system-cpu-resource-scope.ts index e4637a6e7..fcbe410d9 100644 --- a/lib/packages/fabro-api-client/src/models/system-cpu-resource-scope.ts +++ b/lib/packages/fabro-api-client/src/models/system-cpu-resource-scope.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-cpu-resources.ts b/lib/packages/fabro-api-client/src/models/system-cpu-resources.ts index 48fd764fe..99739fb3c 100644 --- a/lib/packages/fabro-api-client/src/models/system-cpu-resources.ts +++ b/lib/packages/fabro-api-client/src/models/system-cpu-resources.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-disk-resource-scope.ts b/lib/packages/fabro-api-client/src/models/system-disk-resource-scope.ts index 79daf2b31..bcbef3ba3 100644 --- a/lib/packages/fabro-api-client/src/models/system-disk-resource-scope.ts +++ b/lib/packages/fabro-api-client/src/models/system-disk-resource-scope.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-disk-resources.ts b/lib/packages/fabro-api-client/src/models/system-disk-resources.ts index f44dca9ad..76c6df14b 100644 --- a/lib/packages/fabro-api-client/src/models/system-disk-resources.ts +++ b/lib/packages/fabro-api-client/src/models/system-disk-resources.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-info-response.ts b/lib/packages/fabro-api-client/src/models/system-info-response.ts index 6b3f8ebd5..8bf9b08b7 100644 --- a/lib/packages/fabro-api-client/src/models/system-info-response.ts +++ b/lib/packages/fabro-api-client/src/models/system-info-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-integration-status.ts b/lib/packages/fabro-api-client/src/models/system-integration-status.ts index 6bc357f56..cb9adf24e 100644 --- a/lib/packages/fabro-api-client/src/models/system-integration-status.ts +++ b/lib/packages/fabro-api-client/src/models/system-integration-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-integrations-response.ts b/lib/packages/fabro-api-client/src/models/system-integrations-response.ts index b00aa3bf1..b557e6b5c 100644 --- a/lib/packages/fabro-api-client/src/models/system-integrations-response.ts +++ b/lib/packages/fabro-api-client/src/models/system-integrations-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-memory-resource-scope.ts b/lib/packages/fabro-api-client/src/models/system-memory-resource-scope.ts index 31340786e..463cee08a 100644 --- a/lib/packages/fabro-api-client/src/models/system-memory-resource-scope.ts +++ b/lib/packages/fabro-api-client/src/models/system-memory-resource-scope.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-memory-resources.ts b/lib/packages/fabro-api-client/src/models/system-memory-resources.ts index 75f1b8e53..b90d1d41d 100644 --- a/lib/packages/fabro-api-client/src/models/system-memory-resources.ts +++ b/lib/packages/fabro-api-client/src/models/system-memory-resources.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-repair-run-issue.ts b/lib/packages/fabro-api-client/src/models/system-repair-run-issue.ts index dc5cc5abb..0167a6d5c 100644 --- a/lib/packages/fabro-api-client/src/models/system-repair-run-issue.ts +++ b/lib/packages/fabro-api-client/src/models/system-repair-run-issue.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-repair-runs-response.ts b/lib/packages/fabro-api-client/src/models/system-repair-runs-response.ts index 9ca571774..fbf50ce8f 100644 --- a/lib/packages/fabro-api-client/src/models/system-repair-runs-response.ts +++ b/lib/packages/fabro-api-client/src/models/system-repair-runs-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-resources-response.ts b/lib/packages/fabro-api-client/src/models/system-resources-response.ts index cdffb88e8..d758bc8eb 100644 --- a/lib/packages/fabro-api-client/src/models/system-resources-response.ts +++ b/lib/packages/fabro-api-client/src/models/system-resources-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/system-run-counts.ts b/lib/packages/fabro-api-client/src/models/system-run-counts.ts index 3fe29ae9b..5d77ff2f4 100644 --- a/lib/packages/fabro-api-client/src/models/system-run-counts.ts +++ b/lib/packages/fabro-api-client/src/models/system-run-counts.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts b/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts index be3839db8..b78a1b31c 100644 --- a/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts +++ b/lib/packages/fabro-api-client/src/models/timeline-entry-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/tls-mode.ts b/lib/packages/fabro-api-client/src/models/tls-mode.ts index 302a16f11..22a837dbb 100644 --- a/lib/packages/fabro-api-client/src/models/tls-mode.ts +++ b/lib/packages/fabro-api-client/src/models/tls-mode.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/todo-list-kind.ts b/lib/packages/fabro-api-client/src/models/todo-list-kind.ts index 20f1da8ee..37a04a71a 100644 --- a/lib/packages/fabro-api-client/src/models/todo-list-kind.ts +++ b/lib/packages/fabro-api-client/src/models/todo-list-kind.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/todo-list-projection.ts b/lib/packages/fabro-api-client/src/models/todo-list-projection.ts index d74332c47..4b0b64b64 100644 --- a/lib/packages/fabro-api-client/src/models/todo-list-projection.ts +++ b/lib/packages/fabro-api-client/src/models/todo-list-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/todo-projection.ts b/lib/packages/fabro-api-client/src/models/todo-projection.ts index 5200eaf50..7cea92aa4 100644 --- a/lib/packages/fabro-api-client/src/models/todo-projection.ts +++ b/lib/packages/fabro-api-client/src/models/todo-projection.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/todo-status.ts b/lib/packages/fabro-api-client/src/models/todo-status.ts index e5be59ed9..a6a47d697 100644 --- a/lib/packages/fabro-api-client/src/models/todo-status.ts +++ b/lib/packages/fabro-api-client/src/models/todo-status.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/update-run-parent-request.ts b/lib/packages/fabro-api-client/src/models/update-run-parent-request.ts index 43bbe96ce..ab2a52e42 100644 --- a/lib/packages/fabro-api-client/src/models/update-run-parent-request.ts +++ b/lib/packages/fabro-api-client/src/models/update-run-parent-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/update-run-request.ts b/lib/packages/fabro-api-client/src/models/update-run-request.ts index 92697fb26..91a18b756 100644 --- a/lib/packages/fabro-api-client/src/models/update-run-request.ts +++ b/lib/packages/fabro-api-client/src/models/update-run-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/update-variable-request.ts b/lib/packages/fabro-api-client/src/models/update-variable-request.ts index 71d925149..ae94ff897 100644 --- a/lib/packages/fabro-api-client/src/models/update-variable-request.ts +++ b/lib/packages/fabro-api-client/src/models/update-variable-request.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/user-response.ts b/lib/packages/fabro-api-client/src/models/user-response.ts index 1debdf685..f8474b3ff 100644 --- a/lib/packages/fabro-api-client/src/models/user-response.ts +++ b/lib/packages/fabro-api-client/src/models/user-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/validate-response.ts b/lib/packages/fabro-api-client/src/models/validate-response.ts index 302222254..f1f4a0dfd 100644 --- a/lib/packages/fabro-api-client/src/models/validate-response.ts +++ b/lib/packages/fabro-api-client/src/models/validate-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/variable-list-response.ts b/lib/packages/fabro-api-client/src/models/variable-list-response.ts index b4ed0ab0a..4895e68a9 100644 --- a/lib/packages/fabro-api-client/src/models/variable-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/variable-list-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/variable.ts b/lib/packages/fabro-api-client/src/models/variable.ts index a5d39e711..aba83d090 100644 --- a/lib/packages/fabro-api-client/src/models/variable.ts +++ b/lib/packages/fabro-api-client/src/models/variable.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/vnc-preview-response.ts b/lib/packages/fabro-api-client/src/models/vnc-preview-response.ts index aed16f52a..40e1e7bef 100644 --- a/lib/packages/fabro-api-client/src/models/vnc-preview-response.ts +++ b/lib/packages/fabro-api-client/src/models/vnc-preview-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/webhook-strategy.ts b/lib/packages/fabro-api-client/src/models/webhook-strategy.ts index 090689f95..d2f617762 100644 --- a/lib/packages/fabro-api-client/src/models/webhook-strategy.ts +++ b/lib/packages/fabro-api-client/src/models/webhook-strategy.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-detail-response.ts b/lib/packages/fabro-api-client/src/models/workflow-detail-response.ts index 0f9722566..3d992b1f5 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-detail-response.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-detail-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-diagnostic.ts b/lib/packages/fabro-api-client/src/models/workflow-diagnostic.ts index 5c5ee9790..e0a9686a1 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-diagnostic.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-diagnostic.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-last-run-summary.ts b/lib/packages/fabro-api-client/src/models/workflow-last-run-summary.ts index e5c443086..af27a3d15 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-last-run-summary.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-last-run-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-list-item.ts b/lib/packages/fabro-api-client/src/models/workflow-list-item.ts index 00ae2f734..839503230 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-list-item.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-list-item.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-namespace.ts b/lib/packages/fabro-api-client/src/models/workflow-namespace.ts index 5501b5b21..7451185ae 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-namespace.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-namespace.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-ref.ts b/lib/packages/fabro-api-client/src/models/workflow-ref.ts index d98cf2148..6189e8bd7 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-ref.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-ref.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-reference.ts b/lib/packages/fabro-api-client/src/models/workflow-reference.ts index f4a83080b..d85c64352 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-reference.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-reference.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-schedule-summary.ts b/lib/packages/fabro-api-client/src/models/workflow-schedule-summary.ts index 3165113d1..fc26a97f1 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-schedule-summary.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-schedule-summary.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-settings.ts b/lib/packages/fabro-api-client/src/models/workflow-settings.ts index 7f5214295..b3a2b8614 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-settings.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-settings.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/workflow-version.ts b/lib/packages/fabro-api-client/src/models/workflow-version.ts index d54f4064e..b96922261 100644 --- a/lib/packages/fabro-api-client/src/models/workflow-version.ts +++ b/lib/packages/fabro-api-client/src/models/workflow-version.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/lib/packages/fabro-api-client/src/models/write-blob-response.ts b/lib/packages/fabro-api-client/src/models/write-blob-response.ts index 17a0ecad9..7e461b9e7 100644 --- a/lib/packages/fabro-api-client/src/models/write-blob-response.ts +++ b/lib/packages/fabro-api-client/src/models/write-blob-response.ts @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). From f1c00a167e1f2b0b650ea7703b2121becb6d83e1 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:27:00 -0400 Subject: [PATCH 11/63] Rename blob read parameters from id to blob_hash Finish the blob-hash vocabulary unification at the defining signatures: RunStoreBackend::read_blob, RunStoreHandle, LocalRunStoreBackend, the HTTP backend impl, RunDatabase::read_blob, and BlobStore::read/exists all said `id`, which kept re-teaching the old vocabulary at every impl site and inlay hint. Co-Authored-By: Claude Fable 5 --- lib/apps/fabro-cli/src/commands/run/runner.rs | 4 ++-- lib/components/fabro-store/src/slate/blob_store.rs | 8 ++++---- lib/components/fabro-store/src/slate/run_store.rs | 4 ++-- lib/components/fabro-workflow/src/handler/command.rs | 7 +++++-- lib/components/fabro-workflow/src/lifecycle/git.rs | 2 +- lib/components/fabro-workflow/src/pipeline/finalize.rs | 2 +- lib/components/fabro-workflow/src/runtime_store.rs | 10 +++++----- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/apps/fabro-cli/src/commands/run/runner.rs b/lib/apps/fabro-cli/src/commands/run/runner.rs index 72044458d..713838555 100644 --- a/lib/apps/fabro-cli/src/commands/run/runner.rs +++ b/lib/apps/fabro-cli/src/commands/run/runner.rs @@ -1018,11 +1018,11 @@ impl RunStoreBackend for HttpRunStore { .await } - async fn read_blob(&self, id: &BlobHash) -> Result> { + async fn read_blob(&self, blob_hash: &BlobHash) -> Result> { self.with_retries("read run blob", || { let client = self.client.clone_for_reuse(); let run_id = self.run_id; - let blob_hash = *id; + let blob_hash = *blob_hash; async move { client.read_run_blob(&run_id, &blob_hash).await } }) .await diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs index cb168cd2b..8cec4c296 100644 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ b/lib/components/fabro-store/src/slate/blob_store.rs @@ -56,12 +56,12 @@ impl BlobStore { Ok(id) } - pub async fn read(&self, id: &BlobHash) -> Result> { - Ok(self.repo.get(id).await?.map(|blob| blob.0)) + pub async fn read(&self, blob_hash: &BlobHash) -> Result> { + Ok(self.repo.get(blob_hash).await?.map(|blob| blob.0)) } - pub async fn exists(&self, id: &BlobHash) -> Result { - self.repo.exists(id).await + pub async fn exists(&self, blob_hash: &BlobHash) -> Result { + self.repo.exists(blob_hash).await } } diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index 9148ede95..c4c590a0d 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -561,8 +561,8 @@ impl RunDatabase { self.inner.blob_store.write(data).await } - pub async fn read_blob(&self, id: &BlobHash) -> Result> { - self.inner.blob_store.read(id).await + pub async fn read_blob(&self, blob_hash: &BlobHash) -> Result> { + self.inner.blob_store.read(blob_hash).await } pub async fn state(&self) -> Result { diff --git a/lib/components/fabro-workflow/src/handler/command.rs b/lib/components/fabro-workflow/src/handler/command.rs index 828a56101..d36ef8be8 100644 --- a/lib/components/fabro-workflow/src/handler/command.rs +++ b/lib/components/fabro-workflow/src/handler/command.rs @@ -398,8 +398,11 @@ mod tests { Ok(blob_hash) } - async fn read_blob(&self, id: &fabro_types::BlobHash) -> anyhow::Result> { - Ok(self.blobs.lock().await.get(id).cloned()) + async fn read_blob( + &self, + blob_hash: &fabro_types::BlobHash, + ) -> anyhow::Result> { + Ok(self.blobs.lock().await.get(blob_hash).cloned()) } async fn read_run_log(&self) -> anyhow::Result>> { diff --git a/lib/components/fabro-workflow/src/lifecycle/git.rs b/lib/components/fabro-workflow/src/lifecycle/git.rs index 73100e6c1..18f4139bf 100644 --- a/lib/components/fabro-workflow/src/lifecycle/git.rs +++ b/lib/components/fabro-workflow/src/lifecycle/git.rs @@ -1328,7 +1328,7 @@ mod tests { Ok(BlobHash::new(data)) } - async fn read_blob(&self, _id: &BlobHash) -> Result> { + async fn read_blob(&self, _blob_hash: &BlobHash) -> Result> { Ok(None) } diff --git a/lib/components/fabro-workflow/src/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index 9f43e86ff..b1e7847ee 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -1823,7 +1823,7 @@ mod tests { Ok(BlobHash::new(data)) } - async fn read_blob(&self, _id: &BlobHash) -> Result> { + async fn read_blob(&self, _blob_hash: &BlobHash) -> Result> { Ok(None) } diff --git a/lib/components/fabro-workflow/src/runtime_store.rs b/lib/components/fabro-workflow/src/runtime_store.rs index a12a7c85a..af4eae425 100644 --- a/lib/components/fabro-workflow/src/runtime_store.rs +++ b/lib/components/fabro-workflow/src/runtime_store.rs @@ -14,7 +14,7 @@ pub trait RunStoreBackend: Send + Sync { async fn list_events(&self) -> Result>; async fn append_run_event(&self, event: &RunEvent) -> Result<()>; async fn write_blob(&self, data: &[u8]) -> Result; - async fn read_blob(&self, id: &BlobHash) -> Result>; + async fn read_blob(&self, blob_hash: &BlobHash) -> Result>; async fn read_run_log(&self) -> Result>>; } @@ -50,8 +50,8 @@ impl RunStoreHandle { self.backend.write_blob(data).await } - pub async fn read_blob(&self, id: &BlobHash) -> Result> { - self.backend.read_blob(id).await + pub async fn read_blob(&self, blob_hash: &BlobHash) -> Result> { + self.backend.read_blob(blob_hash).await } pub async fn read_run_log(&self) -> Result>> { @@ -98,9 +98,9 @@ impl RunStoreBackend for LocalRunStoreBackend { .map_err(anyhow::Error::from) } - async fn read_blob(&self, id: &BlobHash) -> Result> { + async fn read_blob(&self, blob_hash: &BlobHash) -> Result> { self.run_store - .read_blob(id) + .read_blob(blob_hash) .await .map_err(anyhow::Error::from) } From 88b2a01af8248ceddb1eb484672d312a120a1f5c Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:35:18 -0400 Subject: [PATCH 12/63] Type the blob-write response hash as fabro_types::BlobHash Promote BlobHash to a named OpenAPI schema with the ^[0-9a-f]{64}$ pattern, reference it from WriteBlobResponse.hash and the blobHash path parameter, and map it to fabro_types::BlobHash via with_replacement. The server now serializes the domain type directly and the client gets a parsed BlobHash by construction, removing the to_string/parse adapter pair across the wire boundary. Adds the JSON-parity test required for new replacements. Co-Authored-By: Claude Fable 5 --- docs/public/api-reference/fabro-api.yaml | 13 +++-- .../src/server/handler/artifacts.rs | 5 +- lib/foundation/fabro-api/build.rs | 1 + lib/foundation/fabro-api/src/lib.rs | 2 +- .../fabro-api/tests/blob_hash_round_trip.rs | 50 +++++++++++++++++++ lib/foundation/fabro-client/src/client.rs | 8 +-- .../src/models/write-blob-response.ts | 2 +- 7 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 lib/foundation/fabro-api/tests/blob_hash_round_trip.rs diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 6038b9c34..0352d7f62 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -5980,8 +5980,7 @@ components: required: true description: Content-addressed blob hash. schema: - type: string - pattern: '^[0-9a-f]{64}$' + $ref: "#/components/schemas/BlobHash" example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 ArtifactFilename: @@ -10283,6 +10282,12 @@ components: description: Assigned event sequence number. example: 42 + BlobHash: + description: Content-addressed SHA-256 hash of a stored blob. + type: string + pattern: "^[0-9a-f]{64}$" + example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 + WriteBlobResponse: description: Content-addressed hash of a stored blob. type: object @@ -10290,9 +10295,7 @@ components: - hash properties: hash: - type: string - description: Content-addressed hash of the stored blob. - example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 + $ref: "#/components/schemas/BlobHash" CommandTermination: description: Terminal state for a command execution. diff --git a/lib/apps/fabro-server/src/server/handler/artifacts.rs b/lib/apps/fabro-server/src/server/handler/artifacts.rs index c203bdec0..f0f2b6064 100644 --- a/lib/apps/fabro-server/src/server/handler/artifacts.rs +++ b/lib/apps/fabro-server/src/server/handler/artifacts.rs @@ -105,10 +105,7 @@ async fn write_run_blob( } match state.stores.runs.open_run(&id).await { Ok(run_store) => match run_store.write_blob(&body).await { - Ok(blob_hash) => Json(WriteBlobResponse { - hash: blob_hash.to_string(), - }) - .into_response(), + Ok(blob_hash) => Json(WriteBlobResponse { hash: blob_hash }).into_response(), Err(err) => { ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() } diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index ea8ece1eb..0fb3cccd0 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -725,6 +725,7 @@ fn main() { ("WorkflowVersion", "fabro_types::WorkflowVersion", &[]), ("WorkflowPath", "fabro_types::WorkflowPath", &[]), ("WorkflowVersionId", "fabro_types::WorkflowVersionId", &[]), + ("BlobHash", "fabro_types::BlobHash", &[]), ("CostSource", "fabro_model::CostSource", &[]), ]; for (name, path, impls) in replacements { diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index b40087831..c53e682c7 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -42,7 +42,7 @@ pub mod types { pub use fabro_types::{ ActivatedSkill, AgentControlState, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, AgentToolCategory, AgentToolSource, AgentToolSummary, - AgentToolsAvailableProps, AskFabro, AuthMethod, AutomationRef, BilledTokenCounts, + AgentToolsAvailableProps, AskFabro, AuthMethod, AutomationRef, BilledTokenCounts, BlobHash, CommandTermination, Conclusion, ContentPart, CreateVariableRequest, DiffStats, DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail, FailureSignature, GitContext, IdpIdentity, IntegrationConnectionKind, diff --git a/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs b/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs new file mode 100644 index 000000000..397d4d00e --- /dev/null +++ b/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs @@ -0,0 +1,50 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::{BlobHash as ApiBlobHash, WriteBlobResponse}; +use fabro_types::BlobHash; +use serde_json::json; + +const BLOB_HASH: &str = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"; + +#[test] +fn blob_hash_schema_reuses_domain_type() { + assert_same_type::(); +} + +#[test] +fn write_blob_response_round_trips_exact_wire_shape() { + let value = json!({ "hash": BLOB_HASH }); + + let response: WriteBlobResponse = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(&response).unwrap(), value); +} + +#[test] +fn blob_hash_emits_the_documented_lowercase_pattern() { + // Serialization must match the OpenAPI schema pattern `^[0-9a-f]{64}$`. + let hash: ApiBlobHash = serde_json::from_value(json!(BLOB_HASH)).unwrap(); + let emitted = serde_json::to_value(hash).unwrap(); + assert_eq!(emitted, json!(BLOB_HASH)); + + let text = emitted.as_str().unwrap(); + assert_eq!(text.len(), 64); + assert!( + text.bytes() + .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + ); +} + +#[test] +fn blob_hash_rejects_non_hex_values() { + assert!(serde_json::from_value::(json!("not-a-blob-hash")).is_err()); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} must be the domain type {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/foundation/fabro-client/src/client.rs b/lib/foundation/fabro-client/src/client.rs index 8e1c3a1b2..9547dddce 100644 --- a/lib/foundation/fabro-client/src/client.rs +++ b/lib/foundation/fabro-client/src/client.rs @@ -1839,11 +1839,7 @@ impl Client { .await }) .await?; - response - .into_inner() - .hash - .parse() - .context("write_run_blob returned invalid blob hash") + Ok(response.into_inner().hash) } pub async fn read_run_blob( @@ -1856,7 +1852,7 @@ impl Client { .client .read_run_blob() .id(run_id.to_string()) - .blob_hash(blob_hash.to_string()) + .blob_hash(*blob_hash) .send() .await; match response { diff --git a/lib/packages/fabro-api-client/src/models/write-blob-response.ts b/lib/packages/fabro-api-client/src/models/write-blob-response.ts index 7e461b9e7..7058f86ab 100644 --- a/lib/packages/fabro-api-client/src/models/write-blob-response.ts +++ b/lib/packages/fabro-api-client/src/models/write-blob-response.ts @@ -19,7 +19,7 @@ */ export interface WriteBlobResponse { /** - * Content-addressed hash of the stored blob. + * Content-addressed SHA-256 hash of a stored blob. */ 'hash': string; } From af522d1aae03676f74df9e1865b3b092f135e8ae Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:37:32 -0400 Subject: [PATCH 13/63] Share the blob cache across dump Json and Text hydration hydrate_referenced_blobs_with_reader kept a per-call blob cache for the Json entries but the Text branch bypassed it, so offloaded stage responses (referenced by both checkpoint values and response.md) were fetched twice per dump. Both branches now hydrate through the shared cache, and a test pins the single-fetch behavior. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-dump/src/lib.rs | 63 +++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/lib/components/fabro-dump/src/lib.rs b/lib/components/fabro-dump/src/lib.rs index 9210a695d..33f3185f6 100644 --- a/lib/components/fabro-dump/src/lib.rs +++ b/lib/components/fabro-dump/src/lib.rs @@ -4,6 +4,7 @@ )] use std::collections::HashMap; +use std::collections::hash_map::Entry; #[expect( clippy::disallowed_types, reason = "in-memory Vec::write_all for jsonl serialization; no filesystem or network I/O" @@ -233,12 +234,23 @@ impl RunDump { let Some(blob_hash) = parse_blob_ref(text) else { continue; }; - let blob = read_blob(blob_hash) - .await? - .with_context(|| format!("blob {blob_hash:?} is missing from the store"))?; - *text = serde_json::from_slice::(&blob).with_context(|| { - format!("blob {blob_hash:?} is not a JSON string text log") - })?; + let hydrated = match cache.entry(blob_hash) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let blob = read_blob(blob_hash).await?.with_context(|| { + format!("blob {blob_hash:?} is missing from the store") + })?; + let hydrated: serde_json::Value = serde_json::from_slice(&blob) + .with_context(|| format!("blob {blob_hash:?} is not valid JSON"))?; + entry.insert(hydrated) + } + }; + *text = hydrated + .as_str() + .with_context(|| { + format!("blob {blob_hash:?} is not a JSON string text log") + })? + .to_string(); } RunDumpContents::Bytes(_) => {} } @@ -751,4 +763,43 @@ mod tests { }; assert_eq!(value["stdout"], legacy_ref); } + + #[test] + fn hydrate_referenced_blobs_fetches_shared_blobs_once() { + let blob = serde_json::to_vec("offloaded response text").unwrap(); + let blob_hash = fabro_types::BlobHash::new(&blob); + let blob_ref = fabro_types::format_blob_ref(&blob_hash); + let mut dump = RunDump { + entries: vec![ + RunDumpEntry::json("run.json", serde_json::json!({ "response": blob_ref })), + RunDumpEntry::text("stages/001-demo@1/response.md", blob_ref.clone()), + ], + stage_ranks: HashMap::new(), + dump_log_index: None, + }; + + let reads = std::cell::Cell::new(0); + executor::block_on(async { + dump.hydrate_referenced_blobs_with_reader(|read_blob_hash| { + reads.set(reads.get() + 1); + let blob = blob.clone(); + Box::pin(async move { + assert_eq!(read_blob_hash, blob_hash); + Ok(Some(bytes::Bytes::from(blob))) + }) + }) + .await + }) + .unwrap(); + + assert_eq!(reads.get(), 1, "shared blob should be fetched once"); + let RunDumpContents::Json(value) = &dump.entries[0].contents else { + panic!("entry should be JSON"); + }; + assert_eq!(value["response"], "offloaded response text"); + let RunDumpContents::Text(text) = &dump.entries[1].contents else { + panic!("entry should be text"); + }; + assert_eq!(text, "offloaded response text"); + } } From ae5c7342990a2b782d7f6c8030db277ee4ff4190 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:41:42 -0400 Subject: [PATCH 14/63] Probe sandbox locality once per context resolution pass materialize_blob_ref checked is_local_execution for every blob reference, but the sandbox and run directory are invariant across a resolution pass, so each check after the first was a redundant (and on Docker/Daytona, remote) round-trip. The check is now memoized in a per-pass SandboxLocality threaded through resolve_execution_value. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-workflow/src/artifact.rs | 98 ++++++++++++++++--- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/lib/components/fabro-workflow/src/artifact.rs b/lib/components/fabro-workflow/src/artifact.rs index a35714064..a457e7395 100644 --- a/lib/components/fabro-workflow/src/artifact.rs +++ b/lib/components/fabro-workflow/src/artifact.rs @@ -204,8 +204,16 @@ pub async fn resolve_outcomes_for_execution( run_dir: &Path, ) -> Result> { let mut resolved = node_outcomes.clone(); + let mut locality = SandboxLocality::default(); for outcome in resolved.values_mut() { - resolve_execution_values(&mut outcome.context_updates, run_store, env, run_dir).await?; + resolve_execution_values( + &mut outcome.context_updates, + run_store, + env, + run_dir, + &mut locality, + ) + .await?; } Ok(resolved) } @@ -217,7 +225,8 @@ pub async fn resolved_context_snapshot( run_dir: &Path, ) -> Result> { let mut values = context.snapshot(); - resolve_execution_values(&mut values, run_store, env, run_dir).await?; + let mut locality = SandboxLocality::default(); + resolve_execution_values(&mut values, run_store, env, run_dir, &mut locality).await?; Ok(values) } @@ -357,10 +366,12 @@ fn resolve_execution_values<'a>( run_store: &'a RunStoreHandle, env: &'a dyn Sandbox, run_dir: &'a Path, + locality: &'a mut SandboxLocality, ) -> BoxFuture<'a, Result<()>> { Box::pin(async move { for (key, value) in values.iter_mut() { - resolve_execution_value(Some(key.as_str()), value, run_store, env, run_dir).await?; + resolve_execution_value(Some(key.as_str()), value, run_store, env, run_dir, locality) + .await?; } Ok(()) }) @@ -376,6 +387,7 @@ fn resolve_execution_value<'a>( run_store: &'a RunStoreHandle, env: &'a dyn Sandbox, run_dir: &'a Path, + locality: &'a mut SandboxLocality, ) -> BoxFuture<'a, Result<()>> { Box::pin(async move { match value { @@ -383,7 +395,8 @@ fn resolve_execution_value<'a>( if key.is_some_and(is_text_context_key) { *current = resolve_text_or_blob_ref_str(current, run_store).await?; } else if let Some(blob_hash) = parse_blob_ref(current) { - *current = materialize_blob_ref(&blob_hash, run_store, env, run_dir).await?; + *current = + materialize_blob_ref(&blob_hash, run_store, env, run_dir, locality).await?; } else if current.starts_with(ARTIFACT_POINTER_PREFIX) && parse_managed_blob_file_ref(current).is_none() { @@ -392,7 +405,7 @@ fn resolve_execution_value<'a>( } Value::Array(items) => { for item in items { - resolve_execution_value(key, item, run_store, env, run_dir).await?; + resolve_execution_value(key, item, run_store, env, run_dir, locality).await?; } } Value::Object(map) => { @@ -402,8 +415,15 @@ fn resolve_execution_value<'a>( } else { Some(child_key.as_str()) }; - resolve_execution_value(child_context_key, item, run_store, env, run_dir) - .await?; + resolve_execution_value( + child_context_key, + item, + run_store, + env, + run_dir, + locality, + ) + .await?; } } Value::Null | Value::Bool(_) | Value::Number(_) => {} @@ -417,10 +437,11 @@ async fn materialize_blob_ref( run_store: &RunStoreHandle, env: &dyn Sandbox, run_dir: &Path, + locality: &mut SandboxLocality, ) -> Result { // Blobs are content-addressed, so an existing materialized file is always // current — check before paying for the store read. - if is_local_execution(env, run_dir).await? { + if locality.is_local(env, run_dir).await? { let path = local_materialized_blob_path(run_dir, blob_hash); if !path.exists() { let bytes = read_required_blob(blob_hash, run_store).await?; @@ -502,10 +523,26 @@ async fn resolve_explicit_file_ref(value: &str, env: &dyn Sandbox) -> Result Result { - env.file_exists(&run_dir.to_string_lossy()) - .await - .map_err(|e| Error::engine_with_source("failed to inspect sandbox locality", e)) +/// Memoized sandbox locality for one resolution pass. The sandbox and run +/// directory are invariant across a pass, so the (possibly remote) probe is +/// paid at most once instead of once per blob reference. +#[derive(Default)] +struct SandboxLocality { + cached: Option, +} + +impl SandboxLocality { + async fn is_local(&mut self, env: &dyn Sandbox, run_dir: &Path) -> Result { + if let Some(local) = self.cached { + return Ok(local); + } + let local = env + .file_exists(&run_dir.to_string_lossy()) + .await + .map_err(|e| Error::engine_with_source("failed to inspect sandbox locality", e))?; + self.cached = Some(local); + Ok(local) + } } fn local_materialized_blob_path(run_dir: &Path, blob_hash: &BlobHash) -> PathBuf { @@ -787,6 +824,34 @@ mod tests { ); } + #[tokio::test] + async fn resolve_context_probes_sandbox_locality_once_per_pass() { + let run_store = make_run_store("locality-probe-memoization").await; + let first_blob = run_store + .write_blob(&serde_json::to_vec(&serde_json::json!({"a": 1})).unwrap()) + .await + .unwrap(); + let second_blob = run_store + .write_blob(&serde_json::to_vec(&serde_json::json!({"b": 2})).unwrap()) + .await + .unwrap(); + let context = Context::new(); + context.set("first", fabro_types::format_blob_ref(&first_blob).into()); + context.set("second", fabro_types::format_blob_ref(&second_blob).into()); + let env = TestSyncEnv::new(true, "/workspace"); + let run_dir = tempfile::tempdir().unwrap(); + + resolved_context_snapshot(&context, &run_store.clone().into(), &env, run_dir.path()) + .await + .unwrap(); + + assert_eq!( + *env.exists_calls.lock().unwrap(), + 1, + "sandbox locality should be probed once per resolution pass" + ); + } + #[test] fn normalize_durable_updates_rewrites_managed_blob_file_refs_recursively() { let blob_hash = fabro_types::BlobHash::new(b"hello"); @@ -928,9 +993,10 @@ mod tests { use std::sync::Mutex; struct TestSyncEnv { - accessible: bool, - written: Mutex>, - working_dir: String, + accessible: bool, + written: Mutex>, + working_dir: String, + exists_calls: Mutex, } impl TestSyncEnv { @@ -939,6 +1005,7 @@ mod tests { accessible, written: Mutex::new(Vec::new()), working_dir: working_dir.to_string(), + exists_calls: Mutex::new(0), } } } @@ -962,6 +1029,7 @@ mod tests { } async fn file_exists(&self, _path: &str) -> fabro_sandbox::Result { + *self.exists_calls.lock().unwrap() += 1; Ok(self.accessible) } From 80b99e9b7bc197a16f6a65296ba41248a289cd44 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:44:29 -0400 Subject: [PATCH 15/63] Drop duplicated blob-hash rewrites from the attach normalizer The [BLOB_HASH] placeholder was defined both here and in the shared json_snapshot_filters regexes, which had to be edited in lockstep. The fabro_json_snapshot! macro always applies the shared filters to the rendered string, so the normalizer copies were redundant. Co-Authored-By: Claude Fable 5 --- lib/apps/fabro-cli/tests/it/cmd/attach.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/apps/fabro-cli/tests/it/cmd/attach.rs b/lib/apps/fabro-cli/tests/it/cmd/attach.rs index c2434f79d..15709012d 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/attach.rs @@ -66,20 +66,8 @@ fn format_output_snapshot(output: &Output, filters: &[(String, String)]) -> Stri } fn normalize_attach_json_progress_event(mut event: Value) -> Value { - if let Some(properties) = event.get_mut("properties").and_then(Value::as_object_mut) { - if properties.contains_key("manifest_blob") { - properties.insert( - "manifest_blob".to_string(), - Value::String("[BLOB_HASH]".to_string()), - ); - } - if properties.contains_key("definition_blob") { - properties.insert( - "definition_blob".to_string(), - Value::String("[BLOB_HASH]".to_string()), - ); - } - } + // manifest_blob/definition_blob hashes are already rewritten to + // [BLOB_HASH] by the shared json_snapshot_filters regexes. // Strip v2-shape server/version fields that the bridge emits, // since the test fixture's socket path is randomised per run. if let Some(settings) = event From 3524cd76d4b10d4c0a11120f868e7a3bfe37571c Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:45:23 -0400 Subject: [PATCH 16/63] Generate the blob-field snapshot filters from a field list The manifest_blob and definition_blob filter entries were copy-paste twins that had to be edited identically; build them from one loop like the elapsed-ms filters above so the pattern and placeholder cannot drift apart. Co-Authored-By: Claude Fable 5 --- lib/foundation/fabro-test/src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/foundation/fabro-test/src/lib.rs b/lib/foundation/fabro-test/src/lib.rs index cf0cc669a..00af8bf4c 100644 --- a/lib/foundation/fabro-test/src/lib.rs +++ b/lib/foundation/fabro-test/src/lib.rs @@ -1955,14 +1955,12 @@ pub fn json_snapshot_filters(mut filters: Vec<(String, String)>) -> Vec<(String, r#""id": "[EVENT_ID]""#.to_string(), )); filters = json_elapsed_ms_snapshot_filters(filters); - filters.push(( - r#""manifest_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""manifest_blob": "[BLOB_HASH]""#.to_string(), - )); - filters.push(( - r#""definition_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""definition_blob": "[BLOB_HASH]""#.to_string(), - )); + for field in ["manifest_blob", "definition_blob"] { + filters.push(( + format!(r#""{field}":\s*"[0-9a-f]{{64}}""#), + format!(r#""{field}": "[BLOB_HASH]""#), + )); + } filters.push(( r#""run_dir":\s*"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]""#.to_string(), r#""run_dir": "[RUN_DIR]""#.to_string(), From 46d4a1e5c8ed7b4871e22a9ae45024565fcb32b8 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 11:04:05 -0400 Subject: [PATCH 17/63] Inline the blob_hash_from_response alias It was a one-line passthrough to parse_blob_ref with a single caller, leaving two names for the same operation; every other consumer calls parse_blob_ref directly. Co-Authored-By: Claude Fable 5 --- lib/apps/fabro-cli/src/commands/run/output.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/apps/fabro-cli/src/commands/run/output.rs b/lib/apps/fabro-cli/src/commands/run/output.rs index 14d0e7c6f..9de4a3158 100644 --- a/lib/apps/fabro-cli/src/commands/run/output.rs +++ b/lib/apps/fabro-cli/src/commands/run/output.rs @@ -5,7 +5,7 @@ use anyhow::{Context as _, Result}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Style, Table}; use fabro_api::types; -use fabro_types::{BlobHash, PullRequestLink, RunId, StageId, parse_blob_ref}; +use fabro_types::{PullRequestLink, RunId, StageId, parse_blob_ref}; use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; use fabro_util::error::render_with_causes; use fabro_util::printer::Printer; @@ -325,7 +325,7 @@ async fn resolve_response_string( run_id: &RunId, response: &str, ) -> Result> { - let Some(blob_hash) = blob_hash_from_response(response) else { + let Some(blob_hash) = parse_blob_ref(response) else { return Ok(Some(response.to_string())); }; @@ -341,10 +341,6 @@ async fn resolve_response_string( })) } -fn blob_hash_from_response(response: &str) -> Option { - parse_blob_ref(response) -} - async fn list_artifact_display_entries_with_client( client: &server_client::Client, run_id: &RunId, From 8154a0b5fdb3446e7d414561a2d529b1d6d6ef33 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 16:16:26 -0400 Subject: [PATCH 18/63] Trigger CI From 95b511128f37b69153977f7357263f5b77ab94ea Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 17:31:15 -0400 Subject: [PATCH 19/63] Align SHA-256 hash casing contracts --- docs/public/api-reference/fabro-api.yaml | 14 +++--- lib/apps/fabro-server/src/server/tests.rs | 7 ++- .../fabro-api/tests/blob_hash_round_trip.rs | 35 ++++++++++----- .../tests/workflow_version_round_trip.rs | 4 +- lib/foundation/fabro-types/src/blob_hash.rs | 45 ++++++++++++++++--- .../src/models/artifact-batch-upload-entry.ts | 2 +- .../create-workflow-version-response.ts | 2 +- .../src/models/write-blob-response.ts | 2 +- 8 files changed, 80 insertions(+), 31 deletions(-) diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 0352d7f62..8d1c8fa14 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -9151,9 +9151,11 @@ components: example: graphs/main.fabro WorkflowVersionId: - description: SHA-256 identity of validated canonical workflow-version bytes. + description: >- + SHA-256 identity of validated canonical workflow-version bytes. Hex input is + case-insensitive; Fabro emits the canonical lowercase form. type: string - pattern: "^[0-9a-f]{64}$" + pattern: "^[0-9A-Fa-f]{64}$" example: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" WorkflowVersion: @@ -10283,9 +10285,11 @@ components: example: 42 BlobHash: - description: Content-addressed SHA-256 hash of a stored blob. + description: >- + Content-addressed SHA-256 hash of a stored blob. Hex input is case-insensitive; + Fabro emits the canonical lowercase form. type: string - pattern: "^[0-9a-f]{64}$" + pattern: "^[0-9A-Fa-f]{64}$" example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 WriteBlobResponse: @@ -10404,7 +10408,7 @@ components: example: src/lib.rs sha256: type: ["string", "null"] - description: Optional lowercase hex SHA-256 checksum for the file contents. + description: Optional SHA-256 checksum for the file contents; hex input is case-insensitive. example: 3f785df4c5b7d3f1f4c1f0ecb0f55f1d9f6f6a3d9f0a8a98f7a74f29d1f81a2c expected_bytes: type: ["integer", "null"] diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index f97892272..c6562fc3f 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -11034,7 +11034,7 @@ async fn get_checkpoint_returns_null_initially() { } #[tokio::test] -async fn write_and_read_run_blob_round_trip() { +async fn write_and_read_run_blob_accepts_uppercase_hash() { let state = test_app_state(); let app = crate::test_support::build_test_router(Arc::clone(&state)); @@ -11061,7 +11061,10 @@ async fn write_and_read_run_blob_round_trip() { let req = Request::builder() .method("GET") - .uri(api(&format!("/runs/{run_id}/blobs/{blob_hash}"))) + .uri(api(&format!( + "/runs/{run_id}/blobs/{}", + blob_hash.to_uppercase() + ))) .body(Body::empty()) .unwrap(); let response = app.oneshot(req).await.unwrap(); diff --git a/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs b/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs index 397d4d00e..e67818068 100644 --- a/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs +++ b/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs @@ -20,18 +20,15 @@ fn write_blob_response_round_trips_exact_wire_shape() { } #[test] -fn blob_hash_emits_the_documented_lowercase_pattern() { - // Serialization must match the OpenAPI schema pattern `^[0-9a-f]{64}$`. - let hash: ApiBlobHash = serde_json::from_value(json!(BLOB_HASH)).unwrap(); - let emitted = serde_json::to_value(hash).unwrap(); - assert_eq!(emitted, json!(BLOB_HASH)); - - let text = emitted.as_str().unwrap(); - assert_eq!(text.len(), 64); - assert!( - text.bytes() - .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) - ); +fn blob_hash_accepts_any_case_and_emits_lowercase() { + for input in [ + BLOB_HASH.to_string(), + BLOB_HASH.to_uppercase(), + alternating_hex_case(BLOB_HASH), + ] { + let hash: ApiBlobHash = serde_json::from_value(json!(input)).unwrap(); + assert_eq!(serde_json::to_value(hash).unwrap(), json!(BLOB_HASH)); + } } #[test] @@ -48,3 +45,17 @@ fn assert_same_type() { type_name::() ); } + +fn alternating_hex_case(value: &str) -> String { + value + .chars() + .enumerate() + .map(|(index, character)| { + if index % 2 == 0 { + character.to_ascii_uppercase() + } else { + character + } + }) + .collect() +} diff --git a/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs b/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs index 3bf0c8835..b478e393e 100644 --- a/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs +++ b/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs @@ -40,9 +40,7 @@ fn create_workflow_version_response_round_trips_exact_wire_shape() { } #[test] -fn workflow_version_id_emits_the_documented_lowercase_pattern() { - // Input is accepted case-insensitively, but serialization must match the - // OpenAPI schema pattern `^[0-9a-f]{64}$`. +fn workflow_version_id_accepts_any_case_and_emits_lowercase() { let id = serde_json::from_value::(json!(DEPENDENCY_ID.to_uppercase())) .unwrap(); let emitted = serde_json::to_value(id).unwrap(); diff --git a/lib/foundation/fabro-types/src/blob_hash.rs b/lib/foundation/fabro-types/src/blob_hash.rs index a99dd007a..45f9de0ad 100644 --- a/lib/foundation/fabro-types/src/blob_hash.rs +++ b/lib/foundation/fabro-types/src/blob_hash.rs @@ -6,6 +6,10 @@ use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::{Digest, Sha256}; +/// SHA-256 content identity. +/// +/// Parsing accepts exactly 64 hexadecimal digits case-insensitively. Display +/// and serialization emit the canonical lowercase form. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct BlobHash([u8; 32]); @@ -76,10 +80,17 @@ mod tests { } #[test] - fn display_and_parse_round_trip() { + fn parse_accepts_any_case_and_display_normalizes_to_lowercase() { let blob_hash = BlobHash::new(b"hello"); - let parsed: BlobHash = blob_hash.to_string().parse().unwrap(); - assert_eq!(parsed, blob_hash); + let lowercase = blob_hash.to_string(); + let uppercase = lowercase.to_uppercase(); + let mixed_case = alternating_hex_case(&lowercase); + + for value in [&lowercase, &uppercase, &mixed_case] { + let parsed: BlobHash = value.parse().unwrap(); + assert_eq!(parsed, blob_hash); + assert_eq!(parsed.to_string(), lowercase); + } } #[test] @@ -91,8 +102,30 @@ mod tests { } #[test] - fn parse_rejects_non_hex_blob_hashes() { - let parsed = "not-a-blob-hash".parse::(); - assert!(parsed.is_err()); + fn parse_rejects_invalid_shapes() { + for value in [ + String::new(), + "0".repeat(63), + "0".repeat(65), + "g".repeat(64), + format!("0x{}", "0".repeat(64)), + format!(" {}", "0".repeat(64)), + ] { + assert!(value.parse::().is_err(), "accepted {value:?}"); + } + } + + fn alternating_hex_case(value: &str) -> String { + value + .chars() + .enumerate() + .map(|(index, character)| { + if index % 2 == 0 { + character.to_ascii_uppercase() + } else { + character + } + }) + .collect() } } diff --git a/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts b/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts index 29d80b063..160e12f90 100644 --- a/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts +++ b/lib/packages/fabro-api-client/src/models/artifact-batch-upload-entry.ts @@ -27,7 +27,7 @@ export interface ArtifactBatchUploadEntry { */ 'path': string; /** - * Optional lowercase hex SHA-256 checksum for the file contents. + * Optional SHA-256 checksum for the file contents; hex input is case-insensitive. */ 'sha256'?: string | null; /** diff --git a/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts b/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts index 284fda5a5..de58626b1 100644 --- a/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts +++ b/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts @@ -19,7 +19,7 @@ */ export interface CreateWorkflowVersionResponse { /** - * SHA-256 identity of validated canonical workflow-version bytes. + * SHA-256 identity of validated canonical workflow-version bytes. Hex input is case-insensitive; Fabro emits the canonical lowercase form. */ 'workflow_version_id': string; } diff --git a/lib/packages/fabro-api-client/src/models/write-blob-response.ts b/lib/packages/fabro-api-client/src/models/write-blob-response.ts index 7058f86ab..d926f8118 100644 --- a/lib/packages/fabro-api-client/src/models/write-blob-response.ts +++ b/lib/packages/fabro-api-client/src/models/write-blob-response.ts @@ -19,7 +19,7 @@ */ export interface WriteBlobResponse { /** - * Content-addressed SHA-256 hash of a stored blob. + * Content-addressed SHA-256 hash of a stored blob. Hex input is case-insensitive; Fabro emits the canonical lowercase form. */ 'hash': string; } From 14cc56b25f6e647b1cea35477eed27f0372f1ba8 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 17:01:04 -0400 Subject: [PATCH 20/63] Remove stale env-interpolation promises from docs Config {{ env.NAME }} interpolation was removed workspace-wide (tokens still parse only to fail with a migration message), but several doc comments and the server-secrets strategy doc still presented it as a live mechanism, including run goal file paths where the new workflow-version validation now makes the contradiction user-visible. Co-Authored-By: Claude Fable 5 --- docs/internal/server-secrets-strategy.md | 16 +++++++--------- .../fabro-workflow/src/operations/source.rs | 4 ++-- lib/components/fabro-workflow/src/run_options.rs | 4 ++-- lib/foundation/fabro-config/src/layers/run.rs | 6 ++---- lib/foundation/fabro-model/src/catalog.rs | 8 ++++---- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/docs/internal/server-secrets-strategy.md b/docs/internal/server-secrets-strategy.md index 76f12087d..839f2bcec 100644 --- a/docs/internal/server-secrets-strategy.md +++ b/docs/internal/server-secrets-strategy.md @@ -13,7 +13,7 @@ when does it resolve** — see [Which process resolves what](#which-process-reso - Resolution is snapshot-based: env and file are read once at construction, then treated as immutable for the life of the process. - `process env` wins over `server.env` on conflicts. - Optional integration secrets are vault-only in the **server process**. Do not add optional server integrations to `ServerSecrets`, and do not add bespoke env fallback paths to it. -- Not every credential is a `ServerSecrets` or vault lookup. A third mechanism exists: **settings-declared credentials** in `InterpString` fields, resolved at consumption time from `{{ env.NAME }}` or `{{ secrets.NAME }}`. See [Settings-declared credentials](#settings-declared-credentials). +- Not every credential is a `ServerSecrets` or vault lookup. A third mechanism exists: **settings-declared credentials** in `InterpString` fields, resolved at consumption time from `{{ secrets.NAME }}`. See [Settings-declared credentials](#settings-declared-credentials). - `fabro server start` never generates secrets. Missing required secrets are a startup error. - `std::env::set_var` and `std::env::remove_var` are banned workspace-wide. Tests are not exempt. Enforced by clippy via `disallowed_methods` in `clippy.toml`; intentional exceptions must be annotated with a scoped `#[expect(clippy::disallowed_methods, reason = "...")]` at the call site. @@ -70,7 +70,6 @@ than saying "server runtime", which is ambiguous. | Bootstrap server secret | Server process, via `ServerSecrets` | Once at construction, then immutable | | Optional integration secret | Server process or worker, via the vault | At use | | `{{ vars.NAME }}` | Server process | When the run is created, from that run's variable snapshot | -| `{{ env.NAME }}` | The process that owns the value (usually the worker) | At consumption time | | `{{ secrets.NAME }}` | The process that owns the value, against the server vault | At consumption time | `docs/public/agents/mcp.mdx` documents the same split for MCP server configuration and is a good @@ -80,18 +79,17 @@ worked example of the shape. Some credentials are declared in settings rather than looked up by name. Those fields are `InterpString` (`lib/foundation/fabro-types/src/settings/interp.rs`), which supports narrow -`{{ namespace.NAME }}` tokens with no template logic. Three namespaces resolve: `env` (process -environment, consumption time), `secrets` (vault, consumption time), and `vars` (non-sensitive run -variables, substituted early at run creation). A token whose namespace is unavailable in the -resolution context fails loudly. +`{{ namespace.NAME }}` tokens with no template logic. Two namespaces resolve: `secrets` (vault, +consumption time) and `vars` (non-sensitive run variables, substituted early at run creation). +`{{ env.NAME }}` tokens still parse but never resolve; they fail loudly with a migration message. A +token whose namespace is unavailable in the resolution context also fails loudly. -The reference implementation is LLM provider `extra_headers`, resolved against env plus vault at +The reference implementation is LLM provider `extra_headers`, resolved against the vault at `lib/foundation/fabro-auth/src/resolve.rs:376-378`: ```toml [llm.providers.example.extra_headers] authorization = "Bearer {{ secrets.EXAMPLE_TOKEN }}" -x-tenant = "{{ env.EXAMPLE_TENANT }}" ``` Use this mechanism when the credential belongs to an operator-configured integration declared in @@ -149,7 +147,7 @@ First pick the mechanism. These are the only three: |---|---|---| | Bootstrap server secret | Platform env or install-written `server.env` | `state.server_secret(...)` | | Optional integration secret | Vault (`fabro secret set`, `fabro install`) | `state.vault_secret(...)` | -| Settings-declared credential | `{{ secrets.* }}` or `{{ env.* }}` in an `InterpString` settings field | Resolved at consumption time by the owning process | +| Settings-declared credential | `{{ secrets.* }}` in an `InterpString` settings field | Resolved at consumption time by the owning process | Then: diff --git a/lib/components/fabro-workflow/src/operations/source.rs b/lib/components/fabro-workflow/src/operations/source.rs index 3e275937c..195566b7a 100644 --- a/lib/components/fabro-workflow/src/operations/source.rs +++ b/lib/components/fabro-workflow/src/operations/source.rs @@ -109,8 +109,8 @@ pub(crate) fn resolve_workflow(request: ResolveWorkflowInput) -> anyhow::Result< /// Resolve the `run.goal` override for a direct (non-manifest) workflow /// run. Reads the file from disk if the goal layer is the `file` variant. -/// Relative paths that survived config load (e.g. env-interpolated ones) -/// are anchored at `working_directory`. +/// Relative paths that survived config load are anchored at +/// `working_directory`. fn resolve_goal_override( settings: &WorkflowSettings, working_directory: &Path, diff --git a/lib/components/fabro-workflow/src/run_options.rs b/lib/components/fabro-workflow/src/run_options.rs index 46fffe790..7c4ca98d6 100644 --- a/lib/components/fabro-workflow/src/run_options.rs +++ b/lib/components/fabro-workflow/src/run_options.rs @@ -78,8 +78,8 @@ pub struct LifecycleOptions { } /// A single setup (prepare) command and the per-step environment it runs with. -/// Both the command string and the env values are already fully resolved (their -/// `{{ env.* }}` tokens replaced at the run boundary) by the time they reach +/// Both the command string and the env values are already fully resolved +/// (interpolation tokens replaced at the run boundary) by the time they reach /// the sandbox. pub struct SetupCommand { pub command: String, diff --git a/lib/foundation/fabro-config/src/layers/run.rs b/lib/foundation/fabro-config/src/layers/run.rs index f81b4e0f6..8b020666f 100644 --- a/lib/foundation/fabro-config/src/layers/run.rs +++ b/lib/foundation/fabro-config/src/layers/run.rs @@ -116,10 +116,8 @@ impl Combine for RunIntegrationsGithubLayer { /// /// Relative paths inside the `file` variant are resolved against the /// directory of the config file that declared them at load time (see -/// `fabro_config::resolve_goal_file_paths`). `{{ env.NAME }}` interpolation is -/// supported inside the `file` path; env-tokenized relative paths stay -/// unresolved until consume time and are then resolved against the run's -/// effective working directory. +/// `fabro_config::resolve_goal_file_paths`). Interpolation tokens are not +/// supported inside the `file` path; a tokenized path fails to resolve. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged, deny_unknown_fields)] pub enum RunGoalLayer { diff --git a/lib/foundation/fabro-model/src/catalog.rs b/lib/foundation/fabro-model/src/catalog.rs index 7ff4b266f..56c0f5b0c 100644 --- a/lib/foundation/fabro-model/src/catalog.rs +++ b/lib/foundation/fabro-model/src/catalog.rs @@ -61,8 +61,8 @@ pub struct ProviderCatalogSettings { pub api_key_url: Option, #[serde(default)] pub base_url: Option, - /// Unresolved interpolation source strings (literal text, `{{ env.NAME }}`, - /// or `{{ secrets.NAME }}` tokens), resolved at the credential boundary in + /// Unresolved interpolation source strings (literal text or + /// `{{ secrets.NAME }}` tokens), resolved at the credential boundary in /// `fabro-auth`. #[serde(default)] pub extra_headers: Option>, @@ -438,8 +438,8 @@ pub struct CatalogProvider { pub billing_policy: BillingPolicy, pub api_key_url: Option, pub base_url: Option, - /// Unresolved interpolation source strings (literal text, `{{ env.NAME }}`, - /// or `{{ secrets.NAME }}` tokens), resolved at the credential boundary in + /// Unresolved interpolation source strings (literal text or + /// `{{ secrets.NAME }}` tokens), resolved at the credential boundary in /// `fabro-auth`. pub extra_headers: HashMap, pub priority: i32, From 679bc6701bf9dada17797cee1920dfec471038e8 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 17:51:46 -0400 Subject: [PATCH 21/63] Parse template dependencies whose paths collide with discovery roots Dependency discovery pre-seeded roots into the path-keyed result map and reused that map as the traversal-dedup set, so a loaded include target whose path matched a root was recorded but never parsed (an include chain that reaches the file anchoring a root silently skips its content), and a second root occurrence at an already-seeded path was dropped without parsing. Dedup traversal on the full (path, root, content) occurrence instead, so every distinct authored occurrence is parsed exactly once and identical duplicates parse once. Co-Authored-By: Claude Fable 5 --- .../fabro-template/src/dependency.rs | 32 +++-- lib/foundation/fabro-template/src/lib.rs | 117 ++++++++++++++++++ 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/lib/foundation/fabro-template/src/dependency.rs b/lib/foundation/fabro-template/src/dependency.rs index fa9629108..289032e38 100644 --- a/lib/foundation/fabro-template/src/dependency.rs +++ b/lib/foundation/fabro-template/src/dependency.rs @@ -80,15 +80,30 @@ pub fn discover_static_dependency_closure( store: &dyn TemplateStore, ) -> Result { let mut sources = HashMap::new(); + // A root and a loaded file can collide on `path` while carrying different + // content (an inline prompt is anchored at its graph file's path), so + // traversal dedup keys on the full occurrence rather than the path: a + // path-keyed check would leave the collided occurrence unparsed. The + // result map stays path-keyed, with the last distinct occurrence winning. + let mut parsed = HashSet::new(); let mut queue = VecDeque::new(); - for source in roots { - if sources - .insert(source.path.clone(), source.clone()) - .is_none() - { + let mut enqueue = |source: TemplateSource, + sources: &mut HashMap, + queue: &mut VecDeque| { + let occurrence = ( + source.path.clone(), + source.root.clone(), + source.content.clone(), + ); + if parsed.insert(occurrence) { + sources.insert(source.path.clone(), source.clone()); queue.push_back(source); } + }; + + for source in roots { + enqueue(source, &mut sources, &mut queue); } while let Some(source) = queue.pop_front() { @@ -106,12 +121,7 @@ pub fn discover_static_dependency_closure( reference: dependency.reference.clone(), } })?; - if sources - .insert(loaded.path.clone(), loaded.clone()) - .is_none() - { - queue.push_back(loaded); - } + enqueue(loaded, &mut sources, &mut queue); } } diff --git a/lib/foundation/fabro-template/src/lib.rs b/lib/foundation/fabro-template/src/lib.rs index 8381f5fec..9fc89697b 100644 --- a/lib/foundation/fabro-template/src/lib.rs +++ b/lib/foundation/fabro-template/src/lib.rs @@ -1388,6 +1388,123 @@ mod tests { assert!(matches!(err, TemplateDiscoveryError::Dynamic { .. })); } + #[test] + fn static_dependency_closure_visits_colliding_root_occurrences() { + let roots = [ + TemplateSource::new(manifest_path("workflow.fabro"), manifest_path("."), "valid"), + TemplateSource::new( + manifest_path("workflow.fabro"), + manifest_path("."), + r"{% include inputs.partial %}", + ), + ]; + + let error = + discover_static_dependency_closure(roots, bundle_store(&[]).as_ref()).unwrap_err(); + + assert!(matches!( + error, + TemplateDiscoveryError::Dynamic { parent } + if parent == manifest_path("workflow.fabro") + )); + } + + #[test] + fn static_dependency_closure_parses_dependencies_shadowed_by_root_paths() { + // An inline root anchored at its graph file's path must not shadow the + // file itself when another template includes it: the loaded file + // content still gets parsed. + let roots = [ + TemplateSource::new(manifest_path("workflow.fabro"), manifest_path("."), "valid"), + TemplateSource::new( + manifest_path("goal.md"), + manifest_path("."), + r#"{% include "workflow.fabro" %}"#, + ), + ]; + + let error = discover_static_dependency_closure( + roots, + bundle_store(&[("workflow.fabro", r#"{% include "missing.md" %}"#)]).as_ref(), + ) + .unwrap_err(); + + assert!(matches!( + error, + TemplateDiscoveryError::Missing { parent, reference } + if parent == manifest_path("workflow.fabro") && reference == "missing.md" + )); + } + + #[test] + fn static_dependency_closure_parses_identical_root_occurrences_once() { + struct CountingStore { + inner: Arc, + loads: std::sync::atomic::AtomicUsize, + } + + impl TemplateStore for CountingStore { + fn load( + &self, + parent: &TemplateSource, + reference: &str, + ) -> Result, TemplateLoadError> { + self.loads + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.inner.load(parent, reference) + } + } + + let root = TemplateSource::new( + manifest_path("main.md"), + manifest_path("."), + r#"{% include "shared.md" %}"#, + ); + let store = CountingStore { + inner: bundle_store(&[("shared.md", "shared")]), + loads: std::sync::atomic::AtomicUsize::new(0), + }; + + let closure = discover_static_dependency_closure([root.clone(), root], &store).unwrap(); + + assert!(closure.sources.contains_key(&manifest_path("shared.md"))); + assert_eq!(store.loads.load(std::sync::atomic::Ordering::Relaxed), 1); + } + + #[test] + fn static_dependency_closure_deduplicates_loaded_dependencies_across_roots() { + let roots = [ + TemplateSource::new( + manifest_path("first.md"), + manifest_path("."), + r#"{% include "shared.md" %}"#, + ), + TemplateSource::new( + manifest_path("second.md"), + manifest_path("."), + r#"{% include "shared.md" %}"#, + ), + ]; + + let closure = discover_static_dependency_closure( + roots, + bundle_store(&[ + ("shared.md", r#"{% include "nested.md" %}"#), + ("nested.md", "nested"), + ]) + .as_ref(), + ) + .unwrap(); + + assert_eq!( + closure.paths(), + ["first.md", "second.md", "shared.md", "nested.md"] + .into_iter() + .map(manifest_path) + .collect() + ); + } + #[test] fn render_lenient_named_preserves_source_name_for_syntax_errors() { let ctx = TemplateContext::new(); From 9459ce1d041f947e070e1c9d7343d6c04e02d738 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 17:53:13 -0400 Subject: [PATCH 22/63] Attribute template discovery errors to their source by construction TemplateDiscoveryError only named a failing source through the Display strings of its variants: parse and load failures forwarded transparently to inner errors whose source naming varies (parent for some load failures, the child path for dynamic dependencies, nothing for I/O faults), so consumers that need the failing template's path had to string-round-trip error messages. Carry the parent path on every variant, exposing a total source_path() accessor, and render parse and load failures with a parent-naming message above the preserved source chain. Co-Authored-By: Claude Fable 5 --- .../fabro-template/src/dependency.rs | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/foundation/fabro-template/src/dependency.rs b/lib/foundation/fabro-template/src/dependency.rs index 289032e38..2de5fe560 100644 --- a/lib/foundation/fabro-template/src/dependency.rs +++ b/lib/foundation/fabro-template/src/dependency.rs @@ -32,10 +32,18 @@ pub struct ExtractedTemplateDependencies { #[derive(Debug, Error)] pub enum TemplateDiscoveryError { - #[error(transparent)] - Parse(#[from] TemplateError), - #[error(transparent)] - Load(#[from] TemplateLoadError), + #[error("invalid template `{parent}`")] + Parse { + parent: ManifestPath, + #[source] + source: Box, + }, + #[error("failed to load a template dependency of `{parent}`")] + Load { + parent: ManifestPath, + #[source] + source: TemplateLoadError, + }, #[error("missing template dependency `{reference}` from `{parent}`")] Missing { parent: ManifestPath, @@ -45,6 +53,19 @@ pub enum TemplateDiscoveryError { Dynamic { parent: ManifestPath }, } +impl TemplateDiscoveryError { + /// Path of the template source this error is attributed to. + #[must_use] + pub fn source_path(&self) -> &ManifestPath { + match self { + Self::Parse { parent, .. } + | Self::Load { parent, .. } + | Self::Missing { parent, .. } + | Self::Dynamic { parent } => parent, + } + } +} + #[derive(Clone, Debug, Default)] pub struct TemplateDependencyClosure { pub sources: HashMap, @@ -107,20 +128,27 @@ pub fn discover_static_dependency_closure( } while let Some(source) = queue.pop_front() { - let dependencies = - extract_template_dependencies(&source.path.to_string(), &source.content)?; + let dependencies = extract_template_dependencies(&source.path.to_string(), &source.content) + .map_err(|error| TemplateDiscoveryError::Parse { + parent: source.path.clone(), + source: Box::new(error), + })?; if !dependencies.dynamic_references.is_empty() { return Err(TemplateDiscoveryError::Dynamic { parent: source.path, }); } for dependency in dependencies.static_references { - let loaded = store.load(&source, &dependency.reference)?.ok_or_else(|| { - TemplateDiscoveryError::Missing { + let loaded = store + .load(&source, &dependency.reference) + .map_err(|error| TemplateDiscoveryError::Load { + parent: source.path.clone(), + source: error, + })? + .ok_or_else(|| TemplateDiscoveryError::Missing { parent: source.path.clone(), reference: dependency.reference.clone(), - } - })?; + })?; enqueue(loaded, &mut sources, &mut queue); } } From 8dfbfb9aa5f443922664e6b3397446a994376e32 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 17:58:46 -0400 Subject: [PATCH 23/63] Classify graph attributes with a graph-only reference kind reference_kind_for_attribute returned the full ReferenceKind, which includes the config-sourced Dockerfile kind the classifier can never yield, so the shared graph walker carried a silent `continue` and an `unreachable!` for impossible kinds; each new config-sourced kind widens those filler arms, and a classifier extension that reuses an existing kind would be dropped by the walker without validation, visitation, or a compiler error. Return a GraphReferenceKind subset instead (converting into ReferenceKind for validation), making the walker's matches total with every arm meaningful. Co-Authored-By: Claude Fable 5 --- .../src/transforms/variable_expansion.rs | 2 +- .../fabro-template/src/static_reference.rs | 23 +++++------ lib/foundation/fabro-types/src/graph.rs | 38 +++++++++++++++---- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/lib/components/fabro-workflow/src/transforms/variable_expansion.rs b/lib/components/fabro-workflow/src/transforms/variable_expansion.rs index da7aba76d..eb33f250f 100644 --- a/lib/components/fabro-workflow/src/transforms/variable_expansion.rs +++ b/lib/components/fabro-workflow/src/transforms/variable_expansion.rs @@ -519,7 +519,7 @@ impl TemplateTransform { continue; } if let Some(kind) = reference_kind_for_attribute(scope, attr_name, text) { - validate_static_reference(text, kind) + validate_static_reference(text, kind.into()) .map_err(|error| Error::Validation(error.to_string()))?; continue; } diff --git a/lib/foundation/fabro-template/src/static_reference.rs b/lib/foundation/fabro-template/src/static_reference.rs index b832f2956..e2678ea65 100644 --- a/lib/foundation/fabro-template/src/static_reference.rs +++ b/lib/foundation/fabro-template/src/static_reference.rs @@ -11,7 +11,9 @@ //! reference-bearing attribute is added here once instead of drifting between //! per-crate walkers. -use fabro_types::graph::{AttributeScope, Graph, ReferenceKind, reference_kind_for_attribute}; +use fabro_types::graph::{ + AttributeScope, Graph, GraphReferenceKind, ReferenceKind, reference_kind_for_attribute, +}; use crate::contains_template_syntax; @@ -119,20 +121,19 @@ pub fn visit_graph_references<'graph, E>( continue; }; let reference = match kind { - ReferenceKind::Import | ReferenceKind::ChildWorkflow => value, - // Classification only yields FileInline for `@` values. - ReferenceKind::FileInline => value + GraphReferenceKind::Import | GraphReferenceKind::ChildWorkflow => value, + // Classification only yields these kinds for `@` values. + GraphReferenceKind::FileInline | GraphReferenceKind::GraphGoalFile => value .strip_prefix('@') - .expect("file inline classification requires a leading '@'"), - ReferenceKind::Dockerfile | ReferenceKind::GraphGoalFile => continue, + .expect("file reference classification requires a leading '@'"), }; - validate_static_reference(reference, kind) + validate_static_reference(reference, kind.into()) .map_err(GraphReferenceError::StaticReference)?; let event = match kind { - ReferenceKind::Import => GraphReference::Import { reference }, - ReferenceKind::ChildWorkflow => GraphReference::ChildWorkflow { reference }, - ReferenceKind::FileInline => GraphReference::FileInline { key, reference }, - ReferenceKind::Dockerfile | ReferenceKind::GraphGoalFile => unreachable!(), + GraphReferenceKind::Import => GraphReference::Import { reference }, + GraphReferenceKind::ChildWorkflow => GraphReference::ChildWorkflow { reference }, + GraphReferenceKind::FileInline => GraphReference::FileInline { key, reference }, + GraphReferenceKind::GraphGoalFile => GraphReference::GoalFile { reference }, }; visit(event).map_err(GraphReferenceError::Visit)?; } diff --git a/lib/foundation/fabro-types/src/graph.rs b/lib/foundation/fabro-types/src/graph.rs index b9f74b79f..e0d7dc17c 100644 --- a/lib/foundation/fabro-types/src/graph.rs +++ b/lib/foundation/fabro-types/src/graph.rs @@ -598,8 +598,7 @@ pub enum AttributeScope { Edge, } -/// Kinds of static (non-templated) file references a graph attribute can -/// carry. +/// Kinds of static (non-templated) workflow-owned file references. #[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display)] pub enum ReferenceKind { #[strum(to_string = "file inline reference")] @@ -614,25 +613,48 @@ pub enum ReferenceKind { GraphGoalFile, } +/// Kinds of static file references that graph attributes can carry: the +/// subset of [`ReferenceKind`] that [`reference_kind_for_attribute`] can +/// classify. Config-sourced kinds (Dockerfiles) are unrepresentable here by +/// construction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum GraphReferenceKind { + FileInline, + Import, + ChildWorkflow, + GraphGoalFile, +} + +impl From for ReferenceKind { + fn from(kind: GraphReferenceKind) -> Self { + match kind { + GraphReferenceKind::FileInline => Self::FileInline, + GraphReferenceKind::Import => Self::Import, + GraphReferenceKind::ChildWorkflow => Self::ChildWorkflow, + GraphReferenceKind::GraphGoalFile => Self::GraphGoalFile, + } + } +} + /// Classify a graph attribute as a static file reference, if it is one. #[must_use] pub fn reference_kind_for_attribute( scope: AttributeScope, key: &str, value: &str, -) -> Option { +) -> Option { match key { - "import" if matches!(scope, AttributeScope::Node) => Some(ReferenceKind::Import), + "import" if matches!(scope, AttributeScope::Node) => Some(GraphReferenceKind::Import), "stack.child_workflow" if matches!(scope, AttributeScope::Node) => { - Some(ReferenceKind::ChildWorkflow) + Some(GraphReferenceKind::ChildWorkflow) } "goal" if matches!(scope, AttributeScope::Graph) && value.starts_with('@') => { - Some(ReferenceKind::GraphGoalFile) + Some(GraphReferenceKind::GraphGoalFile) } "prompt" | "output_schema" if matches!(scope, AttributeScope::Node) && value.starts_with('@') => { - Some(ReferenceKind::FileInline) + Some(GraphReferenceKind::FileInline) } _ => None, } @@ -1168,7 +1190,7 @@ mod tests { "output_schema", "@schemas/result.schema.json", ), - Some(ReferenceKind::FileInline), + Some(GraphReferenceKind::FileInline), ); } From 400be9f2dcbfc11f5422e1886a0a5b5eb2af36b7 Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Tue, 18 Aug 2026 09:28:05 +0000 Subject: [PATCH 24/63] Bump version to 0.329.0-nightly.0 --- Cargo.lock | 104 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2f53dfa9..56f226964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2673,7 +2673,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2688,7 +2688,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -2711,7 +2711,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2721,7 +2721,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2740,7 +2740,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2755,7 +2755,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2797,7 +2797,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2808,7 +2808,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "fabro-api", @@ -2829,7 +2829,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2895,7 +2895,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2933,7 +2933,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2941,7 +2941,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "cc", "libc", @@ -2950,7 +2950,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2966,7 +2966,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3010,7 +3010,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3127,18 +3127,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" [[package]] name = "fabro-store" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3168,7 +3168,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3208,7 +3208,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3233,7 +3233,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3254,7 +3254,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3268,7 +3268,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3291,7 +3291,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3314,7 +3314,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3327,7 +3327,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3344,7 +3344,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3433,7 +3433,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8544,7 +8544,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "axum", "base64", @@ -8563,7 +8563,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index 0923bda8c..c3b7aaf91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.325.0-nightly.0" +version = "0.329.0-nightly.0" license = "MIT" [workspace.dependencies] From 18a71ac310f8f429896930500b5d5fb4a6e98682 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 18 Aug 2026 12:08:36 -0400 Subject: [PATCH 25/63] Classify provider 412s as failover-eligible account lockouts Fireworks reports an account suspension (spending cap reached or unpaid invoices) as HTTP 412 with code PRECONDITION_FAILED. The status had no explicit mapping, and the openai_compatible dialect extracts error.type ("error") as the code, so the suspension fell through to InvalidRequest -- a deterministic request defect -- which suppressed both retry and the configured model fallback chain. A live run then died mid-stage with five healthy fallback candidates configured. No LLM request carries conditional-request preconditions, so a 412 is never about the request. Map it to AccessDenied, the same family as the account_deactivated error code: non-retryable on the same provider, eligible for failover to a provider with independent billing. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-llm/src/error.rs | 54 ++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/components/fabro-llm/src/error.rs b/lib/components/fabro-llm/src/error.rs index 6ec6d7886..0409d378e 100644 --- a/lib/components/fabro-llm/src/error.rs +++ b/lib/components/fabro-llm/src/error.rs @@ -355,7 +355,12 @@ pub fn error_from_status_code( // error types let kind = match status_code { 401 => ProviderErrorKind::Authentication, - 403 => ProviderErrorKind::AccessDenied, + // A 412 is never about the request: no LLM request carries + // conditional-request preconditions. Fireworks uses it for + // account-level lockouts (suspension over a spending cap or unpaid + // invoices), the same family as `account_deactivated`: deterministic + // here, but another provider has independent billing. + 403 | 412 => ProviderErrorKind::AccessDenied, 404 => ProviderErrorKind::NotFound, 408 => { return Error::RequestTimeout { @@ -728,6 +733,53 @@ mod tests { assert_eq!(err.provider_kind(), Some(ProviderErrorKind::QuotaExceeded)); } + /// Fireworks reports an account suspension (spending cap reached or + /// unpaid invoices) as HTTP 412 with `code: "PRECONDITION_FAILED"` in + /// the body. A chat completion carries no conditional-request + /// preconditions, so a 412 is always an account-level lockout, never a + /// defect in the request: it must not classify as `InvalidRequest`, and + /// a fallback provider with independent billing must stay eligible. + #[test] + fn account_suspension_412_is_failover_eligible() { + let err = error_from_status_code( + 412, + "Account lithoscomputer is suspended, possibly due to reaching \ + the monthly spending limit or failure to pay past invoices." + .into(), + "fireworks".into(), + // The openai_compatible dialect reads `error.type` as the code, + // so the discriminating `PRECONDITION_FAILED` only reaches this + // mapping through the status code. + Some("error".into()), + Some(serde_json::json!({ + "error": { + "message": "Account lithoscomputer is suspended, possibly due to reaching the monthly spending limit or failure to pay past invoices. Please go to https://fireworks.ai/account/billing for more information.", + "param": null, + "code": "PRECONDITION_FAILED", + "type": "error" + }, + "request_id": "chatcmpl-d9652b89a6604931ac27dddd5ef5bdc0" + })), + None, + ); + + assert_eq!(err.provider_kind(), Some(ProviderErrorKind::AccessDenied)); + assert!(!err.retryable()); + assert!(err.failover_eligible()); + + // A bare 412 with no parseable body classifies the same way. + let err = error_from_status_code( + 412, + "Precondition Failed".into(), + "fireworks".into(), + None, + None, + None, + ); + assert_eq!(err.provider_kind(), Some(ProviderErrorKind::AccessDenied)); + assert!(err.failover_eligible()); + } + #[test] fn kind_from_error_code_covers_every_dialect() { for (code, expected) in [ From 1226ed737776c944fad7921601a31377cd82fb29 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 18 Aug 2026 12:10:50 -0400 Subject: [PATCH 26/63] Cite Fireworks' documentation for the 412 mapping Co-Authored-By: Claude Fable 5 --- lib/components/fabro-llm/src/error.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/components/fabro-llm/src/error.rs b/lib/components/fabro-llm/src/error.rs index 0409d378e..dd9a98347 100644 --- a/lib/components/fabro-llm/src/error.rs +++ b/lib/components/fabro-llm/src/error.rs @@ -356,10 +356,12 @@ pub fn error_from_status_code( let kind = match status_code { 401 => ProviderErrorKind::Authentication, // A 412 is never about the request: no LLM request carries - // conditional-request preconditions. Fireworks uses it for - // account-level lockouts (suspension over a spending cap or unpaid - // invoices), the same family as `account_deactivated`: deterministic - // here, but another provider has independent billing. + // conditional-request preconditions. Fireworks documents it as + // "Account is suspended or there's an issue with account status", + // also emitted for a LoRA model that failed to load + // (https://docs.fireworks.ai/guides/inference-error-codes). The same + // family as `account_deactivated`: deterministic here, but another + // provider has independent billing and model inventory. 403 | 412 => ProviderErrorKind::AccessDenied, 404 => ProviderErrorKind::NotFound, 408 => { From 63025bb748d57f73f2603d2d5534d2ca407a2d6c Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Fri, 14 Aug 2026 15:39:56 -0400 Subject: [PATCH 27/63] Close workflow goals over version dependencies --- .../src/server/handler/workflow_versions.rs | 41 ++- .../fabro-workflow-version/src/lib.rs | 323 ++++++++++++++++-- .../fabro-workflow-version/src/store.rs | 207 ++++++++++- lib/foundation/fabro-types/src/graph.rs | 6 +- 4 files changed, 539 insertions(+), 38 deletions(-) diff --git a/lib/apps/fabro-server/src/server/handler/workflow_versions.rs b/lib/apps/fabro-server/src/server/handler/workflow_versions.rs index beb5802f5..1ded22adf 100644 --- a/lib/apps/fabro-server/src/server/handler/workflow_versions.rs +++ b/lib/apps/fabro-server/src/server/handler/workflow_versions.rs @@ -116,7 +116,7 @@ mod tests { use axum::body::{Body, to_bytes}; use axum::http::{Method, Request, StatusCode, header}; use axum::response::IntoResponse; - use fabro_types::WorkflowVersionId; + use fabro_types::{BlobHash, WorkflowVersion, WorkflowVersionId}; use serde_json::{Value, json}; use tower::ServiceExt; @@ -233,6 +233,45 @@ mod tests { ); } + #[tokio::test] + async fn create_rejects_workflow_config_with_missing_goal_file_before_storage() { + let state = TestAppStateBuilder::new().build(); + let app = test_support::build_test_router(Arc::clone(&state)); + let payload = json!({ + "entrypoint": "workflow.fabro", + "files": { + "workflow.fabro": GRAPH, + "workflow.toml": "_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n" + }, + "workflow_dependencies": {} + }); + let version = serde_json::from_value::(payload.clone()).unwrap(); + let id = WorkflowVersionId::from(BlobHash::new(&version.canonical_bytes().unwrap())); + + let response = app + .oneshot(request(serde_json::to_vec(&payload).unwrap())) + .await + .unwrap(); + let body = fabro_test::expect_axum_json( + response, + StatusCode::UNPROCESSABLE_ENTITY, + "POST /api/v1/workflow-versions with missing run goal file", + ) + .await; + + assert_eq!(error_code(&body), INVALID_VERSION_CODE); + assert!( + !state + .store_ref() + .blobs() + .await + .unwrap() + .exists(&id.into()) + .await + .unwrap() + ); + } + #[tokio::test] async fn unavailable_dependency_has_specific_code() { let state = TestAppStateBuilder::new().build(); diff --git a/lib/components/fabro-workflow-version/src/lib.rs b/lib/components/fabro-workflow-version/src/lib.rs index 320d09139..a92226dd6 100644 --- a/lib/components/fabro-workflow-version/src/lib.rs +++ b/lib/components/fabro-workflow-version/src/lib.rs @@ -9,20 +9,23 @@ use std::collections::{BTreeSet, HashMap, VecDeque}; use fabro_config::parse::{SettingsSource, validate_settings_source}; -use fabro_config::{EnvironmentDockerfileLayer, EnvironmentImageLayer, SettingsLayer}; +use fabro_config::{ + EnvironmentDockerfileLayer, EnvironmentImageLayer, RunGoalLayer, SettingsLayer, +}; use fabro_graphviz::parser; use fabro_template::{ BundleTemplateStore, GraphReference, GraphReferenceError, StaticReferenceError, - TemplateDiscoveryError, TemplateSource, discover_static_dependency_closure, + TemplateDiscoveryError, TemplateLoadError, TemplateSource, discover_static_dependency_closure, validate_static_reference, visit_graph_references, }; use fabro_types::graph::ReferenceKind; +use fabro_types::settings::InterpString; use fabro_types::{ManifestPath, WorkflowPath, WorkflowPathParseError, WorkflowVersion}; use thiserror::Error; mod store; -pub use store::{WorkflowVersionStore, WorkflowVersionStoreError}; +pub use store::{LoadedWorkflowVersionClosure, WorkflowVersionStore, WorkflowVersionStoreError}; #[derive(Debug, Error)] pub enum WorkflowVersionError { @@ -88,8 +91,12 @@ pub struct ValidatedWorkflowVersion(WorkflowVersion); impl ValidatedWorkflowVersion { pub fn new(version: WorkflowVersion) -> Result { - validate_config(&version)?; - validate_graph_closure(&version)?; + let template_root = ManifestPath::from_wire(".") + .expect("the template package root must be a valid manifest path"); + let mut template_roots = Vec::new(); + validate_config(&version, &template_root, &mut template_roots)?; + validate_graph_closure(&version, &template_root, &mut template_roots)?; + validate_template_closure(&version, template_roots)?; Ok(Self(version)) } @@ -104,7 +111,11 @@ impl ValidatedWorkflowVersion { } } -fn validate_config(version: &WorkflowVersion) -> Result<(), WorkflowVersionError> { +fn validate_config( + version: &WorkflowVersion, + template_root: &ManifestPath, + template_roots: &mut Vec, +) -> Result<(), WorkflowVersionError> { let config_path = WorkflowPath::new("workflow.toml").expect("the static workflow config path must be valid"); let Some(source) = version.files().get(&config_path) else { @@ -133,9 +144,47 @@ fn validate_config(version: &WorkflowVersion) -> Result<(), WorkflowVersionError for image in layer.environment_images() { validate_dockerfile(version, &config_path, image)?; } + + match layer.run.as_ref().and_then(|run| run.goal.as_ref()) { + Some(RunGoalLayer::Inline(goal)) => template_roots.push(TemplateSource::new( + manifest_path(&config_path), + template_root.clone(), + unresolved_source(goal), + )), + Some(RunGoalLayer::File { file }) => { + let reference = unresolved_source(file); + validate_static_reference(&reference, ReferenceKind::RunGoalFile).map_err( + |source| WorkflowVersionError::StaticReference { + path: config_path.clone(), + source, + }, + )?; + let target = resolve_reference(&config_path, ReferenceKind::RunGoalFile, &reference)?; + let content = require_file( + version, + &config_path, + ReferenceKind::RunGoalFile, + target.clone(), + )?; + template_roots.push(TemplateSource::new( + manifest_path(&target), + template_root.clone(), + content, + )); + } + None => {} + } Ok(()) } +#[expect( + clippy::disallowed_methods, + reason = "workflow-version validation preserves authored template source for dependency discovery" +)] +fn unresolved_source(value: &InterpString) -> String { + value.as_source() +} + fn validate_dockerfile( version: &WorkflowVersion, config_path: &WorkflowPath, @@ -154,10 +203,11 @@ fn validate_dockerfile( require_file(version, config_path, ReferenceKind::Dockerfile, target).map(|_| ()) } -fn validate_graph_closure(version: &WorkflowVersion) -> Result<(), WorkflowVersionError> { - let template_store = template_store(version); - let template_root = ManifestPath::from_wire(".") - .expect("the template package root must be a valid manifest path"); +fn validate_graph_closure( + version: &WorkflowVersion, + template_root: &ManifestPath, + template_roots: &mut Vec, +) -> Result<(), WorkflowVersionError> { let mut queue = VecDeque::from([version.entrypoint().clone()]); let mut visited = BTreeSet::new(); let mut child_workflows = BTreeSet::new(); @@ -185,10 +235,20 @@ fn validate_graph_closure(version: &WorkflowVersion) -> Result<(), WorkflowVersi let target = resolve_reference(&path, ReferenceKind::GraphGoalFile, reference)?; let content = require_file(version, &path, ReferenceKind::GraphGoalFile, target.clone())?; - validate_template(&target, content, &template_store, &template_root) + template_roots.push(TemplateSource::new( + manifest_path(&target), + template_root.clone(), + content, + )); + Ok(()) } GraphReference::GoalInline { content } | GraphReference::InlinePrompt { content } => { - validate_template(&path, content, &template_store, &template_root) + template_roots.push(TemplateSource::new( + manifest_path(&path), + template_root.clone(), + content, + )); + Ok(()) } GraphReference::Import { reference } => { let target = resolve_reference(&path, ReferenceKind::Import, reference)?; @@ -206,7 +266,11 @@ fn validate_graph_closure(version: &WorkflowVersion) -> Result<(), WorkflowVersi let content = require_file(version, &path, ReferenceKind::FileInline, target.clone())?; if key == "prompt" { - validate_template(&target, content, &template_store, &template_root)?; + template_roots.push(TemplateSource::new( + manifest_path(&target), + template_root.clone(), + content, + )); } Ok(()) } @@ -234,24 +298,39 @@ fn validate_graph_closure(version: &WorkflowVersion) -> Result<(), WorkflowVersi Ok(()) } -fn validate_template( - path: &WorkflowPath, - content: &str, - store: &BundleTemplateStore, - root: &ManifestPath, +fn validate_template_closure( + version: &WorkflowVersion, + roots: Vec, ) -> Result<(), WorkflowVersionError> { - let manifest_path = manifest_path(path); - discover_static_dependency_closure( - [TemplateSource::new(manifest_path, root.clone(), content)], - store, - ) - .map_err(|source| WorkflowVersionError::Template { - path: path.clone(), - source: Box::new(source), + discover_static_dependency_closure(roots, &template_store(version)).map_err(|source| { + WorkflowVersionError::Template { + path: template_discovery_path(&source), + source: Box::new(source), + } })?; Ok(()) } +fn template_discovery_path(error: &TemplateDiscoveryError) -> WorkflowPath { + let path = match error { + TemplateDiscoveryError::Parse(source) => source + .source_name() + .expect("dependency extraction must retain its source name") + .to_owned(), + TemplateDiscoveryError::Load(source) => match source { + TemplateLoadError::UnsafeReference { parent, .. } + | TemplateLoadError::EscapesRoot { parent, .. } => parent.to_string(), + TemplateLoadError::DynamicDependency { path } => path.to_string(), + TemplateLoadError::Io { .. } => { + unreachable!("bundle template dependency discovery cannot perform filesystem I/O") + } + }, + TemplateDiscoveryError::Missing { parent, .. } + | TemplateDiscoveryError::Dynamic { parent } => parent.to_string(), + }; + WorkflowPath::new(path).expect("template paths sourced from a workflow version must be valid") +} + fn template_store(version: &WorkflowVersion) -> BundleTemplateStore { BundleTemplateStore::new( version @@ -300,6 +379,10 @@ fn manifest_path(path: &WorkflowPath) -> ManifestPath { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + + use fabro_template::{TemplateDiscoveryError, TemplateLoadError}; + use fabro_types::graph::ReferenceKind; use fabro_types::{BlobHash, WorkflowPath, WorkflowVersion, WorkflowVersionId}; use super::{ValidatedWorkflowVersion, WorkflowVersionError}; @@ -332,6 +415,38 @@ mod tests { ) } + fn version_with_config( + config: String, + extra_files: impl IntoIterator, + ) -> Result { + let mut files = extra_files + .into_iter() + .map(|(path_value, content)| (path(path_value), content.to_owned())) + .collect::>(); + files.insert(path("workflow.fabro"), "digraph W {}".to_owned()); + files.insert(path("workflow.toml"), config); + ValidatedWorkflowVersion::new( + WorkflowVersion::new(path("workflow.fabro"), files, BTreeMap::default()) + .expect("test fixtures must be structurally valid"), + ) + } + + fn version_with_goal_file( + reference: &str, + ) -> Result { + let reference = serde_json::to_string(reference).unwrap(); + version_with_config(format!("_version = 1\n[run.goal]\nfile = {reference}\n"), [ + ]) + } + + fn version_with_inline_goal( + goal: &str, + extra_files: impl IntoIterator, + ) -> Result { + let goal = serde_json::to_string(goal).unwrap(); + version_with_config(format!("_version = 1\n[run]\ngoal = {goal}\n"), extra_files) + } + #[test] fn validates_imports_templates_file_refs_and_dependencies() { let version = version_with( @@ -430,6 +545,162 @@ mod tests { )); } + #[test] + fn rejects_missing_workflow_goal_file() { + let error = version_with( + [ + ("workflow.fabro", "digraph W {}"), + ( + "workflow.toml", + "_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n", + ), + ], + [], + ) + .unwrap_err(); + + assert!(matches!( + error, + WorkflowVersionError::MissingFile { + path: source_path, + kind, + target, + } + if source_path == path("workflow.toml") + && kind == ReferenceKind::RunGoalFile + && target == path("prompts/goal.md") + )); + } + + #[test] + fn accepts_inline_workflow_goal_with_static_template_closure() { + let version = version_with_inline_goal( + r#"Review {{ vars.target }} with {{ inputs.mode }} after {{ goal }}. {% include "prompts/shared.md" %}"#, + [("prompts/shared.md", "Use {{ vars.detail }}")], + ) + .unwrap(); + + assert_eq!(version.version().files().len(), 3); + } + + #[test] + fn accepts_file_workflow_goal_with_transitive_template_closure() { + let version = version_with_config( + "_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n".to_owned(), + [ + ("prompts/goal.md", r#"{% include "partial.md" %}"#), + ("prompts/partial.md", r#"{% include "nested/detail.md" %}"#), + ("prompts/nested/detail.md", "Use {{ vars.detail }}"), + ], + ) + .unwrap(); + + assert_eq!(version.version().files().len(), 5); + } + + #[test] + fn rejects_non_static_or_nonportable_workflow_goal_file_references() { + for reference in ["{{ vars.NAME }}", "{% include \"goal.md\" %}"] { + let error = version_with_goal_file(reference).unwrap_err(); + let WorkflowVersionError::StaticReference { + path: source_path, + source, + } = error + else { + panic!("expected static-reference error for {reference:?}"); + }; + assert_eq!(source_path, path("workflow.toml")); + assert_eq!(source.kind(), ReferenceKind::RunGoalFile); + } + + for reference in [ + "", + "/absolute.md", + "../outside.md", + "~/goal.md", + "C:/goal.md", + "prompts\\goal.md", + "prompts//goal.md", + "prompts/", + "prompts/goal\n.md", + ] { + let error = version_with_goal_file(reference).unwrap_err(); + assert!( + matches!( + &error, + WorkflowVersionError::InvalidReference { + path: source_path, + kind: ReferenceKind::RunGoalFile, + .. + } if *source_path == path("workflow.toml") + ), + "expected invalid-reference error for {reference:?}, got {error:?}" + ); + } + } + + #[test] + fn rejects_invalid_workflow_goal_template_closure() { + let missing = version_with_inline_goal(r#"{% include "missing.md" %}"#, []).unwrap_err(); + let WorkflowVersionError::Template { + path: source_path, + source, + } = missing + else { + panic!("expected missing template dependency"); + }; + assert_eq!(source_path, path("workflow.toml")); + assert!(matches!( + source.as_ref(), + TemplateDiscoveryError::Missing { parent, reference } + if parent.to_string() == "workflow.toml" && reference == "missing.md" + )); + + let dynamic = version_with_inline_goal(r"{% include inputs.partial %}", []).unwrap_err(); + let WorkflowVersionError::Template { source, .. } = dynamic else { + panic!("expected dynamic template dependency"); + }; + assert!(matches!( + source.as_ref(), + TemplateDiscoveryError::Dynamic { parent } + if parent.to_string() == "workflow.toml" + )); + + let escaping = + version_with_inline_goal(r#"{% include "../outside.md" %}"#, []).unwrap_err(); + let WorkflowVersionError::Template { source, .. } = escaping else { + panic!("expected escaping template dependency"); + }; + assert!(matches!( + source.as_ref(), + TemplateDiscoveryError::Load(TemplateLoadError::EscapesRoot { parent, .. }) + if parent.to_string() == "workflow.toml" + )); + } + + #[test] + fn validates_all_inline_graph_roots_that_share_the_graph_path() { + let error = version_with( + [( + "workflow.fabro", + r#"digraph W { + graph [goal="valid"] + step [prompt="{% include inputs.partial %}"] + }"#, + )], + [], + ) + .unwrap_err(); + + assert!(matches!( + error, + WorkflowVersionError::Template { + source, + .. + } if matches!(source.as_ref(), TemplateDiscoveryError::Dynamic { .. }) + )); + } + #[test] fn accepts_root_config_and_all_dockerfile_path_sources() { let version = version_with( diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs index 80dcebd53..21d6293f1 100644 --- a/lib/components/fabro-workflow-version/src/store.rs +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -40,6 +40,48 @@ pub enum WorkflowVersionStoreError { }, } +/// A fully loaded and validated workflow-version dependency graph. +/// +/// The requested root is always present exactly once alongside every unique +/// transitive dependency, keyed by canonical content ID. +#[derive(Clone, Debug)] +pub struct LoadedWorkflowVersionClosure { + root_id: WorkflowVersionId, + versions: BTreeMap, +} + +impl LoadedWorkflowVersionClosure { + #[must_use] + pub fn root_id(&self) -> WorkflowVersionId { + self.root_id + } + + #[must_use] + pub fn root(&self) -> &WorkflowVersion { + self.versions + .get(&self.root_id) + .expect("a loaded workflow-version closure must contain its root") + .version() + } + + #[must_use] + pub fn get(&self, id: &WorkflowVersionId) -> Option<&WorkflowVersion> { + self.versions.get(id).map(ValidatedWorkflowVersion::version) + } + + pub fn versions(&self) -> impl Iterator + '_ { + self.versions + .iter() + .map(|(id, version)| (*id, version.version())) + } + + fn into_root(mut self) -> ValidatedWorkflowVersion { + self.versions + .remove(&self.root_id) + .expect("a loaded workflow-version closure must contain its root") + } +} + /// Content-addressed storage for validated workflow versions. /// /// `put` only accepts semantically validated versions; `get` re-validates @@ -61,7 +103,7 @@ impl WorkflowVersionStore { version: &ValidatedWorkflowVersion, ) -> Result { let canonical = version.version().canonical_bytes()?; - self.validate_dependency_closure(version.version().workflow_dependencies()) + self.load_dependency_closure(version.version().workflow_dependencies(), HashSet::new()) .await?; self.blobs .write(&canonical) @@ -74,12 +116,30 @@ impl WorkflowVersionStore { &self, id: &WorkflowVersionId, ) -> Result, WorkflowVersionStoreError> { - let Some(version) = self.load_one(id).await? else { + let Some(closure) = self.get_closure(id).await? else { return Ok(None); }; - self.validate_dependency_closure(version.version().workflow_dependencies()) + Ok(Some(closure.into_root())) + } + + pub async fn get_closure( + &self, + root_id: &WorkflowVersionId, + ) -> Result, WorkflowVersionStoreError> { + let Some(root) = self.load_one(root_id).await? else { + return Ok(None); + }; + let mut versions = self + .load_dependency_closure( + root.version().workflow_dependencies(), + HashSet::from([*root_id]), + ) .await?; - Ok(Some(version)) + versions.insert(*root_id, root); + Ok(Some(LoadedWorkflowVersionClosure { + root_id: *root_id, + versions, + })) } async fn load_one( @@ -105,15 +165,17 @@ impl WorkflowVersionStore { Ok(Some(validated)) } - async fn validate_dependency_closure( + async fn load_dependency_closure( &self, dependencies: &BTreeMap, - ) -> Result<(), WorkflowVersionStoreError> { + mut visited: HashSet, + ) -> Result, WorkflowVersionStoreError> + { let mut pending = dependencies .iter() .map(|(path, id)| (path.clone(), *id)) .collect::>(); - let mut visited = HashSet::new(); + let mut versions = BTreeMap::new(); while let Some((path, id)) = pending.pop_front() { if !visited.insert(id) { @@ -128,6 +190,7 @@ impl WorkflowVersionStore { .iter() .map(|(path, id)| (path.clone(), *id)), ); + versions.insert(id, dependency); } Ok(None) => { return Err(WorkflowVersionStoreError::DependencyNotFound { path, id }); @@ -144,7 +207,7 @@ impl WorkflowVersionStore { } } } - Ok(()) + Ok(versions) } } @@ -180,6 +243,12 @@ mod tests { .unwrap() } + fn version_id(version: &ValidatedWorkflowVersion) -> WorkflowVersionId { + WorkflowVersionId::from(fabro_types::BlobHash::new( + &version.version().canonical_bytes().unwrap(), + )) + } + async fn stores() -> (Arc, WorkflowVersionStore) { let database = Database::new( Arc::new(InMemory::new()), @@ -273,12 +342,132 @@ mod tests { )); assert!(!blobs.exists(&root_id.into()).await.unwrap()); assert!(matches!( - store.get(&child_id).await.unwrap_err(), + store.get_closure(&child_id).await.unwrap_err(), WorkflowVersionStoreError::DependencyNotFound { id, .. } if id == missing_grandchild_id )); } + #[tokio::test] + async fn get_closure_returns_root_and_transitive_dependencies() { + let (_, store) = stores().await; + let grandchild = version("digraph Grandchild {}", BTreeMap::new()); + let grandchild_id = store.put(&grandchild).await.unwrap(); + let child = version( + r#"digraph Child { grandchild [stack.child_workflow="grandchild.fabro"] }"#, + BTreeMap::from([(path("grandchild.fabro"), grandchild_id)]), + ); + let child_id = store.put(&child).await.unwrap(); + let root = version( + r#"digraph Root { child [stack.child_workflow="child.fabro"] }"#, + BTreeMap::from([(path("child.fabro"), child_id)]), + ); + let root_id = store.put(&root).await.unwrap(); + + let closure = store.get_closure(&root_id).await.unwrap().unwrap(); + + assert_eq!(closure.root_id(), root_id); + assert_eq!(closure.root(), root.version()); + assert_eq!(closure.get(&child_id), Some(child.version())); + assert_eq!(closure.get(&grandchild_id), Some(grandchild.version())); + assert_eq!( + closure + .versions() + .map(|(id, version)| (id, version.clone())) + .collect::>(), + BTreeMap::from([ + (root_id, root.version().clone()), + (child_id, child.version().clone()), + (grandchild_id, grandchild.version().clone()), + ]) + ); + } + + #[tokio::test] + async fn get_closure_deduplicates_a_diamond() { + let (_, store) = stores().await; + let leaf = version("digraph Leaf {}", BTreeMap::new()); + let leaf_id = store.put(&leaf).await.unwrap(); + let left = version( + r#"digraph Left { leaf [stack.child_workflow="leaf.fabro"] }"#, + BTreeMap::from([(path("leaf.fabro"), leaf_id)]), + ); + let left_id = store.put(&left).await.unwrap(); + let right = version( + r#"digraph Right { leaf [stack.child_workflow="leaf.fabro"] }"#, + BTreeMap::from([(path("leaf.fabro"), leaf_id)]), + ); + let right_id = store.put(&right).await.unwrap(); + let root = version( + r#"digraph Root { + left [stack.child_workflow="left.fabro"] + right [stack.child_workflow="right.fabro"] + }"#, + BTreeMap::from([ + (path("left.fabro"), left_id), + (path("right.fabro"), right_id), + ]), + ); + let root_id = store.put(&root).await.unwrap(); + + let closure = store.get_closure(&root_id).await.unwrap().unwrap(); + let ids = closure.versions().map(|(id, _)| id).collect::>(); + + assert_eq!(ids.len(), 4); + assert_eq!(ids.iter().filter(|&&id| id == leaf_id).count(), 1); + } + + #[tokio::test] + async fn get_closure_preserves_noncanonical_dependency_errors() { + let (blobs, store) = stores().await; + let dependency = version("digraph Dependency {}", BTreeMap::new()); + let pretty = serde_json::to_vec_pretty(dependency.version()).unwrap(); + let dependency_id = WorkflowVersionId::from(blobs.write(&pretty).await.unwrap()); + let root = version( + r#"digraph Root { dependency [stack.child_workflow="dependency.fabro"] }"#, + BTreeMap::from([(path("dependency.fabro"), dependency_id)]), + ); + let root_id = WorkflowVersionId::from( + blobs + .write(&root.version().canonical_bytes().unwrap()) + .await + .unwrap(), + ); + + let error = store.get_closure(&root_id).await.unwrap_err(); + let WorkflowVersionStoreError::DependencyInvalid { source, .. } = error else { + panic!("expected invalid dependency error"); + }; + assert!(matches!( + source.as_ref(), + WorkflowVersionStoreError::NonCanonical { id } if *id == dependency_id + )); + assert!(matches!( + store.get_closure(&dependency_id).await.unwrap_err(), + WorkflowVersionStoreError::NonCanonical { id } if id == dependency_id + )); + } + + #[tokio::test] + async fn get_projects_the_same_validated_root_as_get_closure() { + let (_, store) = stores().await; + let child = version("digraph Child {}", BTreeMap::new()); + let child_id = store.put(&child).await.unwrap(); + let root = version( + r#"digraph Root { child [stack.child_workflow="child.fabro"] }"#, + BTreeMap::from([(path("child.fabro"), child_id)]), + ); + let root_id = store.put(&root).await.unwrap(); + + let closure = store.get_closure(&root_id).await.unwrap().unwrap(); + let projected = store.get(&root_id).await.unwrap().unwrap(); + + assert_eq!(projected.version(), closure.root()); + let absent = version_id(&version("digraph Absent {}", BTreeMap::new())); + assert!(store.get_closure(&absent).await.unwrap().is_none()); + assert!(store.get(&absent).await.unwrap().is_none()); + } + #[tokio::test] async fn get_rejects_arbitrary_and_noncanonical_blobs() { let (blobs, store) = stores().await; diff --git a/lib/foundation/fabro-types/src/graph.rs b/lib/foundation/fabro-types/src/graph.rs index e0d7dc17c..ded69954d 100644 --- a/lib/foundation/fabro-types/src/graph.rs +++ b/lib/foundation/fabro-types/src/graph.rs @@ -611,12 +611,14 @@ pub enum ReferenceKind { Dockerfile, #[strum(to_string = "graph goal file reference")] GraphGoalFile, + #[strum(to_string = "run goal file reference")] + RunGoalFile, } /// Kinds of static file references that graph attributes can carry: the /// subset of [`ReferenceKind`] that [`reference_kind_for_attribute`] can -/// classify. Config-sourced kinds (Dockerfiles) are unrepresentable here by -/// construction. +/// classify. Config-sourced kinds (Dockerfiles, run goal files) are +/// unrepresentable here by construction. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum GraphReferenceKind { FileInline, From 408cd2f74596a8687ac87890fa3b1b8e86e3927b Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 15:03:16 -0400 Subject: [PATCH 28/63] Simplify workflow-version closure validation and loading - Replace the discarded dependency-closure map in put/get with a visitor-based walk so only get_closure retains loaded versions - Hold the closure root structurally in LoadedWorkflowVersionClosure instead of asserting its presence in the map with expect() - Drop the visited-set parameter that guarded against impossible content-address cycles - Move template-discovery error source-name extraction into TemplateDiscoveryError::source_name() where the variants are owned - Collapse repeated TemplateSource construction into a TemplateRoots collector and share the config file-reference validation pipeline between dockerfile and run-goal references - Deduplicate test helpers (version_id, version_with_goal_file, impl Into config fixtures) Co-Authored-By: Claude Fable 5 --- .../fabro-workflow-version/src/lib.rs | 160 ++++++++---------- .../fabro-workflow-version/src/store.rs | 90 +++++----- 2 files changed, 114 insertions(+), 136 deletions(-) diff --git a/lib/components/fabro-workflow-version/src/lib.rs b/lib/components/fabro-workflow-version/src/lib.rs index a92226dd6..27edad510 100644 --- a/lib/components/fabro-workflow-version/src/lib.rs +++ b/lib/components/fabro-workflow-version/src/lib.rs @@ -15,7 +15,7 @@ use fabro_config::{ use fabro_graphviz::parser; use fabro_template::{ BundleTemplateStore, GraphReference, GraphReferenceError, StaticReferenceError, - TemplateDiscoveryError, TemplateLoadError, TemplateSource, discover_static_dependency_closure, + TemplateDiscoveryError, TemplateSource, discover_static_dependency_closure, validate_static_reference, visit_graph_references, }; use fabro_types::graph::ReferenceKind; @@ -91,12 +91,10 @@ pub struct ValidatedWorkflowVersion(WorkflowVersion); impl ValidatedWorkflowVersion { pub fn new(version: WorkflowVersion) -> Result { - let template_root = ManifestPath::from_wire(".") - .expect("the template package root must be a valid manifest path"); - let mut template_roots = Vec::new(); - validate_config(&version, &template_root, &mut template_roots)?; - validate_graph_closure(&version, &template_root, &mut template_roots)?; - validate_template_closure(&version, template_roots)?; + let mut template_roots = TemplateRoots::new(); + validate_config(&version, &mut template_roots)?; + validate_graph_closure(&version, &mut template_roots)?; + validate_template_closure(&version, template_roots.sources)?; Ok(Self(version)) } @@ -111,10 +109,34 @@ impl ValidatedWorkflowVersion { } } +/// Template sources that anchor static dependency discovery, all rooted at +/// the workflow package root. +struct TemplateRoots { + package_root: ManifestPath, + sources: Vec, +} + +impl TemplateRoots { + fn new() -> Self { + Self { + package_root: ManifestPath::from_wire(".") + .expect("the template package root must be a valid manifest path"), + sources: Vec::new(), + } + } + + fn push(&mut self, path: &WorkflowPath, content: impl Into) { + self.sources.push(TemplateSource::new( + manifest_path(path), + self.package_root.clone(), + content, + )); + } +} + fn validate_config( version: &WorkflowVersion, - template_root: &ManifestPath, - template_roots: &mut Vec, + template_roots: &mut TemplateRoots, ) -> Result<(), WorkflowVersionError> { let config_path = WorkflowPath::new("workflow.toml").expect("the static workflow config path must be valid"); @@ -146,31 +168,17 @@ fn validate_config( } match layer.run.as_ref().and_then(|run| run.goal.as_ref()) { - Some(RunGoalLayer::Inline(goal)) => template_roots.push(TemplateSource::new( - manifest_path(&config_path), - template_root.clone(), - unresolved_source(goal), - )), + Some(RunGoalLayer::Inline(goal)) => { + template_roots.push(&config_path, unresolved_source(goal)); + } Some(RunGoalLayer::File { file }) => { - let reference = unresolved_source(file); - validate_static_reference(&reference, ReferenceKind::RunGoalFile).map_err( - |source| WorkflowVersionError::StaticReference { - path: config_path.clone(), - source, - }, - )?; - let target = resolve_reference(&config_path, ReferenceKind::RunGoalFile, &reference)?; - let content = require_file( + let (target, content) = validate_config_file_reference( version, &config_path, ReferenceKind::RunGoalFile, - target.clone(), + &unresolved_source(file), )?; - template_roots.push(TemplateSource::new( - manifest_path(&target), - template_root.clone(), - content, - )); + template_roots.push(&target, content); } None => {} } @@ -193,20 +201,32 @@ fn validate_dockerfile( let Some(EnvironmentDockerfileLayer::Path { path }) = image.dockerfile.as_ref() else { return Ok(()); }; - validate_static_reference(path, ReferenceKind::Dockerfile).map_err(|source| { + validate_config_file_reference(version, config_path, ReferenceKind::Dockerfile, path) + .map(|_| ()) +} + +/// Validate a static file reference in `workflow.toml` and require its target +/// to exist in the version, returning the target path and its content. +fn validate_config_file_reference<'version>( + version: &'version WorkflowVersion, + config_path: &WorkflowPath, + kind: ReferenceKind, + reference: &str, +) -> Result<(WorkflowPath, &'version str), WorkflowVersionError> { + validate_static_reference(reference, kind).map_err(|source| { WorkflowVersionError::StaticReference { path: config_path.clone(), source, } })?; - let target = resolve_reference(config_path, ReferenceKind::Dockerfile, path)?; - require_file(version, config_path, ReferenceKind::Dockerfile, target).map(|_| ()) + let target = resolve_reference(config_path, kind, reference)?; + let content = require_file(version, config_path, kind, target.clone())?; + Ok((target, content)) } fn validate_graph_closure( version: &WorkflowVersion, - template_root: &ManifestPath, - template_roots: &mut Vec, + template_roots: &mut TemplateRoots, ) -> Result<(), WorkflowVersionError> { let mut queue = VecDeque::from([version.entrypoint().clone()]); let mut visited = BTreeSet::new(); @@ -235,19 +255,11 @@ fn validate_graph_closure( let target = resolve_reference(&path, ReferenceKind::GraphGoalFile, reference)?; let content = require_file(version, &path, ReferenceKind::GraphGoalFile, target.clone())?; - template_roots.push(TemplateSource::new( - manifest_path(&target), - template_root.clone(), - content, - )); + template_roots.push(&target, content); Ok(()) } GraphReference::GoalInline { content } | GraphReference::InlinePrompt { content } => { - template_roots.push(TemplateSource::new( - manifest_path(&path), - template_root.clone(), - content, - )); + template_roots.push(&path, content); Ok(()) } GraphReference::Import { reference } => { @@ -266,11 +278,7 @@ fn validate_graph_closure( let content = require_file(version, &path, ReferenceKind::FileInline, target.clone())?; if key == "prompt" { - template_roots.push(TemplateSource::new( - manifest_path(&target), - template_root.clone(), - content, - )); + template_roots.push(&target, content); } Ok(()) } @@ -312,23 +320,8 @@ fn validate_template_closure( } fn template_discovery_path(error: &TemplateDiscoveryError) -> WorkflowPath { - let path = match error { - TemplateDiscoveryError::Parse(source) => source - .source_name() - .expect("dependency extraction must retain its source name") - .to_owned(), - TemplateDiscoveryError::Load(source) => match source { - TemplateLoadError::UnsafeReference { parent, .. } - | TemplateLoadError::EscapesRoot { parent, .. } => parent.to_string(), - TemplateLoadError::DynamicDependency { path } => path.to_string(), - TemplateLoadError::Io { .. } => { - unreachable!("bundle template dependency discovery cannot perform filesystem I/O") - } - }, - TemplateDiscoveryError::Missing { parent, .. } - | TemplateDiscoveryError::Dynamic { parent } => parent.to_string(), - }; - WorkflowPath::new(path).expect("template paths sourced from a workflow version must be valid") + WorkflowPath::new(error.source_path().to_string()) + .expect("template paths sourced from a workflow version must be valid") } fn template_store(version: &WorkflowVersion) -> BundleTemplateStore { @@ -416,7 +409,7 @@ mod tests { } fn version_with_config( - config: String, + config: impl Into, extra_files: impl IntoIterator, ) -> Result { let mut files = extra_files @@ -424,7 +417,7 @@ mod tests { .map(|(path_value, content)| (path(path_value), content.to_owned())) .collect::>(); files.insert(path("workflow.fabro"), "digraph W {}".to_owned()); - files.insert(path("workflow.toml"), config); + files.insert(path("workflow.toml"), config.into()); ValidatedWorkflowVersion::new( WorkflowVersion::new(path("workflow.fabro"), files, BTreeMap::default()) .expect("test fixtures must be structurally valid"), @@ -435,8 +428,8 @@ mod tests { reference: &str, ) -> Result { let reference = serde_json::to_string(reference).unwrap(); - version_with_config(format!("_version = 1\n[run.goal]\nfile = {reference}\n"), [ - ]) + let config = format!("_version = 1\n[run.goal]\nfile = {reference}\n"); + version_with_config(config, []) } fn version_with_inline_goal( @@ -547,17 +540,7 @@ mod tests { #[test] fn rejects_missing_workflow_goal_file() { - let error = version_with( - [ - ("workflow.fabro", "digraph W {}"), - ( - "workflow.toml", - "_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n", - ), - ], - [], - ) - .unwrap_err(); + let error = version_with_goal_file("prompts/goal.md").unwrap_err(); assert!(matches!( error, @@ -585,15 +568,13 @@ mod tests { #[test] fn accepts_file_workflow_goal_with_transitive_template_closure() { - let version = version_with_config( - "_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n".to_owned(), - [ + let version = + version_with_config("_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n", [ ("prompts/goal.md", r#"{% include "partial.md" %}"#), ("prompts/partial.md", r#"{% include "nested/detail.md" %}"#), ("prompts/nested/detail.md", "Use {{ vars.detail }}"), - ], - ) - .unwrap(); + ]) + .unwrap(); assert_eq!(version.version().files().len(), 5); } @@ -673,7 +654,10 @@ mod tests { }; assert!(matches!( source.as_ref(), - TemplateDiscoveryError::Load(TemplateLoadError::EscapesRoot { parent, .. }) + TemplateDiscoveryError::Load { + source: TemplateLoadError::EscapesRoot { parent, .. }, + .. + } if parent.to_string() == "workflow.toml" )); } diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs index 21d6293f1..92f61d014 100644 --- a/lib/components/fabro-workflow-version/src/store.rs +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -40,14 +40,14 @@ pub enum WorkflowVersionStoreError { }, } -/// A fully loaded and validated workflow-version dependency graph. -/// -/// The requested root is always present exactly once alongside every unique -/// transitive dependency, keyed by canonical content ID. +/// A fully loaded and validated workflow-version dependency graph: the +/// requested root alongside every unique transitive dependency, keyed by +/// canonical content ID. #[derive(Clone, Debug)] pub struct LoadedWorkflowVersionClosure { - root_id: WorkflowVersionId, - versions: BTreeMap, + root_id: WorkflowVersionId, + root: ValidatedWorkflowVersion, + dependencies: BTreeMap, } impl LoadedWorkflowVersionClosure { @@ -58,27 +58,25 @@ impl LoadedWorkflowVersionClosure { #[must_use] pub fn root(&self) -> &WorkflowVersion { - self.versions - .get(&self.root_id) - .expect("a loaded workflow-version closure must contain its root") - .version() + self.root.version() } #[must_use] pub fn get(&self, id: &WorkflowVersionId) -> Option<&WorkflowVersion> { - self.versions.get(id).map(ValidatedWorkflowVersion::version) + if *id == self.root_id { + return Some(self.root.version()); + } + self.dependencies + .get(id) + .map(ValidatedWorkflowVersion::version) } pub fn versions(&self) -> impl Iterator + '_ { - self.versions - .iter() - .map(|(id, version)| (*id, version.version())) - } - - fn into_root(mut self) -> ValidatedWorkflowVersion { - self.versions - .remove(&self.root_id) - .expect("a loaded workflow-version closure must contain its root") + std::iter::once((self.root_id, self.root.version())).chain( + self.dependencies + .iter() + .map(|(id, version)| (*id, version.version())), + ) } } @@ -103,7 +101,7 @@ impl WorkflowVersionStore { version: &ValidatedWorkflowVersion, ) -> Result { let canonical = version.version().canonical_bytes()?; - self.load_dependency_closure(version.version().workflow_dependencies(), HashSet::new()) + self.walk_dependency_closure(version.version().workflow_dependencies(), |_, _| ()) .await?; self.blobs .write(&canonical) @@ -116,10 +114,12 @@ impl WorkflowVersionStore { &self, id: &WorkflowVersionId, ) -> Result, WorkflowVersionStoreError> { - let Some(closure) = self.get_closure(id).await? else { + let Some(version) = self.load_one(id).await? else { return Ok(None); }; - Ok(Some(closure.into_root())) + self.walk_dependency_closure(version.version().workflow_dependencies(), |_, _| ()) + .await?; + Ok(Some(version)) } pub async fn get_closure( @@ -129,16 +129,15 @@ impl WorkflowVersionStore { let Some(root) = self.load_one(root_id).await? else { return Ok(None); }; - let mut versions = self - .load_dependency_closure( - root.version().workflow_dependencies(), - HashSet::from([*root_id]), - ) - .await?; - versions.insert(*root_id, root); + let mut dependencies = BTreeMap::new(); + self.walk_dependency_closure(root.version().workflow_dependencies(), |id, version| { + dependencies.insert(id, version); + }) + .await?; Ok(Some(LoadedWorkflowVersionClosure { root_id: *root_id, - versions, + root, + dependencies, })) } @@ -165,17 +164,18 @@ impl WorkflowVersionStore { Ok(Some(validated)) } - async fn load_dependency_closure( + /// Walk the transitive dependency closure, validating every dependency + /// and handing each loaded version to `visit` exactly once. + async fn walk_dependency_closure( &self, dependencies: &BTreeMap, - mut visited: HashSet, - ) -> Result, WorkflowVersionStoreError> - { + mut visit: impl FnMut(WorkflowVersionId, ValidatedWorkflowVersion), + ) -> Result<(), WorkflowVersionStoreError> { let mut pending = dependencies .iter() .map(|(path, id)| (path.clone(), *id)) .collect::>(); - let mut versions = BTreeMap::new(); + let mut visited = HashSet::new(); while let Some((path, id)) = pending.pop_front() { if !visited.insert(id) { @@ -190,7 +190,7 @@ impl WorkflowVersionStore { .iter() .map(|(path, id)| (path.clone(), *id)), ); - versions.insert(id, dependency); + visit(id, dependency); } Ok(None) => { return Err(WorkflowVersionStoreError::DependencyNotFound { path, id }); @@ -207,7 +207,7 @@ impl WorkflowVersionStore { } } } - Ok(versions) + Ok(()) } } @@ -266,7 +266,7 @@ mod tests { let (blobs, store) = stores().await; let version = version("digraph W {}", BTreeMap::new()); let expected_bytes = version.version().canonical_bytes().unwrap(); - let expected_id = WorkflowVersionId::from(fabro_types::BlobHash::new(&expected_bytes)); + let expected_id = version_id(&version); let id = store.put(&version).await.unwrap(); assert_eq!(id, expected_id); @@ -296,16 +296,12 @@ mod tests { async fn dependency_must_be_stored_first() { let (blobs, store) = stores().await; let child = version("digraph Child {}", BTreeMap::new()); - let child_id = WorkflowVersionId::from(fabro_types::BlobHash::new( - &child.version().canonical_bytes().unwrap(), - )); + let child_id = version_id(&child); let root = version( r#"digraph Root { child [stack.child_workflow="child.fabro"] }"#, BTreeMap::from([(path("child.fabro"), child_id)]), ); - let root_id = WorkflowVersionId::from(fabro_types::BlobHash::new( - &root.version().canonical_bytes().unwrap(), - )); + let root_id = version_id(&root); let error = store.put(&root).await.unwrap_err(); assert!(matches!( @@ -331,9 +327,7 @@ mod tests { r#"digraph Root { child [stack.child_workflow="child.fabro"] }"#, BTreeMap::from([(path("child.fabro"), child_id)]), ); - let root_id = WorkflowVersionId::from(fabro_types::BlobHash::new( - &root.version().canonical_bytes().unwrap(), - )); + let root_id = version_id(&root); assert!(matches!( store.put(&root).await.unwrap_err(), From 5a5cfbdaa06d57ca85c55a0afb6f56baffbf0ec3 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 16:36:40 -0400 Subject: [PATCH 29/63] Parse template dependencies whose paths collide with discovery roots Batched dependency discovery pre-seeded roots into the path-keyed result map and reused that map as the traversal-dedup set, so a loaded include target whose path matched a root (e.g. a goal template including the graph file that anchors an inline prompt) was recorded but never parsed, silently accepting invalid template content that per-root discovery used to reject. Dedup traversal on the full (path, root, content) occurrence instead, which also stops re-parsing identical duplicate roots. Co-Authored-By: Claude Fable 5 --- .../fabro-workflow-version/src/lib.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/components/fabro-workflow-version/src/lib.rs b/lib/components/fabro-workflow-version/src/lib.rs index 27edad510..226a1d2e3 100644 --- a/lib/components/fabro-workflow-version/src/lib.rs +++ b/lib/components/fabro-workflow-version/src/lib.rs @@ -685,6 +685,37 @@ mod tests { )); } + #[test] + fn validates_graph_files_included_from_goal_templates() { + // The graph file's inline prompt anchors a template root at the graph + // path; that root must not shadow the raw graph content when a goal + // template includes the graph file itself. + let error = version_with( + [ + ( + "workflow.fabro", + r#"digraph W { + graph [goal="@goal.md"] + step [prompt="hello", note="{% include 'missing.md' %}"] + }"#, + ), + ("goal.md", r#"{% include "workflow.fabro" %}"#), + ], + [], + ) + .unwrap_err(); + + assert!(matches!( + error, + WorkflowVersionError::Template { path: source_path, source } + if source_path == path("workflow.fabro") + && matches!( + source.as_ref(), + TemplateDiscoveryError::Missing { reference, .. } if reference == "missing.md" + ) + )); + } + #[test] fn accepts_root_config_and_all_dockerfile_path_sources() { let version = version_with( From 2f2097be548a137696a88c053422eed5f6398608 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 16:39:29 -0400 Subject: [PATCH 30/63] Anchor run-goal template validation at the version entrypoint Create-time validation of workflow.toml run goals anchored includes at workflow.toml for inline goals and at the goal file's directory for file goals, while the run engine inlines the effective goal into the entrypoint graph and renders it under the entrypoint's template source. That divergence rejected layouts `fabro run` executes fine and accepted layouts that fail at render time. Anchor both goal forms at the entrypoint so validation matches the runtime, and pin the anchor with a nested-entrypoint test. Co-Authored-By: Claude Fable 5 --- .../fabro-workflow-version/src/lib.rs | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/lib/components/fabro-workflow-version/src/lib.rs b/lib/components/fabro-workflow-version/src/lib.rs index 226a1d2e3..7e5bbe64a 100644 --- a/lib/components/fabro-workflow-version/src/lib.rs +++ b/lib/components/fabro-workflow-version/src/lib.rs @@ -167,18 +167,21 @@ fn validate_config( validate_dockerfile(version, &config_path, image)?; } + // The run engine inlines the effective goal (file contents included) into + // the entrypoint graph and renders it under the entrypoint's template + // source, so goal includes anchor at the entrypoint for both goal forms. match layer.run.as_ref().and_then(|run| run.goal.as_ref()) { Some(RunGoalLayer::Inline(goal)) => { - template_roots.push(&config_path, unresolved_source(goal)); + template_roots.push(version.entrypoint(), unresolved_source(goal)); } Some(RunGoalLayer::File { file }) => { - let (target, content) = validate_config_file_reference( + let (_, content) = validate_config_file_reference( version, &config_path, ReferenceKind::RunGoalFile, &unresolved_source(file), )?; - template_roots.push(&target, content); + template_roots.push(version.entrypoint(), content); } None => {} } @@ -568,9 +571,12 @@ mod tests { #[test] fn accepts_file_workflow_goal_with_transitive_template_closure() { + // The goal file's own includes anchor at the entrypoint's directory + // (the package root here), not at the goal file's directory; loaded + // dependencies then anchor at their own directories as usual. let version = version_with_config("_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n", [ - ("prompts/goal.md", r#"{% include "partial.md" %}"#), + ("prompts/goal.md", r#"{% include "prompts/partial.md" %}"#), ("prompts/partial.md", r#"{% include "nested/detail.md" %}"#), ("prompts/nested/detail.md", "Use {{ vars.detail }}"), ]) @@ -579,6 +585,43 @@ mod tests { assert_eq!(version.version().files().len(), 5); } + #[test] + fn anchors_workflow_goal_includes_at_the_entrypoint() { + let version_with_entrypoint = |goal_include_target: &'static str| { + ValidatedWorkflowVersion::new( + WorkflowVersion::new( + path("graphs/main.fabro"), + BTreeMap::from([ + (path("graphs/main.fabro"), "digraph W {}".to_owned()), + ( + path("workflow.toml"), + "_version = 1\n[run]\ngoal = \"{% include \\\"shared.md\\\" %}\"\n" + .to_owned(), + ), + (path(goal_include_target), "shared".to_owned()), + ]), + BTreeMap::default(), + ) + .expect("test fixtures must be structurally valid"), + ) + }; + + // The include resolves beside the entrypoint graph, matching where + // the run engine renders the inlined goal. + version_with_entrypoint("graphs/shared.md").unwrap(); + + let error = version_with_entrypoint("shared.md").unwrap_err(); + assert!(matches!( + error, + WorkflowVersionError::Template { path: source_path, source } + if source_path == path("graphs/main.fabro") + && matches!( + source.as_ref(), + TemplateDiscoveryError::Missing { reference, .. } if reference == "shared.md" + ) + )); + } + #[test] fn rejects_non_static_or_nonportable_workflow_goal_file_references() { for reference in ["{{ vars.NAME }}", "{% include \"goal.md\" %}"] { @@ -630,11 +673,11 @@ mod tests { else { panic!("expected missing template dependency"); }; - assert_eq!(source_path, path("workflow.toml")); + assert_eq!(source_path, path("workflow.fabro")); assert!(matches!( source.as_ref(), TemplateDiscoveryError::Missing { parent, reference } - if parent.to_string() == "workflow.toml" && reference == "missing.md" + if parent.to_string() == "workflow.fabro" && reference == "missing.md" )); let dynamic = version_with_inline_goal(r"{% include inputs.partial %}", []).unwrap_err(); @@ -644,7 +687,7 @@ mod tests { assert!(matches!( source.as_ref(), TemplateDiscoveryError::Dynamic { parent } - if parent.to_string() == "workflow.toml" + if parent.to_string() == "workflow.fabro" )); let escaping = @@ -657,8 +700,7 @@ mod tests { TemplateDiscoveryError::Load { source: TemplateLoadError::EscapesRoot { parent, .. }, .. - } - if parent.to_string() == "workflow.toml" + } if parent.to_string() == "workflow.fabro" )); } From 1e293472273583e4d5be6857568c71cf6aa4b6c9 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 16:40:00 -0400 Subject: [PATCH 31/63] Cover rejection of broken transitive includes under file run goals The positive run-goal tests only asserted fixture shape, so a regression that stopped pushing the file-goal template root would keep them green while broken nested includes were silently accepted. Pin the rejection path directly. Co-Authored-By: Claude Fable 5 --- .../fabro-workflow-version/src/lib.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/components/fabro-workflow-version/src/lib.rs b/lib/components/fabro-workflow-version/src/lib.rs index 7e5bbe64a..75731fb49 100644 --- a/lib/components/fabro-workflow-version/src/lib.rs +++ b/lib/components/fabro-workflow-version/src/lib.rs @@ -585,6 +585,28 @@ mod tests { assert_eq!(version.version().files().len(), 5); } + #[test] + fn rejects_broken_transitive_includes_under_a_workflow_goal_file() { + // Guards the root push for file goals: without it the goal file is + // never parsed and the broken include below is silently accepted. + let error = + version_with_config("_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n", [ + ("prompts/goal.md", r#"{% include "prompts/partial.md" %}"#), + ("prompts/partial.md", r#"{% include "missing.md" %}"#), + ]) + .unwrap_err(); + + assert!(matches!( + error, + WorkflowVersionError::Template { path: source_path, source } + if source_path == path("prompts/partial.md") + && matches!( + source.as_ref(), + TemplateDiscoveryError::Missing { reference, .. } if reference == "missing.md" + ) + )); + } + #[test] fn anchors_workflow_goal_includes_at_the_entrypoint() { let version_with_entrypoint = |goal_include_target: &'static str| { From 3e6b23ce7651c0f3b146bf9f3dabc8ec7ed570e2 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 17:01:04 -0400 Subject: [PATCH 32/63] Keep loaded workflow-version closures out of implicit copies LoadedWorkflowVersionClosure owns every file of every version in the dependency graph, so an advertised Clone invites accidental deep copies of the whole set. Drop the derive until a consumer needs owned copies. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-workflow-version/src/store.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs index 92f61d014..f2fb3981b 100644 --- a/lib/components/fabro-workflow-version/src/store.rs +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -43,7 +43,10 @@ pub enum WorkflowVersionStoreError { /// A fully loaded and validated workflow-version dependency graph: the /// requested root alongside every unique transitive dependency, keyed by /// canonical content ID. -#[derive(Clone, Debug)] +/// +/// Deliberately not `Clone`: a closure owns the full file contents of every +/// version in the graph, so copies should be explicit and deliberate. +#[derive(Debug)] pub struct LoadedWorkflowVersionClosure { root_id: WorkflowVersionId, root: ValidatedWorkflowVersion, From cc163625280258038db0175ed7c98016fea7da6b Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 15:09:26 -0400 Subject: [PATCH 33/63] Add SQLite blob store foundation --- lib/components/fabro-store/src/blob_store.rs | 292 ++++++++++++++++++ lib/components/fabro-store/src/error.rs | 6 + lib/components/fabro-store/src/lib.rs | 6 +- .../fabro-store/src/slate/blob_store.rs | 137 -------- lib/components/fabro-store/src/slate/mod.rs | 6 +- .../fabro-store/src/slate/run_store.rs | 4 +- .../fabro-db/migrations/2026081301_blobs.sql | 7 + lib/foundation/fabro-db/tests/sqlite.rs | 94 ++++++ 8 files changed, 407 insertions(+), 145 deletions(-) create mode 100644 lib/components/fabro-store/src/blob_store.rs delete mode 100644 lib/components/fabro-store/src/slate/blob_store.rs create mode 100644 lib/foundation/fabro-db/migrations/2026081301_blobs.sql diff --git a/lib/components/fabro-store/src/blob_store.rs b/lib/components/fabro-store/src/blob_store.rs new file mode 100644 index 000000000..b43c94cdc --- /dev/null +++ b/lib/components/fabro-store/src/blob_store.rs @@ -0,0 +1,292 @@ +use std::sync::Arc; + +use bytes::Bytes; +use fabro_types::BlobHash; +use sqlx::SqlitePool; + +use crate::record::{RawBytesCodec, Record, Repository}; +use crate::{Error, Result}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Blob(pub Bytes); + +impl AsRef<[u8]> for Blob { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From for Blob { + fn from(value: Bytes) -> Self { + Self(value) + } +} + +impl Record for Blob { + type Id = BlobHash; + type Codec = RawBytesCodec; + + const PREFIX: &'static str = "blobs/sha256"; + + fn id(&self) -> Self::Id { + BlobHash::new(&self.0) + } +} + +enum BlobBackend { + Slate(Repository), + Sqlite(SqlitePool), +} + +pub struct BlobStore { + backend: BlobBackend, +} + +impl std::fmt::Debug for BlobStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let backend = match &self.backend { + BlobBackend::Slate(_) => "slate", + BlobBackend::Sqlite(_) => "sqlite", + }; + f.debug_struct("BlobStore") + .field("backend", &backend) + .finish_non_exhaustive() + } +} + +impl BlobStore { + /// Creates a blob store backed by a SQLite pool whose migrations have run. + #[must_use] + pub fn new(pool: SqlitePool) -> Self { + Self { + backend: BlobBackend::Sqlite(pool), + } + } + + pub(crate) fn from_slate(db: Arc) -> Self { + Self { + backend: BlobBackend::Slate(Repository::new(db)), + } + } + + pub async fn write(&self, bytes: &[u8]) -> Result { + match &self.backend { + BlobBackend::Slate(repo) => { + let blob = Blob(Bytes::copy_from_slice(bytes)); + let id = blob.id(); + repo.put(&blob).await?; + Ok(id) + } + BlobBackend::Sqlite(pool) => { + let blob_hash = BlobHash::new(bytes); + let result = sqlx::query( + "INSERT INTO blobs (hash, data) VALUES (?, ?) \ + ON CONFLICT(hash) DO NOTHING", + ) + .bind(blob_hash.to_string()) + .bind(bytes) + .execute(pool) + .await?; + + if result.rows_affected() == 1 { + return Ok(blob_hash); + } + + let stored: Vec = sqlx::query_scalar("SELECT data FROM blobs WHERE hash = ?") + .bind(blob_hash.to_string()) + .fetch_one(pool) + .await?; + if stored == bytes { + Ok(blob_hash) + } else { + Err(Error::BlobHashConflict { blob_hash }) + } + } + } + } + + pub async fn read(&self, blob_hash: &BlobHash) -> Result> { + match &self.backend { + BlobBackend::Slate(repo) => Ok(repo.get(blob_hash).await?.map(|blob| blob.0)), + BlobBackend::Sqlite(pool) => { + let stored: Option> = + sqlx::query_scalar("SELECT data FROM blobs WHERE hash = ?") + .bind(blob_hash.to_string()) + .fetch_optional(pool) + .await?; + let Some(stored) = stored else { + return Ok(None); + }; + if BlobHash::new(&stored) != *blob_hash { + return Err(Error::BlobIntegrity { + blob_hash: *blob_hash, + }); + } + Ok(Some(Bytes::from(stored))) + } + } + } + + pub async fn exists(&self, blob_hash: &BlobHash) -> Result { + match &self.backend { + BlobBackend::Slate(repo) => repo.exists(blob_hash).await, + BlobBackend::Sqlite(pool) => { + let exists: bool = + sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM blobs WHERE hash = ?)") + .bind(blob_hash.to_string()) + .fetch_one(pool) + .await?; + Ok(exists) + } + } + } + +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Duration; + + use bytes::Bytes; + use fabro_types::BlobHash; + use object_store::memory::InMemory; + + use super::BlobStore; + use crate::keys::SlateKey; + use crate::{Database, Error}; + + type TestResult = std::result::Result>; + + async fn slate_store() -> Arc { + let db = Database::new( + Arc::new(InMemory::new()), + "", + Duration::from_millis(1), + None, + ); + db.blobs().await.unwrap() + } + + async fn raw_slate_store(name: &str) -> (Arc, BlobStore) { + let raw_db = Arc::new( + slatedb::Db::open(name, Arc::new(InMemory::new())) + .await + .unwrap(), + ); + let store = BlobStore::from_slate(raw_db.clone()); + (raw_db, store) + } + + async fn sqlite_store() -> TestResult<(tempfile::TempDir, fabro_db::Database, BlobStore)> { + let dir = tempfile::tempdir()?; + let database = fabro_db::Database::connect(dir.path().join("fabro.sqlite3")).await?; + database.migrate().await?; + let store = BlobStore::new(database.clone_pool()); + Ok((dir, database, store)) + } + + #[tokio::test] + async fn slate_writes_reads_and_checks_existence() { + let store = slate_store().await; + let bytes = b"hello world"; + let id = store.write(bytes).await.unwrap(); + + assert_eq!( + store.read(&id).await.unwrap(), + Some(Bytes::from_static(bytes)) + ); + assert_eq!(store.write(bytes).await.unwrap(), id); + assert!(store.exists(&id).await.unwrap()); + assert!(!store.exists(&BlobHash::new(b"missing")).await.unwrap()); + } + + #[tokio::test] + async fn slate_empty_blobs_round_trip() { + let store = slate_store().await; + let id = store.write(b"").await.unwrap(); + + assert_eq!(store.read(&id).await.unwrap(), Some(Bytes::new())); + } + + #[tokio::test] + async fn raw_slate_db_reads_exact_blob_bytes() { + let (raw_db, store) = raw_slate_store("blob-store-tests").await; + let bytes = b"{\"ok\":true}"; + let id = store.write(bytes).await.unwrap(); + + let saved = raw_db + .get(SlateKey::new("blobs").with("sha256").with(id)) + .await + .unwrap() + .unwrap(); + assert_eq!(saved.as_ref(), bytes); + } + + #[tokio::test] + async fn sqlite_writes_reads_and_checks_existence() -> TestResult<()> { + let (_dir, database, store) = sqlite_store().await?; + let store = Arc::new(store); + + let binary = [0_u8, 0xff, 0x80, b'a']; + let (first_write, concurrent_write) = + tokio::join!(store.write(&binary), store.write(&binary)); + let binary_hash = first_write?; + assert_eq!(concurrent_write?, binary_hash); + let empty_hash = store.write(b"").await?; + + assert_eq!(store.write(&binary).await?, binary_hash); + assert_eq!( + store.read(&binary_hash).await?, + Some(Bytes::copy_from_slice(&binary)) + ); + assert_eq!(store.read(&empty_hash).await?, Some(Bytes::new())); + assert!(store.exists(&binary_hash).await?); + assert!(!store.exists(&BlobHash::new(b"missing")).await?); + + let row_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM blobs") + .fetch_one(database.pool()) + .await?; + assert_eq!(row_count, 2); + Ok(()) + } + + #[tokio::test] + async fn sqlite_write_rejects_conflicting_stored_bytes() -> TestResult<()> { + let (_dir, database, store) = sqlite_store().await?; + let expected = b"expected"; + let blob_hash = BlobHash::new(expected); + sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, ?)") + .bind(blob_hash.to_string()) + .bind(b"different".as_slice()) + .execute(database.pool()) + .await?; + + let error = store + .write(expected) + .await + .expect_err("conflicting bytes should fail"); + assert!( + matches!(error, Error::BlobHashConflict { blob_hash: value } if value == blob_hash) + ); + Ok(()) + } + + #[tokio::test] + async fn sqlite_read_rejects_bytes_that_do_not_match_hash() -> TestResult<()> { + let (_dir, database, store) = sqlite_store().await?; + let blob_hash = BlobHash::new(b"expected"); + sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, ?)") + .bind(blob_hash.to_string()) + .bind(b"different".as_slice()) + .execute(database.pool()) + .await?; + + let error = store + .read(&blob_hash) + .await + .expect_err("mismatched stored bytes should fail"); + assert!(matches!(error, Error::BlobIntegrity { blob_hash: value } if value == blob_hash)); + Ok(()) + } +} diff --git a/lib/components/fabro-store/src/error.rs b/lib/components/fabro-store/src/error.rs index 3b6c93f5d..7cc31d9f5 100644 --- a/lib/components/fabro-store/src/error.rs +++ b/lib/components/fabro-store/src/error.rs @@ -1,3 +1,5 @@ +use fabro_types::BlobHash; + pub type Result = std::result::Result; #[derive(Debug, thiserror::Error)] @@ -10,6 +12,10 @@ pub enum Error { Serde(#[from] serde_json::Error), #[error("SQLite error: {0}")] Sqlite(#[from] sqlx::Error), + #[error("stored blob {blob_hash} has bytes that conflict with its hash")] + BlobHashConflict { blob_hash: BlobHash }, + #[error("stored blob data does not match requested hash {blob_hash}")] + BlobIntegrity { blob_hash: BlobHash }, #[error("I/O error: {0}")] Io(#[from] std::io::Error), #[error("Invalid event payload: {0}")] diff --git a/lib/components/fabro-store/src/lib.rs b/lib/components/fabro-store/src/lib.rs index 1b514a8e0..00a95d4e0 100644 --- a/lib/components/fabro-store/src/lib.rs +++ b/lib/components/fabro-store/src/lib.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; mod artifact_store; +mod blob_store; mod error; mod keyed_mutex; mod keys; @@ -18,6 +19,7 @@ pub use artifact_store::{ ArtifactKey, ArtifactStore, NodeArtifact, StageArtifactEntry, retry_storage_segment, stage_storage_segment, }; +pub use blob_store::{Blob, BlobStore}; pub use error::{Error, Result}; pub use fabro_types::{ BlobHash, EventEnvelope, PendingInterviewRecord, Run, RunProjection, StageId, StageProjection, @@ -34,8 +36,8 @@ pub use run_summary_store::{ }; pub use serializable_projection::SerializableProjection; pub use slate::{ - AuthCode, AuthCodeStore, Blob, BlobStore, CachedRunProjection, ConsumeOutcome, Database, - RefreshToken, RefreshTokenStore, RunCatalogIndex, RunDatabase, Runs, UnreadableRun, + AuthCode, AuthCodeStore, CachedRunProjection, ConsumeOutcome, Database, RefreshToken, + RefreshTokenStore, RunCatalogIndex, RunDatabase, Runs, UnreadableRun, }; pub use types::EventPayload; diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs deleted file mode 100644 index 8cec4c296..000000000 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::sync::Arc; - -use bytes::Bytes; -use fabro_types::BlobHash; - -use crate::Result; -use crate::record::{RawBytesCodec, Record, Repository}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Blob(pub Bytes); - -impl AsRef<[u8]> for Blob { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl From for Blob { - fn from(value: Bytes) -> Self { - Self(value) - } -} - -impl Record for Blob { - type Id = BlobHash; - type Codec = RawBytesCodec; - - const PREFIX: &'static str = "blobs/sha256"; - - fn id(&self) -> Self::Id { - BlobHash::new(&self.0) - } -} - -pub struct BlobStore { - repo: Repository, -} - -impl std::fmt::Debug for BlobStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BlobStore").finish_non_exhaustive() - } -} - -impl BlobStore { - pub(crate) fn new(db: Arc) -> Self { - Self { - repo: Repository::new(db), - } - } - - pub async fn write(&self, bytes: &[u8]) -> Result { - let blob = Blob(Bytes::copy_from_slice(bytes)); - let id = blob.id(); - self.repo.put(&blob).await?; - Ok(id) - } - - pub async fn read(&self, blob_hash: &BlobHash) -> Result> { - Ok(self.repo.get(blob_hash).await?.map(|blob| blob.0)) - } - - pub async fn exists(&self, blob_hash: &BlobHash) -> Result { - self.repo.exists(blob_hash).await - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - use std::time::Duration; - - use bytes::Bytes; - use fabro_types::BlobHash; - use object_store::memory::InMemory; - - use super::BlobStore; - use crate::Database; - use crate::keys::SlateKey; - - async fn store() -> Arc { - let db = Database::new( - Arc::new(InMemory::new()), - "", - Duration::from_millis(1), - None, - ); - db.blobs().await.unwrap() - } - - async fn raw_store(name: &str) -> (Arc, BlobStore) { - let raw_db = Arc::new( - slatedb::Db::open(name, Arc::new(InMemory::new())) - .await - .unwrap(), - ); - let store = BlobStore::new(Arc::clone(&raw_db)); - (raw_db, store) - } - - #[tokio::test] - async fn writes_reads_and_checks_existence() { - let store = store().await; - let bytes = b"hello world"; - let id = store.write(bytes).await.unwrap(); - - assert_eq!( - store.read(&id).await.unwrap(), - Some(Bytes::from_static(bytes)) - ); - assert_eq!(store.write(bytes).await.unwrap(), id); - assert!(store.exists(&id).await.unwrap()); - assert!(!store.exists(&BlobHash::new(b"missing")).await.unwrap()); - } - - #[tokio::test] - async fn empty_blobs_round_trip() { - let store = store().await; - let id = store.write(b"").await.unwrap(); - - assert_eq!(store.read(&id).await.unwrap(), Some(Bytes::new())); - } - - #[tokio::test] - async fn raw_db_reads_exact_blob_bytes() { - let (raw_db, store) = raw_store("blob-store-tests").await; - let bytes = b"{\"ok\":true}"; - let id = store.write(bytes).await.unwrap(); - - let saved = raw_db - .get(SlateKey::new("blobs").with("sha256").with(id)) - .await - .unwrap() - .unwrap(); - assert_eq!(saved.as_ref(), bytes); - } -} diff --git a/lib/components/fabro-store/src/slate/mod.rs b/lib/components/fabro-store/src/slate/mod.rs index e0b66d6e8..efed28796 100644 --- a/lib/components/fabro-store/src/slate/mod.rs +++ b/lib/components/fabro-store/src/slate/mod.rs @@ -1,6 +1,5 @@ mod auth_codes; mod auth_tokens; -mod blob_store; mod projection_cache; mod run_catalog_index; mod run_store; @@ -12,7 +11,6 @@ use std::time::Duration; pub use auth_codes::{AuthCode, AuthCodeStore}; pub use auth_tokens::{ConsumeOutcome, RefreshToken, RefreshTokenStore}; -pub use blob_store::{Blob, BlobStore}; use chrono::{DateTime, Utc}; use fabro_types::{Run, RunId, SessionId}; use object_store::ObjectStore; @@ -25,7 +23,7 @@ use slatedb::config::{CompressionCodec, Settings}; use tokio::sync::{Mutex, OnceCell}; use tracing::warn; -use crate::{Error, ListRunsQuery, Result, RunProjection, RunSummaryStore, keys}; +use crate::{BlobStore, Error, ListRunsQuery, Result, RunProjection, RunSummaryStore, keys}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnreadableRun { @@ -449,7 +447,7 @@ impl Database { .blobs .get_or_try_init(|| async { let db = Arc::new(self.open_db().await?); - Ok::<_, Error>(Arc::new(BlobStore::new(db))) + Ok::<_, Error>(Arc::new(BlobStore::from_slate(db))) }) .await?; Ok(Arc::clone(store)) diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index c4c590a0d..81450b44a 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -11,11 +11,11 @@ use tokio::sync::{Mutex, broadcast, mpsc}; use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::warn; -use super::blob_store::BlobStore; use super::projection_cache::{CachedRunProjection, RunProjectionCache}; use crate::run_state::{EventProjectionCache, RunProjectionReducer}; use crate::{ - Error, EventEnvelope, EventPayload, Result, RunProjection, RunSummaryStore, StageId, keys, + BlobStore, Error, EventEnvelope, EventPayload, Result, RunProjection, RunSummaryStore, StageId, + keys, }; const DEFAULT_EVENT_TAIL_LIMIT: usize = 1024; diff --git a/lib/foundation/fabro-db/migrations/2026081301_blobs.sql b/lib/foundation/fabro-db/migrations/2026081301_blobs.sql new file mode 100644 index 000000000..795e705a4 --- /dev/null +++ b/lib/foundation/fabro-db/migrations/2026081301_blobs.sql @@ -0,0 +1,7 @@ +CREATE TABLE blobs ( + hash TEXT PRIMARY KEY NOT NULL, + data BLOB NOT NULL, + CHECK (length(hash) = 64), + CHECK (hash = lower(hash)), + CHECK (hash NOT GLOB '*[^0-9a-f]*') +); diff --git a/lib/foundation/fabro-db/tests/sqlite.rs b/lib/foundation/fabro-db/tests/sqlite.rs index bc4981b06..bdb1179e4 100644 --- a/lib/foundation/fabro-db/tests/sqlite.rs +++ b/lib/foundation/fabro-db/tests/sqlite.rs @@ -73,6 +73,13 @@ async fn connect_creates_parent_directory_and_migrate_is_idempotent() -> anyhow: .await?; assert_eq!(runs_table_count, 1); + let blobs_table_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'blobs'", + ) + .fetch_one(database.pool()) + .await?; + assert_eq!(blobs_table_count, 1); + let legacy_import_table_count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'legacy_imports'", ) @@ -89,6 +96,93 @@ async fn connect_creates_parent_directory_and_migrate_is_idempotent() -> anyhow: Ok(()) } +#[tokio::test] +async fn blobs_schema_enforces_canonical_hashes_and_required_data() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + let database = fabro_db::Database::connect(dir.path().join("fabro.sqlite3")).await?; + database.migrate().await?; + + let columns = sqlx::query("PRAGMA table_info(blobs)") + .fetch_all(database.pool()) + .await?; + assert_eq!(columns.len(), 2); + + assert_eq!(columns[0].get::("name"), "hash"); + assert_eq!(columns[0].get::("type"), "TEXT"); + assert_eq!(columns[0].get::("notnull"), 1); + assert_eq!(columns[0].get::("pk"), 1); + assert_eq!(columns[0].get::, _>("dflt_value"), None); + + assert_eq!(columns[1].get::("name"), "data"); + assert_eq!(columns[1].get::("type"), "BLOB"); + assert_eq!(columns[1].get::("notnull"), 1); + assert_eq!(columns[1].get::("pk"), 0); + assert_eq!(columns[1].get::, _>("dflt_value"), None); + + let binary_hash = "0".repeat(64); + let binary_data = vec![0, 0xff, 0x80, b'a']; + sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, ?)") + .bind(&binary_hash) + .bind(&binary_data) + .execute(database.pool()) + .await?; + let stored_binary: Vec = sqlx::query_scalar("SELECT data FROM blobs WHERE hash = ?") + .bind(&binary_hash) + .fetch_one(database.pool()) + .await?; + assert_eq!(stored_binary, binary_data); + + let empty_hash = "1".repeat(64); + sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, ?)") + .bind(&empty_hash) + .bind(Vec::::new()) + .execute(database.pool()) + .await?; + let stored_empty: Vec = sqlx::query_scalar("SELECT data FROM blobs WHERE hash = ?") + .bind(&empty_hash) + .fetch_one(database.pool()) + .await?; + assert!(stored_empty.is_empty()); + + for invalid_hash in [ + "a".repeat(63), + "a".repeat(65), + "A".repeat(64), + "g".repeat(64), + ] { + let result = sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, ?)") + .bind(&invalid_hash) + .bind(Vec::::new()) + .execute(database.pool()) + .await; + assert!( + result.is_err(), + "invalid blob hash should be rejected: {invalid_hash:?}" + ); + } + + let null_hash = sqlx::query("INSERT INTO blobs (hash, data) VALUES (NULL, ?)") + .bind(Vec::::new()) + .execute(database.pool()) + .await; + assert!(null_hash.is_err()); + + let null_data = sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, NULL)") + .bind("2".repeat(64)) + .execute(database.pool()) + .await; + assert!(null_data.is_err()); + + let duplicate_hash = sqlx::query("INSERT INTO blobs (hash, data) VALUES (?, ?)") + .bind(&binary_hash) + .bind(vec![1_u8]) + .execute(database.pool()) + .await; + assert!(duplicate_hash.is_err()); + + Ok(()) +} + #[tokio::test] async fn mcp_servers_schema_rejects_invalid_transport_rows() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; From 01efe7c883dc813a837d45b1dcd436b531fa3964 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sun, 16 Aug 2026 10:21:45 -0400 Subject: [PATCH 34/63] Document BlobBackend as a transitional enum Mark the Slate arm as temporary and record that the SQLite arm's verified-read and hash-conflict semantics are the intended end state, so the dual-backend enum reads as a rollout vehicle rather than a permanent abstraction. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-store/src/blob_store.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/components/fabro-store/src/blob_store.rs b/lib/components/fabro-store/src/blob_store.rs index b43c94cdc..53d7bd75b 100644 --- a/lib/components/fabro-store/src/blob_store.rs +++ b/lib/components/fabro-store/src/blob_store.rs @@ -33,6 +33,14 @@ impl Record for Blob { } } +/// Which storage engine holds the blobs. +/// +/// This enum is a transition vehicle, not a permanent abstraction: `Slate` +/// preserves current production behavior while the SQLite backend rolls out. +/// Once runtime blob storage switches to SQLite and legacy blobs are +/// imported, delete the `Slate` arm (and this enum) and inline the SQLite +/// implementation into [`BlobStore`]. The SQLite arm's semantics — verified +/// reads and loud failure on hash conflicts — are the intended end state. enum BlobBackend { Slate(Repository), Sqlite(SqlitePool), @@ -140,7 +148,6 @@ impl BlobStore { } } } - } #[cfg(test)] From 9a03b813b20a8acfa64aea1ed685b12753596e9e Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 16:16:18 -0400 Subject: [PATCH 35/63] Trigger CI From 7b47ef2d0536f21c7418a7e4edd7736f6af4f9d2 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 18 Aug 2026 17:43:22 -0400 Subject: [PATCH 36/63] Cover missing SQLite blob reads --- lib/components/fabro-store/src/blob_store.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/components/fabro-store/src/blob_store.rs b/lib/components/fabro-store/src/blob_store.rs index 53d7bd75b..6d19ed978 100644 --- a/lib/components/fabro-store/src/blob_store.rs +++ b/lib/components/fabro-store/src/blob_store.rs @@ -249,7 +249,9 @@ mod tests { ); assert_eq!(store.read(&empty_hash).await?, Some(Bytes::new())); assert!(store.exists(&binary_hash).await?); - assert!(!store.exists(&BlobHash::new(b"missing")).await?); + let missing_hash = BlobHash::new(b"missing"); + assert_eq!(store.read(&missing_hash).await?, None); + assert!(!store.exists(&missing_hash).await?); let row_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM blobs") .fetch_one(database.pool()) From 519e456b28dc6857e1ef3d7085da2e22c4cd466d Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Wed, 19 Aug 2026 09:28:39 +0000 Subject: [PATCH 37/63] Bump version to 0.330.0-nightly.0 --- Cargo.lock | 104 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56f226964..82d469b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2673,7 +2673,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2688,7 +2688,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -2711,7 +2711,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2721,7 +2721,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2740,7 +2740,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2755,7 +2755,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2797,7 +2797,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2808,7 +2808,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "fabro-api", @@ -2829,7 +2829,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2895,7 +2895,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2933,7 +2933,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2941,7 +2941,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "cc", "libc", @@ -2950,7 +2950,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2966,7 +2966,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3010,7 +3010,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3127,18 +3127,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" [[package]] name = "fabro-store" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3168,7 +3168,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3208,7 +3208,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3233,7 +3233,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3254,7 +3254,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3268,7 +3268,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3291,7 +3291,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3314,7 +3314,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3327,7 +3327,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3344,7 +3344,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3433,7 +3433,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8544,7 +8544,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "axum", "base64", @@ -8563,7 +8563,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index c3b7aaf91..d230dfcd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.329.0-nightly.0" +version = "0.330.0-nightly.0" license = "MIT" [workspace.dependencies] From facc6a02f2cf6737f7ee45a4ff2aab0e40e8ba5b Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Wed, 19 Aug 2026 14:10:02 -0400 Subject: [PATCH 38/63] Test blob offloads through production hydration --- .../tests/it/daytona_integration.rs | 45 ++++++++++++++----- .../fabro-workflow/tests/it/integration.rs | 45 ++++++++++--------- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/lib/components/fabro-workflow/tests/it/daytona_integration.rs b/lib/components/fabro-workflow/tests/it/daytona_integration.rs index 0acfb0f60..9932f5a52 100644 --- a/lib/components/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/components/fabro-workflow/tests/it/daytona_integration.rs @@ -27,9 +27,9 @@ use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node}; use fabro_sandbox::daytona::{DaytonaConfig, DaytonaSandbox}; use fabro_static::EnvVars; use fabro_store::{ArtifactKey, ArtifactStore, Database}; -use fabro_types::{RunId, StageId, WorkflowSettings}; +use fabro_types::{RunId, StageId, WorkflowSettings, parse_blob_ref}; use fabro_util::shell; -use fabro_workflow::artifact::sync_artifacts_to_env; +use fabro_workflow::artifact; use fabro_workflow::context::Context; use fabro_workflow::error::Error; use fabro_workflow::event::Emitter; @@ -39,6 +39,7 @@ use fabro_workflow::handler::{Handler, HandlerRegistry}; use fabro_workflow::outcome::{Outcome, StageOutcome}; use fabro_workflow::records::Checkpoint; use fabro_workflow::run_options::{GitCheckpointOptions, RunOptions}; +use fabro_workflow::runtime_store::RunStoreHandle; use fabro_workflow::test_support::{WorkflowRunner, test_store_dir}; use object_store::local::LocalFileSystem; use tokio_util::sync::CancellationToken; @@ -159,6 +160,25 @@ fn load_run_checkpoint(run_dir: &Path) -> Result Result> { + let Some(current) = value.as_str() else { + return Ok(value.to_string()); + }; + if parse_blob_ref(current).is_none() { + return Ok(current.to_string()); + } + + let object_store = Arc::new(LocalFileSystem::new_with_prefix(test_store_dir(run_dir))?); + let store = Database::new(object_store, "", std::time::Duration::from_millis(1), None); + let run = store.open_run_reader(run_id).await?; + let run_store = RunStoreHandle::from(run); + Ok(artifact::resolve_text_or_blob_ref_str(current, &run_store).await?) +} + async fn create_env() -> DaytonaSandbox { let creds = load_github_app_credentials(); create_env_with_github_app(Some(creds)).await @@ -419,7 +439,9 @@ async fn daytona_artifact_sync_uploads_and_rewrites_pointer() { // Sync — the local file doesn't exist in the Daytona sandbox, so it should // upload - sync_artifacts_to_env(&mut updates, &env).await.unwrap(); + artifact::sync_artifacts_to_env(&mut updates, &env) + .await + .unwrap(); // Pointer should be rewritten to the Daytona working directory let new_pointer = updates["response.plan"].as_str().unwrap(); @@ -544,15 +566,18 @@ async fn daytona_pipeline_artifact_offload_and_sync() { .get("response.big_output") .expect("context should have response.big_output"); let pointer_str = pointer_value.as_str().expect("pointer should be a string"); - let expected_blob_hash = fabro_types::BlobHash::new( - &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) - .expect("large value should serialize"), - ); - assert_eq!( - pointer_str, - fabro_types::format_blob_ref(&expected_blob_hash), + assert!( + parse_blob_ref(pointer_str).is_some(), "checkpoint should persist a blob ref" ); + let resolved = resolve_checkpoint_text(dir.path(), &run_options.run_id, pointer_value) + .await + .expect("offloaded value should resolve through the run store"); + assert_eq!( + resolved, + "x".repeat(150 * 1024), + "offloaded value should round-trip through the run store" + ); env.cleanup().await.unwrap(); } diff --git a/lib/components/fabro-workflow/tests/it/integration.rs b/lib/components/fabro-workflow/tests/it/integration.rs index 02e0e74fb..42ea14630 100644 --- a/lib/components/fabro-workflow/tests/it/integration.rs +++ b/lib/components/fabro-workflow/tests/it/integration.rs @@ -35,6 +35,7 @@ use fabro_model::{Catalog, ProviderId}; use fabro_store::{ArtifactKey, ArtifactStore, Database}; use fabro_types::{EventBody, RunEvent, RunId, StageId, WorkflowSettings, parse_blob_ref}; use fabro_validate::{Severity, validate, validate_or_raise}; +use fabro_workflow::artifact; use fabro_workflow::context::Context; use fabro_workflow::error::{Error, FailureSignatureExt}; use fabro_workflow::event::{Emitter, Event}; @@ -54,6 +55,7 @@ use fabro_workflow::model_fallback::ModelFallbackPolicy; use fabro_workflow::outcome::{Outcome, OutcomeExt, StageOutcome}; use fabro_workflow::records::{Checkpoint, CheckpointExt}; use fabro_workflow::run_options::{GitCheckpointOptions, RunOptions}; +use fabro_workflow::runtime_store::RunStoreHandle; use fabro_workflow::test_support::{ WorkflowRunner, collect_events, run_graph_with_hooks, test_store_dir, }; @@ -233,10 +235,11 @@ fn resolve_checkpoint_text( let Some(current) = value.as_str() else { return Ok(value.to_string()); }; - let Some(blob_hash) = parse_blob_ref(current) else { + if parse_blob_ref(current).is_none() { return Ok(current.to_string()); - }; + } + let current = current.to_string(); let run_dir = run_dir.to_path_buf(); let (store_dir, uses_shared_store) = run_store_dir_and_mode(&run_dir)?; std::thread::spawn( @@ -271,10 +274,8 @@ fn resolve_checkpoint_text( .id }; let run = runtime.block_on(store.open_run_reader(&run_id))?; - let bytes = runtime - .block_on(run.read_blob(&blob_hash))? - .ok_or("checkpoint blob should exist")?; - Ok(serde_json::from_slice::(&bytes)?) + let run_store = RunStoreHandle::from(run); + Ok(runtime.block_on(artifact::resolve_text_or_blob_ref_str(¤t, &run_store))?) }, ) .join() @@ -10059,15 +10060,17 @@ async fn large_context_values_are_offloaded_to_artifact_store() { .expect("context should have response.big_output"); let pointer_str = pointer_value.as_str().expect("pointer should be a string"); - let expected_blob_hash = fabro_types::BlobHash::new( - &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) - .expect("large value should serialize"), - ); - assert_eq!( - pointer_str, - fabro_types::format_blob_ref(&expected_blob_hash), + assert!( + parse_blob_ref(pointer_str).is_some(), "value should be a durable blob ref" ); + let resolved = resolve_checkpoint_text(dir.path(), pointer_value) + .expect("offloaded value should resolve through the run store"); + assert_eq!( + resolved, + "x".repeat(150 * 1024), + "offloaded value should round-trip through the run store" + ); // WorkflowRunCompleted artifact_count now tracks captured artifacts, not // offloaded values. @@ -10258,15 +10261,17 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { .get("response.big_output") .expect("context should have response.big_output"); let pointer_str = pointer_value.as_str().expect("pointer should be a string"); - let expected_blob_hash = fabro_types::BlobHash::new( - &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) - .expect("large value should serialize"), - ); - assert_eq!( - pointer_str, - fabro_types::format_blob_ref(&expected_blob_hash), + assert!( + parse_blob_ref(pointer_str).is_some(), "checkpoint should persist a blob ref" ); + let resolved = resolve_checkpoint_text(dir.path(), pointer_value) + .expect("offloaded value should resolve through the run store"); + assert_eq!( + resolved, + "x".repeat(150 * 1024), + "offloaded value should round-trip through the run store" + ); let written = remote_env.written.lock().unwrap(); assert!( From 19aa5940ea3cec65bcc1b4aac4220dac2463afc0 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Wed, 19 Aug 2026 16:50:37 -0400 Subject: [PATCH 39/63] Add a shared RunSpec test fixture and adopt it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RunSpec` has 13 fields and no `Default`, so every test that needed one spelled out all 13 even when it cared about one or two. That put 64 hand-rolled `RunSpec { .. }` literals in `lib/`, and made a single additive field cost a mechanical edit at roughly 30 sites. Add `test_run_spec()` to `fabro-types`' feature-gated `test_support` module: fixed `fixtures::RUN_1`, default settings, a minimal `test` graph, `test_run_provenance()`, and every optional field unset. Tests now spread it and only spell out what they assert on. Adopt it at the 13 literals where the spread removes real duplication, including the crate-local `test_run_spec` helpers in `fabro-store` and `fabro-workflow`, which are now defined in terms of the shared fixture. Tests that populate every field on purpose — the exhaustive `RunSpec` serde round-trip in particular — keep spelling it out. No production code and no behavior changes. Co-Authored-By: Claude Opus 5 (1M context) --- lib/components/fabro-dump/src/lib.rs | 21 ++---- lib/components/fabro-store/src/run_state.rs | 44 ++----------- .../tests/serializable_projection.rs | 19 ++---- .../fabro-workflow/src/billing_rollup.rs | 17 +---- .../fabro-workflow/src/run_lookup.rs | 19 ++---- .../fabro-workflow/src/runtime_store.rs | 18 +---- .../tests/run_projection_round_trip.rs | 17 +---- .../fabro-types/src/run_projection.rs | 65 ++----------------- .../fabro-types/src/test_support.rs | 36 +++++++++- .../fabro-types/tests/run_spec_methods.rs | 10 +-- 10 files changed, 76 insertions(+), 190 deletions(-) diff --git a/lib/components/fabro-dump/src/lib.rs b/lib/components/fabro-dump/src/lib.rs index 33f3185f6..43cf278eb 100644 --- a/lib/components/fabro-dump/src/lib.rs +++ b/lib/components/fabro-dump/src/lib.rs @@ -486,8 +486,7 @@ mod tests { use fabro_types::{ Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance, RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, - StageOutcome, StartRecord, SuccessReason, WorkflowSettings, first_event_seq, fixtures, - test_support, + StageOutcome, StartRecord, SuccessReason, first_event_seq, fixtures, test_support, }; use futures::executor; @@ -495,24 +494,18 @@ mod tests { fn sample_run_spec() -> RunSpec { RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: Graph::new("ship"), - graph_source: Some("digraph Ship {}".to_string()), - workflow_slug: Some("demo".to_string()), - automation: None, + graph: Graph::new("ship"), + graph_source: Some("digraph Ship {}".to_string()), + workflow_slug: Some("demo".to_string()), source_directory: Some("/tmp/project".to_string()), - git: Some(fabro_types::GitContext { + git: Some(fabro_types::GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), sha: None, dirty: fabro_types::DirtyStatus::Clean, }), - labels: HashMap::from([("team".to_string(), "platform".to_string())]), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - fork_source_ref: None, + labels: HashMap::from([("team".to_string(), "platform".to_string())]), + ..test_support::test_run_spec() } } diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index c89587e59..1b8138fbd 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -2281,19 +2281,8 @@ mod tests { fn test_run_spec() -> RunSpec { RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: Graph::new("test"), - graph_source: Some("digraph test {}".to_string()), - workflow_slug: None, - automation: None, - source_directory: None, - labels: HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - git: None, - fork_source_ref: None, + graph_source: Some("digraph test {}".to_string()), + ..test_support::test_run_spec() } } @@ -4086,19 +4075,9 @@ mod tests { fn summary_synthesizes_submitted_when_run_exists_without_status() { let mut state = initialized_projection(); state.spec = fabro_types::RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: fabro_types::Graph::new("test"), - graph_source: None, - workflow_slug: Some("test".to_string()), - automation: None, + workflow_slug: Some("test".to_string()), source_directory: Some("/tmp/repo".to_string()), - git: None, - labels: HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - fork_source_ref: None, + ..test_support::test_run_spec() }; let summary_json = serde_json::to_value(build_summary(&state, &fixtures::RUN_1)).unwrap(); @@ -4112,19 +4091,10 @@ mod tests { fn summary_preserves_absent_workflow_name_and_reports_graph_name() { let mut state = initialized_projection(); state.spec = fabro_types::RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: fabro_types::Graph::new("GraphName"), - graph_source: None, - workflow_slug: Some("release-flow".to_string()), - automation: None, + graph: fabro_types::Graph::new("GraphName"), + workflow_slug: Some("release-flow".to_string()), source_directory: Some("/tmp/repo".to_string()), - git: None, - labels: HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - fork_source_ref: None, + ..test_support::test_run_spec() }; let summary = build_summary(&state, &fixtures::RUN_1); diff --git a/lib/components/fabro-store/tests/serializable_projection.rs b/lib/components/fabro-store/tests/serializable_projection.rs index ef0ed067b..d6fdcc682 100644 --- a/lib/components/fabro-store/tests/serializable_projection.rs +++ b/lib/components/fabro-store/tests/serializable_projection.rs @@ -8,30 +8,23 @@ use fabro_types::{ BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord, ParallelBranchResult, QuestionType, RunDiff, RunSandbox, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, - StageOutcome, StartRecord, WorkflowSettings, first_event_seq, fixtures, test_support, + StageOutcome, StartRecord, first_event_seq, fixtures, test_support, }; use serde_json::json; fn sample_run_spec() -> RunSpec { RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: Graph::new("ship"), - graph_source: None, - workflow_slug: Some("demo".to_string()), - automation: None, + graph: Graph::new("ship"), + workflow_slug: Some("demo".to_string()), source_directory: Some("/tmp/project".to_string()), - labels: HashMap::from([("team".to_string(), "platform".to_string())]), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - git: Some(fabro_types::GitContext { + labels: HashMap::from([("team".to_string(), "platform".to_string())]), + git: Some(fabro_types::GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), sha: None, dirty: fabro_types::DirtyStatus::Clean, }), - fork_source_ref: None, + ..test_support::test_run_spec() } } diff --git a/lib/components/fabro-workflow/src/billing_rollup.rs b/lib/components/fabro-workflow/src/billing_rollup.rs index 986b541d4..1b6487f36 100644 --- a/lib/components/fabro-workflow/src/billing_rollup.rs +++ b/lib/components/fabro-workflow/src/billing_rollup.rs @@ -126,12 +126,10 @@ pub fn billing_rollup_from_projection( #[cfg(test)] mod tests { - use std::collections::HashMap; - use fabro_model::{Catalog, ModelRef, ProviderId}; use fabro_types::{ AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion, - StageOutcome, WorkflowSettings, first_event_seq, fixtures, test_support, + StageOutcome, first_event_seq, test_support, }; use super::billing_rollup_from_projection; @@ -311,19 +309,8 @@ mod tests { }); RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), graph, - graph_source: None, - workflow_slug: None, - automation: None, - source_directory: None, - labels: HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - git: None, - fork_source_ref: None, + ..test_support::test_run_spec() } } } diff --git a/lib/components/fabro-workflow/src/run_lookup.rs b/lib/components/fabro-workflow/src/run_lookup.rs index 99e9825fe..1edd36290 100644 --- a/lib/components/fabro-workflow/src/run_lookup.rs +++ b/lib/components/fabro-workflow/src/run_lookup.rs @@ -445,13 +445,11 @@ fn run_id_matches(run_id: RunId, prefix: &str) -> bool { #[cfg(test)] mod tests { - use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; - use fabro_graphviz::graph::Graph; use fabro_store::Database; - use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support}; + use fabro_types::{RunStatus, fixtures, test_support}; use object_store::memory::InMemory; use super::scan_runs_combined; @@ -470,24 +468,15 @@ mod tests { fn sample_run_spec() -> RunSpec { RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: Graph::new("test"), - graph_source: None, - workflow_slug: Some("test".to_string()), - automation: None, + workflow_slug: Some("test".to_string()), source_directory: Some("/tmp/project".to_string()), - git: Some(fabro_types::GitContext { + git: Some(fabro_types::GitContext { origin_url: String::new(), branch: "main".to_string(), sha: None, dirty: fabro_types::DirtyStatus::Clean, }), - labels: HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - fork_source_ref: None, + ..test_support::test_run_spec() } } diff --git a/lib/components/fabro-workflow/src/runtime_store.rs b/lib/components/fabro-workflow/src/runtime_store.rs index af4eae425..63d55ca64 100644 --- a/lib/components/fabro-workflow/src/runtime_store.rs +++ b/lib/components/fabro-workflow/src/runtime_store.rs @@ -112,15 +112,13 @@ impl RunStoreBackend for LocalRunStoreBackend { #[cfg(test)] mod tests { - use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use chrono::Utc; - use fabro_graphviz::graph::Graph; use fabro_store::Database; use fabro_types::run_event::RunSubmittedProps; - use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures, test_support}; + use fabro_types::{EventBody, RunEvent, fixtures, test_support}; use object_store::memory::InMemory; use super::RunStoreHandle; @@ -139,19 +137,9 @@ mod tests { fn test_run_spec() -> RunSpec { RunSpec { - run_id: fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: Graph::new("test"), - graph_source: None, - workflow_slug: Some("test".to_string()), - automation: None, + workflow_slug: Some("test".to_string()), source_directory: Some("/tmp/test".to_string()), - git: None, - labels: HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - fork_source_ref: None, + ..test_support::test_run_spec() } } diff --git a/lib/foundation/fabro-api/tests/run_projection_round_trip.rs b/lib/foundation/fabro-api/tests/run_projection_round_trip.rs index 77d28178b..f1a00a5a5 100644 --- a/lib/foundation/fabro-api/tests/run_projection_round_trip.rs +++ b/lib/foundation/fabro-api/tests/run_projection_round_trip.rs @@ -1,7 +1,7 @@ use std::any::{TypeId, type_name}; use fabro_api::types::RunProjection as ApiRunProjection; -use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, test_support}; +use fabro_types::{RunProjection, RunSpec, test_support}; use serde_json::json; #[test] fn run_projection_reuses_canonical_type() { @@ -129,19 +129,8 @@ fn run_projection_round_trips_with_pending_control_unset() { fn run_spec_json() -> serde_json::Value { serde_json::to_value(RunSpec { - run_id: fabro_types::fixtures::RUN_1, - settings: WorkflowSettings::default(), - graph: Graph::new("test"), - graph_source: Some("digraph test {}".to_string()), - workflow_slug: None, - automation: None, - source_directory: None, - labels: std::collections::HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - git: None, - fork_source_ref: None, + graph_source: Some("digraph test {}".to_string()), + ..test_support::test_run_spec() }) .unwrap() } diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index 3a6be70d4..d41258298 100644 --- a/lib/foundation/fabro-types/src/run_projection.rs +++ b/lib/foundation/fabro-types/src/run_projection.rs @@ -1061,11 +1061,9 @@ impl RunProjection { #[cfg(test)] mod title_tests { - use std::collections::HashMap; - use chrono::Utc; - use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support}; + use crate::{AttrValue, Graph, RunProjection, RunSpec, test_support}; fn projection_with_goal(goal: Option<&str>) -> RunProjection { let mut graph = Graph::new("test"); @@ -1076,19 +1074,8 @@ mod title_tests { } let spec = RunSpec { - run_id: RunId::new(), - settings: WorkflowSettings::default(), graph, - graph_source: None, - workflow_slug: None, - automation: None, - source_directory: None, - labels: HashMap::new(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - git: None, - fork_source_ref: None, + ..test_support::test_run_spec() }; RunProjection::new(String::new(), spec, Utc::now()) } @@ -1129,7 +1116,6 @@ mod title_tests { #[cfg(test)] mod iter_stages_tests { - use std::collections::HashMap; use std::num::NonZeroU32; use chrono::Utc; @@ -1137,10 +1123,7 @@ mod iter_stages_tests { use serde_json::json; use super::RunProjection; - use crate::{ - AgentControlState, BilledTokenCounts, Graph, RunId, RunSpec, StageProjection, - WorkflowSettings, test_support, - }; + use crate::{AgentControlState, BilledTokenCounts, StageProjection, test_support}; fn seq(n: u32) -> NonZeroU32 { NonZeroU32::new(n).unwrap() @@ -1149,21 +1132,7 @@ mod iter_stages_tests { fn projection() -> RunProjection { RunProjection::new( "Test run".to_string(), - RunSpec { - run_id: RunId::new(), - settings: WorkflowSettings::default(), - graph: Graph::new("test"), - graph_source: None, - workflow_slug: None, - automation: None, - source_directory: None, - labels: HashMap::default(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - git: None, - fork_source_ref: None, - }, + test_support::test_run_spec(), Utc::now(), ) } @@ -1336,14 +1305,12 @@ mod iter_stages_tests { #[cfg(test)] mod live_timing_tests { - use std::collections::HashMap; - use chrono::{DateTime, TimeZone, Utc}; use super::{RunProjection, StageToolBatchProjection}; use crate::{ - Graph, ModelRef, RunId, RunSpec, StageHandler, StageInferenceProjection, StageProjection, - StageState, StageTiming, StartRecord, WorkflowSettings, first_event_seq, test_support, + ModelRef, StageHandler, StageInferenceProjection, StageProjection, StageState, StageTiming, + StartRecord, first_event_seq, test_support, }; fn at(seconds: i64) -> DateTime { @@ -1351,25 +1318,7 @@ mod live_timing_tests { } fn projection() -> RunProjection { - RunProjection::new( - "Test run".to_string(), - RunSpec { - run_id: RunId::new(), - settings: WorkflowSettings::default(), - graph: Graph::new("test"), - graph_source: None, - workflow_slug: None, - automation: None, - source_directory: None, - labels: HashMap::default(), - provenance: test_support::test_run_provenance(), - manifest_blob: None, - definition_blob: None, - git: None, - fork_source_ref: None, - }, - at(0), - ) + RunProjection::new("Test run".to_string(), test_support::test_run_spec(), at(0)) } /// In-flight stage that started at `at(0)`. diff --git a/lib/foundation/fabro-types/src/test_support.rs b/lib/foundation/fabro-types/src/test_support.rs index 994813974..dfd0d7e7d 100644 --- a/lib/foundation/fabro-types/src/test_support.rs +++ b/lib/foundation/fabro-types/src/test_support.rs @@ -1,4 +1,8 @@ -use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance}; +use std::collections::HashMap; + +use crate::{ + AuthMethod, Graph, IdpIdentity, Principal, RunProvenance, RunSpec, WorkflowSettings, fixtures, +}; #[must_use] pub fn test_principal() -> Principal { @@ -17,3 +21,33 @@ pub fn test_run_provenance() -> RunProvenance { subject: test_principal(), } } + +/// Neutral [`RunSpec`] for tests: a fixed run id, default settings, a minimal +/// `test` graph, and every optional field unset. +/// +/// Spread it so a test only spells out the fields it actually asserts on: +/// +/// ```ignore +/// let spec = RunSpec { +/// workflow_slug: Some("release-flow".to_string()), +/// ..test_run_spec() +/// }; +/// ``` +#[must_use] +pub fn test_run_spec() -> RunSpec { + RunSpec { + run_id: fixtures::RUN_1, + settings: WorkflowSettings::default(), + graph: Graph::new("test"), + graph_source: None, + workflow_slug: None, + automation: None, + source_directory: None, + labels: HashMap::new(), + provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, + fork_source_ref: None, + } +} diff --git a/lib/foundation/fabro-types/tests/run_spec_methods.rs b/lib/foundation/fabro-types/tests/run_spec_methods.rs index f6f76fecf..6f6aefaaf 100644 --- a/lib/foundation/fabro-types/tests/run_spec_methods.rs +++ b/lib/foundation/fabro-types/tests/run_spec_methods.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use fabro_types::graph::Graph; use fabro_types::run::{DirtyStatus, GitContext, RunSpec}; use fabro_types::settings::{ProjectNamespace, WorkflowNamespace}; -use fabro_types::test_support::test_run_provenance; +use fabro_types::test_support::test_run_spec; use fabro_types::{WorkflowSettings, fixtures}; fn sample_run_spec() -> RunSpec { @@ -20,24 +20,18 @@ fn sample_run_spec() -> RunSpec { }; RunSpec { - run_id: fixtures::RUN_1, settings, graph: Graph::new("ship"), - graph_source: None, workflow_slug: Some("demo".to_string()), - automation: None, source_directory: Some("/Users/client/project".to_string()), labels: HashMap::from([("team".to_string(), "platform".to_string())]), - provenance: test_run_provenance(), - manifest_blob: None, - definition_blob: None, git: Some(GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), sha: Some("abc123".to_string()), dirty: DirtyStatus::Dirty, }), - fork_source_ref: None, + ..test_run_spec() } } From 1898031d74af7f00e70aa3bb50a22361dfd98e60 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Wed, 19 Aug 2026 17:47:25 -0400 Subject: [PATCH 40/63] Make RunSpec example a checked doctest --- lib/foundation/fabro-types/src/test_support.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/foundation/fabro-types/src/test_support.rs b/lib/foundation/fabro-types/src/test_support.rs index dfd0d7e7d..b00e792b8 100644 --- a/lib/foundation/fabro-types/src/test_support.rs +++ b/lib/foundation/fabro-types/src/test_support.rs @@ -27,11 +27,13 @@ pub fn test_run_provenance() -> RunProvenance { /// /// Spread it so a test only spells out the fields it actually asserts on: /// -/// ```ignore +/// ``` +/// # use fabro_types::{RunSpec, test_support}; /// let spec = RunSpec { /// workflow_slug: Some("release-flow".to_string()), -/// ..test_run_spec() +/// ..test_support::test_run_spec() /// }; +/// # assert_eq!(spec.workflow_slug.as_deref(), Some("release-flow")); /// ``` #[must_use] pub fn test_run_spec() -> RunSpec { From 03c3412e513b845c5acc992b30dede3a34dc6858 Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Thu, 20 Aug 2026 09:26:14 +0000 Subject: [PATCH 41/63] Bump version to 0.331.0-nightly.0 --- Cargo.lock | 104 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82d469b7f..5d47c63c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2673,7 +2673,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2688,7 +2688,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -2711,7 +2711,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2721,7 +2721,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2740,7 +2740,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2755,7 +2755,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2797,7 +2797,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2808,7 +2808,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "fabro-api", @@ -2829,7 +2829,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2895,7 +2895,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2933,7 +2933,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2941,7 +2941,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "cc", "libc", @@ -2950,7 +2950,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2966,7 +2966,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3010,7 +3010,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3127,18 +3127,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" [[package]] name = "fabro-store" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3168,7 +3168,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3208,7 +3208,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3233,7 +3233,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3254,7 +3254,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3268,7 +3268,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3291,7 +3291,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3314,7 +3314,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3327,7 +3327,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3344,7 +3344,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3433,7 +3433,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8544,7 +8544,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "axum", "base64", @@ -8563,7 +8563,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index d230dfcd8..bb339e93f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.330.0-nightly.0" +version = "0.331.0-nightly.0" license = "MIT" [workspace.dependencies] From 579f3db26f9d3538092c0514f97e37974739b36e Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 09:33:43 -0400 Subject: [PATCH 42/63] Add a cached GitHub installation-token source for push credentials Every push previously re-minted a fresh GitHub App installation token and embedded it in the origin URL, so pushes routinely landed inside GitHub's token-replication lag window (run 01M0DH033P2XSTHAGVBHG6922F failed terminally on four consecutive fresh-token 404s). Reusing mature tokens removes the failure trigger and saves two GitHub API calls plus one sandbox exec per push. - New fabro_github::token_source::InstallationTokenSource: one cached, single-flight source per origin repo. Static credentials pass through (generation 0); App credentials mint through the cache and reuse tokens until REFRESH_MARGIN (10 min) before expiry. Every resolve returns a non-secret TokenSnapshot (generation + Minted/Reused/Static provenance), and the source logs mints at INFO and reuses at DEBUG. - Docker and Daytona share the source through PushCredentialState: an embed mutex serializes compare -> set-url -> record, a matching generation skips the set-url exec, and the generation is recorded only after a successful exec. The clone still mints its own token, but now seeds the source cache (generation 1) and the last-embedded state, so a refresh mint failure falls back to the known embedded token instead of believing nothing was ever embedded. - RefreshOutcome now reports the remote action (embedded/unchanged/none) separately from the token snapshot; git_push_via_exec logs token age and provenance with each push, and refresh failures log the last embedded generation. - The run-metadata writer resolves through the sandbox's shared source instead of minting per snapshot (with its own cached source on resume). - The ACP refresh-ahead loop reschedules from the embedded token's expires_at minus the margin instead of a fixed 45-minute interval, which a cached source would have broken for long turns; static credentials stop the loop. Plan: .ai/plans/git-push-token-resilience.md (PR 1: items 3 and 6). Co-Authored-By: Claude Fable 5 --- Cargo.lock | 2 + lib/components/fabro-agent/src/lib.rs | 5 +- lib/components/fabro-agent/src/sandbox.rs | 7 +- lib/components/fabro-github/Cargo.toml | 2 + lib/components/fabro-github/src/lib.rs | 2 + .../fabro-github/src/token_source.rs | 607 ++++++++++++++++++ lib/components/fabro-sandbox/Cargo.toml | 6 +- .../fabro-sandbox/src/daytona/mod.rs | 123 ++-- lib/components/fabro-sandbox/src/docker.rs | 123 ++-- lib/components/fabro-sandbox/src/lib.rs | 13 +- .../fabro-sandbox/src/push_credentials.rs | 350 ++++++++++ lib/components/fabro-sandbox/src/sandbox.rs | 109 +++- .../fabro-workflow/src/handler/llm/acp.rs | 442 +++++++++++-- .../fabro-workflow/src/pipeline/initialize.rs | 27 +- .../fabro-workflow/src/run_metadata.rs | 78 +-- 15 files changed, 1639 insertions(+), 257 deletions(-) create mode 100644 lib/components/fabro-github/src/token_source.rs create mode 100644 lib/components/fabro-sandbox/src/push_credentials.rs diff --git a/Cargo.lock b/Cargo.lock index 5d47c63c3..15bb36035 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2654,6 +2654,7 @@ name = "fabro-github" version = "0.331.0-nightly.0" dependencies = [ "anyhow", + "async-trait", "base64", "chrono", "fabro-http", @@ -2665,6 +2666,7 @@ dependencies = [ "jsonwebtoken", "serde", "serde_json", + "strum 0.28.0", "thiserror 2.0.18", "tokio", "tracing", diff --git a/lib/components/fabro-agent/src/lib.rs b/lib/components/fabro-agent/src/lib.rs index c9d7f2e3d..e9549a716 100644 --- a/lib/components/fabro-agent/src/lib.rs +++ b/lib/components/fabro-agent/src/lib.rs @@ -59,8 +59,9 @@ pub use question_tools::{ }; pub use sandbox::{ CommandOutputCallback, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, - GrepOptions, RefreshOutcome, Sandbox, SandboxEvent, SandboxEventCallback, StderrCollector, - StdioProcess, StdioProcessHandle, format_lines_numbered, shell_quote, + GrepOptions, RefreshOutcome, RemoteCredentialAction, Sandbox, SandboxEvent, + SandboxEventCallback, StderrCollector, StdioProcess, StdioProcessHandle, TokenProvenance, + TokenSnapshot, format_lines_numbered, shell_quote, }; pub use session::{ CompletionCoordinator, Session, SessionControlHandle, SessionInputTiming, diff --git a/lib/components/fabro-agent/src/sandbox.rs b/lib/components/fabro-agent/src/sandbox.rs index 47fbf894c..2f1ade3a4 100644 --- a/lib/components/fabro-agent/src/sandbox.rs +++ b/lib/components/fabro-agent/src/sandbox.rs @@ -3,7 +3,8 @@ // `crate::delegate_sandbox!` invocations continue to work. pub use fabro_sandbox::{ CommandOutputCallback, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, - GrepOptions, RefreshOutcome, Sandbox, SandboxEvent, SandboxEventCallback, SandboxFile, - StderrCollector, StdioProcess, StdioProcessHandle, StdioProcessTermination, WalkOptions, - delegate_sandbox, format_lines_numbered, shell_quote, + GrepOptions, RefreshOutcome, RemoteCredentialAction, Sandbox, SandboxEvent, + SandboxEventCallback, SandboxFile, StderrCollector, StdioProcess, StdioProcessHandle, + StdioProcessTermination, TokenProvenance, TokenSnapshot, WalkOptions, delegate_sandbox, + format_lines_numbered, shell_quote, }; diff --git a/lib/components/fabro-github/Cargo.toml b/lib/components/fabro-github/Cargo.toml index 9db73130e..788c6efe1 100644 --- a/lib/components/fabro-github/Cargo.toml +++ b/lib/components/fabro-github/Cargo.toml @@ -14,7 +14,9 @@ workspace = true [dependencies] anyhow.workspace = true +async-trait.workspace = true serde.workspace = true +strum.workspace = true serde_json.workspace = true fabro-http.workspace = true fabro-redact.workspace = true diff --git a/lib/components/fabro-github/src/lib.rs b/lib/components/fabro-github/src/lib.rs index fd4b98936..9973c4377 100644 --- a/lib/components/fabro-github/src/lib.rs +++ b/lib/components/fabro-github/src/lib.rs @@ -9,6 +9,8 @@ use fabro_types::settings::run::MergeStrategy; use serde::Deserialize; use tokio::process::Command; +pub mod token_source; + pub const GITHUB_API_BASE_URL: &str = "https://api.github.com"; /// Returns the GitHub API base URL, allowing override via `GITHUB_BASE_URL` env diff --git a/lib/components/fabro-github/src/token_source.rs b/lib/components/fabro-github/src/token_source.rs new file mode 100644 index 000000000..11743e077 --- /dev/null +++ b/lib/components/fabro-github/src/token_source.rs @@ -0,0 +1,607 @@ +//! Cached GitHub installation-token source. +//! +//! One [`InstallationTokenSource`] serves every GitHub-token consumer for an +//! origin repository — the clone-based sandbox providers and the run-metadata +//! writer share a single source, so "reuse a token until near expiry" is the +//! default behavior instead of a per-call-site special case. Reusing mature +//! tokens keeps consumers out of GitHub's token-replication lag window, where +//! a token minted milliseconds earlier is rejected with 404 "Repository not +//! found" or an authentication failure. +//! +//! The source also reports *provenance*: when it minted the token it returned, +//! and which mint generation it belongs to. Retry classification, logging, and +//! failure reports all read that one fact instead of threading booleans +//! through call stacks. + +use std::fmt; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context as _; +use chrono::{DateTime, Utc}; +use tokio::sync::Mutex; + +use crate::{GitHubAppCredentials, GitHubCredentials, InstallationToken}; + +/// How long before expiry a cached installation token stops being reused. +/// +/// Must comfortably exceed the longest git operation that pins a resolved +/// token, so a token handed out just above the margin still outlives the +/// operation. GitHub App installation tokens live 60 minutes. +pub const REFRESH_MARGIN: Duration = Duration::from_mins(10); + +/// Where the token a resolve returned came from. +/// +/// Time metadata exists only for tokens this source minted. Static +/// credentials (a PAT, or a pre-minted installation token) carry no +/// `minted_at`, so token age is undefined for them and they are never +/// treated as freshly minted. +#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum TokenProvenance { + /// This resolve minted the token. + Minted { + minted_at: DateTime, + expires_at: DateTime, + }, + /// This resolve returned a token minted by an earlier resolve. + Reused { + minted_at: DateTime, + expires_at: DateTime, + }, + /// A fixed credential the source cannot re-mint. + Static, +} + +/// Non-secret description of the token a resolve returned. Shared by the +/// source, refresh outcomes, logs, and events. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TokenSnapshot { + /// Increments per mint; 0 for `Static`. + pub generation: u64, + pub provenance: TokenProvenance, +} + +impl TokenSnapshot { + #[must_use] + pub fn minted_at(&self) -> Option> { + match self.provenance { + TokenProvenance::Minted { minted_at, .. } + | TokenProvenance::Reused { minted_at, .. } => Some(minted_at), + TokenProvenance::Static => None, + } + } + + #[must_use] + pub fn expires_at(&self) -> Option> { + match self.provenance { + TokenProvenance::Minted { expires_at, .. } + | TokenProvenance::Reused { expires_at, .. } => Some(expires_at), + TokenProvenance::Static => None, + } + } + + /// Age of the token at `now`. `None` for static credentials, whose age is + /// undefined. + #[must_use] + pub fn age_at(&self, now: DateTime) -> Option { + let minted_at = self.minted_at()?; + Some((now - minted_at).to_std().unwrap_or(Duration::ZERO)) + } + + /// Age of the token in milliseconds, measured now. + #[must_use] + pub fn age_ms(&self) -> Option { + self.age_at(Utc::now()) + .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)) + } + + #[must_use] + pub fn is_static(&self) -> bool { + matches!(self.provenance, TokenProvenance::Static) + } +} + +/// A token secret that never appears in `Debug` output. Call +/// [`SecretString::expose`] at the point of use (URL embedding, git +/// credentials) — never in a log line. +#[derive(Clone)] +pub struct SecretString(String); + +impl SecretString { + #[must_use] + pub fn new(secret: String) -> Self { + Self(secret) + } + + #[must_use] + pub fn expose(&self) -> &str { + &self.0 + } +} + +impl fmt::Debug for SecretString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SecretString()") + } +} + +/// A token handed out by [`InstallationTokenSource::resolve`]: the secret plus +/// its non-secret snapshot. Only the snapshot may cross logging or event +/// boundaries. +#[derive(Debug, Clone)] +pub struct ResolvedToken { + pub token: SecretString, + pub snapshot: TokenSnapshot, +} + +/// Mints installation tokens for [`InstallationTokenSource`]. Abstracted so +/// tests can script mint results without HTTP. +#[async_trait::async_trait] +pub trait InstallationTokenMinter: Send + Sync { + async fn mint(&self) -> anyhow::Result; +} + +/// Real minter backed by GitHub App credentials. +struct AppTokenMinter { + creds: GitHubAppCredentials, + http: fabro_http::HttpClient, + owner: String, + repo: String, + base_url: String, + permissions: serde_json::Value, +} + +#[async_trait::async_trait] +impl InstallationTokenMinter for AppTokenMinter { + async fn mint(&self) -> anyhow::Result { + self.creds + .mint_installation_token( + &self.http, + &self.owner, + &self.repo, + &self.base_url, + self.permissions.clone(), + None, + ) + .await + } +} + +/// A minted token plus the metadata the cache tracks for it. +struct CachedToken { + token: InstallationToken, + minted_at: DateTime, + generation: u64, +} + +impl CachedToken { + fn resolved(&self, provenance: TokenProvenance) -> ResolvedToken { + ResolvedToken { + token: SecretString::new(self.token.token.clone()), + snapshot: TokenSnapshot { + generation: self.generation, + provenance, + }, + } + } +} + +enum SourceState { + /// A fixed personal access token — no expiry metadata. + Pat(SecretString), + /// A pre-minted installation token — fixed, rejected client-side once + /// expired. + Installation(InstallationToken), + /// GitHub App credentials that mint installation tokens on demand. + /// + /// The async lock is held across the mint, making `resolve()` + /// single-flight: concurrent near-expiry callers wait and receive the + /// same generation instead of racing to mint. + App { + minter: Box, + cache: Mutex>, + }, +} + +/// Cached installation-token source for one origin repository. +/// +/// Static credentials pass through unchanged. App credentials mint through +/// the shared cache: a resolve reuses the cached token until it is within +/// [`REFRESH_MARGIN`] of expiry, then mints a new generation. +pub struct InstallationTokenSource { + /// `owner/repo`, for logs only. + repo: String, + state: SourceState, +} + +impl InstallationTokenSource { + /// Build a source for `creds` against the repository in `origin_url`. + /// + /// `permissions` scopes minted installation tokens; static credentials + /// pass through and ignore it. + pub fn for_origin( + creds: &GitHubCredentials, + origin_url: &str, + permissions: serde_json::Value, + ) -> anyhow::Result> { + let normalized = crate::normalize_repo_origin_url(origin_url); + let (owner, repo) = crate::parse_github_owner_repo(&normalized) + .context("parsing GitHub origin for token source")?; + let repo_display = format!("{owner}/{repo}"); + let state = match creds { + GitHubCredentials::Pat(token) => SourceState::Pat(SecretString::new(token.clone())), + GitHubCredentials::Installation(token) => SourceState::Installation(token.clone()), + GitHubCredentials::App(app) => { + let http = fabro_http::http_client() + .map_err(anyhow::Error::new) + .context("building HTTP client for token source")?; + SourceState::App { + minter: Box::new(AppTokenMinter { + creds: app.clone(), + http, + owner, + repo, + base_url: crate::github_api_base_url(), + permissions, + }), + cache: Mutex::new(None), + } + } + }; + Ok(Arc::new(Self { + repo: repo_display, + state, + })) + } + + /// Build a minting source over a custom minter. For tests. + #[must_use] + pub fn with_minter(repo: String, minter: Box) -> Arc { + Arc::new(Self { + repo, + state: SourceState::App { + minter, + cache: Mutex::new(None), + }, + }) + } + + /// Whether this source can mint new tokens (GitHub App credentials). + #[must_use] + pub fn mints_installation_tokens(&self) -> bool { + matches!(self.state, SourceState::App { .. }) + } + + /// Resolve a token, reusing the cached one until it nears expiry. + pub async fn resolve(&self) -> anyhow::Result { + match &self.state { + SourceState::Pat(_) | SourceState::Installation(_) => self.resolve_static(), + SourceState::App { minter, cache } => { + let mut cache = cache.lock().await; + // Re-check under the lock: a waiter queued behind a minter + // finds the fresh token here instead of minting again. + if let Some(cached) = cache.as_ref() { + if !cached.token.near_expiry(REFRESH_MARGIN) { + let resolved = cached.resolved(TokenProvenance::Reused { + minted_at: cached.minted_at, + expires_at: cached.token.expires_at, + }); + tracing::debug!( + repo = %self.repo, + generation = cached.generation, + expires_at = %cached.token.expires_at, + "Reusing cached GitHub installation token" + ); + return Ok(resolved); + } + } + self.mint_locked(minter.as_ref(), &mut cache).await + } + } + } + + /// Mint a fresh token for the first repository clone and seed the cache + /// with it. + /// + /// The clone deliberately never reuses a warm cache: retrying a clone with + /// the token minted for it is the established replication-lag recovery, + /// and reuse of older tokens for clones is a separate follow-up. Seeding + /// makes the clone token generation 1, so later refreshes reuse it until + /// it nears expiry. + pub async fn mint_for_clone(&self) -> anyhow::Result { + match &self.state { + SourceState::Pat(_) | SourceState::Installation(_) => self.resolve_static(), + SourceState::App { minter, cache } => { + let mut cache = cache.lock().await; + self.mint_locked(minter.as_ref(), &mut cache).await + } + } + } + + fn resolve_static(&self) -> anyhow::Result { + let secret = match &self.state { + SourceState::Pat(token) => token.clone(), + SourceState::Installation(token) => SecretString::new(token.valid_token()?.to_owned()), + SourceState::App { .. } => unreachable!("resolve_static called for App credentials"), + }; + Ok(ResolvedToken { + token: secret, + snapshot: TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + }, + }) + } + + async fn mint_locked( + &self, + minter: &dyn InstallationTokenMinter, + cache: &mut Option, + ) -> anyhow::Result { + let token = minter + .mint() + .await + .context("minting GitHub installation access token")?; + let generation = cache.as_ref().map_or(0, |cached| cached.generation) + 1; + let minted_at = Utc::now(); + tracing::info!( + repo = %self.repo, + generation, + expires_at = %token.expires_at, + "Minted GitHub installation token" + ); + let cached = CachedToken { + token, + minted_at, + generation, + }; + let resolved = cached.resolved(TokenProvenance::Minted { + minted_at, + expires_at: cached.token.expires_at, + }); + *cache = Some(cached); + Ok(resolved) + } +} + +#[cfg(test)] +mod tests { + use std::collections::VecDeque; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use anyhow::anyhow; + + use super::*; + + enum MintAction { + Token(&'static str, DateTime), + Error(&'static str), + } + + struct MockMinter { + calls: AtomicUsize, + script: Mutex>, + } + + impl MockMinter { + fn new(script: Vec) -> Self { + Self { + calls: AtomicUsize::new(0), + script: Mutex::new(script.into()), + } + } + + fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } + } + + #[async_trait::async_trait] + impl InstallationTokenMinter for MockMinter { + async fn mint(&self) -> anyhow::Result { + self.calls.fetch_add(1, Ordering::SeqCst); + match self.script.lock().await.pop_front().expect("mint script") { + MintAction::Token(token, expires_at) => Ok(InstallationToken { + token: token.to_string(), + expires_at, + }), + MintAction::Error(message) => Err(anyhow!(message)), + } + } + } + + struct SharedMinter(Arc); + + #[async_trait::async_trait] + impl InstallationTokenMinter for SharedMinter { + async fn mint(&self) -> anyhow::Result { + self.0.mint().await + } + } + + fn mintable(script: Vec) -> (Arc, Arc) { + let minter = Arc::new(MockMinter::new(script)); + let source = InstallationTokenSource::with_minter( + "owner/repo".to_string(), + Box::new(SharedMinter(Arc::clone(&minter))), + ); + (source, minter) + } + + #[tokio::test] + async fn pat_resolves_as_static_generation_zero() { + let source = InstallationTokenSource::for_origin( + &GitHubCredentials::Pat("ghp_pat".to_string()), + "https://github.com/owner/repo.git", + serde_json::json!({ "contents": "write" }), + ) + .unwrap(); + + let resolved = source.resolve().await.unwrap(); + assert_eq!(resolved.token.expose(), "ghp_pat"); + assert_eq!(resolved.snapshot.generation, 0); + assert!(resolved.snapshot.is_static()); + assert!(!source.mints_installation_tokens()); + } + + #[tokio::test] + async fn static_installation_token_resolves_until_expiry() { + let valid = InstallationTokenSource::for_origin( + &GitHubCredentials::Installation(InstallationToken { + token: "ghs_static".to_string(), + expires_at: Utc::now() + chrono::Duration::minutes(30), + }), + "https://github.com/owner/repo.git", + serde_json::json!({}), + ) + .unwrap(); + let resolved = valid.resolve().await.unwrap(); + assert_eq!(resolved.token.expose(), "ghs_static"); + assert!(resolved.snapshot.is_static()); + + let expired = InstallationTokenSource::for_origin( + &GitHubCredentials::Installation(InstallationToken { + token: "ghs_expired".to_string(), + expires_at: Utc::now() - chrono::Duration::seconds(1), + }), + "https://github.com/owner/repo.git", + serde_json::json!({}), + ) + .unwrap(); + assert!(expired.resolve().await.is_err()); + } + + #[tokio::test] + async fn resolve_reuses_cached_token_before_the_margin() { + let (source, minter) = mintable(vec![MintAction::Token( + "ghs_gen1", + Utc::now() + chrono::Duration::minutes(30), + )]); + + let first = source.resolve().await.unwrap(); + let second = source.resolve().await.unwrap(); + + assert_eq!(minter.calls(), 1); + assert_eq!(first.snapshot.generation, 1); + assert_eq!(second.snapshot.generation, 1); + assert!(matches!( + first.snapshot.provenance, + TokenProvenance::Minted { .. } + )); + assert!(matches!( + second.snapshot.provenance, + TokenProvenance::Reused { .. } + )); + assert_eq!(second.token.expose(), "ghs_gen1"); + } + + #[tokio::test] + async fn resolve_mints_a_new_generation_inside_the_margin() { + let (source, minter) = mintable(vec![ + // Expires inside REFRESH_MARGIN, so the second resolve re-mints. + MintAction::Token("ghs_gen1", Utc::now() + chrono::Duration::minutes(5)), + MintAction::Token("ghs_gen2", Utc::now() + chrono::Duration::minutes(60)), + ]); + + let first = source.resolve().await.unwrap(); + let second = source.resolve().await.unwrap(); + + assert_eq!(minter.calls(), 2); + assert_eq!(first.snapshot.generation, 1); + assert_eq!(second.snapshot.generation, 2); + assert!(matches!( + second.snapshot.provenance, + TokenProvenance::Minted { .. } + )); + assert_eq!(second.token.expose(), "ghs_gen2"); + } + + #[tokio::test] + async fn concurrent_resolves_share_one_generation() { + // Single mint in the script: a second mint would panic on an empty + // script, so success proves single-flight. + let (source, minter) = mintable(vec![MintAction::Token( + "ghs_gen1", + Utc::now() + chrono::Duration::minutes(60), + )]); + + let handles: Vec<_> = (0..8) + .map(|_| { + let source = Arc::clone(&source); + tokio::spawn(async move { source.resolve().await }) + }) + .collect(); + + for handle in handles { + let resolved = handle.await.unwrap().unwrap(); + assert_eq!(resolved.snapshot.generation, 1); + assert_eq!(resolved.token.expose(), "ghs_gen1"); + } + assert_eq!(minter.calls(), 1); + } + + #[tokio::test] + async fn mint_for_clone_always_mints_and_seeds_the_cache() { + let (source, minter) = mintable(vec![MintAction::Token( + "ghs_clone", + Utc::now() + chrono::Duration::minutes(60), + )]); + + let clone_token = source.mint_for_clone().await.unwrap(); + assert_eq!(clone_token.snapshot.generation, 1); + assert!(matches!( + clone_token.snapshot.provenance, + TokenProvenance::Minted { .. } + )); + + // A later resolve reuses the clone token instead of minting again. + let refreshed = source.resolve().await.unwrap(); + assert_eq!(refreshed.snapshot.generation, 1); + assert_eq!(refreshed.token.expose(), "ghs_clone"); + assert!(matches!( + refreshed.snapshot.provenance, + TokenProvenance::Reused { .. } + )); + assert_eq!(minter.calls(), 1); + } + + #[tokio::test] + async fn mint_failure_surfaces_with_context() { + let (source, _minter) = mintable(vec![MintAction::Error("mint failed")]); + + let err = format!("{:#}", source.resolve().await.unwrap_err()); + assert!(err.contains("mint failed"), "got: {err}"); + assert!( + err.contains("minting GitHub installation access token"), + "got: {err}" + ); + } + + #[test] + fn secret_string_debug_never_prints_the_secret() { + let secret = SecretString::new("ghs_super_secret".to_string()); + let rendered = format!("{secret:?}"); + assert!(!rendered.contains("ghs_super_secret"), "{rendered}"); + } + + #[test] + fn snapshot_age_is_defined_only_for_minted_tokens() { + let now = Utc::now(); + let minted = TokenSnapshot { + generation: 3, + provenance: TokenProvenance::Minted { + minted_at: now - chrono::Duration::seconds(42), + expires_at: now + chrono::Duration::minutes(60), + }, + }; + assert_eq!(minted.age_at(now), Some(Duration::from_secs(42))); + + let fixed = TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + }; + assert_eq!(fixed.age_at(now), None); + assert_eq!(fixed.expires_at(), None); + } +} diff --git a/lib/components/fabro-sandbox/Cargo.toml b/lib/components/fabro-sandbox/Cargo.toml index 908d010e6..7153e3ea1 100644 --- a/lib/components/fabro-sandbox/Cargo.toml +++ b/lib/components/fabro-sandbox/Cargo.toml @@ -9,8 +9,8 @@ description = "Sandbox trait and implementations for Fabro agent execution envir [features] default = ["local"] local = [] -docker = ["dep:bollard", "dep:tar", "dep:fabro-github"] -daytona = ["dep:daytona-sdk", "dep:daytona-api-client", "dep:git2", "dep:fabro-github", "dep:fabro-config", "dep:fabro-http", "dep:reqwest-middleware", "dep:rand", "dep:tokio-tungstenite", "dep:futures-util", "dep:rustls"] +docker = ["dep:bollard", "dep:tar"] +daytona = ["dep:daytona-sdk", "dep:daytona-api-client", "dep:git2", "dep:fabro-config", "dep:fabro-http", "dep:reqwest-middleware", "dep:rand", "dep:tokio-tungstenite", "dep:futures-util", "dep:rustls"] test-support = [] [lib] @@ -47,7 +47,7 @@ tar = { workspace = true, optional = true } # daytona fabro-config = { path = "../../foundation/fabro-config", optional = true } -fabro-github = { path = "../fabro-github", optional = true } +fabro-github = { path = "../fabro-github" } fabro-types = { path = "../../foundation/fabro-types" } chrono = { workspace = true } diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 6371f23d2..79658120e 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -15,6 +15,7 @@ use daytona_sdk::api_types::SignedPortPreviewUrl; use daytona_sdk::toolbox_types::Command as SessionCommandResult; use daytona_sdk::{DaytonaError, SessionCommandLogsResult}; use fabro_github::GitHubCredentials; +use fabro_github::token_source::InstallationTokenSource; use fabro_static::EnvVars; use fabro_types::{CommandOutputStream, CommandTermination, RunId, SandboxProviderKind}; use fabro_util::time::elapsed_ms; @@ -27,6 +28,7 @@ use tokio_util::sync::CancellationToken; use crate::clone_retry::{self, CloneRetryReason}; use crate::clone_source::{self, CloneDecision, EmptyWorkspaceReason}; +use crate::push_credentials::{self, PushCredentialState}; use crate::redact::redact_auth_url; use crate::sandbox::{ self, BASH_ENV_VAR, BASH_PROBE_MARKER, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, REMOTE_BASH, @@ -335,6 +337,7 @@ pub struct DaytonaSandbox { client: daytona_sdk::Client, api_key: Option, github_app: Option, + push_credentials: PushCredentialState, sandbox: OnceCell, snapshot_name: OnceCell, rg_available: OnceCell, @@ -368,11 +371,16 @@ impl DaytonaSandbox { let client = build_daytona_client(api_key.clone()) .await .map_err(|e| crate::Error::context("Failed to create Daytona client", e))?; + let push_credentials = PushCredentialState::new(push_credentials::build_token_source( + github_app.as_ref(), + clone_origin_url.as_deref(), + )?); Ok(Self { config, client, api_key, github_app, + push_credentials, sandbox: OnceCell::new(), snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), @@ -425,6 +433,7 @@ impl DaytonaSandbox { client, api_key, github_app: None, + push_credentials: PushCredentialState::new(None), sandbox: sandbox_cell, snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), @@ -1052,27 +1061,18 @@ impl Sandbox for DaytonaSandbox { let layout = clone_source::github_repo_layout(&origin_url, WORKING_DIRECTORY, REPOS_ROOT) .map_err(|err| self.fail_init(init_start, err))?; - let token_was_freshly_minted = self - .github_app - .as_ref() - .is_some_and(GitHubCredentials::mints_installation_token); self.emit(SandboxEvent::GitCloneStarted { url: origin_url.clone(), branch: branch.clone(), }); let clone_start = Instant::now(); - let (username, password) = match &self.github_app { - Some(creds) => fabro_github::resolve_clone_credentials( - &fabro_github::GitHubContext::new( - creds, - &fabro_github::github_api_base_url(), - ), - &layout.owner, - &layout.repo, - ) - .await - .map_err(|e| { + // The clone mints its own token (never a warm-cache reuse) and + // seeds the shared source, so the first refresh compares + // against the clone token instead of believing nothing was + // ever embedded. + let resolved_token = match self.push_credentials.source() { + Some(source) => Some(source.mint_for_clone().await.map_err(|e| { let err = crate::Error::message(format!( "Failed to get GitHub App credentials for clone: {e}" )); @@ -1082,7 +1082,17 @@ impl Sandbox for DaytonaSandbox { causes: err.causes(), }); self.fail_init(init_start, err) - })?, + })?), + None => None, + }; + let token_was_freshly_minted = resolved_token + .as_ref() + .is_some_and(|token| !token.snapshot.is_static()); + let (username, password) = match &resolved_token { + Some(token) => ( + Some("x-access-token".to_string()), + Some(token.token.expose().to_string()), + ), None => (None, None), }; @@ -1230,8 +1240,11 @@ impl Sandbox for DaytonaSandbox { let _ = self.origin_url.set(origin_url.clone()); self.set_working_directory(layout.execution_directory.clone()) .map_err(|err| self.fail_init(init_start, err))?; - if let Some(token) = password.as_deref() { - match fabro_github::embed_token_in_url(&origin_url, token) { + if let Some(resolved) = resolved_token { + match fabro_github::embed_token_in_url( + &origin_url, + resolved.token.expose(), + ) { Ok(auth_url) => { let cmd = format!( "git -c maintenance.auto=0 remote set-url origin {}", @@ -1264,7 +1277,12 @@ impl Sandbox for DaytonaSandbox { sandbox will fail" ); } - Ok(_) => {} + Ok(_) => { + // Origin now carries this token; + // record it so refreshes compare + // against the clone generation. + self.push_credentials.record_embedded(resolved).await; + } Err(_) => { tracing::warn!( error_class = "daytona_set_url_exec_failed", @@ -1522,50 +1540,38 @@ impl Sandbox for DaytonaSandbox { async fn refresh_push_credentials(&self) -> crate::Result { if !self.repo_cloned() { - return Ok(RefreshOutcome::Skipped); + return Ok(RefreshOutcome::none()); } let Some(origin_url) = self.origin_url.get() else { - return Ok(RefreshOutcome::Skipped); // no authenticated origin — nothing to refresh + return Ok(RefreshOutcome::none()); // no authenticated origin — nothing to refresh }; - let Some(creds) = &self.github_app else { - return Ok(RefreshOutcome::Skipped); - }; - // Only a GitHub App installation token can be re-minted; a static PAT or - // a pre-minted Installation token is fixed, so re-embedding it changes - // nothing. Short-circuit to Skipped before the resolve + set-url exec. - if !creds.mints_installation_token() { - return Ok(RefreshOutcome::Skipped); - } - - let auth_url = fabro_github::resolve_authenticated_url( - &fabro_github::GitHubContext::new(creds, &fabro_github::github_api_base_url()), - origin_url, - ) - .await - .map_err(|_| { - crate::Error::message("Failed to refresh push credentials: token_mint_failed") - })?; - - let cmd = format!( - "git -c maintenance.auto=0 remote set-url origin {}", - shell_quote(auth_url.as_raw_url().as_str()), - ); - let result = self - .exec_command(&cmd, 10_000, None, None, None) + self.push_credentials + .refresh(origin_url, |auth_url| async move { + let cmd = format!( + "git -c maintenance.auto=0 remote set-url origin {}", + shell_quote(auth_url.as_raw_url().as_str()), + ); + let result = self + .exec_command(&cmd, 10_000, None, None, None) + .await + .map_err(|_| { + crate::Error::message( + "Failed to refresh push credentials: set_url_exec_failed", + ) + })?; + if !result.is_success() { + return Err(result.into_exec_error_with_redactor( + "git remote set-url origin (refresh push credentials)", + |s| redact_auth_url(s, Some(&auth_url)), + )); + } + Ok(()) + }) .await - .map_err(|_| { - crate::Error::message("Failed to refresh push credentials: set_url_exec_failed") - })?; - if !result.is_success() { - return Err(result.into_exec_error_with_redactor( - "git remote set-url origin (refresh push credentials)", - |s| redact_auth_url(s, Some(&auth_url)), - )); - } + } - // Static creds were short-circuited to Skipped above; reaching here means - // a GitHub App installation token was freshly minted. - Ok(RefreshOutcome::Refreshed) + fn push_token_source(&self) -> Option> { + self.push_credentials.source().cloned() } async fn set_autostop_interval(&self, minutes: i32) -> crate::Result<()> { @@ -2757,6 +2763,7 @@ mod tests { client, api_key: Some(api_key.to_string()), github_app: None, + push_credentials: PushCredentialState::new(None), sandbox: OnceCell::new(), snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index d69dcca4a..532e7cbb2 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -17,6 +17,7 @@ use bollard::exec::{CreateExecOptions, StartExecOptions, StartExecResults}; use bollard::image::CreateImageOptions; use bollard::models::{ContainerInspectResponse, HostConfig}; use fabro_github::GitHubCredentials; +use fabro_github::token_source::InstallationTokenSource; use fabro_types::{CommandOutputStream, CommandTermination, RunId, SandboxProviderKind}; use fabro_util::time::elapsed_ms; use futures::StreamExt; @@ -27,6 +28,7 @@ use tokio_util::sync::CancellationToken; use crate::clone_source::{self, CloneDecision, EmptyWorkspaceReason}; use crate::managed_labels::{self, MANAGED_LABEL, RUN_ID_LABEL}; +use crate::push_credentials::{self, PushCredentialState}; use crate::redact::redact_auth_url; use crate::sandbox::{ self, BASH_ENV_VAR, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, REMOTE_BASH, @@ -132,6 +134,7 @@ pub struct DockerSandbox { docker: Docker, config: DockerSandboxOptions, github_app: Option, + push_credentials: PushCredentialState, run_id: Option, clone_origin_url: Option, clone_branch: Option, @@ -167,14 +170,14 @@ impl DockerSandbox { clone_branch: Option, ) -> crate::Result { let docker = Docker::connect_with_local_defaults().map_err(crate::Error::docker_connect)?; - Ok(Self::with_docker_client( + Self::with_docker_client( docker, config, github_app, run_id, clone_origin_url, clone_branch, - )) + ) } fn with_docker_client( @@ -184,11 +187,16 @@ impl DockerSandbox { run_id: Option, clone_origin_url: Option, clone_branch: Option, - ) -> Self { - Self { + ) -> crate::Result { + let push_credentials = PushCredentialState::new(push_credentials::build_token_source( + github_app.as_ref(), + clone_origin_url.as_deref(), + )?); + Ok(Self { docker, config, github_app, + push_credentials, run_id, clone_origin_url, clone_branch, @@ -200,7 +208,7 @@ impl DockerSandbox { cached_os_version: std::sync::OnceLock::new(), rg_available: OnceCell::const_new(), event_callback: None, - } + }) } pub async fn reconnect( @@ -741,23 +749,30 @@ impl DockerSandbox { ) -> crate::Result<()> { self.verify_git_available().await?; let layout = clone_source::github_repo_layout(&origin_url, WORKING_DIRECTORY, REPOS_ROOT)?; - let token_was_freshly_minted = self - .github_app + // The clone mints its own token (never a warm-cache reuse) and seeds + // the shared source, so the first refresh compares against the clone + // token instead of believing nothing was ever embedded. + let resolved_token = match self.push_credentials.source() { + Some(source) => Some(source.mint_for_clone().await.map_err(|e| { + crate::Error::message(format!( + "Failed to get GitHub App credentials for clone: {e}" + )) + })?), + None => None, + }; + let token_was_freshly_minted = resolved_token .as_ref() - .is_some_and(GitHubCredentials::mints_installation_token); + .is_some_and(|token| !token.snapshot.is_static()); - let auth_url = match &self.github_app { - Some(creds) => Some( - fabro_github::resolve_authenticated_url( - &fabro_github::GitHubContext::new(creds, &fabro_github::github_api_base_url()), - &origin_url, - ) - .await - .map_err(|e| { - crate::Error::message(format!( - "Failed to get GitHub App credentials for clone: {e}" - )) - })?, + let auth_url = match &resolved_token { + Some(token) => Some( + fabro_github::embed_token_in_url(&origin_url, token.token.expose()).map_err( + |e| { + crate::Error::message(format!( + "Failed to get GitHub App credentials for clone: {e}" + )) + }, + )?, ), None => None, }; @@ -862,6 +877,11 @@ impl DockerSandbox { let _ = self.repo_cloned.set(true); let _ = self.origin_url.set(origin_url.clone()); self.set_working_directory(layout.execution_directory.clone())?; + if let Some(token) = resolved_token { + // The clone URL embedded this token in `origin`; record it so + // refreshes compare against the clone generation. + self.push_credentials.record_embedded(token).await; + } if let Some(auth_url) = auth_url.as_ref() { let command = format!( @@ -2182,47 +2202,33 @@ impl Sandbox for DockerSandbox { async fn refresh_push_credentials(&self) -> crate::Result { if !self.repo_cloned() { - return Ok(RefreshOutcome::Skipped); + return Ok(RefreshOutcome::none()); } let Some(origin_url) = self.origin_url.get() else { - return Ok(RefreshOutcome::Skipped); + return Ok(RefreshOutcome::none()); }; - let Some(creds) = &self.github_app else { - return Ok(RefreshOutcome::Skipped); - }; - // Only a GitHub App installation token can be re-minted; a static PAT or - // a pre-minted Installation token is fixed, so re-embedding it changes - // nothing. Short-circuit to Skipped before the resolve + set-url exec. - if !creds.mints_installation_token() { - return Ok(RefreshOutcome::Skipped); - } + self.push_credentials + .refresh(origin_url, |auth_url| async move { + let command = format!( + "git -c maintenance.auto=0 remote set-url origin {}", + shell_quote(auth_url.as_raw_url().as_str()) + ); + let result = self + .docker_exec_shell(&command, 10_000, Some(self.working_directory()), None, None) + .await?; + if !result.is_success() { + return Err(result.into_exec_error_with_redactor( + "git remote set-url origin (refresh push credentials)", + |s| redact_auth_url(s, Some(&auth_url)), + )); + } + Ok(()) + }) + .await + } - let auth_url = fabro_github::resolve_authenticated_url( - &fabro_github::GitHubContext::new(creds, &fabro_github::github_api_base_url()), - origin_url, - ) - .await - .map_err(|_| { - crate::Error::message("Failed to refresh push credentials: token_mint_failed") - })?; - - let command = format!( - "git -c maintenance.auto=0 remote set-url origin {}", - shell_quote(auth_url.as_raw_url().as_str()) - ); - let result = self - .docker_exec_shell(&command, 10_000, Some(self.working_directory()), None, None) - .await?; - if !result.is_success() { - return Err(result.into_exec_error_with_redactor( - "git remote set-url origin (refresh push credentials)", - |s| redact_auth_url(s, Some(&auth_url)), - )); - } - - // Static creds were short-circuited to Skipped above; reaching here means - // a GitHub App installation token was freshly minted. - Ok(RefreshOutcome::Refreshed) + fn push_token_source(&self) -> Option> { + self.push_credentials.source().cloned() } } @@ -2716,7 +2722,8 @@ mod tests { None, None, None, - ); + ) + .expect("test sandbox should build"); sandbox .container_id .set(container_id.to_string()) diff --git a/lib/components/fabro-sandbox/src/lib.rs b/lib/components/fabro-sandbox/src/lib.rs index 3a5e91ab2..f4418ae1c 100644 --- a/lib/components/fabro-sandbox/src/lib.rs +++ b/lib/components/fabro-sandbox/src/lib.rs @@ -15,6 +15,9 @@ mod clone_retry; #[cfg(any(feature = "docker", feature = "daytona", test))] mod managed_labels; +#[cfg(any(feature = "docker", feature = "daytona", test))] +mod push_credentials; + #[cfg(any(feature = "docker", feature = "daytona", test))] pub mod redact; @@ -39,6 +42,9 @@ pub use details::sandbox_details; #[cfg(feature = "docker")] pub use docker::{DockerSandbox, DockerSandboxOptions}; pub use error::{Error, Result, default_redacted_output_tail, display_for_log}; +pub use fabro_github::token_source::{ + InstallationTokenSource, ResolvedToken, TokenProvenance, TokenSnapshot, +}; pub use fabro_types::{RunSandboxInstance, SandboxProviderKind}; pub use local::LocalSandbox; #[cfg(feature = "daytona")] @@ -53,9 +59,10 @@ pub use reconnect::{reconnect, reconnect_for_run, reconnect_for_run_with_callbac pub use sandbox::{ CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, GitRunInfo, GitSetupIntent, GrepOptions, - RefreshOutcome, Sandbox, SandboxEvent, SandboxEventCallback, SandboxFile, StderrCollector, - StdioProcess, StdioProcessHandle, StdioProcessTermination, WalkOptions, format_lines_numbered, - git_push_via_exec, redacted_output_tail, setup_git_via_exec, shell_quote, + RefreshOutcome, RemoteCredentialAction, Sandbox, SandboxEvent, SandboxEventCallback, + SandboxFile, StderrCollector, StdioProcess, StdioProcessHandle, StdioProcessTermination, + WalkOptions, format_lines_numbered, git_push_via_exec, redacted_output_tail, + setup_git_via_exec, shell_quote, }; pub use sandbox_spec::SandboxSpec; pub use terminal::{TerminalSession, TerminalSize, open_terminal_for_run}; diff --git a/lib/components/fabro-sandbox/src/push_credentials.rs b/lib/components/fabro-sandbox/src/push_credentials.rs new file mode 100644 index 000000000..90116317f --- /dev/null +++ b/lib/components/fabro-sandbox/src/push_credentials.rs @@ -0,0 +1,350 @@ +//! Shared push-credential state for clone-based sandbox providers. +//! +//! Docker and Daytona embed GitHub credentials into the cloned repository's +//! `origin` remote and refresh them before pushes. Both providers hold this +//! state so the compare → `set-url` → record sequence, the generation +//! tracking, and the refresh-error logging behave identically across +//! providers. The token cache itself sits below the providers, in +//! [`fabro_github::token_source::InstallationTokenSource`]. + +use std::future::Future; +use std::sync::Arc; + +use fabro_github::GitHubCredentials; +use fabro_github::token_source::{InstallationTokenSource, ResolvedToken}; +use fabro_redact::DisplaySafeUrl; +use tokio::sync::Mutex; + +use crate::sandbox::{RefreshOutcome, RemoteCredentialAction}; + +/// Build the shared installation-token source for a clone-based sandbox. +/// +/// Returns `None` when there are no managed credentials or no GitHub origin +/// to scope them to. Minted tokens carry the same `contents: write` +/// permission the clone token uses. +pub(crate) fn build_token_source( + github_app: Option<&GitHubCredentials>, + clone_origin_url: Option<&str>, +) -> crate::Result>> { + let Some(creds) = github_app else { + return Ok(None); + }; + let Some(origin_url) = clone_origin_url.filter(|url| !url.trim().is_empty()) else { + return Ok(None); + }; + let normalized = fabro_github::normalize_repo_origin_url(origin_url); + if fabro_github::parse_github_owner_repo(&normalized).is_err() { + // Non-GitHub origins never clone in these providers, so there is no + // remote to keep credentials fresh for. + return Ok(None); + } + InstallationTokenSource::for_origin( + creds, + &normalized, + serde_json::json!({ "contents": "write" }), + ) + .map(Some) + .map_err(|err| crate::Error::message(format!("Failed to build GitHub token source: {err:#}"))) +} + +/// Push-credential state one provider instance tracks for its `origin` +/// remote. +pub(crate) struct PushCredentialState { + source: Option>, + /// Serializes compare → `set-url` → record. The token source's + /// single-flight ends before the sandbox exec, so without this lock a + /// refresh-ahead tick and a push could both see the old embedded + /// generation and race on `.git/config.lock`. Holds the last + /// successfully embedded token: its secret is already in the remote URL + /// inside the sandbox, so retaining it adds no exposure, and it is what + /// a push falls back to when a refresh fails. The tracked value is local + /// belief, not ground truth — agent code inside the sandbox can rewrite + /// `origin`. + embedded: Mutex>, +} + +impl PushCredentialState { + pub(crate) fn new(source: Option>) -> Self { + Self { + source, + embedded: Mutex::new(None), + } + } + + pub(crate) fn source(&self) -> Option<&Arc> { + self.source.as_ref() + } + + /// Record the token embedded in `origin` outside the refresh path — the + /// clone is the first operation to embed a token, and it seeds this + /// state so the first refresh compares against the clone token instead + /// of believing nothing was ever embedded. + pub(crate) async fn record_embedded(&self, token: ResolvedToken) { + *self.embedded.lock().await = Some(token); + } + + /// Refresh the credentials embedded in `origin`. + /// + /// Resolves through the shared source, skips the `set-url` exec when the + /// resolved generation is already embedded, and records the new + /// generation only after `set_url` succeeds. `set_url` receives the + /// authenticated URL to embed and runs under the embed lock. + pub(crate) async fn refresh( + &self, + origin_url: &str, + set_url: F, + ) -> crate::Result + where + F: FnOnce(DisplaySafeUrl) -> Fut, + Fut: Future>, + { + let Some(source) = &self.source else { + return Ok(RefreshOutcome::none()); + }; + let mut embedded = self.embedded.lock().await; + let resolved = match source.resolve().await { + Ok(resolved) => resolved, + Err(err) => { + // The refresh-error path is defined, not incidental: the push + // proceeds with the last embedded token, so log which one + // that is instead of losing the credential state. + if let Some(prev) = embedded.as_ref() { + tracing::warn!( + error = %format!("{err:#}"), + generation = prev.snapshot.generation, + provenance = %prev.snapshot.provenance, + token_age_ms = prev.snapshot.age_ms(), + "GitHub token refresh failed; origin keeps the last embedded credentials" + ); + } else { + tracing::warn!( + error = %format!("{err:#}"), + "GitHub token refresh failed and no credentials were ever embedded" + ); + } + return Err(crate::Error::message( + "Failed to refresh push credentials: token_mint_failed", + )); + } + }; + if embedded + .as_ref() + .is_some_and(|prev| prev.snapshot.generation == resolved.snapshot.generation) + { + return Ok(RefreshOutcome { + action: RemoteCredentialAction::Unchanged, + token: Some(resolved.snapshot), + }); + } + let auth_url = fabro_github::embed_token_in_url(origin_url, resolved.token.expose()) + .map_err(|err| { + crate::Error::message(format!("Failed to build authenticated origin URL: {err:#}")) + })?; + set_url(auth_url).await?; + let snapshot = resolved.snapshot; + *embedded = Some(resolved); + Ok(RefreshOutcome { + action: RemoteCredentialAction::Embedded, + token: Some(snapshot), + }) + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + + use chrono::Utc; + use fabro_github::InstallationToken; + use fabro_github::token_source::InstallationTokenMinter; + + use super::*; + + struct FixedMinter { + calls: AtomicUsize, + ttl: chrono::Duration, + } + + #[async_trait::async_trait] + impl InstallationTokenMinter for FixedMinter { + async fn mint(&self) -> anyhow::Result { + let call = self.calls.fetch_add(1, Ordering::SeqCst) + 1; + Ok(InstallationToken { + token: format!("ghs_gen{call}"), + expires_at: Utc::now() + self.ttl, + }) + } + } + + struct FailingMinter; + + #[async_trait::async_trait] + impl InstallationTokenMinter for FailingMinter { + async fn mint(&self) -> anyhow::Result { + Err(anyhow::anyhow!("mint failed")) + } + } + + fn minting_state(ttl: chrono::Duration) -> PushCredentialState { + PushCredentialState::new(Some(InstallationTokenSource::with_minter( + "owner/repo".to_string(), + Box::new(FixedMinter { + calls: AtomicUsize::new(0), + ttl, + }), + ))) + } + + const ORIGIN: &str = "https://github.com/owner/repo"; + + #[tokio::test] + async fn refresh_without_managed_credentials_reports_none() { + let state = PushCredentialState::new(None); + let outcome = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap(); + assert_eq!(outcome.action, RemoteCredentialAction::None); + assert_eq!(outcome.token, None); + } + + #[tokio::test] + async fn refresh_embeds_a_new_generation_and_skips_matching_ones() { + let state = minting_state(chrono::Duration::minutes(60)); + let set_url_calls = AtomicUsize::new(0); + + let first = state + .refresh(ORIGIN, |auth_url| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + assert!(auth_url.as_raw_url().as_str().contains("ghs_gen1")); + async { Ok(()) } + }) + .await + .unwrap(); + assert_eq!(first.action, RemoteCredentialAction::Embedded); + assert_eq!(first.token.unwrap().generation, 1); + + // The cached token is fresh, so the second refresh must skip set-url. + let second = state + .refresh(ORIGIN, |_| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + async { Ok(()) } + }) + .await + .unwrap(); + assert_eq!(second.action, RemoteCredentialAction::Unchanged); + assert_eq!(second.token.unwrap().generation, 1); + assert_eq!(set_url_calls.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn refresh_embeds_again_when_the_source_mints_a_new_generation() { + // Tokens expire inside the margin, so every resolve re-mints. + let state = minting_state(chrono::Duration::minutes(5)); + let set_url_calls = AtomicUsize::new(0); + + let first = state + .refresh(ORIGIN, |_| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + async { Ok(()) } + }) + .await + .unwrap(); + let second = state + .refresh(ORIGIN, |_| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + async { Ok(()) } + }) + .await + .unwrap(); + + assert_eq!(first.token.unwrap().generation, 1); + assert_eq!(second.action, RemoteCredentialAction::Embedded); + assert_eq!(second.token.unwrap().generation, 2); + assert_eq!(set_url_calls.load(Ordering::SeqCst), 2); + } + + #[tokio::test] + async fn clone_seed_makes_the_first_refresh_a_no_op() { + let state = minting_state(chrono::Duration::minutes(60)); + let clone_token = state.source().unwrap().mint_for_clone().await.unwrap(); + state.record_embedded(clone_token).await; + + let outcome = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap(); + assert_eq!(outcome.action, RemoteCredentialAction::Unchanged); + assert_eq!(outcome.token.unwrap().generation, 1); + } + + #[tokio::test] + async fn failed_set_url_does_not_record_the_new_generation() { + let state = minting_state(chrono::Duration::minutes(60)); + + let err = state + .refresh(ORIGIN, |_| async { + Err(crate::Error::message("set-url failed")) + }) + .await + .unwrap_err(); + assert!(err.to_string().contains("set-url failed")); + + // The generation was not recorded, so the retry embeds again instead + // of wrongly skipping. + let retried = state.refresh(ORIGIN, |_| async { Ok(()) }).await.unwrap(); + assert_eq!(retried.action, RemoteCredentialAction::Embedded); + assert_eq!(retried.token.unwrap().generation, 1); + } + + #[tokio::test] + async fn static_credentials_seeded_at_clone_skip_set_url() { + let source = InstallationTokenSource::for_origin( + &GitHubCredentials::Pat("ghp_pat".to_string()), + ORIGIN, + serde_json::json!({ "contents": "write" }), + ) + .unwrap(); + let state = PushCredentialState::new(Some(source)); + let clone_token = state.source().unwrap().mint_for_clone().await.unwrap(); + state.record_embedded(clone_token).await; + + let outcome = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap(); + assert_eq!(outcome.action, RemoteCredentialAction::Unchanged); + assert!(outcome.token.unwrap().is_static()); + } + + #[tokio::test] + async fn mint_failure_maps_to_the_token_mint_failed_error() { + let state = PushCredentialState::new(Some(InstallationTokenSource::with_minter( + "owner/repo".to_string(), + Box::new(FailingMinter), + ))); + + let err = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap_err(); + assert!(err.to_string().contains("token_mint_failed"), "{err}"); + } + + #[test] + fn token_source_requires_managed_credentials_and_a_github_origin() { + assert!(build_token_source(None, Some(ORIGIN)).unwrap().is_none()); + let pat = GitHubCredentials::Pat("ghp_pat".to_string()); + assert!(build_token_source(Some(&pat), None).unwrap().is_none()); + assert!( + build_token_source(Some(&pat), Some("https://gitlab.com/owner/repo")) + .unwrap() + .is_none() + ); + assert!( + build_token_source(Some(&pat), Some(ORIGIN)) + .unwrap() + .is_some() + ); + } +} diff --git a/lib/components/fabro-sandbox/src/sandbox.rs b/lib/components/fabro-sandbox/src/sandbox.rs index 6e6e2b336..fb8cb0102 100644 --- a/lib/components/fabro-sandbox/src/sandbox.rs +++ b/lib/components/fabro-sandbox/src/sandbox.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; +use fabro_github::token_source::{InstallationTokenSource, TokenSnapshot}; use fabro_types::{CommandOutputStream, CommandTermination}; use fabro_util::shell; use fabro_util::workspace_glob::WorkspaceGlob; @@ -280,6 +281,12 @@ macro_rules! delegate_sandbox { self.$field.refresh_push_credentials().await } + fn push_token_source( + &self, + ) -> Option> { + self.$field.push_token_source() + } + async fn set_autostop_interval(&self, minutes: i32) -> $crate::Result<()> { self.$field.set_autostop_interval(minutes).await } @@ -1013,16 +1020,42 @@ pub struct GrepOptions { pub max_results: Option, } -/// Outcome of [`Sandbox::refresh_push_credentials`]: whether a fresh token was -/// actually minted and applied to the origin remote, or the call was a no-op -/// (no clone, no authenticated origin, or no GitHub App credentials to rotate). -/// Lets callers log accurately instead of assuming every `Ok` re-minted. +/// What [`Sandbox::refresh_push_credentials`] did to the origin remote. +/// +/// Distinct from what the token *is* — the two are independent facts. A token +/// minted by another consumer and embedded here for the first time is an +/// `Embedded` action carrying a `Reused` provenance. +#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum RemoteCredentialAction { + /// `set-url` ran with a different generation than last embedded. + Embedded, + /// The resolved generation matched the last embedded one; `set-url` was + /// skipped. + Unchanged, + /// No managed credentials to embed (no clone, no authenticated origin, or + /// no GitHub credentials). + None, +} + +/// Outcome of [`Sandbox::refresh_push_credentials`]: what this call did to the +/// remote, and the non-secret description of the token embedded in it. +/// `token` is `None` only when `action` is [`RemoteCredentialAction::None`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RefreshOutcome { - /// A fresh token was minted and the origin remote URL was updated. - Refreshed, - /// Nothing to refresh (no clone / no origin / no managed credentials). - Skipped, +pub struct RefreshOutcome { + pub action: RemoteCredentialAction, + pub token: Option, +} + +impl RefreshOutcome { + /// No managed credentials to refresh. + #[must_use] + pub fn none() -> Self { + Self { + action: RemoteCredentialAction::None, + token: None, + } + } } #[async_trait] @@ -1232,11 +1265,22 @@ pub trait Sandbox: Send + Sync { } /// Refresh git push credentials (e.g. rotate an expiring GitHub App token). - /// Default is a no-op; Docker/Daytona override to update the remote URL - /// with a fresh token. Returns [`RefreshOutcome`] so callers can tell - /// an actual re-mint from a skipped no-op. + /// Default is a no-op; Docker/Daytona override to resolve a token through + /// the shared source and update the remote URL when the embedded + /// generation is stale. Returns [`RefreshOutcome`] so callers can tell + /// what happened to the remote and which token it carries. async fn refresh_push_credentials(&self) -> crate::Result { - Ok(RefreshOutcome::Skipped) + Ok(RefreshOutcome::none()) + } + + /// The shared installation-token source feeding this sandbox's push + /// credentials, when the provider manages GitHub credentials. + /// + /// Consumers outside the sandbox (e.g. the run-metadata writer) share + /// this source so every GitHub-token consumer for the origin repository + /// reuses one cached token instead of minting its own. + fn push_token_source(&self) -> Option> { + None } /// Set the auto-stop interval in minutes (0 to disable). @@ -1504,13 +1548,31 @@ pub(crate) async fn fetch_source_run_ref( /// Helper for sandbox implementations that manage git internally. /// Pushes a refspec to origin via exec_command inside the sandbox. pub async fn git_push_via_exec(sandbox: &dyn Sandbox, refspec: &str) -> crate::Result<()> { - if let Err(e) = sandbox.refresh_push_credentials().await { - tracing::warn!( - refspec = %refspec, - error = %crate::display_for_log(&e), - "Failed to refresh push credentials before git push" - ); - } + let token = match sandbox.refresh_push_credentials().await { + Ok(outcome) => { + if let Some(token) = outcome.token { + tracing::debug!( + refspec = %refspec, + action = %outcome.action, + generation = token.generation, + provenance = %token.provenance, + token_age_ms = token.age_ms(), + "Resolved push credentials before git push" + ); + } + outcome.token + } + Err(e) => { + // The provider logged which token stays embedded; the push + // proceeds with the old origin URL. + tracing::warn!( + refspec = %refspec, + error = %crate::display_for_log(&e), + "Failed to refresh push credentials before git push" + ); + None + } + }; let cmd = format!("{GIT} push origin {}", shell_quote(refspec)); let label = format!("git push origin {refspec}"); sandbox @@ -1518,7 +1580,12 @@ pub async fn git_push_via_exec(sandbox: &dyn Sandbox, refspec: &str) -> crate::R .await .map_err(|e| crate::Error::context(label.clone(), e))? .into_result(&label)?; - tracing::info!(refspec = %refspec, "Pushed git ref to origin"); + tracing::info!( + refspec = %refspec, + token_generation = token.map(|token| token.generation), + token_age_ms = token.and_then(|token| token.age_ms()), + "Pushed git ref to origin" + ); Ok(()) } diff --git a/lib/components/fabro-workflow/src/handler/llm/acp.rs b/lib/components/fabro-workflow/src/handler/llm/acp.rs index 10837272a..4edcf3e81 100644 --- a/lib/components/fabro-workflow/src/handler/llm/acp.rs +++ b/lib/components/fabro-workflow/src/handler/llm/acp.rs @@ -11,8 +11,10 @@ use fabro_acp::{ render_stop_reason, }; use fabro_agent::{ - AgentEvent, RefreshOutcome, Sandbox, StaticEnvProvider, SteeringItem, ToolEnvProvider, + AgentEvent, RefreshOutcome, RemoteCredentialAction, Sandbox, StaticEnvProvider, SteeringItem, + ToolEnvProvider, }; +use fabro_github::token_source::REFRESH_MARGIN; use fabro_graphviz::graph::Node; use fabro_static::EnvVars; use fabro_types::{ @@ -32,8 +34,13 @@ use crate::handler::NodeTimeoutPolicy; use crate::steering_hub::{ActiveControlHandle, SteeringHub}; /// Default refresh-ahead interval — comfortably under the ~60-min GitHub App -/// installation-token TTL. +/// installation-token TTL. Used as the loop cadence when a tick reports no +/// managed credentials; ticks that see a real token reschedule from its +/// expiry instead. const REFRESH_INTERVAL_DEFAULT: Duration = Duration::from_mins(45); +/// Floor for expiry-driven rescheduling, so a token already inside the cache +/// margin cannot pin the loop in a hot cycle. +const REFRESH_RESCHEDULE_FLOOR: Duration = Duration::from_secs(30); /// Upper bound on a single push-credential refresh (token mint + `git remote /// set-url` exec). The turn-entry refresh runs before the ACP process spawns /// and the ACP node uses `NodeTimeoutPolicy::HandlerManaged`, so without this @@ -100,12 +107,35 @@ fn push_cred_refresh_interval() -> Option { ) } -/// Background loop that re-mints the sandbox's push credentials every -/// `interval` for the duration of one ACP turn, so a single turn that outlives -/// the installation-token TTL still pushes with a fresh token. Bounded by -/// `cancel` (the drop-guard cancels it at turn end). A failed or timed-out tick -/// retries after a shorter delay so a transient error does not leave a -/// longer-than-interval window with an expired token. +/// Delay until the next refresh-ahead tick after a successful refresh. +/// +/// With a cached token source, a fixed interval is unsafe: a tick landing +/// just outside the cache margin returns a reused token, and a fixed +/// 45-minute sleep would leave the embedded token expired until the next +/// tick. Schedule from the token's own `expires_at` instead: wake when the +/// cache margin opens, so that tick re-mints. `None` disables the loop — +/// static credentials cannot be re-minted by waiting. +fn next_refresh_delay(outcome: &RefreshOutcome, fallback: Duration) -> Option { + let Some(token) = outcome.token else { + // No managed credentials to watch; keep the configured cadence in + // case a later tick sees them (e.g. after a reconnect). + return Some(fallback); + }; + let expires_at = token.expires_at()?; + let margin = chrono::Duration::from_std(REFRESH_MARGIN).unwrap_or(chrono::Duration::MAX); + let until_margin = ((expires_at - margin) - chrono::Utc::now()) + .to_std() + .unwrap_or(Duration::ZERO); + Some(until_margin.max(REFRESH_RESCHEDULE_FLOOR)) +} + +/// Background loop that keeps the sandbox's push credentials fresh for the +/// duration of one ACP turn, so a single turn that outlives the +/// installation-token TTL still pushes with a fresh token. Bounded by +/// `cancel` (the drop-guard cancels it at turn end). Each successful tick +/// reschedules from the embedded token's expiry ([`next_refresh_delay`]); a +/// failed or timed-out tick retries after a shorter delay so a transient +/// error does not leave a longer-than-interval window with an expired token. async fn refresh_ahead_loop( sandbox: Arc, cancel: CancellationToken, @@ -120,19 +150,34 @@ async fn refresh_ahead_loop( match timeout(REFRESH_MINT_TIMEOUT, sandbox.refresh_push_credentials()) .await { - Ok(Ok(RefreshOutcome::Refreshed)) => { - tracing::info!( - interval_secs = interval.as_secs(), - "refresh-ahead re-minted push credentials mid-turn" - ); - delay = interval; - } - Ok(Ok(RefreshOutcome::Skipped)) => { - tracing::debug!( - interval_secs = interval.as_secs(), - "refresh-ahead tick: no managed push credentials to refresh" - ); - delay = interval; + Ok(Ok(outcome)) => { + match outcome.action { + RemoteCredentialAction::Embedded => { + tracing::info!( + generation = outcome.token.map(|token| token.generation), + "refresh-ahead re-embedded push credentials mid-turn" + ); + } + RemoteCredentialAction::Unchanged => { + tracing::debug!( + generation = outcome.token.map(|token| token.generation), + "refresh-ahead tick: embedded push credentials still fresh" + ); + } + RemoteCredentialAction::None => { + tracing::debug!( + "refresh-ahead tick: no managed push credentials to refresh" + ); + } + } + if let Some(next) = next_refresh_delay(&outcome, interval) { + delay = next; + } else { + tracing::debug!( + "refresh-ahead loop stopped: static credentials cannot be re-minted" + ); + break; + } } Ok(Err(e)) => { tracing::warn!( @@ -271,38 +316,51 @@ impl AgentAcpBackend { // turn so the agent's own `git push` uses a live token instead of the one // baked into the clone at run start. // - // Part 2 (turn-entry): re-mint + rewrite the origin URL before the ACP - // process spawns, covering a push early in the turn. Non-fatal and - // timeout-bounded — a stalled mint must neither fail nor hang node entry. - // Part 3 (loop): a background task re-mints every ~45 min so a single turn - // that itself outlives the ~60-min installation-token TTL still pushes - // with a fresh token; a normal sub-interval turn never ticks (the - // drop-guard aborts the task at turn end before the first tick). + // Part 2 (turn-entry): resolve through the cached token source and + // rewrite the origin URL before the ACP process spawns, covering a push + // early in the turn. A fresh cached token makes this a no-op exec-wise. + // Non-fatal and timeout-bounded — a stalled mint must neither fail nor + // hang node entry. Part 3 (loop): a background task keeps the embedded + // token fresh so a single turn that outlives the ~60-min + // installation-token TTL still pushes with a fresh token; ticks + // reschedule from the embedded token's expiry, so a normal short turn + // never ticks (the drop-guard aborts the task at turn end). // // FABRO_PUSH_CRED_REFRESH_AHEAD=0 (or false/off/no/empty, case- - // insensitive) disables the WHOLE feature — turn-entry re-mint AND loop — + // insensitive) disables the WHOLE feature — turn-entry refresh AND loop — // so an operator who manages `origin` themselves can opt out of all // fabro-side origin rewriting. FABRO_PUSH_CRED_REFRESH_INTERVAL_SECONDS - // overrides the loop interval; 0 disables just the loop. + // overrides the loop cadence for ticks without token expiry info; 0 + // disables just the loop. // // Known limitations tracked as follow-ups (not addressed here): (a) // resumed/parked runs reconnect the sandbox with no GitHub App creds, so // refresh no-ops until those creds are threaded through the reconnect - // path; (b) the turn-entry re-mint has no freshness check, so it mints - // once per node entry even when the current token is still fresh; (c) the - // background `git remote set-url` can contend with the agent's own git on - // `.git/config.lock`; (d) parallel ACP branches each run their own loop; - // (e) this refresh lives in the ACP handler only, though the stale-origin - // problem is stage-type-agnostic (native/command stages that push are not - // covered); (f) refresh failures are logged via tracing but not surfaced - // as a RunNotice event on the run stream. + // path; (b) the background `git remote set-url` can contend with the + // agent's own git on `.git/config.lock` (skipped entirely while the + // cached generation is already embedded); (c) parallel ACP branches each + // run their own loop; (d) this refresh lives in the ACP handler only, + // though the stale-origin problem is stage-type-agnostic (native/command + // stages that push are not covered); (e) refresh failures are logged via + // tracing but not surfaced as a RunNotice event on the run stream. let refresh_enabled = push_cred_refresh_enabled(); if refresh_enabled { match timeout(REFRESH_MINT_TIMEOUT, sandbox.refresh_push_credentials()).await { - Ok(Ok(RefreshOutcome::Refreshed)) => { - tracing::debug!("refreshed sandbox push credentials at ACP turn entry"); - } - Ok(Ok(RefreshOutcome::Skipped)) => {} + Ok(Ok(outcome)) => match outcome.action { + RemoteCredentialAction::Embedded => { + tracing::debug!( + generation = outcome.token.map(|token| token.generation), + "refreshed sandbox push credentials at ACP turn entry" + ); + } + RemoteCredentialAction::Unchanged => { + tracing::debug!( + generation = outcome.token.map(|token| token.generation), + "sandbox push credentials already fresh at ACP turn entry" + ); + } + RemoteCredentialAction::None => {} + }, Ok(Err(e)) => { tracing::warn!( error = %fabro_sandbox::display_for_log(&e), @@ -611,14 +669,18 @@ mod tests { use fabro_acp::test_support::fake_acp_agent_script; use fabro_acp::{AcpError, AcpProcessExit}; - use fabro_agent::{LocalSandbox, RefreshOutcome, Sandbox, shell_quote}; + use fabro_agent::{ + LocalSandbox, RefreshOutcome, RemoteCredentialAction, Sandbox, TokenProvenance, + TokenSnapshot, shell_quote, + }; use fabro_graphviz::graph::{AttrValue, Node}; use fabro_sandbox::test_support::MockSandbox; use fabro_types::{CommandTermination, EventBody, ExecOutputTail}; use tokio_util::sync::CancellationToken; use super::{ - AgentAcpBackend, acp_error_to_workflow, parse_refresh_enabled, parse_refresh_interval, + AgentAcpBackend, REFRESH_RESCHEDULE_FLOOR, acp_error_to_workflow, next_refresh_delay, + parse_refresh_enabled, parse_refresh_interval, refresh_ahead_loop, }; use crate::context::Context; use crate::event::Emitter; @@ -671,17 +733,299 @@ mod tests { } #[tokio::test] - async fn refresh_reports_skipped_without_managed_credentials() { - // MockSandbox uses the trait default (no GitHub App creds), so refresh is - // a no-op that must report Skipped — the signal the refresh-ahead loop - // relies on to log at debug rather than falsely claim a re-mint. + async fn refresh_reports_no_action_without_managed_credentials() { + // MockSandbox uses the trait default (no GitHub App creds), so refresh + // is a no-op that must report no remote action and no token — the + // signal the refresh-ahead loop relies on to log at debug rather than + // falsely claim a re-embed. let sandbox = MockSandbox::linux(); assert_eq!( sandbox.refresh_push_credentials().await.unwrap(), - RefreshOutcome::Skipped + RefreshOutcome::none() ); } + fn minted_outcome( + action: RemoteCredentialAction, + generation: u64, + minted_ago: chrono::Duration, + expires_in: chrono::Duration, + reused: bool, + ) -> RefreshOutcome { + let now = chrono::Utc::now(); + let minted_at = now - minted_ago; + let expires_at = now + expires_in; + let provenance = if reused { + TokenProvenance::Reused { + minted_at, + expires_at, + } + } else { + TokenProvenance::Minted { + minted_at, + expires_at, + } + }; + RefreshOutcome { + action, + token: Some(TokenSnapshot { + generation, + provenance, + }), + } + } + + fn static_outcome() -> RefreshOutcome { + RefreshOutcome { + action: RemoteCredentialAction::Unchanged, + token: Some(TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + }), + } + } + + #[test] + fn next_refresh_delay_schedules_from_token_expiry_minus_margin() { + let outcome = minted_outcome( + RemoteCredentialAction::Embedded, + 1, + chrono::Duration::zero(), + chrono::Duration::minutes(60), + false, + ); + let delay = next_refresh_delay(&outcome, Duration::from_mins(45)).unwrap(); + // Expiry minus the 10-minute refresh margin: ~50 minutes out. + assert!(delay > Duration::from_mins(49), "{delay:?}"); + assert!(delay <= Duration::from_mins(50), "{delay:?}"); + } + + #[test] + fn next_refresh_delay_floors_when_the_margin_is_already_open() { + let outcome = minted_outcome( + RemoteCredentialAction::Unchanged, + 1, + chrono::Duration::minutes(55), + chrono::Duration::minutes(5), + true, + ); + assert_eq!( + next_refresh_delay(&outcome, Duration::from_mins(45)), + Some(REFRESH_RESCHEDULE_FLOOR) + ); + } + + #[test] + fn next_refresh_delay_disables_the_loop_for_static_credentials() { + assert_eq!( + next_refresh_delay(&static_outcome(), Duration::from_mins(45)), + None + ); + } + + #[test] + fn next_refresh_delay_keeps_the_cadence_without_managed_credentials() { + assert_eq!( + next_refresh_delay(&RefreshOutcome::none(), Duration::from_mins(45)), + Some(Duration::from_mins(45)) + ); + } + + /// Sandbox stub whose refresh outcomes are scripted, recording when each + /// refresh tick lands on the (paused) tokio clock. + struct ScriptedRefreshSandbox { + script: Mutex>, + ticks: Mutex>, + } + + impl ScriptedRefreshSandbox { + fn new(script: Vec) -> Arc { + Arc::new(Self { + script: Mutex::new(script.into()), + ticks: Mutex::new(Vec::new()), + }) + } + + fn ticks(&self) -> Vec { + self.ticks.lock().expect("ticks lock").clone() + } + } + + #[async_trait::async_trait] + impl Sandbox for ScriptedRefreshSandbox { + async fn refresh_push_credentials(&self) -> fabro_sandbox::Result { + self.ticks + .lock() + .expect("ticks lock") + .push(tokio::time::Instant::now()); + Ok(self + .script + .lock() + .expect("script lock") + .pop_front() + .expect("refresh script exhausted")) + } + + async fn read_file_bytes(&self, _path: &str) -> fabro_sandbox::Result> { + unimplemented!("refresh loop only calls refresh_push_credentials") + } + + async fn write_file(&self, _path: &str, _content: &str) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn delete_file(&self, _path: &str) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn file_exists(&self, _path: &str) -> fabro_sandbox::Result { + unimplemented!() + } + + async fn list_directory( + &self, + _path: &str, + _depth: Option, + ) -> fabro_sandbox::Result> { + unimplemented!() + } + + async fn exec_command( + &self, + _command: &str, + _timeout_ms: u64, + _working_dir: Option<&str>, + _env_vars: Option<&HashMap>, + _cancel_token: Option, + ) -> fabro_sandbox::Result { + unimplemented!() + } + + async fn grep( + &self, + _pattern: &str, + _path: &str, + _options: &fabro_sandbox::GrepOptions, + ) -> fabro_sandbox::Result> { + unimplemented!() + } + + async fn download_file_to_local( + &self, + _remote_path: &str, + _local_path: &std::path::Path, + ) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn upload_file_from_local( + &self, + _local_path: &std::path::Path, + _remote_path: &str, + ) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn initialize(&self) -> fabro_sandbox::Result<()> { + Ok(()) + } + + async fn cleanup(&self) -> fabro_sandbox::Result<()> { + Ok(()) + } + + fn working_directory(&self) -> &str { + "/workspace" + } + + fn platform(&self) -> &str { + "linux" + } + + fn os_version(&self) -> String { + "linux".to_string() + } + } + + /// Long-turn timeline: the clone/turn-entry mint happened at minute 0 with + /// a 60-minute TTL. The loop's first tick at minute 45 sees the cached + /// token reused with ~15 minutes left and must NOT sleep another fixed 45 + /// minutes (that would cross expiry at minute 60) — it reschedules for the + /// margin opening (~5 minutes out). That margin-crossing tick re-mints and + /// reschedules from the fresh token's expiry (~50 minutes out). + #[tokio::test(start_paused = true)] + async fn refresh_ahead_reschedules_from_token_expiry_across_a_long_turn() { + let interval = Duration::from_mins(45); + let sandbox = ScriptedRefreshSandbox::new(vec![ + // Minute 45: cache still fresh (expires minute 60, margin opens + // minute 50). + minted_outcome( + RemoteCredentialAction::Unchanged, + 1, + chrono::Duration::minutes(45), + chrono::Duration::minutes(15), + true, + ), + // Minute ~50: margin open → the source minted generation 2. + minted_outcome( + RemoteCredentialAction::Embedded, + 2, + chrono::Duration::zero(), + chrono::Duration::minutes(60), + false, + ), + // Minute ~100: generation 2 still fresh. + minted_outcome( + RemoteCredentialAction::Unchanged, + 2, + chrono::Duration::minutes(50), + chrono::Duration::minutes(10), + true, + ), + ]); + let cancel = CancellationToken::new(); + let start = tokio::time::Instant::now(); + let loop_task = tokio::spawn(refresh_ahead_loop( + Arc::clone(&sandbox) as Arc, + cancel.clone(), + interval, + )); + + while sandbox.ticks().len() < 3 { + tokio::time::sleep(Duration::from_secs(1)).await; + } + cancel.cancel(); + loop_task.await.expect("refresh loop should exit cleanly"); + + let ticks = sandbox.ticks(); + assert_eq!(ticks[0] - start, interval, "first tick uses the interval"); + // Reused token expiring in 15 minutes → next tick when the 10-minute + // margin opens, ~5 minutes later (never another fixed 45 minutes). + let second_gap = ticks[1] - ticks[0]; + assert!(second_gap <= Duration::from_mins(5), "{second_gap:?}"); + assert!(second_gap > Duration::from_mins(4), "{second_gap:?}"); + // Fresh 60-minute token → next tick ~50 minutes out. + let third_gap = ticks[2] - ticks[1]; + assert!(third_gap <= Duration::from_mins(50), "{third_gap:?}"); + assert!(third_gap > Duration::from_mins(49), "{third_gap:?}"); + } + + #[tokio::test(start_paused = true)] + async fn refresh_ahead_stops_by_itself_for_static_credentials() { + let sandbox = ScriptedRefreshSandbox::new(vec![static_outcome()]); + let cancel = CancellationToken::new(); + let loop_task = tokio::spawn(refresh_ahead_loop( + Arc::clone(&sandbox) as Arc, + cancel.clone(), + Duration::from_mins(45), + )); + + // The loop exits after the first tick without being cancelled: static + // credentials cannot be re-minted, so there is nothing to keep fresh. + loop_task.await.expect("refresh loop should stop by itself"); + assert_eq!(sandbox.ticks().len(), 1); + } + #[tokio::test] async fn acp_backend_run_sends_prompt_and_returns_text() { let tempdir = tempfile::tempdir().unwrap(); diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 5a7dd6069..2ccb3b6f2 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -600,20 +600,21 @@ pub async fn initialize( }); } - let metadata_writer = match build_metadata_writer(&options.run_options) { - Ok(writer) => writer, - Err(err) => { - let message = format!("failed to initialize checkpoint metadata writer: {err}"); - if metadata_runtime.mark_metadata_degraded() { - options.emitter.notice( - RunNoticeLevel::Warn, - RunNoticeCode::CheckpointMetadataWriteFailed, - message, - ); + let metadata_writer = + match build_metadata_writer(&options.run_options, sandbox.push_token_source()) { + Ok(writer) => writer, + Err(err) => { + let message = format!("failed to initialize checkpoint metadata writer: {err}"); + if metadata_runtime.mark_metadata_degraded() { + options.emitter.notice( + RunNoticeLevel::Warn, + RunNoticeCode::CheckpointMetadataWriteFailed, + message, + ); + } + None } - None - } - }; + }; let run_services = RunServices::new( options.run_store.clone(), diff --git a/lib/components/fabro-workflow/src/run_metadata.rs b/lib/components/fabro-workflow/src/run_metadata.rs index 74be989df..9fa002b57 100644 --- a/lib/components/fabro-workflow/src/run_metadata.rs +++ b/lib/components/fabro-workflow/src/run_metadata.rs @@ -1,12 +1,11 @@ -use std::collections::HashMap; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, LazyLock, Mutex}; +use std::sync::{Arc, Mutex}; -use anyhow::Context as _; use async_trait::async_trait; use fabro_checkpoint::git::{FileMode, Store, TreeEntries}; use fabro_dump::RunDump; +use fabro_github::token_source::InstallationTokenSource; use git2::{ Cred, Direction, ErrorClass, ErrorCode, FetchOptions, Oid, PushOptions, RemoteCallbacks, Repository, Signature, @@ -16,13 +15,6 @@ use tokio::task::{self, JoinError}; use crate::git::{GitAuthor, META_BRANCH_PREFIX}; use crate::run_options::RunOptions; -static METADATA_PERMISSIONS: LazyLock> = LazyLock::new(|| { - [("contents", "write")] - .into_iter() - .map(|(key, value)| (key.to_string(), value.to_string())) - .collect() -}); - pub(crate) fn metadata_branch_name(run_id: &str) -> String { format!("{META_BRANCH_PREFIX}{run_id}") } @@ -92,22 +84,22 @@ pub(crate) trait AuthProvider: Send + Sync { } struct GitHubAuthProvider { - creds: fabro_github::GitHubCredentials, - origin_url: String, + source: Arc, } impl GitHubAuthProvider { - fn new(creds: fabro_github::GitHubCredentials, origin_url: String) -> Self { - Self { creds, origin_url } + fn new(source: Arc) -> Self { + Self { source } } } #[async_trait] impl AuthProvider for GitHubAuthProvider { async fn token(&self) -> Result, RunMetadataError> { - mint_token(&self.creds, &self.origin_url, &METADATA_PERMISSIONS) + self.source + .resolve() .await - .map(Some) + .map(|resolved| Some(resolved.token.expose().to_owned())) .map_err(RunMetadataError::TokenMint) } } @@ -207,6 +199,7 @@ impl RunMetadataWriterHandle { pub(crate) fn build_metadata_writer( run_options: &RunOptions, + token_source: Option>, ) -> Result, RunMetadataError> { if !run_options.settings.run.meta_branch.enabled { return Ok(None); @@ -233,10 +226,20 @@ pub(crate) fn build_metadata_writer( return Ok(None); } - let auth = Arc::new(GitHubAuthProvider::new( - creds.clone(), - normalized_url.clone(), - )); + // Share the sandbox's token source so the metadata writer reuses the + // same cached token as every other consumer for this origin. Resumed + // runs reconnect the sandbox without one; they build their own cached + // source from the run's credentials. + let source = match token_source { + Some(source) => source, + None => InstallationTokenSource::for_origin( + creds, + &normalized_url, + serde_json::json!({ "contents": "write" }), + ) + .map_err(RunMetadataError::TokenMint)?, + }; + let auth = Arc::new(GitHubAuthProvider::new(source)); let writer = RunMetadataWriter::new( normalized_url, meta_branch.clone(), @@ -247,28 +250,6 @@ pub(crate) fn build_metadata_writer( Ok(Some(RunMetadataWriterHandle::new(writer, auth))) } -pub(crate) async fn mint_token( - creds: &fabro_github::GitHubCredentials, - origin_url: &str, - permissions: &HashMap, -) -> anyhow::Result { - let normalized_url = fabro_github::normalize_repo_origin_url(origin_url); - let (owner, repo) = - fabro_github::parse_github_owner_repo(&normalized_url).context("parsing GitHub origin")?; - let client = fabro_http::http_client().map_err(anyhow::Error::new)?; - let permissions = - serde_json::to_value(permissions).context("serializing GitHub permissions")?; - creds - .resolve_bearer_token( - &client, - &owner, - &repo, - &fabro_github::github_api_base_url(), - permissions, - ) - .await -} - pub(crate) struct RunMetadataWriter { store: Store, tempdir: tempfile::TempDir, @@ -1010,16 +991,19 @@ mod tests { for (origin, expected) in cases { let options = run_options_for_origin(origin); - let handle = build_metadata_writer(&options).unwrap().unwrap(); + let handle = build_metadata_writer(&options, None).unwrap().unwrap(); assert_eq!(handle.remote_url_for_test(), expected); assert!(!handle.remote_url_for_test().contains("ghs_aaaaaa")); assert!(!handle.remote_url_for_test().contains('@')); } assert!( - build_metadata_writer(&run_options_for_origin("https://gitlab.com/owner/repo.git")) - .unwrap() - .is_none() + build_metadata_writer( + &run_options_for_origin("https://gitlab.com/owner/repo.git"), + None + ) + .unwrap() + .is_none() ); } @@ -1028,6 +1012,6 @@ mod tests { let mut options = run_options_for_origin("https://github.com/owner/repo.git"); options.settings.run.meta_branch.enabled = false; - assert!(build_metadata_writer(&options).unwrap().is_none()); + assert!(build_metadata_writer(&options, None).unwrap().is_none()); } } From 1688cd5b912297c08f638aeff6203d62e9b12945 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 10:16:10 -0400 Subject: [PATCH 43/63] Retry git pushes with a pinned token and record attempt history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 01M0DH033P2XSTHAGVBHG6922F completed 2.8 hours of work, then failed terminally because four consecutive publish pushes hit GitHub's token-replication lag (404 "Repository not found") — the push path had no retry, the failure was misclassified as deterministic, and the same fresh-mint-then-push pattern silently disabled metadata snapshots. This generalizes the clone retry machinery to pushes and makes attempt detail durable. - clone_retry -> git_retry: the classifier's boolean becomes a CredentialContext derived from the token snapshot (fresh App tokens retry 404s as replication lag, mature ones as transient infra, static credentials fail fast), and the attempt/backoff limits become a RetryPlan with layered optional bounds. Clone behavior is preserved: Docker keeps its absolute five-minute deadline, Daytona keeps no deadline. - Pushes take a scoped CredentialLease before the first attempt: it owns the embed mutex for the whole operation, pins the single successful resolve, retries only failed resolves, falls back to the last embedded token when a mint fails, and force-re-embeds the pinned token once after the first auth-shaped failure (drift repair). The margin invariant (REFRESH_MARGIN > every push plan's max_elapsed) guarantees the pinned token outlives the operation; a unit test asserts it. - Sandbox::git_push_ref now takes a RetryPlan and returns PushReport / PushError with per-attempt records (classification, redacted output tail, token generation/provenance/age, credential action, refresh errors). Checkpoint pushes use a 90-second budget; the terminal publish push gets 5 attempts over at most 4 minutes. - The single durable git.push event per push gains a nested attempts array (GitPushAttemptProps, token snapshot flattened to flat fields); stored events without it still deserialize. Publish push failures now carry an explicit failure category — exhausted transient retries stay transient_infra instead of deterministic — plus one bounded cause line per attempt and the last successful push time in the message. - Metadata snapshot degradation records why it degraded: push failures with retryable classifications leave the writer eligible to re-probe at each later checkpoint, and a successful snapshot clears the degraded state and re-arms the warning. Permanent failures keep today's latch. Plan: .ai/plans/git-push-token-resilience.md (PR 2: items 1, 2, 4, 7 and the metadata re-probe). Co-Authored-By: Claude Fable 5 --- .../fabro-sandbox/src/clone_retry.rs | 400 -------- .../fabro-sandbox/src/daytona/mod.rs | 84 +- lib/components/fabro-sandbox/src/docker.rs | 53 +- lib/components/fabro-sandbox/src/git_retry.rs | 743 +++++++++++++++ lib/components/fabro-sandbox/src/lib.rs | 15 +- lib/components/fabro-sandbox/src/local.rs | 23 +- .../fabro-sandbox/src/push_credentials.rs | 253 ++++- lib/components/fabro-sandbox/src/sandbox.rs | 874 +++++++++++++++++- lib/components/fabro-workflow/src/error.rs | 42 +- lib/components/fabro-workflow/src/event.rs | 2 +- .../fabro-workflow/src/event/convert.rs | 161 ++++ .../fabro-workflow/src/event/events.rs | 8 +- .../fabro-workflow/src/lifecycle/event.rs | 1 + .../fabro-workflow/src/lifecycle/git.rs | 85 +- .../fabro-workflow/src/pipeline/finalize.rs | 40 +- .../fabro-workflow/src/pipeline/initialize.rs | 2 +- .../fabro-workflow/src/pipeline/publish.rs | 231 ++++- .../fabro-workflow/src/run_metadata.rs | 128 ++- .../fabro-workflow/src/sandbox_git_runtime.rs | 23 +- .../fabro-types/src/run_event/misc.rs | 44 + .../fabro-types/src/run_event/mod.rs | 2 + 21 files changed, 2654 insertions(+), 560 deletions(-) delete mode 100644 lib/components/fabro-sandbox/src/clone_retry.rs create mode 100644 lib/components/fabro-sandbox/src/git_retry.rs diff --git a/lib/components/fabro-sandbox/src/clone_retry.rs b/lib/components/fabro-sandbox/src/clone_retry.rs deleted file mode 100644 index f0f3f0572..000000000 --- a/lib/components/fabro-sandbox/src/clone_retry.rs +++ /dev/null @@ -1,400 +0,0 @@ -//! Retry for the first repository clone in a clone-based sandbox. -//! -//! Clone-based providers can mint a GitHub App installation token and clone -//! with it immediately. GitHub can reject that first clone before the token is -//! available to the git endpoint. On a private repository, the rejection can -//! arrive as `Repository not found.` or an authentication failure. -//! -//! Only a token minted during the current clone operation makes those messages -//! safe to retry. Static PATs and pre-minted installation tokens fail fast. -//! -//! Retries reuse the same token on purpose. Replication of a given token only -//! makes progress, so each attempt strictly improves the odds, while re-minting -//! would restart the replication clock. - -use std::future::Future; -use std::time::Duration; - -use fabro_types::SandboxProviderKind; -use fabro_util::backoff::BackoffPolicy; -use tokio::time; - -/// Total clone attempts, including the first. -const MAX_ATTEMPTS: u32 = 3; - -/// Why a failed clone attempt is worth repeating. -#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] -#[strum(serialize_all = "snake_case")] -pub(crate) enum CloneRetryReason { - /// A freshly minted installation token has not reached the GitHub edge - /// cache site serving this clone yet. - TokenReplication, - /// The clone failed on infrastructure, unrelated to credentials. - TransientInfra, -} - -/// What a clone failure message tells us about retry safety. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum CloneMessageClass { - Retry(CloneRetryReason), - Permanent, - Unknown, -} - -impl CloneMessageClass { - pub(crate) fn retry_reason(self) -> Option { - match self { - Self::Retry(reason) => Some(reason), - Self::Permanent | Self::Unknown => None, - } - } -} - -/// Message fragments that mean the clone failed on infrastructure. -/// -/// These are safe to retry whether or not the clone was authenticated. -const TRANSIENT_HINTS: &[&str] = &[ - "could not resolve host", - "temporary failure in name resolution", - "connection refused", - "connection reset", - "connection timed out", - "timed out", - "network is unreachable", - "no route to host", - "tls handshake", - "early eof", - "rpc failed", - "unexpected disconnect", - "the remote end hung up unexpectedly", - "index-pack failed", - "service unavailable", - "gateway timeout", - "too many requests", - "rate limit", -]; - -/// Message fragments GitHub uses when a token is not yet visible. -/// -/// Only meaningful when the clone carried credentials. The same lag surfaces as -/// 404 or as an auth failure depending on which endpoint answers first. -const TOKEN_REPLICATION_HINTS: &[&str] = &[ - "repository not found", - "authentication failed", - "invalid username or password", - "bad credentials", -]; - -/// Classify a failed clone by its rendered message. -/// -/// `token_was_freshly_minted` gates the token-replication reading. A static -/// credential cannot become valid during backoff, so auth failures for it are -/// permanent. -pub(crate) fn classify_message(message: &str, token_was_freshly_minted: bool) -> CloneMessageClass { - let lower = message.to_ascii_lowercase(); - - if TRANSIENT_HINTS.iter().any(|hint| lower.contains(hint)) { - return CloneMessageClass::Retry(CloneRetryReason::TransientInfra); - } - if TOKEN_REPLICATION_HINTS - .iter() - .any(|hint| lower.contains(hint)) - { - return if token_was_freshly_minted { - CloneMessageClass::Retry(CloneRetryReason::TokenReplication) - } else { - CloneMessageClass::Permanent - }; - } - let permanent = lower.contains("could not read username") - || lower.contains("terminal prompts disabled") - || lower.contains("permission denied") - || (lower.contains("permission to") && lower.contains("denied")) - || (lower.contains("destination path") && lower.contains("already exists")) - || (lower.contains("remote branch") && lower.contains("not found")); - if permanent { - return CloneMessageClass::Permanent; - } - CloneMessageClass::Unknown -} - -/// Backoff between clone attempts: 3s, then 9s. -/// -/// GitHub's guidance for token replication is to wait a few seconds and retry -/// with the same token. Sub-second delays land inside the same replication -/// window and spend an attempt for nothing. -fn backoff() -> BackoffPolicy { - BackoffPolicy { - initial_delay: Duration::from_secs(3), - factor: 3.0, - max_delay: Duration::from_secs(10), - jitter: false, - } -} - -/// Run a clone, repeating it while the failure looks transient. -/// -/// `attempt` receives the 1-based attempt number. `classify` decides whether an -/// error is worth repeating; `None` returns it to the caller untouched. When a -/// deadline is present, a retry starts only when its backoff fits before that -/// deadline. The final error is returned as-is. -pub(crate) async fn retry_clone( - provider: SandboxProviderKind, - deadline: Option, - mut attempt: Attempt, - classify: Classify, -) -> Result -where - Attempt: FnMut(u32) -> Fut, - Fut: Future>, - Classify: Fn(&E) -> Option, -{ - let backoff = backoff(); - - for attempt_number in 1..MAX_ATTEMPTS { - match attempt(attempt_number).await { - Ok(value) => return Ok(value), - Err(err) => { - let Some(reason) = classify(&err) else { - return Err(err); - }; - let delay = backoff.delay_for_attempt(attempt_number); - if deadline.is_some_and(|deadline| { - delay >= deadline.saturating_duration_since(time::Instant::now()) - }) { - return Err(err); - } - // The failure text can carry git stderr, so log the category - // rather than the message. The caller still reports the full - // error if the attempts run out. - tracing::warn!( - provider = %provider, - attempt = attempt_number, - max_attempts = MAX_ATTEMPTS, - reason = %reason, - delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), - "Git clone failed, retrying" - ); - time::sleep(delay).await; - } - } - } - - attempt(MAX_ATTEMPTS).await -} - -#[cfg(test)] -mod tests { - use std::sync::Mutex; - - use super::*; - - /// Records the attempt numbers a closure was called with. - #[derive(Default)] - struct Attempts(Mutex>); - - impl Attempts { - fn record(&self, attempt: u32) { - self.0.lock().expect("attempt log mutex").push(attempt); - } - - fn recorded(&self) -> Vec { - self.0.lock().expect("attempt log mutex").clone() - } - } - - /// A classifier that treats every failure as worth repeating. - const ALWAYS_RETRY: fn(&String) -> Option = - |_| Some(CloneRetryReason::TokenReplication); - - #[test] - fn private_repo_not_found_after_a_successful_mint_is_a_replication_lag() { - assert_eq!( - classify_message("repository not found: Repository not found.", true), - CloneMessageClass::Retry(CloneRetryReason::TokenReplication) - ); - } - - #[test] - fn not_found_without_a_fresh_token_is_permanent() { - assert_eq!( - classify_message("repository not found: Repository not found.", false), - CloneMessageClass::Permanent - ); - } - - #[test] - fn auth_failure_with_a_fresh_token_is_a_replication_lag() { - assert_eq!( - classify_message( - "fatal: Authentication failed for 'https://github.com/owner/repo'", - true - ), - CloneMessageClass::Retry(CloneRetryReason::TokenReplication) - ); - assert_eq!( - classify_message( - "fatal: Authentication failed for 'https://github.com/owner/repo'", - false - ), - CloneMessageClass::Permanent - ); - } - - #[test] - fn infra_failures_retry_without_credentials() { - for message in [ - "fatal: unable to access: Could not resolve host: github.com", - "error: RPC failed; curl 56 recv failure", - "fatal: early EOF", - "Operation timed out", - ] { - assert_eq!( - classify_message(message, false), - CloneMessageClass::Retry(CloneRetryReason::TransientInfra), - "expected {message:?} to be transient" - ); - } - } - - #[test] - fn genuine_failures_are_not_retried() { - for message in [ - "fatal: could not read Username for 'https://github.com'", - "remote: Permission to owner/repo.git denied", - "fatal: destination path 'repo' already exists", - ] { - assert_eq!( - classify_message(message, true), - CloneMessageClass::Permanent, - "expected {message:?} to fail fast" - ); - } - } - - #[test] - fn unrecognized_failures_remain_unknown() { - assert_eq!( - classify_message("git clone stopped for an unexpected reason", true), - CloneMessageClass::Unknown - ); - } - - #[test] - fn backoff_waits_seconds_not_milliseconds() { - let backoff = backoff(); - assert_eq!(backoff.delay_for_attempt(1), Duration::from_secs(3)); - assert_eq!(backoff.delay_for_attempt(2), Duration::from_secs(9)); - } - - #[tokio::test(start_paused = true)] - async fn first_success_runs_one_attempt() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { Ok::<_, String>(attempt) } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!(result, Ok(1)); - assert_eq!(attempts.recorded(), vec![1]); - } - - #[tokio::test(start_paused = true)] - async fn retries_until_a_later_attempt_succeeds() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { - if attempt < 3 { - Err("Repository not found.".to_string()) - } else { - Ok(attempt) - } - } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!(result, Ok(3)); - assert_eq!(attempts.recorded(), vec![1, 2, 3]); - } - - #[tokio::test(start_paused = true)] - async fn exhausted_attempts_return_the_final_error() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { Err::<(), _>(format!("Repository not found. (attempt {attempt})")) } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!( - result, - Err("Repository not found. (attempt 3)".to_string()), - "the caller should see the last failure, not the first" - ); - assert_eq!(attempts.recorded(), vec![1, 2, 3]); - } - - #[tokio::test(start_paused = true)] - async fn unretryable_failure_stops_immediately() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { Err::<(), _>("permission denied".to_string()) } - }, - |_: &String| None, - ) - .await; - - assert_eq!(result, Err("permission denied".to_string())); - assert_eq!( - attempts.recorded(), - vec![1], - "a deterministic failure should not wait out the backoff" - ); - } - - #[tokio::test(start_paused = true)] - async fn deadline_stops_retry_when_backoff_does_not_fit() { - let attempts = Attempts::default(); - let deadline = time::Instant::now() + Duration::from_secs(2); - - let result = retry_clone( - SandboxProviderKind::Docker, - Some(deadline), - |attempt| { - attempts.record(attempt); - async move { Err::<(), _>("temporary failure".to_string()) } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!(result, Err("temporary failure".to_string())); - assert_eq!(attempts.recorded(), vec![1]); - assert_eq!(time::Instant::now() + Duration::from_secs(2), deadline); - } -} diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 79658120e..7337d9cf0 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -26,8 +26,8 @@ use tokio::task::JoinHandle; use tokio::{fs, time}; use tokio_util::sync::CancellationToken; -use crate::clone_retry::{self, CloneRetryReason}; use crate::clone_source::{self, CloneDecision, EmptyWorkspaceReason}; +use crate::git_retry::{self, CredentialContext, GitRetryReason}; use crate::push_credentials::{self, PushCredentialState}; use crate::redact::redact_auth_url; use crate::sandbox::{ @@ -1085,9 +1085,15 @@ impl Sandbox for DaytonaSandbox { })?), None => None, }; - let token_was_freshly_minted = resolved_token - .as_ref() - .is_some_and(|token| !token.snapshot.is_static()); + // The clone call site maps its mint knowledge onto the + // credential context: a token minted for this clone is + // FreshApp; a static credential cannot become valid by + // waiting. + let clone_credential_context = match &resolved_token { + Some(token) if !token.snapshot.is_static() => CredentialContext::FreshApp, + Some(_) => CredentialContext::Static, + None => CredentialContext::None, + }; let (username, password) = match &resolved_token { Some(token) => ( Some("x-access-token".to_string()), @@ -1157,9 +1163,11 @@ impl Sandbox for DaytonaSandbox { self.fail_init(init_start, err) })?; - let clone_result = clone_retry::retry_clone( + let clone_plan = git_retry::RetryPlan::clone_default(None); + let clone_result = git_retry::retry_git( SandboxProviderKind::Daytona, - None, + "clone", + &clone_plan, |_attempt| { let git_svc = &git_svc; let origin = origin_url.as_str(); @@ -1172,7 +1180,7 @@ impl Sandbox for DaytonaSandbox { }; async move { git_svc.clone(origin, target, options).await } }, - |err: &DaytonaError| classify_clone_failure(err, token_was_freshly_minted), + |err: &DaytonaError| classify_clone_failure(err, clone_credential_context), ) .await; @@ -1501,11 +1509,19 @@ impl Sandbox for DaytonaSandbox { )] } - async fn git_push_ref(&self, refspec: &str) -> crate::Result<()> { + async fn git_push_ref( + &self, + refspec: &str, + plan: &crate::RetryPlan, + ) -> Result { if !self.repo_cloned() { - return Ok(()); + return Ok(crate::PushReport::default()); } - crate::git_push_via_exec(self, refspec).await + let credentials = self + .origin_url + .get() + .map(|origin_url| (&self.push_credentials, origin_url.as_str())); + sandbox::git_push_via_exec(self, credentials, refspec, plan).await } async fn ssh_access_command(&self) -> crate::Result> { @@ -2612,23 +2628,20 @@ fn daytona_bash_session_probe_outcome(execution: crate::Result) -> c /// inside the sandbox, so its stderr comes back through the toolbox as the /// error message — the credential race has to be matched on text. Daytona's own /// transport failures are visible structurally. -fn classify_clone_failure( - err: &DaytonaError, - token_was_freshly_minted: bool, -) -> Option { +fn classify_clone_failure(err: &DaytonaError, cred: CredentialContext) -> Option { // A Daytona request timeout does not prove that the remote clone stopped. // Retrying could overlap the still-running first request. if matches!(err, DaytonaError::Timeout { .. }) { return None; } - match clone_retry::classify_message(err.message(), token_was_freshly_minted) { - clone_retry::CloneMessageClass::Retry(reason) => Some(reason), - clone_retry::CloneMessageClass::Permanent => None, - clone_retry::CloneMessageClass::Unknown => match err { - DaytonaError::RateLimit { .. } => Some(CloneRetryReason::TransientInfra), + match git_retry::classify_message(err.message(), cred) { + git_retry::GitMessageClass::Retry(reason) => Some(reason), + git_retry::GitMessageClass::Permanent => None, + git_retry::GitMessageClass::Unknown => match err { + DaytonaError::RateLimit { .. } => Some(GitRetryReason::TransientInfra), DaytonaError::Api { status_code, .. } if (500..600).contains(status_code) => { - Some(CloneRetryReason::TransientInfra) + Some(GitRetryReason::TransientInfra) } DaytonaError::Timeout { .. } | DaytonaError::Api { .. } @@ -3139,11 +3152,11 @@ mod tests { let err = DaytonaError::general("repository not found: Repository not found."); assert_eq!( - classify_clone_failure(&err, true), - Some(CloneRetryReason::TokenReplication) + classify_clone_failure(&err, CredentialContext::FreshApp), + Some(GitRetryReason::TokenReplication) ); assert_eq!( - classify_clone_failure(&err, false), + classify_clone_failure(&err, CredentialContext::None), None, "without credentials there is no token to replicate" ); @@ -3156,8 +3169,8 @@ mod tests { DaytonaError::api(503, ""), ] { assert_eq!( - classify_clone_failure(&err, false), - Some(CloneRetryReason::TransientInfra), + classify_clone_failure(&err, CredentialContext::None), + Some(GitRetryReason::TransientInfra), "expected {err:?} to be transient" ); } @@ -3167,24 +3180,33 @@ mod tests { fn clone_timeout_is_not_retried_without_remote_termination() { let err = DaytonaError::timeout("request timed out"); - assert_eq!(classify_clone_failure(&err, true), None); + assert_eq!( + classify_clone_failure(&err, CredentialContext::FreshApp), + None + ); } #[test] fn clone_api_failure_message_takes_precedence_over_status() { let not_found = DaytonaError::api(500, "repository not found: Repository not found."); assert_eq!( - classify_clone_failure(¬_found, true), - Some(CloneRetryReason::TokenReplication) + classify_clone_failure(¬_found, CredentialContext::FreshApp), + Some(GitRetryReason::TokenReplication) + ); + assert_eq!( + classify_clone_failure(¬_found, CredentialContext::Static), + None ); - assert_eq!(classify_clone_failure(¬_found, false), None); for message in [ "fatal: destination path 'fabro' already exists", "remote: Permission to fabro-sh/fabro.git denied", ] { assert_eq!( - classify_clone_failure(&DaytonaError::api(500, message), true), + classify_clone_failure( + &DaytonaError::api(500, message), + CredentialContext::FreshApp + ), None, "expected {message:?} to take precedence over HTTP 500" ); @@ -3200,7 +3222,7 @@ mod tests { DaytonaError::general("fatal: could not read Username for 'https://github.com'"), ] { assert_eq!( - classify_clone_failure(&err, true), + classify_clone_failure(&err, CredentialContext::FreshApp), None, "expected {err:?} to fail fast" ); diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index 532e7cbb2..5232487f4 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -27,6 +27,7 @@ use tokio::{fs, time}; use tokio_util::sync::CancellationToken; use crate::clone_source::{self, CloneDecision, EmptyWorkspaceReason}; +use crate::git_retry::{self, CredentialContext}; use crate::managed_labels::{self, MANAGED_LABEL, RUN_ID_LABEL}; use crate::push_credentials::{self, PushCredentialState}; use crate::redact::redact_auth_url; @@ -39,7 +40,7 @@ use crate::{ CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, GrepOptions, Sandbox, SandboxEvent, SandboxEventCallback, SandboxFile, StderrCollector, StdioProcess, StdioProcessHandle, - StdioProcessTermination, WalkOptions, clone_retry, format_lines_numbered, shell_quote, + StdioProcessTermination, WalkOptions, format_lines_numbered, shell_quote, }; const DOCKER_BASH_REQUIREMENT: &str = "Docker sandboxes require /bin/bash for every command, with no `sh` fallback; use an \ @@ -60,7 +61,7 @@ const EXEC_TERM_GRACE_SECONDS: &str = "0.2"; struct DockerCloneFailure { error: crate::Error, - retry_reason: Option, + retry_reason: Option, } fn env_entry_name(entry: &str) -> &str { @@ -760,9 +761,14 @@ impl DockerSandbox { })?), None => None, }; - let token_was_freshly_minted = resolved_token - .as_ref() - .is_some_and(|token| !token.snapshot.is_static()); + // The clone call site maps its mint knowledge onto the credential + // context: a token minted for this clone is FreshApp; a static + // credential cannot become valid by waiting. + let clone_credential_context = match &resolved_token { + Some(token) if !token.snapshot.is_static() => CredentialContext::FreshApp, + Some(_) => CredentialContext::Static, + None => CredentialContext::None, + }; let auth_url = match &resolved_token { Some(token) => Some( @@ -807,9 +813,11 @@ impl DockerSandbox { let command = git_clone_command(clone_url, branch.as_deref(), &layout.primary_repo_path); let clone_deadline = time::Instant::now() + GIT_CLONE_TIMEOUT; - let clone_result = clone_retry::retry_clone( + let clone_plan = git_retry::RetryPlan::clone_default(Some(clone_deadline)); + let clone_result = git_retry::retry_git( SandboxProviderKind::Docker, - Some(clone_deadline), + "clone", + &clone_plan, |_attempt| { let command = command.as_str(); let auth_url = auth_url.as_ref(); @@ -843,7 +851,7 @@ impl DockerSandbox { return Ok(()); } let retry_reason = - classify_docker_clone_result(&result, token_was_freshly_minted); + classify_docker_clone_result(&result, clone_credential_context); Err(DockerCloneFailure { error: self.clone_failure_error(result, auth_url), retry_reason, @@ -1397,12 +1405,12 @@ fn git_clone_command(clone_url: &str, branch: Option<&str>, checkout_path: &str) fn classify_docker_clone_result( result: &ExecResult, - token_was_freshly_minted: bool, -) -> Option { - let stderr = clone_retry::classify_message(&result.stderr, token_was_freshly_minted); + cred: CredentialContext, +) -> Option { + let stderr = git_retry::classify_message(&result.stderr, cred); match stderr { - clone_retry::CloneMessageClass::Unknown => { - clone_retry::classify_message(&result.stdout, token_was_freshly_minted).retry_reason() + git_retry::GitMessageClass::Unknown => { + git_retry::classify_message(&result.stdout, cred).retry_reason() } class => class.retry_reason(), } @@ -2186,11 +2194,19 @@ impl Sandbox for DockerSandbox { )] } - async fn git_push_ref(&self, refspec: &str) -> crate::Result<()> { + async fn git_push_ref( + &self, + refspec: &str, + plan: &crate::RetryPlan, + ) -> Result { if !self.repo_cloned() { - return Ok(()); + return Ok(crate::PushReport::default()); } - crate::git_push_via_exec(self, refspec).await + let credentials = self + .origin_url + .get() + .map(|origin_url| (&self.push_credentials, origin_url.as_str())); + sandbox::git_push_via_exec(self, credentials, refspec, plan).await } fn origin_url(&self) -> Option<&str> { @@ -2436,7 +2452,10 @@ mod tests { duration_ms: 1, }; - assert_eq!(classify_docker_clone_result(&result, true), None); + assert_eq!( + classify_docker_clone_result(&result, CredentialContext::FreshApp), + None + ); } #[test] diff --git a/lib/components/fabro-sandbox/src/git_retry.rs b/lib/components/fabro-sandbox/src/git_retry.rs new file mode 100644 index 000000000..5e767abdb --- /dev/null +++ b/lib/components/fabro-sandbox/src/git_retry.rs @@ -0,0 +1,743 @@ +//! Retry for git operations against GitHub from clone-based sandboxes. +//! +//! Clone-based providers can mint a GitHub App installation token and use it +//! immediately. GitHub can reject that first operation before the token is +//! available to the git endpoint. On a private repository, the rejection can +//! arrive as `Repository not found.` or an authentication failure. +//! +//! Only a token minted recently makes those messages safe to retry. Static +//! PATs and pre-minted installation tokens fail fast; a mature App token can +//! still hit a service-side blip that presents the same surface, so it +//! retries as transient infrastructure. +//! +//! Retries reuse the same token on purpose. Replication of a given token only +//! makes progress, so each attempt strictly improves the odds, while +//! re-minting would restart the replication clock. + +use std::future::Future; +use std::time::Duration; + +use chrono::Utc; +use fabro_github::token_source::TokenSnapshot; +#[cfg(test)] +use fabro_github::token_source::{REFRESH_MARGIN, TokenProvenance}; +use fabro_types::SandboxProviderKind; +use fabro_util::backoff::BackoffPolicy; +use tokio::time; + +/// How long after its mint a token is presumed to still be replicating to +/// GitHub's git endpoints. Matches the observed scale of the lag (seconds, +/// occasionally tens of seconds). +pub(crate) const REPLICATION_HORIZON: Duration = Duration::from_mins(1); + +/// Why a failed git attempt is worth repeating. +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum GitRetryReason { + /// A recently minted installation token has not reached the GitHub edge + /// cache site serving this operation yet. + TokenReplication, + /// The operation failed on infrastructure, unrelated to credentials. + TransientInfra, +} + +/// What a git failure message tells us about retry safety. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum GitMessageClass { + Retry(GitRetryReason), + Permanent, + Unknown, +} + +impl GitMessageClass { + pub(crate) fn retry_reason(self) -> Option { + match self { + Self::Retry(reason) => Some(reason), + Self::Permanent | Self::Unknown => None, + } + } +} + +/// What the operation's credentials say about retrying auth-shaped failures. +/// +/// Derived from the [`TokenSnapshot`] of the token embedded for the attempt, +/// so classification reads provenance as data instead of threading booleans +/// through call stacks. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CredentialContext { + /// An installation token younger than [`REPLICATION_HORIZON`] — a 404 or + /// auth failure is likely replication lag; retry with the same token. + FreshApp, + /// An installation token older than the horizon. A 404 with it is + /// indistinguishable from a service-side blip at this layer, so it stays + /// transient rather than proving access loss. + MatureApp, + /// A PAT or pre-minted token — it cannot become valid by waiting. + Static, + /// No credentials at all. + None, +} + +impl CredentialContext { + #[must_use] + pub fn from_snapshot(snapshot: Option<&TokenSnapshot>) -> Self { + match snapshot { + None => Self::None, + Some(snapshot) => match snapshot.age_at(Utc::now()) { + None => Self::Static, + Some(age) if age < REPLICATION_HORIZON => Self::FreshApp, + Some(_) => Self::MatureApp, + }, + } + } +} + +/// Message fragments that mean the operation failed on infrastructure. +/// +/// These are safe to retry whether or not the operation was authenticated. +const TRANSIENT_HINTS: &[&str] = &[ + "could not resolve host", + "temporary failure in name resolution", + "connection refused", + "connection reset", + "connection timed out", + "timed out", + "network is unreachable", + "no route to host", + "tls handshake", + "early eof", + "rpc failed", + "unexpected disconnect", + "the remote end hung up unexpectedly", + "index-pack failed", + "service unavailable", + "gateway timeout", + "too many requests", + "rate limit", +]; + +/// Message fragments GitHub uses when a token is not yet visible. +/// +/// Only meaningful when the operation carried credentials. The same lag +/// surfaces as 404 or as an auth failure depending on which endpoint answers +/// first. +const TOKEN_REPLICATION_HINTS: &[&str] = &[ + "repository not found", + "authentication failed", + "invalid username or password", + "bad credentials", + // git CLI over HTTP. + "the requested url returned error: 401", + "the requested url returned error: 403", + "the requested url returned error: 404", + // libgit2 (the run-metadata writer pushes through git2). + "unexpected http status code: 401", + "unexpected http status code: 403", + "unexpected http status code: 404", +]; + +/// Whether a failure message has the 404/auth-failure shape GitHub produces +/// for both token-replication lag and a drifted or missing embedded token. +pub(crate) fn matches_auth_failure_hints(message: &str) -> bool { + let lower = message.to_ascii_lowercase(); + TOKEN_REPLICATION_HINTS + .iter() + .any(|hint| lower.contains(hint)) +} + +/// Classify a failed git operation by its rendered message. +/// +/// `cred` gates the reading of 404/auth-failure messages: a fresh App token +/// retries as replication lag, a mature one as transient infrastructure, and +/// a static credential (or none) fails fast because waiting cannot make it +/// valid. +pub(crate) fn classify_message(message: &str, cred: CredentialContext) -> GitMessageClass { + let lower = message.to_ascii_lowercase(); + + if TRANSIENT_HINTS.iter().any(|hint| lower.contains(hint)) { + return GitMessageClass::Retry(GitRetryReason::TransientInfra); + } + if TOKEN_REPLICATION_HINTS + .iter() + .any(|hint| lower.contains(hint)) + { + return match cred { + CredentialContext::FreshApp => GitMessageClass::Retry(GitRetryReason::TokenReplication), + CredentialContext::MatureApp => GitMessageClass::Retry(GitRetryReason::TransientInfra), + CredentialContext::Static | CredentialContext::None => GitMessageClass::Permanent, + }; + } + let permanent = lower.contains("could not read username") + || lower.contains("terminal prompts disabled") + || lower.contains("permission denied") + || (lower.contains("permission to") && lower.contains("denied")) + || (lower.contains("destination path") && lower.contains("already exists")) + || (lower.contains("remote branch") && lower.contains("not found")); + if permanent { + return GitMessageClass::Permanent; + } + GitMessageClass::Unknown +} + +/// Classify a rendered git failure message, returning the retry reason when +/// the failure is transient for these credentials. `None` means the failure +/// is permanent or unrecognized. +#[must_use] +pub fn classify_failure(message: &str, cred: CredentialContext) -> Option { + classify_message(message, cred).retry_reason() +} + +/// Backoff between attempts: 3s, then 9s. +/// +/// GitHub's guidance for token replication is to wait a few seconds and retry +/// with the same token. Sub-second delays land inside the same replication +/// window and spend an attempt for nothing. +fn clone_backoff() -> BackoffPolicy { + BackoffPolicy { + initial_delay: Duration::from_secs(3), + factor: 3.0, + max_delay: Duration::from_secs(10), + jitter: false, + } +} + +/// Attempt and time bounds for one retried git operation. +/// +/// All bounds are optional so existing behaviors are expressible unchanged. +/// The effective deadline is the minimum of the bounds that are present +/// (`start + max_elapsed`, `outer_deadline`); each attempt runs with +/// `min(per_attempt_timeout, remaining)` over the caps that are present, and +/// no attempt or backoff starts past the effective deadline. +#[derive(Debug, Clone)] +pub struct RetryPlan { + /// Total attempts, including the first. + pub max_attempts: u32, + pub backoff: BackoffPolicy, + /// Wall clock for this whole operation. + pub max_elapsed: Option, + /// Cap for any single attempt. + pub per_attempt_timeout: Option, + /// Caller-supplied absolute bound. + pub outer_deadline: Option, +} + +impl RetryPlan { + /// The clone policy both providers already trust: 3 attempts, 3s/9s + /// backoff, no plan-level bounds. Docker supplies its existing absolute + /// five-minute deadline through `outer_deadline`; Daytona supplies none. + #[must_use] + pub fn clone_default(outer_deadline: Option) -> Self { + Self { + max_attempts: 3, + backoff: clone_backoff(), + max_elapsed: None, + per_attempt_timeout: None, + outer_deadline, + } + } + + /// Checkpoint pushes stay cheap: the next checkpoint re-pushes the same + /// branch anyway. Worst case ~90 seconds of wall clock. + #[must_use] + pub fn checkpoint_push() -> Self { + Self { + max_attempts: 3, + backoff: clone_backoff(), + max_elapsed: Some(Duration::from_secs(90)), + per_attempt_timeout: Some(Duration::from_mins(1)), + outer_deadline: None, + } + } + + /// The terminal publish push guards the whole run's value, so it gets a + /// real budget: 5 attempts with growing backoff (~3s/10s/33s/60s), + /// bounded at 4 minutes of wall clock. The 4-minute bound must stay + /// under the token source's `REFRESH_MARGIN` (see the margin-invariant + /// test) so a pinned token always outlives the operation. + #[must_use] + pub fn publish_push() -> Self { + Self { + max_attempts: 5, + backoff: BackoffPolicy { + initial_delay: Duration::from_secs(3), + factor: 10.0 / 3.0, + max_delay: Duration::from_mins(1), + jitter: false, + }, + max_elapsed: Some(Duration::from_mins(4)), + per_attempt_timeout: Some(Duration::from_mins(1)), + outer_deadline: None, + } + } + + /// The absolute deadline this operation must finish by, if any bound is + /// present. + pub(crate) fn effective_deadline(&self, start: time::Instant) -> Option { + let elapsed_deadline = self.max_elapsed.map(|max| start + max); + match (elapsed_deadline, self.outer_deadline) { + (Some(a), Some(b)) => Some(a.min(b)), + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + (None, None) => None, + } + } + + /// Time cap for an attempt starting now: the per-attempt cap bounded by + /// the time remaining before the effective deadline. + pub(crate) fn attempt_timeout(&self, deadline: Option) -> Option { + let remaining = deadline.map(|d| d.saturating_duration_since(time::Instant::now())); + match (self.per_attempt_timeout, remaining) { + (Some(cap), Some(remaining)) => Some(cap.min(remaining)), + (Some(cap), None) => Some(cap), + (None, remaining) => remaining, + } + } +} + +/// Run a git operation, repeating it while the failure looks transient. +/// +/// `attempt` receives the 1-based attempt number. `classify` decides whether +/// an error is worth repeating; `None` returns it to the caller untouched. +/// A retry starts only when its backoff fits before the plan's effective +/// deadline. The final error is returned as-is. +pub(crate) async fn retry_git( + provider: SandboxProviderKind, + op: &str, + plan: &RetryPlan, + mut attempt: Attempt, + classify: Classify, +) -> Result +where + Attempt: FnMut(u32) -> Fut, + Fut: Future>, + Classify: Fn(&E) -> Option, +{ + let deadline = plan.effective_deadline(time::Instant::now()); + + for attempt_number in 1..plan.max_attempts.max(1) { + match attempt(attempt_number).await { + Ok(value) => return Ok(value), + Err(err) => { + let Some(reason) = classify(&err) else { + return Err(err); + }; + let delay = plan.backoff.delay_for_attempt(attempt_number); + if deadline.is_some_and(|deadline| { + delay >= deadline.saturating_duration_since(time::Instant::now()) + }) { + return Err(err); + } + // The failure text can carry git stderr, so log the category + // rather than the message. The caller still reports the full + // error if the attempts run out. + tracing::warn!( + provider = %provider, + op, + attempt = attempt_number, + max_attempts = plan.max_attempts, + reason = %reason, + delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), + "Git operation failed, retrying" + ); + time::sleep(delay).await; + } + } + } + + attempt(plan.max_attempts.max(1)).await +} + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + + use super::*; + + /// Records the attempt numbers a closure was called with. + #[derive(Default)] + struct Attempts(Mutex>); + + impl Attempts { + fn record(&self, attempt: u32) { + self.0.lock().expect("attempt log mutex").push(attempt); + } + + fn recorded(&self) -> Vec { + self.0.lock().expect("attempt log mutex").clone() + } + } + + /// A classifier that treats every failure as worth repeating. + const ALWAYS_RETRY: fn(&String) -> Option = + |_| Some(GitRetryReason::TokenReplication); + + fn fresh_snapshot(age: Duration, ttl: Duration) -> TokenSnapshot { + let now = Utc::now(); + TokenSnapshot { + generation: 1, + provenance: TokenProvenance::Minted { + minted_at: now - chrono::Duration::from_std(age).unwrap(), + expires_at: now + chrono::Duration::from_std(ttl).unwrap(), + }, + } + } + + #[test] + fn credential_context_reads_token_age_from_provenance() { + assert_eq!( + CredentialContext::from_snapshot(None), + CredentialContext::None + ); + assert_eq!( + CredentialContext::from_snapshot(Some(&TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + })), + CredentialContext::Static + ); + assert_eq!( + CredentialContext::from_snapshot(Some(&fresh_snapshot( + Duration::from_secs(5), + Duration::from_hours(1) + ))), + CredentialContext::FreshApp + ); + assert_eq!( + CredentialContext::from_snapshot(Some(&fresh_snapshot( + Duration::from_mins(2), + Duration::from_hours(1) + ))), + CredentialContext::MatureApp + ); + } + + #[test] + fn private_repo_not_found_with_a_fresh_token_is_a_replication_lag() { + assert_eq!( + classify_message( + "repository not found: Repository not found.", + CredentialContext::FreshApp + ), + GitMessageClass::Retry(GitRetryReason::TokenReplication) + ); + } + + #[test] + fn not_found_with_a_mature_token_is_transient_not_permanent() { + // A service-side blip is indistinguishable from access loss at this + // layer, so a mature-App 404 stays retryable. + assert_eq!( + classify_message( + "repository not found: Repository not found.", + CredentialContext::MatureApp + ), + GitMessageClass::Retry(GitRetryReason::TransientInfra) + ); + } + + #[test] + fn not_found_with_static_or_no_credentials_is_permanent() { + for cred in [CredentialContext::Static, CredentialContext::None] { + assert_eq!( + classify_message("repository not found: Repository not found.", cred), + GitMessageClass::Permanent, + "{cred:?} cannot become valid by waiting" + ); + } + } + + #[test] + fn auth_failure_classification_follows_the_credential_context() { + let message = "fatal: Authentication failed for 'https://github.com/owner/repo'"; + assert_eq!( + classify_message(message, CredentialContext::FreshApp), + GitMessageClass::Retry(GitRetryReason::TokenReplication) + ); + assert_eq!( + classify_message(message, CredentialContext::MatureApp), + GitMessageClass::Retry(GitRetryReason::TransientInfra) + ); + assert_eq!( + classify_message(message, CredentialContext::Static), + GitMessageClass::Permanent + ); + } + + #[test] + fn infra_failures_retry_without_credentials() { + for message in [ + "fatal: unable to access: Could not resolve host: github.com", + "error: RPC failed; curl 56 recv failure", + "fatal: early EOF", + "Operation timed out", + ] { + assert_eq!( + classify_message(message, CredentialContext::None), + GitMessageClass::Retry(GitRetryReason::TransientInfra), + "expected {message:?} to be transient" + ); + } + } + + #[test] + fn genuine_failures_are_not_retried() { + for message in [ + "fatal: could not read Username for 'https://github.com'", + "remote: Permission to owner/repo.git denied", + "fatal: destination path 'repo' already exists", + ] { + assert_eq!( + classify_message(message, CredentialContext::FreshApp), + GitMessageClass::Permanent, + "expected {message:?} to fail fast" + ); + } + } + + #[test] + fn unrecognized_failures_remain_unknown() { + assert_eq!( + classify_message( + "git operation stopped for an unexpected reason", + CredentialContext::FreshApp + ), + GitMessageClass::Unknown + ); + } + + #[test] + fn backoff_waits_seconds_not_milliseconds() { + let plan = RetryPlan::clone_default(None); + assert_eq!(plan.backoff.delay_for_attempt(1), Duration::from_secs(3)); + assert_eq!(plan.backoff.delay_for_attempt(2), Duration::from_secs(9)); + } + + #[test] + fn publish_backoff_grows_toward_a_one_minute_cap() { + let plan = RetryPlan::publish_push(); + assert_eq!(plan.backoff.delay_for_attempt(1), Duration::from_secs(3)); + assert_eq!(plan.backoff.delay_for_attempt(2), Duration::from_secs(10)); + assert!(plan.backoff.delay_for_attempt(3) < Duration::from_secs(35)); + assert_eq!(plan.backoff.delay_for_attempt(4), Duration::from_mins(1)); + } + + /// `REFRESH_MARGIN` must exceed every push plan's `max_elapsed`: a push + /// pins the token of its single successful resolve, and any token the + /// source returns has at least the margin of validity left, so the pinned + /// token must outlive the whole operation. + #[test] + fn refresh_margin_exceeds_every_push_plan_elapsed_bound() { + for plan in [RetryPlan::checkpoint_push(), RetryPlan::publish_push()] { + let max_elapsed = plan.max_elapsed.expect("push plans bound elapsed time"); + assert!( + REFRESH_MARGIN > max_elapsed, + "margin invariant violated: {max_elapsed:?}" + ); + } + } + + #[test] + fn effective_deadline_takes_the_minimum_of_present_bounds() { + let start = time::Instant::now(); + let outer = start + Duration::from_secs(30); + + let unbounded = RetryPlan::clone_default(None); + assert_eq!(unbounded.effective_deadline(start), None); + + let outer_only = RetryPlan::clone_default(Some(outer)); + assert_eq!(outer_only.effective_deadline(start), Some(outer)); + + let mut both = RetryPlan::checkpoint_push(); + both.outer_deadline = Some(outer); + assert_eq!(both.effective_deadline(start), Some(outer)); + + both.outer_deadline = Some(start + Duration::from_mins(10)); + assert_eq!( + both.effective_deadline(start), + Some(start + Duration::from_secs(90)) + ); + } + + #[tokio::test(start_paused = true)] + async fn attempt_timeout_is_capped_by_the_remaining_deadline() { + let plan = RetryPlan::checkpoint_push(); + let deadline = Some(time::Instant::now() + Duration::from_secs(20)); + assert_eq!( + plan.attempt_timeout(deadline), + Some(Duration::from_secs(20)) + ); + assert_eq!(plan.attempt_timeout(None), Some(Duration::from_mins(1))); + + let unbounded = RetryPlan::clone_default(None); + assert_eq!(unbounded.attempt_timeout(None), None); + } + + #[tokio::test(start_paused = true)] + async fn first_success_runs_one_attempt() { + let attempts = Attempts::default(); + + let result = retry_git( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Ok::<_, String>(attempt) } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!(result, Ok(1)); + assert_eq!(attempts.recorded(), vec![1]); + } + + #[tokio::test(start_paused = true)] + async fn retries_until_a_later_attempt_succeeds() { + let attempts = Attempts::default(); + + let result = retry_git( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { + if attempt < 3 { + Err("Repository not found.".to_string()) + } else { + Ok(attempt) + } + } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!(result, Ok(3)); + assert_eq!(attempts.recorded(), vec![1, 2, 3]); + } + + #[tokio::test(start_paused = true)] + async fn exhausted_attempts_return_the_final_error() { + let attempts = Attempts::default(); + + let result = retry_git( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>(format!("Repository not found. (attempt {attempt})")) } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!( + result, + Err("Repository not found. (attempt 3)".to_string()), + "the caller should see the last failure, not the first" + ); + assert_eq!(attempts.recorded(), vec![1, 2, 3]); + } + + #[tokio::test(start_paused = true)] + async fn unretryable_failure_stops_immediately() { + let attempts = Attempts::default(); + + let result = retry_git( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("permission denied".to_string()) } + }, + |_: &String| None, + ) + .await; + + assert_eq!(result, Err("permission denied".to_string())); + assert_eq!( + attempts.recorded(), + vec![1], + "a deterministic failure should not wait out the backoff" + ); + } + + /// Docker clone parity: the caller's absolute deadline stops retries when + /// the backoff no longer fits before it. + #[tokio::test(start_paused = true)] + async fn outer_deadline_stops_retry_when_backoff_does_not_fit() { + let attempts = Attempts::default(); + let deadline = time::Instant::now() + Duration::from_secs(2); + + let result = retry_git( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(Some(deadline)), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("temporary failure".to_string()) } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!(result, Err("temporary failure".to_string())); + assert_eq!(attempts.recorded(), vec![1]); + assert_eq!(time::Instant::now() + Duration::from_secs(2), deadline); + } + + /// Daytona clone parity: with no bounds at all, attempts are limited only + /// by `max_attempts` and backoff. + #[tokio::test(start_paused = true)] + async fn unbounded_plan_runs_all_attempts() { + let attempts = Attempts::default(); + + let result = retry_git( + SandboxProviderKind::Daytona, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("temporary failure".to_string()) } + }, + |_: &String| Some(GitRetryReason::TransientInfra), + ) + .await; + + assert!(result.is_err()); + assert_eq!(attempts.recorded(), vec![1, 2, 3]); + } + + #[tokio::test(start_paused = true)] + async fn max_elapsed_stops_retry_when_backoff_does_not_fit() { + let attempts = Attempts::default(); + let plan = RetryPlan { + max_attempts: 5, + backoff: clone_backoff(), + max_elapsed: Some(Duration::from_secs(4)), + per_attempt_timeout: None, + outer_deadline: None, + }; + + let result = retry_git( + SandboxProviderKind::Docker, + "push", + &plan, + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("temporary failure".to_string()) } + }, + ALWAYS_RETRY, + ) + .await; + + assert!(result.is_err()); + // Attempt 1 fails instantly, 3s backoff fits inside 4s, attempt 2 + // fails, and the 9s backoff no longer fits. + assert_eq!(attempts.recorded(), vec![1, 2]); + } +} diff --git a/lib/components/fabro-sandbox/src/lib.rs b/lib/components/fabro-sandbox/src/lib.rs index f4418ae1c..5777beccf 100644 --- a/lib/components/fabro-sandbox/src/lib.rs +++ b/lib/components/fabro-sandbox/src/lib.rs @@ -9,16 +9,13 @@ pub mod sandbox_spec; #[cfg(any(feature = "docker", feature = "daytona"))] mod clone_source; -#[cfg(any(feature = "docker", feature = "daytona", test))] -mod clone_retry; +pub mod git_retry; #[cfg(any(feature = "docker", feature = "daytona", test))] mod managed_labels; -#[cfg(any(feature = "docker", feature = "daytona", test))] mod push_credentials; -#[cfg(any(feature = "docker", feature = "daytona", test))] pub mod redact; pub mod details; @@ -46,6 +43,7 @@ pub use fabro_github::token_source::{ InstallationTokenSource, ResolvedToken, TokenProvenance, TokenSnapshot, }; pub use fabro_types::{RunSandboxInstance, SandboxProviderKind}; +pub use git_retry::{CredentialContext, GitRetryReason, RetryPlan}; pub use local::LocalSandbox; #[cfg(feature = "daytona")] pub use provider::daytona::DaytonaSandboxProvider; @@ -55,14 +53,15 @@ pub use provider::{ LocalSandboxProvider, SandboxCreateSpec, SandboxLookupError, SandboxProvider, SandboxProviderRegistry, }; +pub use push_credentials::RefreshErrorKind; pub use reconnect::{reconnect, reconnect_for_run, reconnect_for_run_with_callback}; pub use sandbox::{ CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, GitRunInfo, GitSetupIntent, GrepOptions, - RefreshOutcome, RemoteCredentialAction, Sandbox, SandboxEvent, SandboxEventCallback, - SandboxFile, StderrCollector, StdioProcess, StdioProcessHandle, StdioProcessTermination, - WalkOptions, format_lines_numbered, git_push_via_exec, redacted_output_tail, - setup_git_via_exec, shell_quote, + PushAttempt, PushError, PushReport, RefreshOutcome, RemoteCredentialAction, Sandbox, + SandboxEvent, SandboxEventCallback, SandboxFile, StderrCollector, StdioProcess, + StdioProcessHandle, StdioProcessTermination, WalkOptions, format_lines_numbered, + redacted_output_tail, setup_git_via_exec, shell_quote, }; pub use sandbox_spec::SandboxSpec; pub use terminal::{TerminalSession, TerminalSize, open_terminal_for_run}; diff --git a/lib/components/fabro-sandbox/src/local.rs b/lib/components/fabro-sandbox/src/local.rs index 49737780d..650d57c47 100644 --- a/lib/components/fabro-sandbox/src/local.rs +++ b/lib/components/fabro-sandbox/src/local.rs @@ -13,8 +13,8 @@ use tokio::{fs, time}; use tokio_util::sync::CancellationToken; use crate::sandbox::{ - BASH_ENV_VAR, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, StdioProcessControl, optional_timeout, - validate_bash_probe, write_process_stdin, + self, BASH_ENV_VAR, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, StdioProcessControl, + optional_timeout, validate_bash_probe, write_process_stdin, }; use crate::{ CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult, @@ -878,20 +878,31 @@ impl Sandbox for LocalSandbox { Ok(()) } - async fn git_push_ref(&self, refspec: &str) -> crate::Result<()> { + async fn git_push_ref( + &self, + refspec: &str, + plan: &crate::RetryPlan, + ) -> Result { let has_origin = match self .exec_command("git remote get-url origin", 10_000, None, None, None) .await { Ok(result) if result.is_success() => true, Ok(_) => false, - Err(err) => return Err(crate::Error::context("git remote get-url origin", err)), + Err(err) => { + return Err(crate::PushError { + report: crate::PushReport::default(), + error: crate::Error::context("git remote get-url origin", err), + }); + } }; if !has_origin { - return Ok(()); + return Ok(crate::PushReport::default()); } - crate::git_push_via_exec(self, refspec).await + // Local pushes use whatever credentials the host repository already + // carries; there is no managed credential state to lease. + sandbox::git_push_via_exec(self, None, refspec, plan).await } async fn cleanup(&self) -> crate::Result<()> { diff --git a/lib/components/fabro-sandbox/src/push_credentials.rs b/lib/components/fabro-sandbox/src/push_credentials.rs index 90116317f..5ec3cc48b 100644 --- a/lib/components/fabro-sandbox/src/push_credentials.rs +++ b/lib/components/fabro-sandbox/src/push_credentials.rs @@ -11,10 +11,11 @@ use std::future::Future; use std::sync::Arc; use fabro_github::GitHubCredentials; -use fabro_github::token_source::{InstallationTokenSource, ResolvedToken}; +use fabro_github::token_source::{InstallationTokenSource, ResolvedToken, TokenSnapshot}; use fabro_redact::DisplaySafeUrl; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, MutexGuard}; +use crate::redact; use crate::sandbox::{RefreshOutcome, RemoteCredentialAction}; /// Build the shared installation-token source for a clone-based sandbox. @@ -150,6 +151,218 @@ impl PushCredentialState { } } +/// Which refresh step failed while a push held the credential lease. +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum RefreshErrorKind { + /// Minting a replacement token failed; the push proceeded with the last + /// embedded token. + Mint, + /// Rewriting `origin` with the resolved token failed; the push proceeded + /// with the last embedded token. + SetUrl, +} + +/// What [`CredentialLease::ensure_embedded`] did for one push attempt. +#[derive(Debug, Clone, Copy)] +pub(crate) struct EnsureOutcome { + pub action: RemoteCredentialAction, + /// The token embedded in the remote right now — never an unembedded mint. + pub token: Option, + pub refresh_error: Option, +} + +/// Scoped pin of push credentials for one push operation. +/// +/// Holds the provider's embed mutex until dropped, so no other refresh can +/// re-embed mid-operation — a refresh-ahead tick crossing the cache margin +/// during a retrying push waits here instead of swapping the remote out from +/// under the pin. Internally retains up to two secrets: the last successfully +/// embedded token (the fallback) and the operation's resolved target, so both +/// drift re-embedding and the refresh-error fallback work. Only non-secret +/// snapshots leave the lease. +/// +/// A successful resolve happens at most once per operation and is never +/// replaced; the pin transitions to the target only through a successful +/// embed. The token source's refresh margin exceeds every push plan's elapsed +/// bound, so the pinned token always outlives the operation. +pub(crate) struct CredentialLease<'a> { + source: Option<&'a InstallationTokenSource>, + /// Embed-mutex guard: the last successfully embedded token. + embedded: MutexGuard<'a, Option>, + /// The operation's single successful resolve. + target: Option, +} + +impl PushCredentialState { + /// Acquire the push-credential lease for one push operation. + /// + /// Resolves the operation's target token up front, pinning one token + /// generation for every attempt. A failed resolve still acquires the + /// lease when an earlier operation embedded a token (the push falls back + /// to it and [`CredentialLease::ensure_embedded`] retries the resolve on + /// later attempts); with managed credentials but nothing ever embedded, + /// acquisition fails — there is nothing to push with. + pub(crate) async fn lease(&self) -> crate::Result> { + let embedded = self.embedded.lock().await; + let Some(source) = self.source.as_deref() else { + return Ok(CredentialLease { + source: None, + embedded, + target: None, + }); + }; + match source.resolve().await { + Ok(resolved) => Ok(CredentialLease { + source: Some(source), + embedded, + target: Some(resolved), + }), + Err(err) => { + if let Some(prev) = embedded.as_ref() { + tracing::warn!( + error = %format!("{err:#}"), + generation = prev.snapshot.generation, + provenance = %prev.snapshot.provenance, + token_age_ms = prev.snapshot.age_ms(), + "token resolve failed; push pins the last embedded credentials" + ); + Ok(CredentialLease { + source: Some(source), + embedded, + target: None, + }) + } else { + tracing::warn!( + error = %format!("{err:#}"), + "token resolve failed and no credentials were ever embedded" + ); + Err(crate::Error::message( + "Failed to refresh push credentials: token_mint_failed", + )) + } + } + } + } +} + +impl CredentialLease<'_> { + /// Non-secret description of the token embedded in the remote right now. + pub(crate) fn snapshot(&self) -> Option { + self.embedded.as_ref().map(|token| token.snapshot) + } + + /// Embed the pinned generation if the remote does not carry it. + /// + /// One call covers the initial embed, a deferred embed after an earlier + /// failure, and drift repair (`force` re-embeds even when the tracked + /// generation matches, for remotes rewritten inside the sandbox). While + /// the lease has no target, this retries the failed `resolve()` first — + /// retrying a failed resolve discards no fresh token, so it cannot + /// restart any replication clock. Refresh failures are recorded, never + /// propagated: the push proceeds with the last embedded token. + pub(crate) async fn ensure_embedded( + &mut self, + sandbox: &dyn crate::Sandbox, + origin_url: &str, + force: bool, + ) -> EnsureOutcome { + let Some(source) = self.source else { + return EnsureOutcome { + action: RemoteCredentialAction::None, + token: None, + refresh_error: None, + }; + }; + let mut refresh_error = None; + if self.target.is_none() { + match source.resolve().await { + Ok(resolved) => self.target = Some(resolved), + Err(err) => { + tracing::warn!( + error = %format!("{err:#}"), + "token resolve retry failed; pushing with the last embedded token" + ); + refresh_error = Some(RefreshErrorKind::Mint); + } + } + } + let Some(desired) = self.target.as_ref().or(self.embedded.as_ref()).cloned() else { + // Managed credentials with nothing resolved or embedded: + // acquisition fails before any attempt runs, so pushes never see + // this state. + return EnsureOutcome { + action: RemoteCredentialAction::None, + token: None, + refresh_error, + }; + }; + let embedded_generation = self + .embedded + .as_ref() + .map(|token| token.snapshot.generation); + if !force && embedded_generation == Some(desired.snapshot.generation) { + return EnsureOutcome { + action: RemoteCredentialAction::Unchanged, + token: Some(desired.snapshot), + refresh_error, + }; + } + match set_url_via_exec(sandbox, origin_url, &desired).await { + Ok(()) => { + let snapshot = desired.snapshot; + *self.embedded = Some(desired); + EnsureOutcome { + action: RemoteCredentialAction::Embedded, + token: Some(snapshot), + refresh_error, + } + } + Err(err) => { + tracing::warn!( + error = %crate::display_for_log(&err), + "embedding push credentials in origin failed; pushing with the last embedded token" + ); + EnsureOutcome { + action: RemoteCredentialAction::Unchanged, + token: self.snapshot(), + refresh_error: Some(RefreshErrorKind::SetUrl), + } + } + } + } +} + +/// Rewrite `origin` with the token embedded, through the sandbox's uniform +/// exec surface. +async fn set_url_via_exec( + sandbox: &dyn crate::Sandbox, + origin_url: &str, + token: &ResolvedToken, +) -> crate::Result<()> { + let auth_url = + fabro_github::embed_token_in_url(origin_url, token.token.expose()).map_err(|err| { + crate::Error::message(format!("Failed to build authenticated origin URL: {err:#}")) + })?; + let command = format!( + "git -c maintenance.auto=0 remote set-url origin {}", + crate::shell_quote(auth_url.as_raw_url().as_str()) + ); + let result = sandbox + .exec_command(&command, 10_000, None, None, None) + .await + .map_err(|_| { + crate::Error::message("Failed to refresh push credentials: set_url_exec_failed") + })?; + if !result.is_success() { + return Err(result.into_exec_error_with_redactor( + "git remote set-url origin (push credential lease)", + |s| redact::redact_auth_url(s, Some(&auth_url)), + )); + } + Ok(()) +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; @@ -157,6 +370,7 @@ mod tests { use chrono::Utc; use fabro_github::InstallationToken; use fabro_github::token_source::InstallationTokenMinter; + use tokio::time::sleep; use super::*; @@ -196,6 +410,41 @@ mod tests { } const ORIGIN: &str = "https://github.com/owner/repo"; + /// Long enough for a blocked task to be observably pending on paused time. + const SHORT_WAIT: std::time::Duration = std::time::Duration::from_secs(5); + + /// A refresh-ahead tick crossing the cache margin during a push waits on + /// the embed mutex until the operation releases the lease, so the remote + /// can never be swapped out from under the pinned generation. + #[tokio::test(start_paused = true)] + async fn refresh_waits_for_the_lease_to_release() { + let state = std::sync::Arc::new(minting_state(chrono::Duration::minutes(60))); + + let lease = state.lease().await.expect("lease acquires"); + + let refresh_task = { + let state = std::sync::Arc::clone(&state); + tokio::spawn(async move { + state + .refresh(ORIGIN, |_| async { Ok(()) }) + .await + .expect("refresh succeeds after the lease releases") + }) + }; + + // The refresh must be blocked while the lease holds the embed mutex. + sleep(SHORT_WAIT).await; + assert!( + !refresh_task.is_finished(), + "refresh must wait on the embed mutex" + ); + + drop(lease); + let outcome = refresh_task.await.expect("refresh task completes"); + // The lease's resolve minted generation 1; the deferred refresh + // reuses it (the operation never embedded, so the refresh embeds). + assert_eq!(outcome.token.unwrap().generation, 1); + } #[tokio::test] async fn refresh_without_managed_credentials_reports_none() { diff --git a/lib/components/fabro-sandbox/src/sandbox.rs b/lib/components/fabro-sandbox/src/sandbox.rs index fb8cb0102..d5adfc701 100644 --- a/lib/components/fabro-sandbox/src/sandbox.rs +++ b/lib/components/fabro-sandbox/src/sandbox.rs @@ -18,6 +18,9 @@ use tokio::task::JoinHandle; use tokio::time; use tokio_util::sync::CancellationToken; +use crate::git_retry::{self, CredentialContext, GitMessageClass, GitRetryReason, RetryPlan}; +use crate::push_credentials::{CredentialLease, PushCredentialState, RefreshErrorKind}; + /// Git command prefix that disables background maintenance. const GIT: &str = "git -c maintenance.auto=0 -c gc.auto=0"; @@ -299,8 +302,12 @@ macro_rules! delegate_sandbox { self.$field.resume_setup_commands(run_branch) } - async fn git_push_ref(&self, refspec: &str) -> $crate::Result<()> { - self.$field.git_push_ref(refspec).await + async fn git_push_ref( + &self, + refspec: &str, + plan: &$crate::RetryPlan, + ) -> Result<$crate::PushReport, $crate::PushError> { + self.$field.git_push_ref(refspec, plan).await } async fn ssh_access_command(&self) -> $crate::Result> { @@ -1302,11 +1309,18 @@ pub trait Sandbox: Send + Sync { Vec::new() } - /// Push a full refspec to origin from inside the sandbox. - async fn git_push_ref(&self, _refspec: &str) -> crate::Result<()> { - Err(crate::Error::message( - "git_push_ref not implemented for this sandbox", - )) + /// Push a full refspec to origin from inside the sandbox, retrying per + /// `plan` with a pinned credential generation. Failures keep their + /// attempt history in the returned [`PushError`]. + async fn git_push_ref( + &self, + _refspec: &str, + _plan: &RetryPlan, + ) -> Result { + Err(PushError { + report: PushReport::default(), + error: crate::Error::message("git_push_ref not implemented for this sandbox"), + }) } /// Return an SSH command string for connecting to this sandbox, if @@ -1545,48 +1559,822 @@ pub(crate) async fn fetch_source_run_ref( Err(crate::Error::message(last_error)) } -/// Helper for sandbox implementations that manage git internally. -/// Pushes a refspec to origin via exec_command inside the sandbox. -pub async fn git_push_via_exec(sandbox: &dyn Sandbox, refspec: &str) -> crate::Result<()> { - let token = match sandbox.refresh_push_credentials().await { - Ok(outcome) => { - if let Some(token) = outcome.token { - tracing::debug!( - refspec = %refspec, - action = %outcome.action, - generation = token.generation, - provenance = %token.provenance, - token_age_ms = token.age_ms(), - "Resolved push credentials before git push" - ); +/// One push attempt inside a retried push operation. Runtime detail only — +/// the durable serialized shape lives in `fabro-types` and the workflow layer +/// owns the conversion. +#[derive(Debug, Clone)] +pub struct PushAttempt { + /// 1-based attempt number within this operation. + pub attempt: u32, + pub started_at: chrono::DateTime, + pub success: bool, + /// The classifier's verdict for a failed attempt — recorded on the + /// terminal attempt too; whether a retry actually followed is positional + /// (every entry except the last). + pub retry_reason: Option, + /// Redacted, bounded output tail; failed attempts only. + pub exec_output_tail: Option, + /// The token embedded in the remote during this attempt. + pub token: Option, + /// What `ensure_embedded` did to the remote this attempt. + pub credential_action: Option, + /// A mint or `set-url` failure this attempt pushed through. + pub refresh_error: Option, +} + +/// The attempt history of one push operation. +#[derive(Debug, Clone, Default)] +pub struct PushReport { + pub attempts: Vec, +} + +/// A failed push operation: the final typed error plus the attempt history. +/// The error type stays the safety boundary for output tails. +#[derive(Debug)] +pub struct PushError { + pub report: PushReport, + pub error: crate::Error, +} + +impl std::fmt::Display for PushError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(f) + } +} + +impl std::error::Error for PushError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.error) + } +} + +/// Classify a failed push attempt by the failure's rendered output. +fn classify_push_error(error: &crate::Error, cred: CredentialContext) -> Option { + let class = match error { + crate::Error::Exec { result, .. } => { + let by_stderr = git_retry::classify_message(&result.stderr, cred); + if by_stderr == GitMessageClass::Unknown { + git_retry::classify_message(&result.stdout, cred) + } else { + by_stderr } - outcome.token - } - Err(e) => { - // The provider logged which token stays embedded; the push - // proceeds with the old origin URL. - tracing::warn!( - refspec = %refspec, - error = %crate::display_for_log(&e), - "Failed to refresh push credentials before git push" - ); - None } + other => git_retry::classify_message(&crate::display_for_log(other), cred), }; + class.retry_reason() +} + +/// Whether a failed push attempt has the 404/auth-failure shape that a +/// drifted or missing embedded token also produces. +fn push_failure_looks_auth_shaped(error: &crate::Error) -> bool { + match error { + crate::Error::Exec { result, .. } => { + git_retry::matches_auth_failure_hints(&result.stderr) + || git_retry::matches_auth_failure_hints(&result.stdout) + } + other => git_retry::matches_auth_failure_hints(&crate::display_for_log(other)), + } +} + +/// Helper for sandbox implementations that manage git internally. +/// +/// Pushes a refspec to origin via `exec_command` inside the sandbox, +/// retrying per `plan` with one pinned credential generation for the whole +/// operation. `credentials` is the provider's push-credential state plus the +/// origin URL; `None` pushes with whatever the remote already carries (the +/// local sandbox, or a workspace without managed credentials). +pub(crate) async fn git_push_via_exec( + sandbox: &dyn Sandbox, + credentials: Option<(&PushCredentialState, &str)>, + refspec: &str, + plan: &RetryPlan, +) -> Result { + use CredentialContext; + use CredentialLease; + + // The lease pins one token generation and owns the embed mutex for the + // whole operation; no concurrent refresh can re-embed mid-operation, and + // no attempt can cross the refresh margin and restart the replication + // clock. + let mut lease: Option<(CredentialLease<'_>, &str)> = match credentials { + Some((state, origin_url)) => match state.lease().await { + Ok(lease) => Some((lease, origin_url)), + Err(error) => { + return Err(PushError { + report: PushReport::default(), + error, + }); + } + }, + None => None, + }; + + let start = time::Instant::now(); + let deadline = plan.effective_deadline(start); + let mut attempts: Vec = Vec::new(); + let mut force_reembed = false; + let mut drift_repaired = false; let cmd = format!("{GIT} push origin {}", shell_quote(refspec)); let label = format!("git push origin {refspec}"); - sandbox - .exec_command(&cmd, 60_000, None, None, None) + + loop { + let attempt_number = u32::try_from(attempts.len()).unwrap_or(u32::MAX) + 1; + let started_at = chrono::Utc::now(); + let (token, credential_action, refresh_error) = match lease.as_mut() { + Some((lease, origin_url)) => { + let ensured = lease + .ensure_embedded(sandbox, origin_url, force_reembed) + .await; + force_reembed = false; + (ensured.token, Some(ensured.action), ensured.refresh_error) + } + None => (None, None, None), + }; + + let timeout = plan + .attempt_timeout(deadline) + .unwrap_or(Duration::from_mins(1)); + let timeout_ms = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX); + let push_result = match sandbox + .exec_command(&cmd, timeout_ms, None, None, None) + .await + { + Ok(result) => result.into_result(&label).map(|_| ()), + Err(err) => Err(crate::Error::context(label.clone(), err)), + }; + + match push_result { + Ok(()) => { + attempts.push(PushAttempt { + attempt: attempt_number, + started_at, + success: true, + retry_reason: None, + exec_output_tail: None, + token, + credential_action, + refresh_error, + }); + tracing::info!( + refspec = %refspec, + attempt = attempt_number, + token_generation = token.map(|token| token.generation), + token_age_ms = token.and_then(|token| token.age_ms()), + "Pushed git ref to origin" + ); + return Ok(PushReport { attempts }); + } + Err(error) => { + // Drift recovery: the tracked generation is local belief, and + // agent code inside the sandbox can rewrite `origin`. The + // first auth/not-found failure earns one forced re-embed of + // the pinned token, inside the same retry budget. + if !drift_repaired && lease.is_some() && push_failure_looks_auth_shaped(&error) { + drift_repaired = true; + force_reembed = true; + } + let cred = CredentialContext::from_snapshot(token.as_ref()); + let retry_reason = classify_push_error(&error, cred); + attempts.push(PushAttempt { + attempt: attempt_number, + started_at, + success: false, + retry_reason, + exec_output_tail: error.default_redacted_output_tail(), + token, + credential_action, + refresh_error, + }); + + let exhausted = attempt_number >= plan.max_attempts.max(1); + let Some(reason) = retry_reason.filter(|_| !exhausted) else { + return Err(PushError { + report: PushReport { attempts }, + error, + }); + }; + let delay = plan.backoff.delay_for_attempt(attempt_number); + if deadline.is_some_and(|deadline| { + delay >= deadline.saturating_duration_since(time::Instant::now()) + }) { + return Err(PushError { + report: PushReport { attempts }, + error, + }); + } + // The failure text can carry git stderr, so log the category + // rather than the message. + tracing::warn!( + refspec = %refspec, + attempt = attempt_number, + max_attempts = plan.max_attempts, + reason = %reason, + token_generation = token.map(|token| token.generation), + token_age_ms = token.and_then(|token| token.age_ms()), + delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), + "Git push failed, retrying with the same token" + ); + time::sleep(delay).await; + } + } + } +} + +#[cfg(test)] +mod push_tests { + use std::collections::VecDeque; + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use chrono::Utc; + use fabro_github::InstallationToken; + use fabro_github::token_source::{ + InstallationTokenMinter, InstallationTokenSource, REFRESH_MARGIN, + }; + use tokio::sync::Mutex as AsyncMutex; + + use super::*; + use crate::git_retry::{GitRetryReason, RetryPlan}; + use crate::push_credentials::{PushCredentialState, RefreshErrorKind}; + + const ORIGIN: &str = "https://github.com/fabro-testing/repo"; + const REFSPEC: &str = "refs/heads/fabro/run/01M0DH033P2XSTHAGVBHG6922F"; + + fn ok_exec() -> ExecResult { + ExecResult { + stdout: String::new(), + stderr: String::new(), + exit_code: Some(0), + termination: CommandTermination::Exited, + duration_ms: 5, + } + } + + fn failed_exec(stderr: &str) -> ExecResult { + ExecResult { + stdout: String::new(), + stderr: stderr.to_string(), + exit_code: Some(128), + termination: CommandTermination::Exited, + duration_ms: 5, + } + } + + /// Sandbox stub that scripts `git push` results and records the exec + /// commands the push driver runs. `git remote set-url` execs succeed + /// unless scripted otherwise. + struct ScriptedGitSandbox { + push_results: Mutex>, + set_url_results: Mutex>, + push_commands: Mutex>, + set_url_commands: Mutex>, + } + + impl ScriptedGitSandbox { + fn new(push_results: Vec) -> Self { + Self { + push_results: Mutex::new(push_results.into()), + set_url_results: Mutex::new(VecDeque::new()), + push_commands: Mutex::new(Vec::new()), + set_url_commands: Mutex::new(Vec::new()), + } + } + + fn with_set_url_results(self, results: Vec) -> Self { + *self.set_url_results.lock().unwrap() = results.into(); + self + } + + fn push_count(&self) -> usize { + self.push_commands.lock().unwrap().len() + } + + fn set_url_commands(&self) -> Vec { + self.set_url_commands.lock().unwrap().clone() + } + } + + #[async_trait] + impl Sandbox for ScriptedGitSandbox { + async fn exec_command( + &self, + command: &str, + _timeout_ms: u64, + _working_dir: Option<&str>, + _env_vars: Option<&HashMap>, + _cancel_token: Option, + ) -> crate::Result { + if command.contains("remote set-url") { + self.set_url_commands + .lock() + .unwrap() + .push(command.to_string()); + return Ok(self + .set_url_results + .lock() + .unwrap() + .pop_front() + .unwrap_or_else(ok_exec)); + } + assert!( + command.contains("push origin"), + "unexpected exec: {command}" + ); + self.push_commands.lock().unwrap().push(command.to_string()); + Ok(self + .push_results + .lock() + .unwrap() + .pop_front() + .expect("push script exhausted")) + } + + async fn read_file_bytes(&self, _path: &str) -> crate::Result> { + unimplemented!() + } + + async fn write_file(&self, _path: &str, _content: &str) -> crate::Result<()> { + unimplemented!() + } + + async fn delete_file(&self, _path: &str) -> crate::Result<()> { + unimplemented!() + } + + async fn file_exists(&self, _path: &str) -> crate::Result { + unimplemented!() + } + + async fn list_directory( + &self, + _path: &str, + _depth: Option, + ) -> crate::Result> { + unimplemented!() + } + + async fn grep( + &self, + _pattern: &str, + _path: &str, + _options: &GrepOptions, + ) -> crate::Result> { + unimplemented!() + } + + async fn download_file_to_local( + &self, + _remote_path: &str, + _local_path: &Path, + ) -> crate::Result<()> { + unimplemented!() + } + + async fn upload_file_from_local( + &self, + _local_path: &Path, + _remote_path: &str, + ) -> crate::Result<()> { + unimplemented!() + } + + async fn initialize(&self) -> crate::Result<()> { + Ok(()) + } + + async fn cleanup(&self) -> crate::Result<()> { + Ok(()) + } + + fn working_directory(&self) -> &'static str { + "/workspace" + } + + fn platform(&self) -> &'static str { + "linux" + } + + fn os_version(&self) -> String { + "linux".to_string() + } + } + + enum MintAction { + Token(&'static str, chrono::Duration), + Error(&'static str), + } + + struct ScriptedMinter { + calls: AtomicUsize, + script: AsyncMutex>, + } + + impl ScriptedMinter { + fn new(script: Vec) -> std::sync::Arc { + std::sync::Arc::new(Self { + calls: AtomicUsize::new(0), + script: AsyncMutex::new(script.into()), + }) + } + + fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } + } + + struct SharedMinter(std::sync::Arc); + + #[async_trait] + impl InstallationTokenMinter for SharedMinter { + async fn mint(&self) -> anyhow::Result { + self.0.calls.fetch_add(1, Ordering::SeqCst); + match self.0.script.lock().await.pop_front().expect("mint script") { + MintAction::Token(token, ttl) => Ok(InstallationToken { + token: token.to_string(), + expires_at: Utc::now() + ttl, + }), + MintAction::Error(message) => Err(anyhow::anyhow!(message)), + } + } + } + + fn minting_state( + script: Vec, + ) -> (PushCredentialState, std::sync::Arc) { + let minter = ScriptedMinter::new(script); + let source = InstallationTokenSource::with_minter( + "fabro-testing/repo".to_string(), + Box::new(SharedMinter(std::sync::Arc::clone(&minter))), + ); + (PushCredentialState::new(Some(source)), minter) + } + + async fn seed_clone_token(state: &PushCredentialState) { + let clone_token = state + .source() + .expect("state has a source") + .mint_for_clone() + .await + .expect("clone mint succeeds"); + state.record_embedded(clone_token).await; + } + + /// Regression for run `01M0DH033P2XSTHAGVBHG6922F` (the push variant of + /// `clone_not_found_after_a_successful_mint_is_retried`): GitHub rejected + /// pushes with 404 "Repository not found" milliseconds after a token + /// mint. The retry must reuse the same token — replication of a given + /// token only makes progress — and recover inside the plan's budget. + #[tokio::test(start_paused = true)] + async fn push_not_found_after_a_successful_mint_is_retried_with_the_same_token() { + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::minutes(60), + )]); + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) .await - .map_err(|e| crate::Error::context(label.clone(), e))? - .into_result(&label)?; - tracing::info!( - refspec = %refspec, - token_generation = token.map(|token| token.generation), - token_age_ms = token.and_then(|token| token.age_ms()), - "Pushed git ref to origin" - ); - Ok(()) + .expect("push should recover within the checkpoint plan"); + + assert_eq!(report.attempts.len(), 3); + assert_eq!(minter.calls(), 1, "retries must not re-mint"); + for attempt in &report.attempts { + assert_eq!(attempt.token.expect("token recorded").generation, 1); + } + assert_eq!( + report.attempts[0].retry_reason, + Some(GitRetryReason::TokenReplication) + ); + assert!(report.attempts[0].exec_output_tail.is_some()); + assert_eq!( + report.attempts[0].credential_action, + Some(RemoteCredentialAction::Embedded), + "first attempt embeds the resolved token" + ); + assert!(report.attempts[2].success); + assert!(report.attempts[2].exec_output_tail.is_none()); + assert_eq!(sandbox.push_count(), 3); + } + + /// The publish plan gives the terminal push a real budget: four + /// replication-lag failures still recover on the fifth attempt. + #[tokio::test(start_paused = true)] + async fn publish_plan_survives_four_not_found_failures() { + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::minutes(60), + )]); + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::publish_push(), + ) + .await + .expect("push should recover within the publish plan"); + + assert_eq!(report.attempts.len(), 5); + assert_eq!(minter.calls(), 1); + assert!(report.attempts[4].success); + } + + /// Margin-boundary pinning: a token resolved just above the refresh + /// margin stays pinned through a full retry sequence — the operation + /// never re-resolves mid-flight, so no fresh mint can restart the + /// replication clock. + #[tokio::test(start_paused = true)] + async fn token_resolved_just_above_the_margin_stays_pinned_through_retries() { + let ttl = REFRESH_MARGIN + Duration::from_secs(5); + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::from_std(ttl).unwrap(), + )]); + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("push should recover"); + + assert_eq!(minter.calls(), 1, "no mid-operation mint"); + let generations: Vec = report + .attempts + .iter() + .map(|attempt| attempt.token.expect("token recorded").generation) + .collect(); + assert_eq!(generations, vec![1, 1, 1]); + } + + #[tokio::test(start_paused = true)] + async fn static_credential_auth_failure_fails_fast() { + let source = InstallationTokenSource::for_origin( + &fabro_github::GitHubCredentials::Pat("ghp_pat".to_string()), + ORIGIN, + serde_json::json!({ "contents": "write" }), + ) + .unwrap(); + let state = PushCredentialState::new(Some(source)); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![failed_exec( + "fatal: Authentication failed for 'https://github.com/fabro-testing/repo'", + )]); + + let push_error = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::publish_push(), + ) + .await + .expect_err("static credentials cannot become valid by waiting"); + + assert_eq!(push_error.report.attempts.len(), 1); + assert_eq!(push_error.report.attempts[0].retry_reason, None); + assert!(push_error.report.attempts[0].token.unwrap().is_static()); + } + + /// Clone seeding closes the "nothing was ever embedded" hole: when the + /// first refresh mint fails, the push falls back to the clone token + /// recorded as last-embedded instead of aborting. + #[tokio::test(start_paused = true)] + async fn mint_failure_falls_back_to_the_clone_token() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_clone", chrono::Duration::minutes(5)), + // The clone token is inside the margin, so the lease acquisition + // re-mints — and fails. So does the attempt-level retry. + MintAction::Error("mint failed"), + MintAction::Error("mint failed"), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ok_exec()]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("push proceeds with the still-valid clone token"); + + assert_eq!(minter.calls(), 3); + let attempt = &report.attempts[0]; + assert!(attempt.success); + assert_eq!(attempt.refresh_error, Some(RefreshErrorKind::Mint)); + assert_eq!( + attempt.token.expect("fallback token recorded").generation, + 1, + "attempts classify against the embedded clone token, never None" + ); + assert_eq!( + attempt.credential_action, + Some(RemoteCredentialAction::Unchanged) + ); + } + + #[tokio::test(start_paused = true)] + async fn acquisition_fails_when_mint_fails_and_nothing_was_embedded() { + let (state, _minter) = minting_state(vec![MintAction::Error("mint failed")]); + let sandbox = ScriptedGitSandbox::new(vec![]); + + let push_error = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect_err("there is nothing to push with"); + + assert!(push_error.report.attempts.is_empty()); + assert!(push_error.error.to_string().contains("token_mint_failed")); + assert_eq!(sandbox.push_count(), 0); + } + + /// Late-mint recovery: the fallback push fails on the expired-ish old + /// token, a later attempt's resolve retry succeeds, the target embeds, + /// and the push recovers — all inside one operation's budget. + #[tokio::test(start_paused = true)] + async fn late_mint_recovery_lands_the_target_inside_the_operation() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_gen1", chrono::Duration::minutes(5)), + MintAction::Error("mint failed"), + MintAction::Error("mint failed"), + MintAction::Token("ghs_gen2", chrono::Duration::minutes(60)), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("fatal: Authentication failed for 'https://github.com'"), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("late mint should recover the push"); + + assert_eq!(minter.calls(), 4); + let first = &report.attempts[0]; + assert_eq!(first.refresh_error, Some(RefreshErrorKind::Mint)); + assert_eq!(first.token.unwrap().generation, 1); + let second = &report.attempts[1]; + assert!(second.success); + assert_eq!(second.refresh_error, None); + assert_eq!(second.token.unwrap().generation, 2); + assert_eq!( + second.credential_action, + Some(RemoteCredentialAction::Embedded), + "the report shows the single generation transition" + ); + } + + /// A failed `set-url` defers the embed: attempt 1 records the old + /// generation with the refresh error, attempt 2 lands the target, and the + /// report shows the one generation transition via `credential_action`. + #[tokio::test(start_paused = true)] + async fn set_url_failure_defers_the_embed_until_the_next_attempt() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_gen1", chrono::Duration::minutes(5)), + MintAction::Token("ghs_gen2", chrono::Duration::minutes(60)), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("error: RPC failed; connection reset by peer"), + ok_exec(), + ]) + .with_set_url_results(vec![failed_exec("error: could not lock config file")]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("deferred embed should land on the retry"); + + assert_eq!( + minter.calls(), + 2, + "the successful resolve is never repeated" + ); + let first = &report.attempts[0]; + assert_eq!(first.refresh_error, Some(RefreshErrorKind::SetUrl)); + assert_eq!( + first.token.unwrap().generation, + 1, + "pin stays on the old token" + ); + assert_eq!( + first.credential_action, + Some(RemoteCredentialAction::Unchanged) + ); + let second = &report.attempts[1]; + assert_eq!(second.token.unwrap().generation, 2); + assert_eq!( + second.credential_action, + Some(RemoteCredentialAction::Embedded) + ); + assert!(second.success); + } + + /// Remote drift: agent code rewrote `origin`, so the push fails on auth + /// even though the tracked generation looks current. The first + /// auth-shaped failure earns one forced re-embed of the pinned token. + #[tokio::test(start_paused = true)] + async fn remote_drift_gets_one_forced_reembed_of_the_pinned_token() { + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::minutes(60), + )]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec( + "fatal: could not read Username for 'https://github.com': No such device or address\nremote: Repository not found.", + ), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("drift repair should restore the pinned credentials"); + + assert_eq!(minter.calls(), 1, "drift repair re-embeds, never re-mints"); + assert_eq!( + report.attempts[0].credential_action, + Some(RemoteCredentialAction::Unchanged), + "before the failure the tracked generation matched" + ); + assert_eq!( + report.attempts[1].credential_action, + Some(RemoteCredentialAction::Embedded), + "the retry force-re-embeds the pinned token" + ); + let set_urls = sandbox.set_url_commands(); + assert_eq!(set_urls.len(), 1); + assert!(set_urls[0].contains("ghs_gen1")); + } + + #[tokio::test(start_paused = true)] + async fn push_without_managed_credentials_reports_no_token() { + let sandbox = ScriptedGitSandbox::new(vec![ok_exec()]); + + let report = git_push_via_exec(&sandbox, None, REFSPEC, &RetryPlan::checkpoint_push()) + .await + .expect("push succeeds"); + + assert_eq!(report.attempts.len(), 1); + assert_eq!(report.attempts[0].token, None); + assert_eq!(report.attempts[0].credential_action, None); + } + + #[tokio::test(start_paused = true)] + async fn unauthenticated_auth_failure_is_permanent() { + let sandbox = ScriptedGitSandbox::new(vec![failed_exec( + "fatal: Authentication failed for 'https://github.com/fabro-testing/repo'", + )]); + + let push_error = git_push_via_exec(&sandbox, None, REFSPEC, &RetryPlan::publish_push()) + .await + .expect_err("no credentials to wait on"); + + assert_eq!(push_error.report.attempts.len(), 1); + assert_eq!(push_error.report.attempts[0].retry_reason, None); + } } #[cfg(test)] diff --git a/lib/components/fabro-workflow/src/error.rs b/lib/components/fabro-workflow/src/error.rs index f8c08ca15..4d7add9eb 100644 --- a/lib/components/fabro-workflow/src/error.rs +++ b/lib/components/fabro-workflow/src/error.rs @@ -307,6 +307,10 @@ pub enum Error { message: String, failure_class: FailureCategory, exec_output_tail: Option, + /// Structured context lines appended after the source chain in + /// `causes()` — e.g. one line per push attempt on a publish push + /// failure. + extra_causes: Vec, #[source] source: Option, }, @@ -355,6 +359,7 @@ impl Error { message, failure_class, exec_output_tail, + extra_causes: Vec::new(), source: None, } } @@ -376,6 +381,7 @@ impl Error { message, failure_class, exec_output_tail, + extra_causes: Vec::new(), source: Some(source), } } @@ -452,12 +458,42 @@ impl Error { Self::stage_with_source(ErrorStage::Publish, message, source, exec_output_tail) } + /// Build a publish error with an explicitly determined failure category, + /// for callers that know more than message sniffing can recover — e.g. + /// exhausted push retries whose attempts all classified as transient. + /// `extra_causes` lines land after the source chain in the failure + /// detail (one line per push attempt). + pub fn publish_with_source_and_class( + message: impl Into, + source: impl Into, + failure_class: FailureCategory, + exec_output_tail: Option, + extra_causes: Vec, + ) -> Self { + Self::Stage { + stage: ErrorStage::Publish, + message: message.into(), + failure_class, + exec_output_tail, + extra_causes, + source: Some(SharedError::new(source.into())), + } + } + #[must_use] pub fn causes(&self) -> Vec { match self { - Self::Stage { source, .. } => source - .as_ref() - .map_or_else(Vec::new, |source| collect_chain(source)), + Self::Stage { + source, + extra_causes, + .. + } => { + let mut causes = source + .as_ref() + .map_or_else(Vec::new, |source| collect_chain(source)); + causes.extend(extra_causes.iter().cloned()); + causes + } Self::Template { source, .. } => collect_chain(source), Self::ScriptInterpolation { source, .. } => collect_chain(source), Self::Llm(err) => collect_causes(err), diff --git a/lib/components/fabro-workflow/src/event.rs b/lib/components/fabro-workflow/src/event.rs index a5c1f583e..19c61c43d 100644 --- a/lib/components/fabro-workflow/src/event.rs +++ b/lib/components/fabro-workflow/src/event.rs @@ -10,7 +10,7 @@ mod test_support; pub use fabro_types::{EventBody, RunNoticeCode, RunNoticeLevel}; -pub use self::convert::{to_run_event, to_run_event_at}; +pub use self::convert::{git_push_attempt_props, to_run_event, to_run_event_at}; pub use self::emitter::Emitter; pub use self::events::Event; pub use self::names::event_name; diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index 6403d8933..ded494571 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -23,6 +23,36 @@ fn stage_status_from_string(status: &str) -> StageOutcome { }) } +/// Project the sandbox layer's runtime push attempts into the durable +/// `git.push` attempt shape. +/// +/// This is the only place the runtime attempt record crosses into stored +/// events: the token snapshot flattens into the three flat `token_*` fields +/// (a nested provenance enum never appears in stored events), and the retry +/// classifier's verdict becomes `classified_reason`. +pub fn git_push_attempt_props( + attempts: &[fabro_sandbox::PushAttempt], +) -> Vec { + attempts + .iter() + .map(|attempt| fabro_types::GitPushAttemptProps { + attempt: attempt.attempt, + started_at: attempt.started_at, + success: attempt.success, + classified_reason: attempt.retry_reason.map(|reason| reason.to_string()), + exec_output_tail: attempt.exec_output_tail.clone(), + token_generation: attempt.token.map(|token| token.generation), + token_provenance: attempt.token.map(|token| token.provenance.to_string()), + token_age_ms: attempt + .token + .and_then(|token| token.age_at(attempt.started_at)) + .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)), + credential_action: attempt.credential_action.map(|action| action.to_string()), + refresh_error: attempt.refresh_error.map(|kind| kind.to_string()), + }) + .collect() +} + fn event_body_from_event(event: &Event) -> EventBody { match event { Event::RunCreated { @@ -520,10 +550,12 @@ fn event_body_from_event(event: &Event) -> EventBody { branch, success, exec_output_tail, + attempts, } => EventBody::GitPush(fabro_types::GitPushProps { branch: branch.clone(), success: *success, exec_output_tail: exec_output_tail.clone(), + attempts: attempts.clone(), }), Event::GitFetch { branch, success } => EventBody::GitFetch(fabro_types::GitFetchProps { branch: branch.clone(), @@ -2170,12 +2202,141 @@ mod tests { } } + /// The `git.push` attempts contract: every runtime attempt fact + /// round-trips through `GitPushAttemptProps`, the token snapshot is + /// flattened to the three flat token fields (a nested provenance enum + /// never appears in stored events), and optional failure fields are + /// omitted when absent. + #[test] + fn git_push_attempts_round_trip_through_the_durable_shape() { + let started_at = Utc::now(); + let minted_at = started_at - chrono::Duration::milliseconds(180); + let expires_at = started_at + chrono::Duration::minutes(60); + let attempts = git_push_attempt_props(&[ + fabro_sandbox::PushAttempt { + attempt: 1, + started_at, + success: false, + retry_reason: Some(fabro_sandbox::GitRetryReason::TokenReplication), + exec_output_tail: Some(exec_tail()), + token: Some(fabro_sandbox::TokenSnapshot { + generation: 14, + provenance: fabro_sandbox::TokenProvenance::Minted { + minted_at, + expires_at, + }, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Embedded), + refresh_error: None, + }, + // Terminal classified failure with a refresh error: the last + // attempt carries its classification too. + fabro_sandbox::PushAttempt { + attempt: 2, + started_at: started_at + chrono::Duration::seconds(3), + success: false, + retry_reason: Some(fabro_sandbox::GitRetryReason::TransientInfra), + exec_output_tail: Some(exec_tail()), + token: Some(fabro_sandbox::TokenSnapshot { + generation: 14, + provenance: fabro_sandbox::TokenProvenance::Reused { + minted_at, + expires_at, + }, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), + refresh_error: Some(fabro_sandbox::RefreshErrorKind::SetUrl), + }, + ]); + + let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { + branch: "fabro/run/01M0DH033P2XSTHAGVBHG6922F".to_string(), + success: false, + exec_output_tail: Some(exec_tail()), + attempts: attempts.clone(), + }); + + let json = serde_json::to_value(&stored).unwrap(); + let serialized = &json["properties"]["attempts"]; + assert_eq!(serialized[0]["attempt"], 1); + assert_eq!(serialized[0]["classified_reason"], "token_replication"); + assert_eq!(serialized[0]["token_generation"], 14); + assert_eq!(serialized[0]["token_provenance"], "minted"); + assert_eq!(serialized[0]["token_age_ms"], 180); + assert_eq!(serialized[0]["credential_action"], "embedded"); + assert!(serialized[0].get("refresh_error").is_none()); + assert_eq!(serialized[1]["classified_reason"], "transient_infra"); + assert_eq!(serialized[1]["token_provenance"], "reused"); + assert_eq!(serialized[1]["refresh_error"], "set_url"); + // The provenance enum never nests in stored events. + assert!(serialized[0].get("token").is_none()); + + let round_tripped: ::fabro_types::RunEvent = serde_json::from_value(json).unwrap(); + match round_tripped.body { + EventBody::GitPush(props) => { + assert!(!props.success); + assert_eq!(props.attempts, attempts); + } + other => panic!("expected GitPush body, got {other:?}"), + } + } + + #[test] + fn successful_single_attempt_push_omits_failure_fields() { + let attempts = git_push_attempt_props(&[fabro_sandbox::PushAttempt { + attempt: 1, + started_at: Utc::now(), + success: true, + retry_reason: None, + exec_output_tail: None, + token: Some(fabro_sandbox::TokenSnapshot { + generation: 0, + provenance: fabro_sandbox::TokenProvenance::Static, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), + refresh_error: None, + }]); + let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { + branch: "fabro/run/run-1".to_string(), + success: true, + exec_output_tail: None, + attempts, + }); + + let json = serde_json::to_value(&stored).unwrap(); + let attempt = &json["properties"]["attempts"][0]; + assert_eq!(attempt["success"], true); + assert_eq!(attempt["token_provenance"], "static"); + for absent in [ + "classified_reason", + "exec_output_tail", + "token_age_ms", + "refresh_error", + ] { + assert!(attempt.get(absent).is_none(), "{absent} should be omitted"); + } + } + + /// Events stored before attempts were recorded deserialize with the field + /// absent; the pre-existing three fields are untouched. + #[test] + fn stored_git_push_without_attempts_still_deserializes() { + let json = serde_json::json!({ + "branch": "fabro/run/old", + "success": true + }); + let props: fabro_types::GitPushProps = serde_json::from_value(json).unwrap(); + assert!(props.attempts.is_empty()); + assert!(props.exec_output_tail.is_none()); + } + #[test] fn git_push_maps_exec_output_tail_to_props() { let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { branch: "refs/heads/run:refs/heads/run".to_string(), success: false, exec_output_tail: Some(exec_tail()), + attempts: Vec::new(), }); match stored.body { diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index a38cefbd0..7d18ec911 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -432,6 +432,10 @@ pub enum Event { success: bool, #[serde(default, skip_serializing_if = "Option::is_none")] exec_output_tail: Option, + /// Per-attempt history of the push operation, already projected to + /// the durable shape by the emit site. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + attempts: Vec, }, GitFetch { branch: String, @@ -1230,14 +1234,16 @@ impl Event { branch, success, exec_output_tail, + attempts, } => { if *success { - debug!(branch, "Git push succeeded"); + debug!(branch, attempts = attempts.len(), "Git push succeeded"); } else { let tail = fabro_types::ExecOutputTail::trace_summary(exec_output_tail.as_ref()); warn!( branch, + attempts = attempts.len(), exec_output_tail_present = tail.present, exec_stdout_tail_bytes = tail.stdout_bytes, exec_stderr_tail_bytes = tail.stderr_bytes, diff --git a/lib/components/fabro-workflow/src/lifecycle/event.rs b/lib/components/fabro-workflow/src/lifecycle/event.rs index 8922708e0..6303ea1f3 100644 --- a/lib/components/fabro-workflow/src/lifecycle/event.rs +++ b/lib/components/fabro-workflow/src/lifecycle/event.rs @@ -493,6 +493,7 @@ impl RunLifecycle for EventLifecycle { branch: push.branch.clone(), success: push.success, exec_output_tail: push.exec_output_tail.clone(), + attempts: push.attempts.clone(), }); } } diff --git a/lib/components/fabro-workflow/src/lifecycle/git.rs b/lib/components/fabro-workflow/src/lifecycle/git.rs index 18f4139bf..a744d0ba0 100644 --- a/lib/components/fabro-workflow/src/lifecycle/git.rs +++ b/lib/components/fabro-workflow/src/lifecycle/git.rs @@ -8,13 +8,18 @@ use fabro_core::lifecycle::RunLifecycle; use fabro_core::outcome::NodeResult; use fabro_core::state::ExecutionState; use fabro_dump::RunDump; -use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; +use fabro_sandbox::git_retry; +use fabro_types::run_event::{ + GitPushAttemptProps, MetadataSnapshotFailureKind, MetadataSnapshotPhase, +}; use fabro_types::{CheckpointRecord, DiffSummary, RunDiff, RunId}; use fabro_util::error::collect_causes; use fabro_util::time::elapsed_ms; use crate::artifact; -use crate::event::{Emitter, Event, RunNoticeCode, RunNoticeLevel, StageScope}; +use crate::event::{ + Emitter, Event, RunNoticeCode, RunNoticeLevel, StageScope, git_push_attempt_props, +}; use crate::graph::{WorkflowGraph, WorkflowNode}; use crate::lifecycle::event::stage_scope_for; use crate::outcome::BilledModelUsage; @@ -72,21 +77,38 @@ pub(crate) struct PushResult { pub branch: String, pub success: bool, pub exec_output_tail: Option, + /// Per-attempt history, already projected to the durable event shape. + pub attempts: Vec, } /// Push a run branch to its remote counterpart. /// /// Owns the refspec convention so the checkpoint push and the terminal publish -/// push cannot drift apart. +/// push cannot drift apart. The caller picks the retry budget: cheap for +/// checkpoint pushes (the next checkpoint re-pushes the same branch anyway), +/// generous for the terminal publish push. pub(crate) async fn push_run_branch( sandbox: &dyn fabro_sandbox::Sandbox, branch: &str, -) -> fabro_sandbox::Result<()> { + plan: &fabro_sandbox::RetryPlan, +) -> Result { sandbox - .git_push_ref(&format!("refs/heads/{branch}:refs/heads/{branch}")) + .git_push_ref(&format!("refs/heads/{branch}:refs/heads/{branch}"), plan) .await } +/// Whether a metadata push failure leaves the writer eligible for re-probing +/// at later checkpoints. Only push failures with retryable classifications +/// (replication lag on a fresh token, transient infrastructure) qualify; +/// everything else keeps the permanent latch. +fn metadata_push_failure_is_transient( + detail: &str, + token: Option<&fabro_sandbox::TokenSnapshot>, +) -> bool { + let cred = fabro_sandbox::CredentialContext::from_snapshot(token); + git_retry::classify_failure(detail, cred).is_some() +} + /// Sub-lifecycle responsible for git operations (checkpoint commits, pushes, /// diffs). pub(crate) struct GitLifecycle { @@ -117,7 +139,7 @@ impl RunLifecycle for GitLifecycle { "git lifecycle mutex should not be poisoned: no code panics while holding this lock", ) = None; if let Some(meta_branch) = self.metadata_branch().map(str::to_string) { - if self.metadata_writer.is_none() || self.metadata_runtime.metadata_degraded() { + if self.metadata_writer.is_none() || self.metadata_runtime.metadata_suspended() { return Ok(()); } let phase = MetadataSnapshotPhase::Init; @@ -154,6 +176,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); } }, @@ -174,6 +197,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); } } @@ -208,7 +232,7 @@ impl RunLifecycle for GitLifecycle { None, ); let shadow_sha = if let Some(meta_branch) = self.metadata_branch().map(str::to_string) { - if self.metadata_writer.is_none() || self.metadata_runtime.metadata_degraded() { + if self.metadata_writer.is_none() || self.metadata_runtime.metadata_suspended() { None } else { let phase = MetadataSnapshotPhase::Checkpoint; @@ -253,6 +277,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); None } @@ -276,6 +301,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); None } @@ -320,30 +346,41 @@ impl RunLifecycle for GitLifecycle { .as_ref() .and_then(|g| g.run_branch.as_ref()) { - let (push_ok, exec_output_tail) = - match push_run_branch(self.sandbox.as_ref(), branch).await { - Ok(()) => (true, None), - Err(err) => { + let plan = fabro_sandbox::RetryPlan::checkpoint_push(); + let (push_ok, exec_output_tail, attempts) = + match push_run_branch(self.sandbox.as_ref(), branch, &plan).await { + Ok(report) => { + self.sandbox_git.record_successful_push(); + (true, None, report.attempts) + } + Err(push_error) => { let exec_output_tail = - fabro_sandbox::default_redacted_output_tail(&err); + fabro_sandbox::default_redacted_output_tail( + &push_error.error, + ); tracing::warn!( branch = %branch, - error = %fabro_sandbox::display_for_log(&err), + attempts = push_error.report.attempts.len(), + error = %fabro_sandbox::display_for_log(&push_error.error), "git push from run lifecycle failed" ); self.emitter.notice_with_tail( RunNoticeLevel::Warn, RunNoticeCode::GitPushFailed, - format!("Failed to push run branch {branch}: {err}"), + format!( + "Failed to push run branch {branch}: {}", + push_error.error + ), exec_output_tail.clone(), ); - (false, exec_output_tail) + (false, exec_output_tail, push_error.report.attempts) } }; git_result.push_results.push(PushResult { branch: branch.clone(), success: push_ok, exec_output_tail, + attempts: git_push_attempt_props(&attempts), }); } } @@ -452,7 +489,7 @@ impl GitLifecycle { message: &str, scope: Option<&StageScope>, ) -> Option { - if self.metadata_runtime.metadata_degraded() { + if self.metadata_runtime.metadata_suspended() { return None; } let writer = self.metadata_writer.as_ref()?; @@ -477,8 +514,12 @@ impl GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataPushFailed, message, + metadata_push_failure_is_transient(detail, snapshot.token.as_ref()), ); } else { + // One good snapshot ends the degradation; a later + // independent failure warns again. + self.metadata_runtime.clear_metadata_degraded(); self.emit_metadata_snapshot_completed( phase, meta_branch, @@ -503,7 +544,11 @@ impl GitLifecycle { None, scope, ); - self.emit_metadata_warning(RunNoticeCode::CheckpointMetadataWriteFailed, message); + self.emit_metadata_warning( + RunNoticeCode::CheckpointMetadataWriteFailed, + message, + false, + ); None } } @@ -588,8 +633,8 @@ impl GitLifecycle { } } - fn emit_metadata_warning(&self, code: RunNoticeCode, message: String) { - if self.metadata_runtime.mark_metadata_degraded() { + fn emit_metadata_warning(&self, code: RunNoticeCode, message: String, transient: bool) { + if self.metadata_runtime.mark_metadata_degraded(transient) { self.emitter.notice(RunNoticeLevel::Warn, code, message); } } @@ -1202,7 +1247,7 @@ mod tests { let repo_dir = tempfile::tempdir().unwrap(); init_git_repo(repo_dir.path()); let runtime = Arc::new(RunMetadataRuntime::new()); - runtime.mark_metadata_degraded(); + runtime.mark_metadata_degraded(false); let emitter = Arc::new(Emitter::new(fixtures::RUN_1)); let events = record_events(&emitter); let lifecycle = git_lifecycle( diff --git a/lib/components/fabro-workflow/src/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index b1e7847ee..f30bbc27c 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -4,6 +4,7 @@ use std::time::Instant; use fabro_dump::RunDump; use fabro_hooks::{HookContext, HookEvent}; +use fabro_sandbox::git_retry; use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; use fabro_types::{BilledTokenCounts, DiffSummary, EventBody, RunFailure, RunProjection}; use fabro_util::error::collect_causes; @@ -202,7 +203,7 @@ pub async fn write_finalize_commit( services: &RunServices, conclusion: &Conclusion, ) { - if services.metadata_runtime.metadata_degraded() { + if services.metadata_runtime.metadata_suspended() { return; } let Some(writer) = services.metadata_writer.as_ref() else { @@ -240,6 +241,7 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); return; } @@ -265,6 +267,7 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); return; } @@ -290,8 +293,10 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataPushFailed, message, + metadata_push_failure_is_transient(detail, snapshot.token.as_ref()), ); } else { + services.metadata_runtime.clear_metadata_degraded(); emit_metadata_snapshot_completed(services, phase, meta_branch, started, &snapshot); } } @@ -313,6 +318,7 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); } } @@ -376,8 +382,23 @@ fn emit_metadata_snapshot_failed( }); } -fn emit_metadata_warning(services: &RunServices, code: RunNoticeCode, message: String) { - if services.metadata_runtime.mark_metadata_degraded() { +/// Whether a metadata push failure leaves the writer eligible for re-probing. +/// See `lifecycle::git`: only retryable push classifications qualify. +fn metadata_push_failure_is_transient( + detail: &str, + token: Option<&fabro_sandbox::TokenSnapshot>, +) -> bool { + let cred = fabro_sandbox::CredentialContext::from_snapshot(token); + git_retry::classify_failure(detail, cred).is_some() +} + +fn emit_metadata_warning( + services: &RunServices, + code: RunNoticeCode, + message: String, + transient: bool, +) { + if services.metadata_runtime.mark_metadata_degraded(transient) { services.emitter.notice(RunNoticeLevel::Warn, code, message); } } @@ -1257,7 +1278,7 @@ mod tests { let emitter = Arc::new(Emitter::new(test_run_id())); let events = record_events(&emitter); let runtime = Arc::new(RunMetadataRuntime::new()); - runtime.mark_metadata_degraded(); + runtime.mark_metadata_degraded(false); let services = test_services( RunStoreHandle::local(run_store), emitter, @@ -1461,7 +1482,18 @@ mod tests { ); let events = events.lock().unwrap(); let names = events.iter().map(RunEvent::event_name).collect::>(); + // Exactly one durable git.push event per high-level push — retries + // nest inside it as attempts, never as extra events. assert_eq!(names, vec!["git.push", "run.failed"]); + match &events.first().unwrap().body { + EventBody::GitPush(props) => { + assert!(!props.success); + // MockSandbox's default git_push_ref fails before any attempt + // runs, so the nested history is empty here. + assert!(props.attempts.is_empty()); + } + other => panic!("expected git.push, got {other:?}"), + } match &events.last().unwrap().body { EventBody::RunFailed(props) => { assert_eq!(props.failure.reason, FailureReason::PublishFailed); diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 2ccb3b6f2..da80f5085 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -605,7 +605,7 @@ pub async fn initialize( Ok(writer) => writer, Err(err) => { let message = format!("failed to initialize checkpoint metadata writer: {err}"); - if metadata_runtime.mark_metadata_degraded() { + if metadata_runtime.mark_metadata_degraded(false) { options.emitter.notice( RunNoticeLevel::Warn, RunNoticeCode::CheckpointMetadataWriteFailed, diff --git a/lib/components/fabro-workflow/src/pipeline/publish.rs b/lib/components/fabro-workflow/src/pipeline/publish.rs index 30fe5b055..2191f7a7e 100644 --- a/lib/components/fabro-workflow/src/pipeline/publish.rs +++ b/lib/components/fabro-workflow/src/pipeline/publish.rs @@ -1,9 +1,13 @@ +use std::fmt::Write as _; use std::sync::Arc; +use fabro_types::ExecOutputTail; +use fabro_types::run_event::GitPushAttemptProps; + use super::pull_request::{AutoMergeOptions, OpenPullRequestRequest, open_pull_request}; use super::types::{Concluded, PublishOptions, PublishOutcome, Published}; -use crate::error::Error; -use crate::event::Event; +use crate::error::{Error, FailureCategory, classify_failure_reason}; +use crate::event::{Event, git_push_attempt_props}; use crate::lifecycle::git::push_run_branch; /// PUBLISH phase: push the final run commit and, when configured, open a pull @@ -35,6 +39,76 @@ pub async fn publish(concluded: Concluded, options: &PublishOptions) -> Publishe } } +/// Build the terminal publish error from a failed push operation. +/// +/// Retries exhausted on transient classifications stay `TransientInfra`: a +/// mature-token 404 is not proof of permanent access loss — a service-side +/// failure presents the same surface — so `Deterministic` would need +/// independent evidence this path does not gather. Each attempt becomes one +/// bounded cause line in the failure detail; git output stays inside the +/// exec output tail. +fn publish_push_error( + run_branch: &str, + push_error: fabro_sandbox::PushError, + exec_output_tail: Option, + attempts: &[GitPushAttemptProps], + last_successful_push_at: Option>, +) -> Error { + let message = match last_successful_push_at { + Some(at) => format!( + "failed to push run branch '{run_branch}' (last successful push at {})", + at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true) + ), + None => format!("failed to push run branch '{run_branch}'"), + }; + let failure_class = match push_error + .report + .attempts + .last() + .and_then(|attempt| attempt.retry_reason) + { + Some(_) => FailureCategory::TransientInfra, + None => classify_failure_reason(&format!( + "{message}: {}", + fabro_sandbox::display_for_log(&push_error.error) + )), + }; + let causes = attempts.iter().map(push_attempt_cause).collect(); + Error::publish_with_source_and_class( + message, + push_error, + failure_class, + exec_output_tail, + causes, + ) +} + +/// One bounded line per push attempt for the failure detail. +fn push_attempt_cause(attempt: &GitPushAttemptProps) -> String { + let outcome = if attempt.success { + "succeeded" + } else { + attempt + .classified_reason + .as_deref() + .unwrap_or("unclassified") + }; + let mut line = format!( + "push attempt {} at {}: {outcome}", + attempt.attempt, + attempt + .started_at + .to_rfc3339_opts(chrono::SecondsFormat::Millis, true) + ); + if let Some(age_ms) = attempt.token_age_ms { + let _ = write!(line, " (token age {age_ms}ms)"); + } + if let Some(refresh_error) = &attempt.refresh_error { + let _ = write!(line, ", refresh error: {refresh_error}"); + } + line +} + impl Concluded { /// Run the publish steps, recording each one into `outcome` as it lands. /// @@ -159,26 +233,36 @@ impl Concluded { } async fn push_final_commit(&self, run_branch: &str) -> Result<(), Error> { - match push_run_branch(self.services.sandbox.as_ref(), run_branch).await { - Ok(()) => { + // The terminal push guards the whole run's value, so it gets a real + // retry budget; attempts are nearly free at this point. + let plan = fabro_sandbox::RetryPlan::publish_push(); + match push_run_branch(self.services.sandbox.as_ref(), run_branch, &plan).await { + Ok(report) => { + self.services.sandbox_git.record_successful_push(); self.services.emitter.emit(&Event::GitPush { branch: run_branch.to_string(), success: true, exec_output_tail: None, + attempts: git_push_attempt_props(&report.attempts), }); Ok(()) } - Err(error) => { - let exec_output_tail = fabro_sandbox::default_redacted_output_tail(&error); + Err(push_error) => { + let exec_output_tail = + fabro_sandbox::default_redacted_output_tail(&push_error.error); + let attempts = git_push_attempt_props(&push_error.report.attempts); self.services.emitter.emit(&Event::GitPush { branch: run_branch.to_string(), success: false, exec_output_tail: exec_output_tail.clone(), + attempts: attempts.clone(), }); - Err(Error::publish_with_source_and_exec_output_tail( - format!("failed to push run branch '{run_branch}'"), - error, + Err(publish_push_error( + run_branch, + push_error, exec_output_tail, + &attempts, + self.services.sandbox_git.last_successful_push_at(), )) } } @@ -192,3 +276,132 @@ impl Concluded { Error::publish(message) } } + +#[cfg(test)] +mod tests { + use chrono::Utc; + use fabro_types::run_event::GitPushAttemptProps; + + use super::*; + use crate::error::FailureCategory; + + fn attempt_props( + attempt: u32, + classified_reason: Option<&str>, + token_age_ms: Option, + refresh_error: Option<&str>, + ) -> GitPushAttemptProps { + GitPushAttemptProps { + attempt, + started_at: Utc::now(), + success: false, + classified_reason: classified_reason.map(str::to_string), + exec_output_tail: None, + token_generation: Some(14), + token_provenance: Some("minted".to_string()), + token_age_ms, + credential_action: Some("unchanged".to_string()), + refresh_error: refresh_error.map(str::to_string), + } + } + + fn push_error_with_reasons( + reasons: &[Option], + ) -> fabro_sandbox::PushError { + let attempts = reasons + .iter() + .enumerate() + .map(|(index, reason)| fabro_sandbox::PushAttempt { + attempt: u32::try_from(index).unwrap() + 1, + started_at: Utc::now(), + success: false, + retry_reason: *reason, + exec_output_tail: None, + token: None, + credential_action: None, + refresh_error: None, + }) + .collect(); + fabro_sandbox::PushError { + report: fabro_sandbox::PushReport { attempts }, + error: fabro_sandbox::Error::message("remote: Repository not found."), + } + } + + /// Exhausted retries on a retryable classification are transient + /// infrastructure, not deterministic: the same push succeeded manually an + /// hour after run 01M0DH033P2XSTHAGVBHG6922F failed, with no + /// configuration change. + #[test] + fn exhausted_transient_retries_classify_as_transient_infra() { + let error = publish_push_error( + "fabro/run/test", + push_error_with_reasons(&[ + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(fabro_sandbox::GitRetryReason::TokenReplication), + ]), + None, + &[], + None, + ); + assert_eq!(error.failure_category(), FailureCategory::TransientInfra); + } + + #[test] + fn permanently_classified_push_falls_back_to_message_sniffing() { + let error = publish_push_error( + "fabro/run/test", + push_error_with_reasons(&[None]), + None, + &[], + None, + ); + // "Repository not found." carries no transient hint for the + // heuristic, so the fallback stays deterministic. + assert_eq!(error.failure_category(), FailureCategory::Deterministic); + } + + #[test] + fn failure_detail_renders_one_cause_line_per_attempt() { + let attempts = vec![ + attempt_props(1, Some("token_replication"), Some(180), None), + attempt_props(2, Some("token_replication"), Some(3320), Some("set_url")), + ]; + let last_push = Utc::now() - chrono::Duration::seconds(67); + let error = publish_push_error( + "fabro/run/test", + push_error_with_reasons(&[ + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(fabro_sandbox::GitRetryReason::TokenReplication), + ]), + None, + &attempts, + Some(last_push), + ); + + let detail = error.to_failure_detail(); + assert!( + detail.message.contains("last successful push at"), + "{}", + detail.message + ); + let attempt_lines: Vec<&String> = detail + .causes + .iter() + .filter(|cause| cause.starts_with("push attempt")) + .collect(); + assert_eq!(attempt_lines.len(), 2); + assert!( + attempt_lines[0].contains("token_replication"), + "{attempt_lines:?}" + ); + assert!( + attempt_lines[0].contains("(token age 180ms)"), + "{attempt_lines:?}" + ); + assert!( + attempt_lines[1].contains("refresh error: set_url"), + "{attempt_lines:?}" + ); + } +} diff --git a/lib/components/fabro-workflow/src/run_metadata.rs b/lib/components/fabro-workflow/src/run_metadata.rs index 9fa002b57..6f18632da 100644 --- a/lib/components/fabro-workflow/src/run_metadata.rs +++ b/lib/components/fabro-workflow/src/run_metadata.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; use fabro_checkpoint::git::{FileMode, Store, TreeEntries}; use fabro_dump::RunDump; -use fabro_github::token_source::InstallationTokenSource; +use fabro_github::token_source::{InstallationTokenSource, TokenSnapshot}; use git2::{ Cred, Direction, ErrorClass, ErrorCode, FetchOptions, Oid, PushOptions, RemoteCallbacks, Repository, Signature, @@ -47,26 +47,63 @@ pub(crate) struct MetadataSnapshot { pub push_error: Option, pub entry_count: usize, pub bytes: u64, + /// The token the push authenticated with, for classifying a push + /// failure against the credential context. + pub token: Option, } pub(crate) struct RunMetadataRuntime { - degraded: AtomicBool, - warning_emitted: AtomicBool, + degraded: AtomicBool, + /// A transiently degraded writer stays eligible for one snapshot attempt + /// at each subsequent checkpoint; a permanent failure latches snapshots + /// off for the rest of the run. + reprobe_eligible: AtomicBool, + warning_emitted: AtomicBool, } impl RunMetadataRuntime { pub(crate) fn new() -> Self { Self { - degraded: AtomicBool::new(false), - warning_emitted: AtomicBool::new(false), + degraded: AtomicBool::new(false), + reprobe_eligible: AtomicBool::new(false), + warning_emitted: AtomicBool::new(false), } } - pub(crate) fn mark_metadata_degraded(&self) -> bool { - self.degraded.store(true, Ordering::SeqCst); + /// Record a metadata failure. `transient` failures (push failures with + /// retryable classifications) leave the writer eligible for re-probing; + /// initialization, discovery, serialization, and permanent-authentication + /// failures latch it off — repeating known-failing work at every + /// checkpoint is noise, not resilience. A permanent latch is never + /// upgraded by a later transient failure. Returns whether the caller + /// should emit the degradation warning (once per degradation). + pub(crate) fn mark_metadata_degraded(&self, transient: bool) -> bool { + let was_degraded = self.degraded.swap(true, Ordering::SeqCst); + if was_degraded { + if !transient { + self.reprobe_eligible.store(false, Ordering::SeqCst); + } + } else { + self.reprobe_eligible.store(transient, Ordering::SeqCst); + } !self.warning_emitted.swap(true, Ordering::SeqCst) } + /// A successful snapshot clears the degraded state and re-arms the + /// warning, so a later independent failure warns again instead of + /// failing silently. + pub(crate) fn clear_metadata_degraded(&self) { + self.degraded.store(false, Ordering::SeqCst); + self.reprobe_eligible.store(false, Ordering::SeqCst); + self.warning_emitted.store(false, Ordering::SeqCst); + } + + /// Whether snapshot writes should be skipped: degraded with no re-probe + /// eligibility. + pub(crate) fn metadata_suspended(&self) -> bool { + self.degraded.load(Ordering::SeqCst) && !self.reprobe_eligible.load(Ordering::SeqCst) + } + pub(crate) fn metadata_degraded(&self) -> bool { self.degraded.load(Ordering::SeqCst) } @@ -78,9 +115,16 @@ impl Default for RunMetadataRuntime { } } +/// A resolved metadata push token: the secret plus the non-secret snapshot +/// used to classify push failures against the credential context. +pub(crate) struct MetadataToken { + pub secret: String, + pub snapshot: Option, +} + #[async_trait] pub(crate) trait AuthProvider: Send + Sync { - async fn token(&self) -> Result, RunMetadataError>; + async fn token(&self) -> Result, RunMetadataError>; } struct GitHubAuthProvider { @@ -95,11 +139,16 @@ impl GitHubAuthProvider { #[async_trait] impl AuthProvider for GitHubAuthProvider { - async fn token(&self) -> Result, RunMetadataError> { + async fn token(&self) -> Result, RunMetadataError> { self.source .resolve() .await - .map(|resolved| Some(resolved.token.expose().to_owned())) + .map(|resolved| { + Some(MetadataToken { + secret: resolved.token.expose().to_owned(), + snapshot: Some(resolved.snapshot), + }) + }) .map_err(RunMetadataError::TokenMint) } } @@ -110,7 +159,7 @@ struct NoAuth; #[cfg(test)] #[async_trait] impl AuthProvider for NoAuth { - async fn token(&self) -> Result, RunMetadataError> { + async fn token(&self) -> Result, RunMetadataError> { Ok(None) } } @@ -182,6 +231,8 @@ impl RunMetadataWriterHandle { message: &str, ) -> Result { let token = self.auth.token().await?; + let token_snapshot = token.as_ref().and_then(|token| token.snapshot); + let secret = token.map(|token| token.secret); let entries = dump .git_entries() .map_err(RunMetadataError::DumpSerialize)?; @@ -190,10 +241,14 @@ impl RunMetadataWriterHandle { task::spawn_blocking(move || { let mut guard = writer.lock().expect("metadata writer mutex poisoned"); - guard.write_snapshot_blocking(&entries, &message, token.as_deref()) + guard.write_snapshot_blocking(&entries, &message, secret.as_deref()) }) .await .map_err(RunMetadataError::Join)? + .map(|mut snapshot| { + snapshot.token = token_snapshot; + snapshot + }) } } @@ -339,6 +394,7 @@ impl RunMetadataWriter { push_error, entry_count, bytes, + token: None, }) } @@ -1014,4 +1070,52 @@ mod tests { assert!(build_metadata_writer(&options, None).unwrap().is_none()); } + #[test] + fn transient_degradation_stays_eligible_for_reprobe() { + let runtime = RunMetadataRuntime::new(); + assert!(runtime.mark_metadata_degraded(true), "first failure warns"); + assert!(runtime.metadata_degraded()); + assert!( + !runtime.metadata_suspended(), + "a transiently degraded writer re-probes at later checkpoints" + ); + assert!( + !runtime.mark_metadata_degraded(true), + "repeat failures do not warn again" + ); + } + + #[test] + fn permanent_degradation_latches_snapshots_off() { + let runtime = RunMetadataRuntime::new(); + runtime.mark_metadata_degraded(false); + assert!(runtime.metadata_suspended()); + + // A later transient failure never upgrades a permanent latch. + runtime.mark_metadata_degraded(true); + assert!(runtime.metadata_suspended()); + } + + #[test] + fn permanent_failure_latches_a_transiently_degraded_writer() { + let runtime = RunMetadataRuntime::new(); + runtime.mark_metadata_degraded(true); + assert!(!runtime.metadata_suspended()); + runtime.mark_metadata_degraded(false); + assert!(runtime.metadata_suspended()); + } + + #[test] + fn successful_snapshot_clears_degradation_and_rearms_the_warning() { + let runtime = RunMetadataRuntime::new(); + assert!(runtime.mark_metadata_degraded(true)); + runtime.clear_metadata_degraded(); + + assert!(!runtime.metadata_degraded()); + assert!(!runtime.metadata_suspended()); + assert!( + runtime.mark_metadata_degraded(false), + "a later independent failure warns again instead of failing silently" + ); + } } diff --git a/lib/components/fabro-workflow/src/sandbox_git_runtime.rs b/lib/components/fabro-workflow/src/sandbox_git_runtime.rs index 2bd246e13..1178d5c69 100644 --- a/lib/components/fabro-workflow/src/sandbox_git_runtime.rs +++ b/lib/components/fabro-workflow/src/sandbox_git_runtime.rs @@ -6,16 +6,35 @@ use tokio::sync::OnceCell; use crate::sandbox_git::{GIT_REMOTE, exec_err}; pub(crate) struct SandboxGitRuntime { - probe: OnceCell>, + probe: OnceCell>, + /// When the run last pushed its branch successfully (checkpoint or + /// publish). Read by the publish failure report so "last success 67s + /// before the failure" is visible from the run conclusion. + last_successful_push_at: std::sync::Mutex>>, } impl SandboxGitRuntime { pub(crate) fn new() -> Self { Self { - probe: OnceCell::new(), + probe: OnceCell::new(), + last_successful_push_at: std::sync::Mutex::new(None), } } + pub(crate) fn record_successful_push(&self) { + *self + .last_successful_push_at + .lock() + .expect("last push timestamp mutex poisoned") = Some(chrono::Utc::now()); + } + + pub(crate) fn last_successful_push_at(&self) -> Option> { + *self + .last_successful_push_at + .lock() + .expect("last push timestamp mutex poisoned") + } + pub(crate) async fn ensure_git_available( &self, sandbox: &dyn Sandbox, diff --git a/lib/foundation/fabro-types/src/run_event/misc.rs b/lib/foundation/fabro-types/src/run_event/misc.rs index cde08f65f..9643f9fb1 100644 --- a/lib/foundation/fabro-types/src/run_event/misc.rs +++ b/lib/foundation/fabro-types/src/run_event/misc.rs @@ -112,12 +112,56 @@ pub struct GitCommitProps { pub sha: String, } +/// One attempt of a retried git push, nested inside [`GitPushProps`]. +/// +/// The durable projection of the sandbox layer's runtime attempt record. +/// Token identity is flattened into the three `token_*` fields — a nested +/// provenance enum never appears in stored events. `classified_reason` is the +/// retry classifier's verdict for a failed attempt (the terminal attempt +/// carries its classification too); whether an attempt was actually retried +/// is positional — every entry except the last. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GitPushAttemptProps { + /// 1-based attempt number within this push operation. + pub attempt: u32, + pub started_at: chrono::DateTime, + pub success: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub classified_reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exec_output_tail: Option, + /// Generation of the token embedded during this attempt (0 for static + /// credentials). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_generation: Option, + /// `minted`, `reused`, or `static`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_provenance: Option, + /// Token age at the attempt; absent for static credentials. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_age_ms: Option, + /// What the credential refresh did to the remote this attempt: + /// `embedded`, `unchanged`, or `none`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credential_action: Option, + /// A credential `mint` or `set_url` failure this attempt pushed through. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub refresh_error: Option, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitPushProps { pub branch: String, + /// Final outcome of the whole push operation — one `git.push` event per + /// high-level push, so finality is unambiguous. pub success: bool, + /// The final attempt's output tail, unchanged for existing consumers. #[serde(default, skip_serializing_if = "Option::is_none")] pub exec_output_tail: Option, + /// Per-attempt history. Absent on events stored before attempts were + /// recorded. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub attempts: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/lib/foundation/fabro-types/src/run_event/mod.rs b/lib/foundation/fabro-types/src/run_event/mod.rs index 99c9c4aa5..6323e5071 100644 --- a/lib/foundation/fabro-types/src/run_event/mod.rs +++ b/lib/foundation/fabro-types/src/run_event/mod.rs @@ -1954,6 +1954,7 @@ mod tests { branch: "refs/heads/run:refs/heads/run".to_string(), success: false, exec_output_tail: Some(tail.clone()), + attempts: Vec::new(), }), ] { let value = serde_json::to_value(&body).unwrap(); @@ -1985,6 +1986,7 @@ mod tests { branch: "refs/heads/run:refs/heads/run".to_string(), success: false, exec_output_tail: None, + attempts: Vec::new(), }), ] { let value = serde_json::to_value(&body).unwrap(); From 0845c331cb38c25db5265d2e1056dc3eb0fee6ec Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 17:26:13 -0400 Subject: [PATCH 44/63] Default Daytona auto-stop to 120 minutes Omitting autoStopInterval from the create-sandbox request inherits Daytona's server-side default of 15 idle minutes. Daytona counts inactivity from the last sandbox interaction, and LLM inference never touches the sandbox, so a single long inference call is enough for the sandbox to auto-stop mid-run: a workflow failed exactly this way, with the sandbox entering its stop transition 15 minutes after the last command while the agent was still thinking. Send an explicit 120-minute default when lifecycle.auto_stop is unset. That clears any realistic inference call while still reclaiming sandboxes leaked by a dead worker. An explicit auto_stop = "0s" still disables auto-stop entirely. Co-Authored-By: Claude Fable 5 --- docs/public/execution/run-configuration.mdx | 2 +- docs/public/integrations/daytona.mdx | 4 +++ .../fabro-sandbox/src/daytona/mod.rs | 36 ++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx index a8f1bc927..d2ce9bd5e 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -321,7 +321,7 @@ memory = "8GB" | `network.allow` | CIDRs for `cidr_allow_list`; entries are validated as CIDRs. | | `lifecycle.preserve` | Keep the created sandbox after the run finishes. | | `lifecycle.stop_on_terminal` | Stop the sandbox when the run reaches a terminal state. | -| `lifecycle.auto_stop` | Daytona auto-stop duration, such as `"30m"`. | +| `lifecycle.auto_stop` | Daytona auto-stop duration, such as `"30m"`. Defaults to `"120m"`; `"0s"` disables auto-stop. | | `labels` | Provider labels. Merge by key across layers. | | `env` | Environment variables passed to command and agent execution. Merge by key across layers. | diff --git a/docs/public/integrations/daytona.mdx b/docs/public/integrations/daytona.mdx index 6ad19b9b3..1a2f25790 100644 --- a/docs/public/integrations/daytona.mdx +++ b/docs/public/integrations/daytona.mdx @@ -198,6 +198,10 @@ The `lifecycle.auto_stop` setting tells Daytona to stop the sandbox after a peri auto_stop = "30m" ``` +When `auto_stop` is unset, Fabro applies a default of 120 minutes so a sandbox leaked by an interrupted run is still reclaimed. Set `auto_stop = "0s"` to disable auto-stop and let the sandbox run indefinitely. + +Daytona counts inactivity from the last sandbox interaction (a command, file operation, or other API call). Time an agent spends on LLM inference does not touch the sandbox, so intervals shorter than your longest inference call risk stopping the sandbox mid-run. + ## Server defaults When running via `fabro server start`, the server config at `~/.fabro/settings.toml` can set default Daytona settings for all runs. Run config TOML values override server defaults. Labels are **merged** — run config labels win on key collisions. The `network` setting uses simple override (run config replaces the server default entirely). diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 6371f23d2..91a0cc797 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -68,6 +68,12 @@ const DAYTONA_START_TIMEOUT: Duration = Duration::from_mins(1); /// deletion, temporary stdin files) so a stalled REST call cannot block /// cancellation/timeout paths indefinitely. const DAYTONA_CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +/// Auto-stop applied when `lifecycle.auto_stop` is unset. Omitting the field +/// would inherit Daytona's server-side default of 15 idle minutes, which is +/// shorter than a single long inference call and stops the sandbox mid-run; +/// 120 minutes clears any realistic call while still reclaiming sandboxes +/// leaked by a dead worker. An explicit `0` disables auto-stop entirely. +const DEFAULT_AUTO_STOP_INTERVAL_MINUTES: i32 = 120; /// Permissions a Daytona API key needs for Fabro's snapshot and sandbox flow. pub const REQUIRED_DAYTONA_PERMISSIONS: &[Permissions] = &[ @@ -727,7 +733,10 @@ impl DaytonaSandbox { daytona_sdk::SandboxBaseParams { name: Some(name), env_vars: Some(clean_bash_env(None)), - auto_stop_interval: self.config.auto_stop_interval, + auto_stop_interval: self + .config + .auto_stop_interval + .or(Some(DEFAULT_AUTO_STOP_INTERVAL_MINUTES)), labels: Some(managed_labels::merge_for_run( self.config.labels.as_ref(), self.run_id.as_ref(), @@ -2950,6 +2959,10 @@ mod tests { assert_eq!(params.ephemeral, Some(false)); assert_eq!(params.auto_delete_interval, Some(-1)); + assert_eq!( + params.auto_stop_interval, + Some(DEFAULT_AUTO_STOP_INTERVAL_MINUTES) + ); assert_eq!( params.env_vars, Some(HashMap::from([(BASH_ENV_VAR.to_string(), String::new())])) @@ -2963,6 +2976,27 @@ mod tests { ); } + #[tokio::test] + async fn base_params_passes_explicit_auto_stop_through() { + for interval in [0, 45] { + let sandbox = DaytonaSandbox::new( + DaytonaConfig { + auto_stop_interval: Some(interval), + ..DaytonaConfig::default() + }, + None, + None, + None, + None, + Some("dtn_test".to_string()), + ) + .await + .expect("sandbox config should be valid"); + + assert_eq!(sandbox.base_params().auto_stop_interval, Some(interval)); + } + } + #[tokio::test] async fn activate_skips_start_when_daytona_reports_started() { let server = MockServer::start_async().await; From 0eedb1798c0f9da34917ad5544571d6fb7f054ce Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 17:35:11 -0400 Subject: [PATCH 45/63] Treat Daytona state transitions as wait-and-retry in activate/start/stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Daytona rejects start/stop with HTTP 400 "State change in progress" while a lifecycle transition is in flight, and activate() only handled the Started and Starting states: any other state fell through to start(), which surfaced the rejection as a hard failure. A run died exactly this way when an inactivity auto-stop began seconds before the stage finished — activate() saw the sandbox mid-stop and failed the whole run 35ms later. The cleanup stop() then failed on the same rejection. Transitions finish on their own within seconds, so treat them as wait-and-retry conditions: - activate() now waits out a Stopping sandbox and dispatches on whatever state the transition lands on. - start() and stop() retry the rejected call within a bounded budget, re-inspecting state between attempts: a transition that lands on Started needs no further start, and one that lands on Stopped or Destroyed needs no further stop. All call sites go through these three provider methods, so no lifecycle-layer changes are needed. Co-Authored-By: Claude Fable 5 --- .../fabro-sandbox/src/daytona/mod.rs | 404 +++++++++++++++--- 1 file changed, 342 insertions(+), 62 deletions(-) diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 6371f23d2..22d3adbca 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -68,6 +68,12 @@ const DAYTONA_START_TIMEOUT: Duration = Duration::from_mins(1); /// deletion, temporary stdin files) so a stalled REST call cannot block /// cancellation/timeout paths indefinitely. const DAYTONA_CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +/// Budget for waiting out an in-flight Daytona lifecycle transition (for +/// example an auto-stop racing an activation) before giving up. Transitions +/// normally finish within seconds; the budget only bounds a wedged sandbox. +const DAYTONA_STATE_CHANGE_TIMEOUT: Duration = Duration::from_mins(2); +/// Poll interval while waiting out an in-flight Daytona lifecycle transition. +const DAYTONA_STATE_CHANGE_POLL_INTERVAL: Duration = Duration::from_secs(1); /// Permissions a Daytona API key needs for Fabro's snapshot and sandbox flow. pub const REQUIRED_DAYTONA_PERMISSIONS: &[Permissions] = &[ @@ -852,6 +858,118 @@ impl DaytonaSandbox { "Timed out waiting for snapshot '{name}' to become active" ))) } + + /// Start the sandbox, retrying while Daytona reports a lifecycle + /// transition in flight, up to `deadline`. + /// + /// Daytona rejects `start` with "state change in progress" while a + /// transition (such as an inactivity auto-stop) is still running. The + /// transition finishes on its own within seconds, so the rejection is a + /// wait-and-retry condition, not a failure. Between attempts the state is + /// re-inspected: a transition that lands on `Started` (a concurrent + /// activation won the race) needs no further start call. + async fn start_with_deadline(&self, deadline: time::Instant) -> crate::Result<()> { + self.emit(SandboxEvent::StartStarted { + provider: "daytona".into(), + }); + let start = Instant::now(); + let sandbox = self.sandbox()?; + loop { + match self.client.start(&sandbox.name).await { + Ok(_) => break, + Err(e) if is_state_change_in_progress(&e) && time::Instant::now() < deadline => { + tracing::debug!( + "Daytona start rejected while a state change is in progress; retrying" + ); + time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; + if let Ok(current) = self.client.get(&sandbox.name).await { + if current.state == Some(SandboxState::Started) { + break; + } + } + } + Err(e) => { + let err = crate::Error::context("Failed to start Daytona sandbox", e); + self.emit(SandboxEvent::StartFailed { + provider: "daytona".into(), + error: err.to_string(), + causes: err.causes(), + }); + return Err(err); + } + } + } + if let Err(err) = Self::probe_bash(sandbox).await { + self.emit(SandboxEvent::StartFailed { + provider: "daytona".into(), + error: err.to_string(), + causes: err.causes(), + }); + return Err(err); + } + let duration_ms = elapsed_ms(start); + self.emit(SandboxEvent::StartCompleted { + provider: "daytona".into(), + duration_ms, + }); + Ok(()) + } + + /// Stop the sandbox, retrying while Daytona reports a lifecycle + /// transition in flight, up to `deadline`. + /// + /// The in-flight transition may be the stop itself (an inactivity + /// auto-stop): between attempts the state is re-inspected, and a sandbox + /// that landed on `Stopped` or `Destroyed` needs no further stop call. + async fn stop_with_deadline(&self, deadline: time::Instant) -> crate::Result<()> { + self.emit(SandboxEvent::StopStarted { + provider: "daytona".into(), + }); + let start = Instant::now(); + let sandbox = self.sandbox()?; + loop { + match self.client.stop(&sandbox.name).await { + Ok(_) => break, + Err(e) if is_state_change_in_progress(&e) && time::Instant::now() < deadline => { + tracing::debug!( + "Daytona stop rejected while a state change is in progress; retrying" + ); + time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; + if let Ok(current) = self.client.get(&sandbox.name).await { + if matches!( + current.state, + Some(SandboxState::Stopped | SandboxState::Destroyed) + ) { + break; + } + } + } + Err(e) => { + let err = crate::Error::context("Failed to stop Daytona sandbox", e); + self.emit(SandboxEvent::StopFailed { + provider: "daytona".into(), + error: err.to_string(), + causes: err.causes(), + }); + return Err(err); + } + } + } + let duration_ms = elapsed_ms(start); + self.emit(SandboxEvent::StopCompleted { + provider: "daytona".into(), + duration_ms, + }); + Ok(()) + } +} + +/// Whether a Daytona API error reports a lifecycle transition in flight +/// (HTTP 400 "State change in progress" on start/stop). +fn is_state_change_in_progress(err: &DaytonaError) -> bool { + err.to_string() + .to_ascii_lowercase() + .contains("state change in progress") } /// Detect the git remote URL and current branch from a local repository. @@ -1336,76 +1454,51 @@ impl Sandbox for DaytonaSandbox { } async fn start(&self) -> crate::Result<()> { - self.emit(SandboxEvent::StartStarted { - provider: "daytona".into(), - }); - let start = Instant::now(); - let sandbox = self.sandbox()?; - if let Err(e) = self.client.start(&sandbox.name).await { - let err = crate::Error::context("Failed to start Daytona sandbox", e); - self.emit(SandboxEvent::StartFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - if let Err(err) = Self::probe_bash(sandbox).await { - self.emit(SandboxEvent::StartFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - let duration_ms = elapsed_ms(start); - self.emit(SandboxEvent::StartCompleted { - provider: "daytona".into(), - duration_ms, - }); - Ok(()) + self.start_with_deadline(time::Instant::now() + DAYTONA_STATE_CHANGE_TIMEOUT) + .await } async fn activate(&self) -> crate::Result<()> { let sandbox = self.sandbox()?; - let current = self.client.get(&sandbox.name).await.map_err(|e| { - crate::Error::context("Failed to inspect Daytona sandbox before activation", e) - })?; - if current.state == Some(SandboxState::Started) { - return Ok(()); + let deadline = time::Instant::now() + DAYTONA_STATE_CHANGE_TIMEOUT; + loop { + let current = self.client.get(&sandbox.name).await.map_err(|e| { + crate::Error::context("Failed to inspect Daytona sandbox before activation", e) + })?; + match current.state { + Some(SandboxState::Started) => return Ok(()), + Some(SandboxState::Starting) => { + return current + .wait_for_start(Some(DAYTONA_START_TIMEOUT)) + .await + .map_err(|e| { + crate::Error::context( + "Failed to wait for Daytona sandbox activation", + e, + ) + }); + } + // An inactivity auto-stop can be in flight when a stage + // returns after a long period with no sandbox traffic (LLM + // inference generates none). Wait out the transition and + // dispatch on whatever state it lands on. + Some(SandboxState::Stopping) => { + if time::Instant::now() >= deadline { + return Err(crate::Error::message(format!( + "Daytona sandbox stop still in progress after {}s", + DAYTONA_STATE_CHANGE_TIMEOUT.as_secs() + ))); + } + time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; + } + _ => return self.start_with_deadline(deadline).await, + } } - if current.state == Some(SandboxState::Starting) { - return current - .wait_for_start(Some(DAYTONA_START_TIMEOUT)) - .await - .map_err(|e| { - crate::Error::context("Failed to wait for Daytona sandbox activation", e) - }); - } - self.start().await } async fn stop(&self) -> crate::Result<()> { - self.emit(SandboxEvent::StopStarted { - provider: "daytona".into(), - }); - let start = Instant::now(); - let sandbox = self.sandbox()?; - if let Err(e) = self.client.stop(&sandbox.name).await { - let err = crate::Error::context("Failed to stop Daytona sandbox", e); - self.emit(SandboxEvent::StopFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - let duration_ms = elapsed_ms(start); - self.emit(SandboxEvent::StopCompleted { - provider: "daytona".into(), - duration_ms, - }); - Ok(()) + self.stop_with_deadline(time::Instant::now() + DAYTONA_STATE_CHANGE_TIMEOUT) + .await } async fn delete(&self) -> crate::Result<()> { @@ -3064,6 +3157,193 @@ mod tests { start_sandbox.assert_calls_async(0).await; } + #[tokio::test] + async fn activate_waits_out_a_stop_in_progress() { + let server = MockServer::start_async().await; + let response_count = Arc::new(AtomicU32::new(0)); + let get_sandbox = server + .mock_async({ + let response_count = Arc::clone(&response_count); + move |when, then| { + when.method(GET) + .path("/sandbox/test-sandbox") + .header("authorization", "Bearer dtn_test"); + then.respond_with(move |_| { + let state = if response_count.fetch_add(1, Ordering::Relaxed) == 1 { + SandboxState::Stopping + } else { + SandboxState::Started + }; + HttpMockResponse::builder() + .status(200) + .header("content-type", "application/json") + .body(sandbox_body("test-sandbox", state).to_string()) + .build() + }); + } + }) + .await; + let start_sandbox = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox/test-sandbox/start") + .header("authorization", "Bearer dtn_test"); + then.status(200) + .header("content-type", "application/json") + .json_body(sandbox_body("test-sandbox", SandboxState::Started)); + }) + .await; + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("test-sandbox") + .await + .expect("test sandbox should load"); + sandbox + .sandbox + .set(sdk_sandbox) + .expect("test sandbox should initialize once"); + + let get_calls_before = get_sandbox.calls_async().await; + sandbox + .activate() + .await + .expect("an in-progress stop should be waited out"); + + assert_eq!(get_sandbox.calls_async().await, get_calls_before + 2); + start_sandbox.assert_calls_async(0).await; + } + + #[tokio::test] + async fn stop_succeeds_when_a_pending_auto_stop_finishes_first() { + let server = MockServer::start_async().await; + let response_count = Arc::new(AtomicU32::new(0)); + let get_sandbox = server + .mock_async({ + let response_count = Arc::clone(&response_count); + move |when, then| { + when.method(GET) + .path("/sandbox/test-sandbox") + .header("authorization", "Bearer dtn_test"); + then.respond_with(move |_| { + let state = if response_count.fetch_add(1, Ordering::Relaxed) == 0 { + SandboxState::Started + } else { + SandboxState::Stopped + }; + HttpMockResponse::builder() + .status(200) + .header("content-type", "application/json") + .body(sandbox_body("test-sandbox", state).to_string()) + .build() + }); + } + }) + .await; + let stop_sandbox = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox/test-sandbox/stop") + .header("authorization", "Bearer dtn_test"); + then.status(400) + .header("content-type", "application/json") + .json_body(serde_json::json!({ + "message": "Sandbox state change in progress", + "statusCode": 400 + })); + }) + .await; + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("test-sandbox") + .await + .expect("test sandbox should load"); + sandbox + .sandbox + .set(sdk_sandbox) + .expect("test sandbox should initialize once"); + + let get_calls_before = get_sandbox.calls_async().await; + sandbox + .stop() + .await + .expect("a stop already in flight should count as stopped"); + + stop_sandbox.assert_calls_async(1).await; + assert_eq!(get_sandbox.calls_async().await, get_calls_before + 1); + } + + #[tokio::test] + async fn start_surfaces_state_change_rejection_after_the_deadline() { + let server = MockServer::start_async().await; + let _get_sandbox = server + .mock_async(|when, then| { + when.method(GET) + .path("/sandbox/test-sandbox") + .header("authorization", "Bearer dtn_test"); + then.status(200) + .header("content-type", "application/json") + .json_body(sandbox_body("test-sandbox", SandboxState::Stopping)); + }) + .await; + let start_sandbox = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox/test-sandbox/start") + .header("authorization", "Bearer dtn_test"); + then.status(400) + .header("content-type", "application/json") + .json_body(serde_json::json!({ + "message": "Sandbox state change in progress", + "statusCode": 400 + })); + }) + .await; + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("test-sandbox") + .await + .expect("test sandbox should load"); + sandbox + .sandbox + .set(sdk_sandbox) + .expect("test sandbox should initialize once"); + + let err = sandbox + .start_with_deadline(time::Instant::now() + Duration::from_millis(1500)) + .await + .expect_err("a state change that outlives the deadline should fail"); + + assert!( + start_sandbox.calls_async().await >= 2, + "start should be retried while the deadline allows" + ); + assert!( + err.causes() + .iter() + .any(|cause| cause.to_ascii_lowercase().contains("state change in progress")), + "error should carry the Daytona rejection: {err}" + ); + } + + #[test] + fn state_change_in_progress_matcher_ignores_case_and_context() { + assert!(is_state_change_in_progress(&DaytonaError::api( + 400, + "Sandbox state change in progress" + ))); + assert!(is_state_change_in_progress(&DaytonaError::api( + 400, + "State Change In Progress" + ))); + assert!(!is_state_change_in_progress(&DaytonaError::api( + 400, + "Sandbox already started" + ))); + } + #[tokio::test] async fn base_params_merges_managed_daytona_labels() { let run_id: RunId = "01HY0000000000000000000000".parse().unwrap(); From f88df59163ad287a093bf26dc600c13be5343daa Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 17:39:19 -0400 Subject: [PATCH 46/63] Classify sandbox state-change rejections as transient infra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Daytona "Sandbox state change in progress" rejection surfacing through the pipeline lifecycle path ("Pipeline lifecycle operation failed") matched no transient-infra hint, so the run failure was categorized deterministic. The condition is a provider lifecycle transition that finishes on its own — the definition of transient infrastructure — and the deterministic label misinforms retry machinery and anyone reading the failure. Add two transient-infra hints: the provider rejection ("state change in progress") and the bounded-wait timeout an activation reports when a stop transition outlives its budget ("sandbox stop still in progress"). Co-Authored-By: Claude Fable 5 --- lib/components/fabro-workflow/src/error.rs | 35 +++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/components/fabro-workflow/src/error.rs b/lib/components/fabro-workflow/src/error.rs index f8c08ca15..21833a8df 100644 --- a/lib/components/fabro-workflow/src/error.rs +++ b/lib/components/fabro-workflow/src/error.rs @@ -84,6 +84,8 @@ const TRANSIENT_INFRA_HINTS: &[&str] = &[ "cross-device link", "invalid cross-device link", "os error 18", + "state change in progress", + "sandbox stop still in progress", ]; const BUDGET_EXHAUSTED_HINTS: &[&str] = &[ @@ -807,6 +809,18 @@ mod tests { assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } + #[test] + fn engine_error_with_sandbox_state_change_cause_classifies_transient() { + let source = TestOuterError { + message: "Failed to start Daytona sandbox", + source: TestCause("Sandbox state change in progress"), + }; + let err = Error::engine_with_source("Pipeline lifecycle operation failed", source); + + assert_eq!(err.failure_category(), FailureCategory::TransientInfra); + assert!(err.is_retryable()); + } + #[test] fn handler_error_display() { let err = Error::handler("LLM call failed"); @@ -1281,7 +1295,7 @@ mod tests { #[test] fn transient_infra_hints_count() { - assert_eq!(TRANSIENT_INFRA_HINTS.len(), 38); + assert_eq!(TRANSIENT_INFRA_HINTS.len(), 40); } #[test] @@ -1450,6 +1464,25 @@ mod tests { ); } + #[test] + fn classify_reason_sandbox_state_change_in_progress() { + assert_eq!( + classify_failure_reason( + "Pipeline lifecycle operation failed: failed to activate sandbox after node \ + attempt survey: Failed to start Daytona sandbox: Sandbox state change in progress" + ), + FailureCategory::TransientInfra + ); + } + + #[test] + fn classify_reason_sandbox_stop_still_in_progress() { + assert_eq!( + classify_failure_reason("Daytona sandbox stop still in progress after 120s"), + FailureCategory::TransientInfra + ); + } + #[test] fn classify_reason_500() { assert_eq!( From 2456356a9fd4bb3181880ec4740f8d6de2ba3786 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 10:19:27 -0400 Subject: [PATCH 47/63] Label sandbox git execs with git_op tracing spans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sandbox exec logs previously required command_len fingerprinting to tell a push from a credential refresh or a checkpoint commit. The shared git helpers now instrument their futures with a git_op span, so Daytona's and Docker's `exec_command: entered` lines inherit the operation label and the log renders as `git_op{op=push}: exec_command: entered timeout_ms=...`. Ops: push (git_push_via_exec), refresh-credentials (both providers' refresh_push_credentials), checkpoint-commit (checked_git_checkpoint), fetch (fetch_source_run_ref), and metadata-push (the run-metadata snapshot write). Spans are attached with #[tracing::instrument] — attached to the future, never an entered() guard held across an await — so they follow the task across worker threads. No trait or signature changes. Plan: .ai/plans/git-push-token-resilience.md (PR 3: item 10). Co-Authored-By: Claude Fable 5 --- lib/components/fabro-sandbox/src/daytona/mod.rs | 1 + lib/components/fabro-sandbox/src/docker.rs | 1 + lib/components/fabro-sandbox/src/sandbox.rs | 2 ++ lib/components/fabro-workflow/src/run_metadata.rs | 1 + lib/components/fabro-workflow/src/sandbox_git.rs | 1 + 5 files changed, 6 insertions(+) diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 0375a51cf..5a6da1d6c 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -1542,6 +1542,7 @@ impl Sandbox for DaytonaSandbox { Ok(Some((preview.url, headers))) } + #[tracing::instrument(name = "git_op", skip_all, fields(op = "refresh-credentials"))] async fn refresh_push_credentials(&self) -> crate::Result { if !self.repo_cloned() { return Ok(RefreshOutcome::Skipped); diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index d69dcca4a..21530cc94 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -2180,6 +2180,7 @@ impl Sandbox for DockerSandbox { self.origin_url.get().map(String::as_str) } + #[tracing::instrument(name = "git_op", skip_all, fields(op = "refresh-credentials"))] async fn refresh_push_credentials(&self) -> crate::Result { if !self.repo_cloned() { return Ok(RefreshOutcome::Skipped); diff --git a/lib/components/fabro-sandbox/src/sandbox.rs b/lib/components/fabro-sandbox/src/sandbox.rs index 31a873300..c70c13ee9 100644 --- a/lib/components/fabro-sandbox/src/sandbox.rs +++ b/lib/components/fabro-sandbox/src/sandbox.rs @@ -1465,6 +1465,7 @@ pub async fn setup_git_via_exec( }) } +#[tracing::instrument(name = "git_op", skip_all, fields(op = "fetch"))] pub(crate) async fn fetch_source_run_ref( sandbox: &dyn Sandbox, source_run_id: &str, @@ -1513,6 +1514,7 @@ pub(crate) async fn fetch_source_run_ref( /// Helper for sandbox implementations that manage git internally. /// Pushes a refspec to origin via exec_command inside the sandbox. +#[tracing::instrument(name = "git_op", skip_all, fields(op = "push"))] pub async fn git_push_via_exec(sandbox: &dyn Sandbox, refspec: &str) -> crate::Result<()> { if let Err(e) = sandbox.refresh_push_credentials().await { tracing::warn!( diff --git a/lib/components/fabro-workflow/src/run_metadata.rs b/lib/components/fabro-workflow/src/run_metadata.rs index 74be989df..39d686857 100644 --- a/lib/components/fabro-workflow/src/run_metadata.rs +++ b/lib/components/fabro-workflow/src/run_metadata.rs @@ -184,6 +184,7 @@ impl RunMetadataWriterHandle { .unwrap() } + #[tracing::instrument(name = "git_op", skip_all, fields(op = "metadata-push"))] pub(crate) async fn write_snapshot( &self, dump: &RunDump, diff --git a/lib/components/fabro-workflow/src/sandbox_git.rs b/lib/components/fabro-workflow/src/sandbox_git.rs index 914153955..c6084a977 100644 --- a/lib/components/fabro-workflow/src/sandbox_git.rs +++ b/lib/components/fabro-workflow/src/sandbox_git.rs @@ -158,6 +158,7 @@ pub async fn git_checkpoint( clippy::too_many_arguments, reason = "Checkpointing needs explicit run metadata, checkpoint settings, and author inputs." )] +#[tracing::instrument(name = "git_op", skip_all, fields(op = "checkpoint-commit"))] pub(crate) async fn checked_git_checkpoint( runtime: &SandboxGitRuntime, sandbox: &dyn Sandbox, From 4eea9b816a365e48755bb2d43665d2c768acba7f Mon Sep 17 00:00:00 2001 From: Release Repro Date: Thu, 20 Aug 2026 19:38:10 -0400 Subject: [PATCH 48/63] Clarify visit-limit counter comment --- lib/foundation/fabro-core/src/executor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foundation/fabro-core/src/executor.rs b/lib/foundation/fabro-core/src/executor.rs index f6e3a6aa3..6701e30be 100644 --- a/lib/foundation/fabro-core/src/executor.rs +++ b/lib/foundation/fabro-core/src/executor.rs @@ -181,8 +181,8 @@ impl Executor { // Check visit limits before entry: a node with a limit of N may // execute N times, matching the documented contract. The count - // covers completed entries only, so the refused visit is not - // reported as one. + // covers previously admitted entries, so the refused visit is + // not reported as one. let visits = state.visits(node.id()); if let Some(max) = node.max_visits() { if visits >= max { From f8a82d6865eea4b5d9e7c91e87f10043857352ac Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 19:56:06 -0400 Subject: [PATCH 49/63] fix: harden GitHub token refresh handling --- lib/components/fabro-github/Cargo.toml | 3 + lib/components/fabro-github/src/lib.rs | 3 + .../fabro-github/src/test_support.rs | 26 ++ .../fabro-github/src/token_source.rs | 89 +++++- lib/components/fabro-sandbox/Cargo.toml | 1 + .../fabro-sandbox/src/daytona/mod.rs | 24 +- lib/components/fabro-sandbox/src/docker.rs | 25 +- lib/components/fabro-sandbox/src/error.rs | 14 + .../fabro-sandbox/src/provider/docker.rs | 9 +- .../fabro-sandbox/src/push_credentials.rs | 81 +++--- lib/components/fabro-sandbox/src/sandbox.rs | 52 +++- .../fabro-sandbox/src/sandbox_spec.rs | 2 +- lib/components/fabro-workflow/Cargo.toml | 1 + .../fabro-workflow/src/github_token_source.rs | 274 ------------------ .../fabro-workflow/src/handler/command.rs | 7 +- .../fabro-workflow/src/handler/llm/acp.rs | 191 ++++++------ lib/components/fabro-workflow/src/lib.rs | 1 - .../fabro-workflow/src/pipeline/initialize.rs | 33 +-- .../fabro-workflow/src/run_metadata.rs | 13 +- lib/components/fabro-workflow/src/services.rs | 28 +- 20 files changed, 365 insertions(+), 512 deletions(-) create mode 100644 lib/components/fabro-github/src/test_support.rs delete mode 100644 lib/components/fabro-workflow/src/github_token_source.rs diff --git a/lib/components/fabro-github/Cargo.toml b/lib/components/fabro-github/Cargo.toml index 788c6efe1..8f755dd79 100644 --- a/lib/components/fabro-github/Cargo.toml +++ b/lib/components/fabro-github/Cargo.toml @@ -9,6 +9,9 @@ description = "GitHub App authentication and API helpers for Fabro" [lib] doctest = false +[features] +test-support = [] + [lints] workspace = true diff --git a/lib/components/fabro-github/src/lib.rs b/lib/components/fabro-github/src/lib.rs index 9973c4377..a38c6c8ce 100644 --- a/lib/components/fabro-github/src/lib.rs +++ b/lib/components/fabro-github/src/lib.rs @@ -11,6 +11,9 @@ use tokio::process::Command; pub mod token_source; +#[cfg(any(test, feature = "test-support"))] +pub mod test_support; + pub const GITHUB_API_BASE_URL: &str = "https://api.github.com"; /// Returns the GitHub API base URL, allowing override via `GITHUB_BASE_URL` env diff --git a/lib/components/fabro-github/src/test_support.rs b/lib/components/fabro-github/src/test_support.rs new file mode 100644 index 000000000..09cca93ce --- /dev/null +++ b/lib/components/fabro-github/src/test_support.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use crate::InstallationToken; +use crate::token_source::{InstallationTokenMinter as InnerMinter, InstallationTokenSource}; + +#[async_trait::async_trait] +pub trait InstallationTokenMinter: Send + Sync { + async fn mint(&self) -> anyhow::Result; +} + +struct TestMinterAdapter(Arc); + +#[async_trait::async_trait] +impl InnerMinter for TestMinterAdapter { + async fn mint(&self) -> anyhow::Result { + self.0.mint().await + } +} + +#[must_use] +pub fn installation_token_source( + repo: impl Into, + minter: Arc, +) -> Arc { + InstallationTokenSource::with_minter(repo.into(), Box::new(TestMinterAdapter(minter))) +} diff --git a/lib/components/fabro-github/src/token_source.rs b/lib/components/fabro-github/src/token_source.rs index 11743e077..5af872af6 100644 --- a/lib/components/fabro-github/src/token_source.rs +++ b/lib/components/fabro-github/src/token_source.rs @@ -1,12 +1,10 @@ //! Cached GitHub installation-token source. //! -//! One [`InstallationTokenSource`] serves every GitHub-token consumer for an -//! origin repository — the clone-based sandbox providers and the run-metadata -//! writer share a single source, so "reuse a token until near expiry" is the -//! default behavior instead of a per-call-site special case. Reusing mature -//! tokens keeps consumers out of GitHub's token-replication lag window, where -//! a token minted milliseconds earlier is rejected with 404 "Repository not -//! found" or an authentication failure. +//! One [`InstallationTokenSource`] can serve GitHub-token consumers that share +//! a repository and permission scope. Reusing mature tokens keeps consumers +//! out of GitHub's token-replication lag window, where a token minted +//! milliseconds earlier is rejected with 404 "Repository not found" or an +//! authentication failure. //! //! The source also reports *provenance*: when it minted the token it returned, //! and which mint generation it belongs to. Retry classification, logging, and @@ -138,7 +136,7 @@ pub struct ResolvedToken { /// Mints installation tokens for [`InstallationTokenSource`]. Abstracted so /// tests can script mint results without HTTP. #[async_trait::async_trait] -pub trait InstallationTokenMinter: Send + Sync { +pub(crate) trait InstallationTokenMinter: Send + Sync { async fn mint(&self) -> anyhow::Result; } @@ -228,6 +226,16 @@ impl InstallationTokenSource { let normalized = crate::normalize_repo_origin_url(origin_url); let (owner, repo) = crate::parse_github_owner_repo(&normalized) .context("parsing GitHub origin for token source")?; + Self::for_repository(creds, owner, repo, permissions) + } + + /// Build a source for an already parsed GitHub repository. + pub fn for_repository( + creds: &GitHubCredentials, + owner: String, + repo: String, + permissions: serde_json::Value, + ) -> anyhow::Result> { let repo_display = format!("{owner}/{repo}"); let state = match creds { GitHubCredentials::Pat(token) => SourceState::Pat(SecretString::new(token.clone())), @@ -255,9 +263,28 @@ impl InstallationTokenSource { })) } - /// Build a minting source over a custom minter. For tests. + /// Build a source for a personal access token. #[must_use] - pub fn with_minter(repo: String, minter: Box) -> Arc { + pub fn pat(token: String) -> Arc { + Arc::new(Self { + repo: String::new(), + state: SourceState::Pat(SecretString::new(token)), + }) + } + + /// Build a source for a pre-minted installation token. + #[must_use] + pub fn installation(token: InstallationToken) -> Arc { + Arc::new(Self { + repo: String::new(), + state: SourceState::Installation(token), + }) + } + + /// Build a minting source over a custom minter. + #[cfg(any(test, feature = "test-support"))] + #[must_use] + pub(crate) fn with_minter(repo: String, minter: Box) -> Arc { Arc::new(Self { repo, state: SourceState::App { @@ -296,7 +323,27 @@ impl InstallationTokenSource { return Ok(resolved); } } - self.mint_locked(minter.as_ref(), &mut cache).await + match self.mint_locked(minter.as_ref(), &mut cache).await { + Ok(resolved) => Ok(resolved), + Err(err) => { + if let Some(cached) = cache.as_ref() { + if cached.token.valid_token().is_ok() { + tracing::warn!( + error = %format!("{err:#}"), + repo = %self.repo, + generation = cached.generation, + expires_at = %cached.token.expires_at, + "GitHub installation token refresh failed; using cached token" + ); + return Ok(cached.resolved(TokenProvenance::Reused { + minted_at: cached.minted_at, + expires_at: cached.token.expires_at, + })); + } + } + Err(err) + } + } } } } @@ -517,6 +564,26 @@ mod tests { assert_eq!(second.token.expose(), "ghs_gen2"); } + #[tokio::test] + async fn resolve_uses_a_valid_cached_token_when_refresh_fails() { + let (source, minter) = mintable(vec![ + MintAction::Token("ghs_gen1", Utc::now() + chrono::Duration::minutes(5)), + MintAction::Error("mint failed"), + ]); + + let first = source.resolve().await.unwrap(); + let second = source.resolve().await.unwrap(); + + assert_eq!(minter.calls(), 2); + assert_eq!(first.snapshot.generation, 1); + assert_eq!(second.snapshot.generation, 1); + assert!(matches!( + second.snapshot.provenance, + TokenProvenance::Reused { .. } + )); + assert_eq!(second.token.expose(), "ghs_gen1"); + } + #[tokio::test] async fn concurrent_resolves_share_one_generation() { // Single mint in the script: a second mint would panic on an empty diff --git a/lib/components/fabro-sandbox/Cargo.toml b/lib/components/fabro-sandbox/Cargo.toml index 7153e3ea1..251e6bd77 100644 --- a/lib/components/fabro-sandbox/Cargo.toml +++ b/lib/components/fabro-sandbox/Cargo.toml @@ -64,6 +64,7 @@ futures-util = { workspace = true, optional = true } rustls = { version = "0.23", default-features = false, features = ["std", "ring"], optional = true } [dev-dependencies] +fabro-github = { path = "../fabro-github", features = ["test-support"] } tokio = { workspace = true, features = ["test-util", "macros"] } tempfile = "3" serde_json.workspace = true diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 79658120e..e78877ef7 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -336,7 +336,6 @@ pub struct DaytonaSandbox { config: DaytonaConfig, client: daytona_sdk::Client, api_key: Option, - github_app: Option, push_credentials: PushCredentialState, sandbox: OnceCell, snapshot_name: OnceCell, @@ -379,7 +378,6 @@ impl DaytonaSandbox { config, client, api_key, - github_app, push_credentials, sandbox: OnceCell::new(), snapshot_name: OnceCell::new(), @@ -432,7 +430,6 @@ impl DaytonaSandbox { config: DaytonaConfig::default(), client, api_key, - github_app: None, push_credentials: PushCredentialState::new(None), sandbox: sandbox_cell, snapshot_name: OnceCell::new(), @@ -1072,10 +1069,11 @@ impl Sandbox for DaytonaSandbox { // against the clone token instead of believing nothing was // ever embedded. let resolved_token = match self.push_credentials.source() { - Some(source) => Some(source.mint_for_clone().await.map_err(|e| { - let err = crate::Error::message(format!( - "Failed to get GitHub App credentials for clone: {e}" - )); + Some(source) => Some(source.mint_for_clone().await.map_err(|source| { + let err = crate::Error::context_anyhow( + "Failed to get GitHub App credentials for clone", + source, + ); self.emit(SandboxEvent::GitCloneFailed { url: origin_url.clone(), error: err.to_string(), @@ -1295,7 +1293,7 @@ impl Sandbox for DaytonaSandbox { } Err(e) => { tracing::warn!( - origin = %origin_url, + origin = %fabro_redact::redacted_url_for_log(&origin_url), error = %e, "Failed to build authenticated origin URL — \ subsequent git push from this sandbox will fail" @@ -1304,7 +1302,7 @@ impl Sandbox for DaytonaSandbox { } } } - Err(e) if self.github_app.is_none() => { + Err(e) if self.push_credentials.source().is_none() => { let err = crate::Error::context( "Git clone failed. If this is a private repository, \ configure a GitHub App with `fabro install` and install it \ @@ -1554,9 +1552,10 @@ impl Sandbox for DaytonaSandbox { let result = self .exec_command(&cmd, 10_000, None, None, None) .await - .map_err(|_| { - crate::Error::message( - "Failed to refresh push credentials: set_url_exec_failed", + .map_err(|err| { + crate::Error::context( + "Failed to refresh push credentials: set origin URL", + err, ) })?; if !result.is_success() { @@ -2762,7 +2761,6 @@ mod tests { config, client, api_key: Some(api_key.to_string()), - github_app: None, push_credentials: PushCredentialState::new(None), sandbox: OnceCell::new(), snapshot_name: OnceCell::new(), diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index 532e7cbb2..70c4da230 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -133,7 +133,6 @@ impl Default for DockerSandboxOptions { pub struct DockerSandbox { docker: Docker, config: DockerSandboxOptions, - github_app: Option, push_credentials: PushCredentialState, run_id: Option, clone_origin_url: Option, @@ -164,7 +163,7 @@ enum ContainerStartAction { impl DockerSandbox { pub fn new( config: DockerSandboxOptions, - github_app: Option, + github_app: Option<&GitHubCredentials>, run_id: Option, clone_origin_url: Option, clone_branch: Option, @@ -183,19 +182,18 @@ impl DockerSandbox { fn with_docker_client( docker: Docker, config: DockerSandboxOptions, - github_app: Option, + github_app: Option<&GitHubCredentials>, run_id: Option, clone_origin_url: Option, clone_branch: Option, ) -> crate::Result { let push_credentials = PushCredentialState::new(push_credentials::build_token_source( - github_app.as_ref(), + github_app, clone_origin_url.as_deref(), )?); Ok(Self { docker, config, - github_app, push_credentials, run_id, clone_origin_url, @@ -724,7 +722,7 @@ impl DockerSandbox { ) -> crate::Error { let error = result .into_exec_error_with_redactor("git clone", |output| redact_auth_url(output, auth_url)); - let message = if self.github_app.is_none() { + let message = if self.push_credentials.source().is_none() { "Git clone failed. If this is a private repository, configure a GitHub App with \ `fabro install` and install it for your organization." } else { @@ -753,10 +751,8 @@ impl DockerSandbox { // the shared source, so the first refresh compares against the clone // token instead of believing nothing was ever embedded. let resolved_token = match self.push_credentials.source() { - Some(source) => Some(source.mint_for_clone().await.map_err(|e| { - crate::Error::message(format!( - "Failed to get GitHub App credentials for clone: {e}" - )) + Some(source) => Some(source.mint_for_clone().await.map_err(|err| { + crate::Error::context_anyhow("Failed to get GitHub App credentials for clone", err) })?), None => None, }; @@ -767,10 +763,11 @@ impl DockerSandbox { let auth_url = match &resolved_token { Some(token) => Some( fabro_github::embed_token_in_url(&origin_url, token.token.expose()).map_err( - |e| { - crate::Error::message(format!( - "Failed to get GitHub App credentials for clone: {e}" - )) + |err| { + crate::Error::context_anyhow( + "Failed to build authenticated GitHub clone URL", + err, + ) }, )?, ), diff --git a/lib/components/fabro-sandbox/src/error.rs b/lib/components/fabro-sandbox/src/error.rs index 8d6bf0d73..76f096d4b 100644 --- a/lib/components/fabro-sandbox/src/error.rs +++ b/lib/components/fabro-sandbox/src/error.rs @@ -18,6 +18,13 @@ pub enum Error { source: Box, }, + #[error("{message}")] + AnyhowContext { + message: String, + #[source] + source: anyhow::Error, + }, + #[cfg(feature = "docker")] #[error("Failed to connect to Docker daemon")] DockerConnect { @@ -68,6 +75,13 @@ impl Error { } } + pub fn context_anyhow(message: impl Into, source: anyhow::Error) -> Self { + Self::AnyhowContext { + message: message.into(), + source, + } + } + pub fn exec(label: impl Into, result: ExecResult) -> Self { Self::Exec { label: label.into(), diff --git a/lib/components/fabro-sandbox/src/provider/docker.rs b/lib/components/fabro-sandbox/src/provider/docker.rs index f1f8bb725..865e361be 100644 --- a/lib/components/fabro-sandbox/src/provider/docker.rs +++ b/lib/components/fabro-sandbox/src/provider/docker.rs @@ -98,8 +98,13 @@ impl SandboxProvider for DockerSandboxProvider { )); }; - let sandbox = - DockerSandbox::new(config, github_app, run_id, clone_origin_url, clone_branch)?; + let sandbox = DockerSandbox::new( + config, + github_app.as_ref(), + run_id, + clone_origin_url, + clone_branch, + )?; sandbox.initialize().await?; let container_id = sandbox.container_identifier()?.to_string(); self.get(&container_id).await?.ok_or_else(|| { diff --git a/lib/components/fabro-sandbox/src/push_credentials.rs b/lib/components/fabro-sandbox/src/push_credentials.rs index 90116317f..2c0496544 100644 --- a/lib/components/fabro-sandbox/src/push_credentials.rs +++ b/lib/components/fabro-sandbox/src/push_credentials.rs @@ -15,7 +15,7 @@ use fabro_github::token_source::{InstallationTokenSource, ResolvedToken}; use fabro_redact::DisplaySafeUrl; use tokio::sync::Mutex; -use crate::sandbox::{RefreshOutcome, RemoteCredentialAction}; +use crate::sandbox::RefreshOutcome; /// Build the shared installation-token source for a clone-based sandbox. /// @@ -33,18 +33,19 @@ pub(crate) fn build_token_source( return Ok(None); }; let normalized = fabro_github::normalize_repo_origin_url(origin_url); - if fabro_github::parse_github_owner_repo(&normalized).is_err() { + let Ok((owner, repo)) = fabro_github::parse_github_owner_repo(&normalized) else { // Non-GitHub origins never clone in these providers, so there is no // remote to keep credentials fresh for. return Ok(None); - } - InstallationTokenSource::for_origin( + }; + InstallationTokenSource::for_repository( creds, - &normalized, + owner, + repo, serde_json::json!({ "contents": "write" }), ) .map(Some) - .map_err(|err| crate::Error::message(format!("Failed to build GitHub token source: {err:#}"))) + .map_err(|err| crate::Error::context_anyhow("Failed to build GitHub token source", err)) } /// Push-credential state one provider instance tracks for its `origin` @@ -122,8 +123,9 @@ impl PushCredentialState { "GitHub token refresh failed and no credentials were ever embedded" ); } - return Err(crate::Error::message( - "Failed to refresh push credentials: token_mint_failed", + return Err(crate::Error::context_anyhow( + "Failed to refresh push credentials", + err, )); } }; @@ -131,22 +133,16 @@ impl PushCredentialState { .as_ref() .is_some_and(|prev| prev.snapshot.generation == resolved.snapshot.generation) { - return Ok(RefreshOutcome { - action: RemoteCredentialAction::Unchanged, - token: Some(resolved.snapshot), - }); + return Ok(RefreshOutcome::unchanged(resolved.snapshot)); } let auth_url = fabro_github::embed_token_in_url(origin_url, resolved.token.expose()) .map_err(|err| { - crate::Error::message(format!("Failed to build authenticated origin URL: {err:#}")) + crate::Error::context_anyhow("Failed to build authenticated origin URL", err) })?; set_url(auth_url).await?; let snapshot = resolved.snapshot; *embedded = Some(resolved); - Ok(RefreshOutcome { - action: RemoteCredentialAction::Embedded, - token: Some(snapshot), - }) + Ok(RefreshOutcome::embedded(snapshot)) } } @@ -156,9 +152,10 @@ mod tests { use chrono::Utc; use fabro_github::InstallationToken; - use fabro_github::token_source::InstallationTokenMinter; + use fabro_github::test_support::{InstallationTokenMinter, installation_token_source}; use super::*; + use crate::sandbox::RemoteCredentialAction; struct FixedMinter { calls: AtomicUsize, @@ -186,9 +183,9 @@ mod tests { } fn minting_state(ttl: chrono::Duration) -> PushCredentialState { - PushCredentialState::new(Some(InstallationTokenSource::with_minter( - "owner/repo".to_string(), - Box::new(FixedMinter { + PushCredentialState::new(Some(installation_token_source( + "owner/repo", + Arc::new(FixedMinter { calls: AtomicUsize::new(0), ttl, }), @@ -204,8 +201,7 @@ mod tests { .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) .await .unwrap(); - assert_eq!(outcome.action, RemoteCredentialAction::None); - assert_eq!(outcome.token, None); + assert_eq!(outcome, RefreshOutcome::none()); } #[tokio::test] @@ -221,8 +217,8 @@ mod tests { }) .await .unwrap(); - assert_eq!(first.action, RemoteCredentialAction::Embedded); - assert_eq!(first.token.unwrap().generation, 1); + assert_eq!(first.action(), RemoteCredentialAction::Embedded); + assert_eq!(first.token().unwrap().generation, 1); // The cached token is fresh, so the second refresh must skip set-url. let second = state @@ -232,8 +228,8 @@ mod tests { }) .await .unwrap(); - assert_eq!(second.action, RemoteCredentialAction::Unchanged); - assert_eq!(second.token.unwrap().generation, 1); + assert_eq!(second.action(), RemoteCredentialAction::Unchanged); + assert_eq!(second.token().unwrap().generation, 1); assert_eq!(set_url_calls.load(Ordering::SeqCst), 1); } @@ -258,9 +254,9 @@ mod tests { .await .unwrap(); - assert_eq!(first.token.unwrap().generation, 1); - assert_eq!(second.action, RemoteCredentialAction::Embedded); - assert_eq!(second.token.unwrap().generation, 2); + assert_eq!(first.token().unwrap().generation, 1); + assert_eq!(second.action(), RemoteCredentialAction::Embedded); + assert_eq!(second.token().unwrap().generation, 2); assert_eq!(set_url_calls.load(Ordering::SeqCst), 2); } @@ -274,8 +270,8 @@ mod tests { .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) .await .unwrap(); - assert_eq!(outcome.action, RemoteCredentialAction::Unchanged); - assert_eq!(outcome.token.unwrap().generation, 1); + assert_eq!(outcome.action(), RemoteCredentialAction::Unchanged); + assert_eq!(outcome.token().unwrap().generation, 1); } #[tokio::test] @@ -293,8 +289,8 @@ mod tests { // The generation was not recorded, so the retry embeds again instead // of wrongly skipping. let retried = state.refresh(ORIGIN, |_| async { Ok(()) }).await.unwrap(); - assert_eq!(retried.action, RemoteCredentialAction::Embedded); - assert_eq!(retried.token.unwrap().generation, 1); + assert_eq!(retried.action(), RemoteCredentialAction::Embedded); + assert_eq!(retried.token().unwrap().generation, 1); } #[tokio::test] @@ -313,22 +309,25 @@ mod tests { .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) .await .unwrap(); - assert_eq!(outcome.action, RemoteCredentialAction::Unchanged); - assert!(outcome.token.unwrap().is_static()); + assert_eq!(outcome.action(), RemoteCredentialAction::Unchanged); + assert!(outcome.token().unwrap().is_static()); } #[tokio::test] - async fn mint_failure_maps_to_the_token_mint_failed_error() { - let state = PushCredentialState::new(Some(InstallationTokenSource::with_minter( - "owner/repo".to_string(), - Box::new(FailingMinter), + async fn mint_failure_preserves_the_mint_error_chain() { + let state = PushCredentialState::new(Some(installation_token_source( + "owner/repo", + Arc::new(FailingMinter), ))); let err = state .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) .await .unwrap_err(); - assert!(err.to_string().contains("token_mint_failed"), "{err}"); + assert_eq!(err.causes(), vec![ + "minting GitHub installation access token", + "mint failed" + ]); } #[test] diff --git a/lib/components/fabro-sandbox/src/sandbox.rs b/lib/components/fabro-sandbox/src/sandbox.rs index fb8cb0102..94def6760 100644 --- a/lib/components/fabro-sandbox/src/sandbox.rs +++ b/lib/components/fabro-sandbox/src/sandbox.rs @@ -1038,22 +1038,48 @@ pub enum RemoteCredentialAction { None, } -/// Outcome of [`Sandbox::refresh_push_credentials`]: what this call did to the -/// remote, and the non-secret description of the token embedded in it. -/// `token` is `None` only when `action` is [`RemoteCredentialAction::None`]. +/// Outcome of [`Sandbox::refresh_push_credentials`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RefreshOutcome { - pub action: RemoteCredentialAction, - pub token: Option, +pub enum RefreshOutcome { + /// No managed credentials exist for this sandbox. + None, + /// The remote already carried this token generation. + Unchanged(TokenSnapshot), + /// The remote was updated to carry this token generation. + Embedded(TokenSnapshot), } impl RefreshOutcome { /// No managed credentials to refresh. #[must_use] - pub fn none() -> Self { - Self { - action: RemoteCredentialAction::None, - token: None, + pub const fn none() -> Self { + Self::None + } + + #[must_use] + pub const fn unchanged(token: TokenSnapshot) -> Self { + Self::Unchanged(token) + } + + #[must_use] + pub const fn embedded(token: TokenSnapshot) -> Self { + Self::Embedded(token) + } + + #[must_use] + pub const fn action(self) -> RemoteCredentialAction { + match self { + Self::None => RemoteCredentialAction::None, + Self::Unchanged(_) => RemoteCredentialAction::Unchanged, + Self::Embedded(_) => RemoteCredentialAction::Embedded, + } + } + + #[must_use] + pub const fn token(self) -> Option { + match self { + Self::None => None, + Self::Unchanged(token) | Self::Embedded(token) => Some(token), } } } @@ -1550,17 +1576,17 @@ pub(crate) async fn fetch_source_run_ref( pub async fn git_push_via_exec(sandbox: &dyn Sandbox, refspec: &str) -> crate::Result<()> { let token = match sandbox.refresh_push_credentials().await { Ok(outcome) => { - if let Some(token) = outcome.token { + if let Some(token) = outcome.token() { tracing::debug!( refspec = %refspec, - action = %outcome.action, + action = %outcome.action(), generation = token.generation, provenance = %token.provenance, token_age_ms = token.age_ms(), "Resolved push credentials before git push" ); } - outcome.token + outcome.token() } Err(e) => { // The provider logged which token stays embedded; the push diff --git a/lib/components/fabro-sandbox/src/sandbox_spec.rs b/lib/components/fabro-sandbox/src/sandbox_spec.rs index b6a56bd40..7fcc7232a 100644 --- a/lib/components/fabro-sandbox/src/sandbox_spec.rs +++ b/lib/components/fabro-sandbox/src/sandbox_spec.rs @@ -205,7 +205,7 @@ impl SandboxSpec { } => { let mut sandbox = DockerSandbox::new( config.clone(), - github_app.clone(), + github_app.as_ref(), *run_id, clone_origin_url.clone(), clone_branch.clone(), diff --git a/lib/components/fabro-workflow/Cargo.toml b/lib/components/fabro-workflow/Cargo.toml index b3ab70260..0024c8b79 100644 --- a/lib/components/fabro-workflow/Cargo.toml +++ b/lib/components/fabro-workflow/Cargo.toml @@ -76,6 +76,7 @@ toml.workspace = true fabro-vault = { path = "../../foundation/fabro-vault" } [dev-dependencies] fabro-auth = { path = "../../foundation/fabro-auth", features = ["test-support"] } +fabro-github = { path = "../fabro-github", features = ["test-support"] } base64.workspace = true fabro-acp = { path = "../fabro-acp", features = ["test-support"] } fabro-workflow = { path = ".", features = ["test-support"] } diff --git a/lib/components/fabro-workflow/src/github_token_source.rs b/lib/components/fabro-workflow/src/github_token_source.rs deleted file mode 100644 index 2abf95bd3..000000000 --- a/lib/components/fabro-workflow/src/github_token_source.rs +++ /dev/null @@ -1,274 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context as _; -use fabro_github::{GitHubAppCredentials, InstallationToken}; -use tokio::sync::Mutex; -use tracing::warn; - -const REFRESH_THRESHOLD: Duration = Duration::from_mins(15); - -#[async_trait::async_trait] -pub trait IatMinter: Send + Sync { - async fn mint(&self) -> anyhow::Result; -} - -pub struct AppIatMinter { - creds: GitHubAppCredentials, - http: fabro_http::HttpClient, - owner: String, - repo: String, - api_base: String, - install_url: Option, - permissions: serde_json::Value, -} - -impl AppIatMinter { - #[must_use] - pub fn new( - creds: GitHubAppCredentials, - http: fabro_http::HttpClient, - owner: String, - repo: String, - api_base: String, - install_url: Option, - permissions: serde_json::Value, - ) -> Self { - Self { - creds, - http, - owner, - repo, - api_base, - install_url, - permissions, - } - } -} - -#[async_trait::async_trait] -impl IatMinter for AppIatMinter { - async fn mint(&self) -> anyhow::Result { - self.creds - .mint_installation_token( - &self.http, - &self.owner, - &self.repo, - &self.api_base, - self.permissions.clone(), - self.install_url.as_deref(), - ) - .await - } -} - -pub struct GitHubTokenSource { - state: SourceState, -} - -enum SourceState { - Pat(String), - StaticIat(InstallationToken), - Mintable { - minter: Arc, - cache: Mutex>, - }, -} - -impl GitHubTokenSource { - #[must_use] - pub fn pat(token: String) -> Self { - Self { - state: SourceState::Pat(token), - } - } - - #[must_use] - pub fn static_iat(token: InstallationToken) -> Self { - Self { - state: SourceState::StaticIat(token), - } - } - - #[must_use] - pub fn mintable(minter: Arc) -> Self { - Self { - state: SourceState::Mintable { - minter, - cache: Mutex::new(None), - }, - } - } - - #[must_use] - pub fn is_refreshable(&self) -> bool { - matches!(self.state, SourceState::Mintable { .. }) - } - - pub async fn current_token(&self) -> anyhow::Result { - match &self.state { - SourceState::Pat(token) => Ok(token.clone()), - SourceState::StaticIat(token) => token.valid_token().map(str::to_owned), - SourceState::Mintable { minter, cache } => { - let mut cache = cache.lock().await; - let cached_is_fresh = cache - .as_ref() - .is_some_and(|token| !token.near_expiry(REFRESH_THRESHOLD)); - - if !cached_is_fresh { - match minter.mint().await { - Ok(token) => *cache = Some(token), - Err(err) => { - if let Some(token) = cache.as_ref() { - if let Ok(value) = token.valid_token() { - warn!( - error = %err, - "GitHub installation token refresh failed; using cached token" - ); - return Ok(value.to_owned()); - } - } - return Err(err) - .context("failed to mint GitHub installation access token"); - } - } - } - - let token = cache - .as_ref() - .ok_or_else(|| anyhow::anyhow!("mintable token source has no cached token"))?; - token.valid_token().map(str::to_owned) - } - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::VecDeque; - use std::sync::atomic::{AtomicUsize, Ordering}; - - use anyhow::anyhow; - - use super::*; - - enum MintAction { - Token(&'static str, chrono::DateTime), - Error(&'static str), - } - - struct MockMinter { - calls: AtomicUsize, - script: Mutex>, - } - - impl MockMinter { - fn new(script: Vec) -> Self { - Self { - calls: AtomicUsize::new(0), - script: Mutex::new(script.into()), - } - } - - fn calls(&self) -> usize { - self.calls.load(Ordering::SeqCst) - } - } - - #[async_trait::async_trait] - impl IatMinter for MockMinter { - async fn mint(&self) -> anyhow::Result { - self.calls.fetch_add(1, Ordering::SeqCst); - match self.script.lock().await.pop_front().expect("mint script") { - MintAction::Token(token, expires_at) => Ok(InstallationToken { - token: token.to_string(), - expires_at, - }), - MintAction::Error(message) => Err(anyhow!(message)), - } - } - } - - #[tokio::test] - async fn pat_returns_same_token_without_minting() { - let source = GitHubTokenSource::pat("ghp_pat".to_string()); - - assert_eq!(source.current_token().await.unwrap(), "ghp_pat"); - assert_eq!(source.current_token().await.unwrap(), "ghp_pat"); - assert!(!source.is_refreshable()); - } - - #[tokio::test] - async fn static_iat_returns_valid_token_and_rejects_expired_token() { - let valid = GitHubTokenSource::static_iat(InstallationToken { - token: "ghs_valid".to_string(), - expires_at: chrono::Utc::now() + chrono::Duration::minutes(30), - }); - assert_eq!(valid.current_token().await.unwrap(), "ghs_valid"); - assert!(!valid.is_refreshable()); - - let expired = GitHubTokenSource::static_iat(InstallationToken { - token: "ghs_expired".to_string(), - expires_at: chrono::Utc::now() - chrono::Duration::seconds(1), - }); - assert!(expired.current_token().await.is_err()); - } - - #[tokio::test] - async fn mintable_reuses_cached_token_until_refresh_threshold() { - let minter = Arc::new(MockMinter::new(vec![MintAction::Token( - "ghs_cached", - chrono::Utc::now() + chrono::Duration::minutes(30), - )])); - let source = GitHubTokenSource::mintable(minter.clone()); - - assert!(source.is_refreshable()); - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(minter.calls(), 1); - } - - #[tokio::test] - async fn mintable_refreshes_cached_token_near_expiry() { - let minter = Arc::new(MockMinter::new(vec![ - MintAction::Token( - "ghs_first", - chrono::Utc::now() + chrono::Duration::minutes(10), - ), - MintAction::Token( - "ghs_second", - chrono::Utc::now() + chrono::Duration::minutes(30), - ), - ])); - let source = GitHubTokenSource::mintable(minter.clone()); - - assert_eq!(source.current_token().await.unwrap(), "ghs_first"); - assert_eq!(source.current_token().await.unwrap(), "ghs_second"); - assert_eq!(minter.calls(), 2); - } - - #[tokio::test] - async fn mintable_uses_valid_cached_token_when_refresh_fails() { - let minter = Arc::new(MockMinter::new(vec![ - MintAction::Token( - "ghs_cached", - chrono::Utc::now() + chrono::Duration::minutes(10), - ), - MintAction::Error("mint failed"), - ])); - let source = GitHubTokenSource::mintable(minter.clone()); - - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(minter.calls(), 2); - } - - #[tokio::test] - async fn mintable_errors_when_no_cached_token_can_cover_mint_failure() { - let minter = Arc::new(MockMinter::new(vec![MintAction::Error("mint failed")])); - let source = GitHubTokenSource::mintable(minter); - - let err = format!("{:#}", source.current_token().await.unwrap_err()); - assert!(err.contains("mint failed"), "got: {err}"); - } -} diff --git a/lib/components/fabro-workflow/src/handler/command.rs b/lib/components/fabro-workflow/src/handler/command.rs index d36ef8be8..82593c59b 100644 --- a/lib/components/fabro-workflow/src/handler/command.rs +++ b/lib/components/fabro-workflow/src/handler/command.rs @@ -1693,7 +1693,7 @@ mod tests { } #[async_trait::async_trait] - impl crate::github_token_source::IatMinter for RefreshingMinter { + impl fabro_github::test_support::InstallationTokenMinter for RefreshingMinter { async fn mint(&self) -> anyhow::Result { let call = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1; Ok(fabro_github::InstallationToken { @@ -1834,8 +1834,9 @@ mod tests { calls: std::sync::atomic::AtomicUsize::new(0), }); let mut services = make_sandbox_services(spy.clone()); - services.github_token = Some(std::sync::Arc::new( - crate::github_token_source::GitHubTokenSource::mintable(minter.clone()), + services.github_token = Some(fabro_github::test_support::installation_token_source( + "owner/repo", + minter.clone(), )); let handler = CommandHandler; diff --git a/lib/components/fabro-workflow/src/handler/llm/acp.rs b/lib/components/fabro-workflow/src/handler/llm/acp.rs index 4edcf3e81..cfd498beb 100644 --- a/lib/components/fabro-workflow/src/handler/llm/acp.rs +++ b/lib/components/fabro-workflow/src/handler/llm/acp.rs @@ -11,8 +11,7 @@ use fabro_acp::{ render_stop_reason, }; use fabro_agent::{ - AgentEvent, RefreshOutcome, RemoteCredentialAction, Sandbox, StaticEnvProvider, SteeringItem, - ToolEnvProvider, + AgentEvent, RefreshOutcome, Sandbox, StaticEnvProvider, SteeringItem, ToolEnvProvider, }; use fabro_github::token_source::REFRESH_MARGIN; use fabro_graphviz::graph::Node; @@ -115,12 +114,8 @@ fn push_cred_refresh_interval() -> Option { /// tick. Schedule from the token's own `expires_at` instead: wake when the /// cache margin opens, so that tick re-mints. `None` disables the loop — /// static credentials cannot be re-minted by waiting. -fn next_refresh_delay(outcome: &RefreshOutcome, fallback: Duration) -> Option { - let Some(token) = outcome.token else { - // No managed credentials to watch; keep the configured cadence in - // case a later tick sees them (e.g. after a reconnect). - return Some(fallback); - }; +fn next_refresh_delay(outcome: &RefreshOutcome) -> Option { + let token = outcome.token()?; let expires_at = token.expires_at()?; let margin = chrono::Duration::from_std(REFRESH_MARGIN).unwrap_or(chrono::Duration::MAX); let until_margin = ((expires_at - margin) - chrono::Utc::now()) @@ -140,9 +135,10 @@ async fn refresh_ahead_loop( sandbox: Arc, cancel: CancellationToken, interval: Duration, + initial_delay: Duration, ) { let retry_delay = interval.min(Duration::from_mins(1)); - let mut delay = interval; + let mut delay = initial_delay; loop { tokio::select! { () = cancel.cancelled() => break, @@ -151,26 +147,26 @@ async fn refresh_ahead_loop( .await { Ok(Ok(outcome)) => { - match outcome.action { - RemoteCredentialAction::Embedded => { + match outcome { + RefreshOutcome::Embedded(token) => { tracing::info!( - generation = outcome.token.map(|token| token.generation), + generation = token.generation, "refresh-ahead re-embedded push credentials mid-turn" ); } - RemoteCredentialAction::Unchanged => { + RefreshOutcome::Unchanged(token) => { tracing::debug!( - generation = outcome.token.map(|token| token.generation), + generation = token.generation, "refresh-ahead tick: embedded push credentials still fresh" ); } - RemoteCredentialAction::None => { + RefreshOutcome::None => { tracing::debug!( "refresh-ahead tick: no managed push credentials to refresh" ); } } - if let Some(next) = next_refresh_delay(&outcome, interval) { + if let Some(next) = next_refresh_delay(&outcome) { delay = next; } else { tracing::debug!( @@ -312,77 +308,57 @@ impl AgentAcpBackend { }) as Arc) + Send + Sync> }); - // Keep the sandbox's push credentials fresh for the duration of this ACP - // turn so the agent's own `git push` uses a live token instead of the one - // baked into the clone at run start. - // - // Part 2 (turn-entry): resolve through the cached token source and - // rewrite the origin URL before the ACP process spawns, covering a push - // early in the turn. A fresh cached token makes this a no-op exec-wise. - // Non-fatal and timeout-bounded — a stalled mint must neither fail nor - // hang node entry. Part 3 (loop): a background task keeps the embedded - // token fresh so a single turn that outlives the ~60-min - // installation-token TTL still pushes with a fresh token; ticks - // reschedule from the embedded token's expiry, so a normal short turn - // never ticks (the drop-guard aborts the task at turn end). - // - // FABRO_PUSH_CRED_REFRESH_AHEAD=0 (or false/off/no/empty, case- - // insensitive) disables the WHOLE feature — turn-entry refresh AND loop — - // so an operator who manages `origin` themselves can opt out of all - // fabro-side origin rewriting. FABRO_PUSH_CRED_REFRESH_INTERVAL_SECONDS - // overrides the loop cadence for ticks without token expiry info; 0 - // disables just the loop. - // - // Known limitations tracked as follow-ups (not addressed here): (a) - // resumed/parked runs reconnect the sandbox with no GitHub App creds, so - // refresh no-ops until those creds are threaded through the reconnect - // path; (b) the background `git remote set-url` can contend with the - // agent's own git on `.git/config.lock` (skipped entirely while the - // cached generation is already embedded); (c) parallel ACP branches each - // run their own loop; (d) this refresh lives in the ACP handler only, - // though the stale-origin problem is stage-type-agnostic (native/command - // stages that push are not covered); (e) refresh failures are logged via - // tracing but not surfaced as a RunNotice event on the run stream. + // Refresh before launch for early pushes. Schedule later refreshes from + // token expiry so the loop cannot sleep past the cache margin. let refresh_enabled = push_cred_refresh_enabled(); - if refresh_enabled { + let refresh_interval = refresh_enabled.then(push_cred_refresh_interval).flatten(); + let refresh_schedule = if refresh_enabled { match timeout(REFRESH_MINT_TIMEOUT, sandbox.refresh_push_credentials()).await { - Ok(Ok(outcome)) => match outcome.action { - RemoteCredentialAction::Embedded => { - tracing::debug!( - generation = outcome.token.map(|token| token.generation), - "refreshed sandbox push credentials at ACP turn entry" - ); + Ok(Ok(outcome)) => { + match outcome { + RefreshOutcome::Embedded(token) => { + tracing::debug!( + generation = token.generation, + "refreshed sandbox push credentials at ACP turn entry" + ); + } + RefreshOutcome::Unchanged(token) => { + tracing::debug!( + generation = token.generation, + "sandbox push credentials already fresh at ACP turn entry" + ); + } + RefreshOutcome::None => {} } - RemoteCredentialAction::Unchanged => { - tracing::debug!( - generation = outcome.token.map(|token| token.generation), - "sandbox push credentials already fresh at ACP turn entry" - ); - } - RemoteCredentialAction::None => {} - }, + refresh_interval.zip(next_refresh_delay(&outcome)) + } Ok(Err(e)) => { tracing::warn!( error = %fabro_sandbox::display_for_log(&e), "node-entry push-credential refresh failed (non-fatal)" ); + refresh_interval + .map(|interval| (interval, interval.min(Duration::from_mins(1)))) } Err(_elapsed) => { tracing::warn!( timeout_secs = REFRESH_MINT_TIMEOUT.as_secs(), "node-entry push-credential refresh timed out (non-fatal)" ); + refresh_interval + .map(|interval| (interval, interval.min(Duration::from_mins(1)))) } } - } - let _refresh_ahead_guard: Option = refresh_enabled - .then(push_cred_refresh_interval) - .flatten() - .map(|interval| { + } else { + None + }; + let _refresh_ahead_guard: Option = + refresh_schedule.map(|(interval, initial_delay)| { AbortOnDrop(tokio::spawn(refresh_ahead_loop( Arc::clone(sandbox), cancel_token.child_token(), interval, + initial_delay, ))) }); @@ -766,23 +742,22 @@ mod tests { expires_at, } }; - RefreshOutcome { - action, - token: Some(TokenSnapshot { - generation, - provenance, - }), + let token = TokenSnapshot { + generation, + provenance, + }; + match action { + RemoteCredentialAction::Embedded => RefreshOutcome::embedded(token), + RemoteCredentialAction::Unchanged => RefreshOutcome::unchanged(token), + RemoteCredentialAction::None => RefreshOutcome::none(), } } fn static_outcome() -> RefreshOutcome { - RefreshOutcome { - action: RemoteCredentialAction::Unchanged, - token: Some(TokenSnapshot { - generation: 0, - provenance: TokenProvenance::Static, - }), - } + RefreshOutcome::unchanged(TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + }) } #[test] @@ -794,7 +769,7 @@ mod tests { chrono::Duration::minutes(60), false, ); - let delay = next_refresh_delay(&outcome, Duration::from_mins(45)).unwrap(); + let delay = next_refresh_delay(&outcome).unwrap(); // Expiry minus the 10-minute refresh margin: ~50 minutes out. assert!(delay > Duration::from_mins(49), "{delay:?}"); assert!(delay <= Duration::from_mins(50), "{delay:?}"); @@ -809,26 +784,17 @@ mod tests { chrono::Duration::minutes(5), true, ); - assert_eq!( - next_refresh_delay(&outcome, Duration::from_mins(45)), - Some(REFRESH_RESCHEDULE_FLOOR) - ); + assert_eq!(next_refresh_delay(&outcome), Some(REFRESH_RESCHEDULE_FLOOR)); } #[test] fn next_refresh_delay_disables_the_loop_for_static_credentials() { - assert_eq!( - next_refresh_delay(&static_outcome(), Duration::from_mins(45)), - None - ); + assert_eq!(next_refresh_delay(&static_outcome()), None); } #[test] - fn next_refresh_delay_keeps_the_cadence_without_managed_credentials() { - assert_eq!( - next_refresh_delay(&RefreshOutcome::none(), Duration::from_mins(45)), - Some(Duration::from_mins(45)) - ); + fn next_refresh_delay_disables_the_loop_without_managed_credentials() { + assert_eq!(next_refresh_delay(&RefreshOutcome::none()), None); } /// Sandbox stub whose refresh outcomes are scripted, recording when each @@ -989,6 +955,7 @@ mod tests { Arc::clone(&sandbox) as Arc, cancel.clone(), interval, + interval, )); while sandbox.ticks().len() < 3 { @@ -1011,19 +978,41 @@ mod tests { } #[tokio::test(start_paused = true)] - async fn refresh_ahead_stops_by_itself_for_static_credentials() { - let sandbox = ScriptedRefreshSandbox::new(vec![static_outcome()]); + async fn refresh_ahead_honors_the_expiry_based_initial_delay() { + let interval = Duration::from_mins(45); + let entry_outcome = minted_outcome( + RemoteCredentialAction::Unchanged, + 1, + chrono::Duration::minutes(45), + chrono::Duration::minutes(15), + true, + ); + let initial_delay = next_refresh_delay(&entry_outcome).unwrap(); + let sandbox = ScriptedRefreshSandbox::new(vec![minted_outcome( + RemoteCredentialAction::Embedded, + 2, + chrono::Duration::zero(), + chrono::Duration::minutes(60), + false, + )]); let cancel = CancellationToken::new(); + let start = tokio::time::Instant::now(); let loop_task = tokio::spawn(refresh_ahead_loop( Arc::clone(&sandbox) as Arc, cancel.clone(), - Duration::from_mins(45), + interval, + initial_delay, )); - // The loop exits after the first tick without being cancelled: static - // credentials cannot be re-minted, so there is nothing to keep fresh. - loop_task.await.expect("refresh loop should stop by itself"); - assert_eq!(sandbox.ticks().len(), 1); + while sandbox.ticks().is_empty() { + tokio::time::sleep(Duration::from_secs(1)).await; + } + cancel.cancel(); + loop_task.await.expect("refresh loop should exit cleanly"); + + let first_tick = sandbox.ticks()[0] - start; + assert!(first_tick <= Duration::from_mins(5), "{first_tick:?}"); + assert!(first_tick > Duration::from_mins(4), "{first_tick:?}"); } #[tokio::test] diff --git a/lib/components/fabro-workflow/src/lib.rs b/lib/components/fabro-workflow/src/lib.rs index c34bec62c..3178542d7 100644 --- a/lib/components/fabro-workflow/src/lib.rs +++ b/lib/components/fabro-workflow/src/lib.rs @@ -293,7 +293,6 @@ pub mod error; pub mod event; pub mod file_resolver; pub mod git; -pub mod github_token_source; pub(crate) mod graph; pub mod handler; mod hook_context; diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 2ccb3b6f2..4d4d529c4 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -7,6 +7,7 @@ use fabro_agent::{Sandbox, ToolSecrets}; use fabro_auth::{ CredentialSource, ExtraHeadersCredentialSource, VaultCredentialSource, auth_issue_message, }; +use fabro_github::token_source::InstallationTokenSource; use fabro_graphviz::graph; use fabro_hooks::{HookContext, HookDecision, HookEvent, HookExecutionContext, HookRunner}; use fabro_model::Catalog; @@ -23,7 +24,6 @@ use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec} use crate::error::Error; use crate::event::{Event, RunNoticeCode, RunNoticeLevel}; use crate::git::GitAuthor; -use crate::github_token_source::{AppIatMinter, GitHubTokenSource}; use crate::handler::llm::{AgentAcpBackend, AgentApiBackend, BackendRouter, routing}; use crate::handler::{HandlerRegistry, default_registry}; #[cfg(test)] @@ -37,7 +37,10 @@ use crate::services::{ use crate::stage_execution::{StageExecutionSeed, StageExecutionTracker}; use crate::steering_hub::SteeringHub; -type BuiltSandboxEnv = (HashMap, Option>); +type BuiltSandboxEnv = ( + HashMap, + Option>, +); async fn run_hooks( hook_runner: Option<&HookRunner>, @@ -99,12 +102,12 @@ fn build_sandbox_env( let source = match creds { fabro_github::GitHubCredentials::Pat(token) => { - Some(Arc::new(GitHubTokenSource::pat(token.clone()))) + Some(InstallationTokenSource::pat(token.clone())) } fabro_github::GitHubCredentials::Installation(token) => { - Some(Arc::new(GitHubTokenSource::static_iat(token.clone()))) + Some(InstallationTokenSource::installation(token.clone())) } - fabro_github::GitHubCredentials::App(app) => { + fabro_github::GitHubCredentials::App(_) => { let Some(origin_url) = spec.origin_url.as_deref() else { return Ok((env, None)); }; @@ -114,19 +117,11 @@ fn build_sandbox_env( let permissions = serde_json::to_value(permissions).map_err(|err| { Error::engine_with_source("Failed to serialize GitHub permissions", err) })?; - let http = fabro_http::http_client() - .map_err(|err| Error::engine_with_source("Failed to build HTTP client", err))?; - let install_url = app.installation_url(&owner); - let minter = AppIatMinter::new( - app.clone(), - http, - owner, - repo, - fabro_github::github_api_base_url(), - install_url, - permissions, - ); - Some(Arc::new(GitHubTokenSource::mintable(Arc::new(minter)))) + Some( + InstallationTokenSource::for_repository(creds, owner, repo, permissions).map_err( + |err| Error::engine_with_anyhow("Failed to build GitHub token source", err), + )?, + ) } }; @@ -458,7 +453,7 @@ pub async fn initialize( }); let github_token_refresh_managed = github_token .as_deref() - .is_some_and(GitHubTokenSource::is_refreshable); + .is_some_and(InstallationTokenSource::mints_installation_tokens); let (registry, effective_dry_run) = if let Some(registry) = options.registry_override.clone() { // A caller-supplied registry owns execution behavior for its handlers. (registry, options.dry_run) diff --git a/lib/components/fabro-workflow/src/run_metadata.rs b/lib/components/fabro-workflow/src/run_metadata.rs index 9fa002b57..76b9fa23d 100644 --- a/lib/components/fabro-workflow/src/run_metadata.rs +++ b/lib/components/fabro-workflow/src/run_metadata.rs @@ -222,19 +222,16 @@ pub(crate) fn build_metadata_writer( if !normalized_url.starts_with("https://") { return Ok(None); } - if fabro_github::parse_github_owner_repo(&normalized_url).is_err() { + let Ok((owner, repo)) = fabro_github::parse_github_owner_repo(&normalized_url) else { return Ok(None); - } + }; - // Share the sandbox's token source so the metadata writer reuses the - // same cached token as every other consumer for this origin. Resumed - // runs reconnect the sandbox without one; they build their own cached - // source from the run's credentials. let source = match token_source { Some(source) => source, - None => InstallationTokenSource::for_origin( + None => InstallationTokenSource::for_repository( creds, - &normalized_url, + owner, + repo, serde_json::json!({ "contents": "write" }), ) .map_err(RunMetadataError::TokenMint)?, diff --git a/lib/components/fabro-workflow/src/services.rs b/lib/components/fabro-workflow/src/services.rs index 2af76a9c7..64facc23e 100644 --- a/lib/components/fabro-workflow/src/services.rs +++ b/lib/components/fabro-workflow/src/services.rs @@ -8,6 +8,7 @@ use fabro_agent::{Sandbox, ToolEnvProvider}; use fabro_auth::CredentialSource; #[cfg(test)] use fabro_auth::ResolvedCredentials; +use fabro_github::token_source::InstallationTokenSource; use fabro_hooks::{HookContext, HookDecision, HookExecutionContext, HookRunner}; use fabro_interview::Interviewer; use fabro_model::{Catalog, ProviderId}; @@ -15,7 +16,6 @@ use fabro_types::{ManifestPath, RunId}; use tokio_util::sync::CancellationToken; use crate::event::Emitter; -use crate::github_token_source::GitHubTokenSource; use crate::handler::HandlerRegistry; use crate::interview_runtime::RunInterviewBlocker; use crate::run_metadata::{RunMetadataRuntime, RunMetadataWriterHandle}; @@ -238,7 +238,7 @@ pub struct EngineServices { /// Environment variables from `[sandbox.env]` config. pub base_env: HashMap, /// GitHub token source used to inject `GITHUB_TOKEN` at the point of use. - pub github_token: Option>, + pub github_token: Option>, /// Typed values from `[run.inputs]`, available to prompt templates. pub inputs: HashMap, /// When true, handlers should skip real execution and return simulated @@ -342,7 +342,7 @@ impl EngineServices { pub struct WorkflowToolEnvProvider { pub base_env: HashMap, - pub github_token: Option>, + pub github_token: Option>, } #[async_trait::async_trait] @@ -354,11 +354,15 @@ impl ToolEnvProvider for WorkflowToolEnvProvider { async fn resolve_workflow_env( base_env: &HashMap, - github_token: Option<&Arc>, + github_token: Option<&Arc>, ) -> anyhow::Result> { let mut env = base_env.clone(); if let Some(source) = github_token { - env.insert("GITHUB_TOKEN".to_string(), source.current_token().await?); + let resolved = source.resolve().await?; + env.insert( + "GITHUB_TOKEN".to_string(), + resolved.token.expose().to_owned(), + ); } Ok(env) } @@ -371,9 +375,10 @@ mod tests { use anyhow::anyhow; use fabro_agent::ToolEnvProvider as _; use fabro_github::InstallationToken; + use fabro_github::test_support::{InstallationTokenMinter, installation_token_source}; + use fabro_github::token_source::InstallationTokenSource; use super::{EngineServices, WorkflowToolEnvProvider}; - use crate::github_token_source::{GitHubTokenSource, IatMinter}; #[tokio::test] async fn test_default_uses_stub_credential_source() { @@ -406,7 +411,7 @@ mod tests { async fn workflow_tool_env_provider_merges_current_github_token() { let provider = WorkflowToolEnvProvider { base_env: HashMap::from([("FOO".to_string(), "bar".to_string())]), - github_token: Some(Arc::new(GitHubTokenSource::pat("ghp_pat".to_string()))), + github_token: Some(InstallationTokenSource::pat("ghp_pat".to_string())), }; let env = provider.resolve().await.unwrap(); @@ -418,7 +423,7 @@ mod tests { struct FailingMinter; #[async_trait::async_trait] - impl IatMinter for FailingMinter { + impl InstallationTokenMinter for FailingMinter { async fn mint(&self) -> anyhow::Result { Err(anyhow!("GITHUB_TOKEN refresh failed")) } @@ -428,9 +433,10 @@ mod tests { async fn workflow_tool_env_provider_propagates_token_refresh_errors() { let provider = WorkflowToolEnvProvider { base_env: HashMap::new(), - github_token: Some(Arc::new(GitHubTokenSource::mintable(Arc::new( - FailingMinter, - )))), + github_token: Some(installation_token_source( + "owner/repo", + Arc::new(FailingMinter), + )), }; let err = format!("{:#}", provider.resolve().await.unwrap_err()); From e5c0301ccb8e56a441a790db7145e0605f3fed73 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 19:57:19 -0400 Subject: [PATCH 50/63] Simplify Daytona lifecycle retries --- .../fabro-sandbox/src/daytona/mod.rs | 315 +++++++++++------- 1 file changed, 192 insertions(+), 123 deletions(-) diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 22d3adbca..8ffccfa81 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -63,7 +63,6 @@ pub(crate) const DAYTONA_DASHBOARD_SANDBOXES_URL: &str = "https://app.daytona.io/dashboard/sandboxes"; const FABRO_SANDBOX_USER_AGENT: &str = concat!("fabro-sandbox/", env!("CARGO_PKG_VERSION")); const DAYTONA_PROBE_TIMEOUT: Duration = Duration::from_secs(20); -const DAYTONA_START_TIMEOUT: Duration = Duration::from_mins(1); /// Upper bound on explicit and Drop-triggered Daytona cleanup calls (session /// deletion, temporary stdin files) so a stalled REST call cannot block /// cancellation/timeout paths indefinitely. @@ -335,6 +334,33 @@ fn command_kind(command: &str) -> &'static str { } } +#[derive(Clone, Copy, strum::Display)] +#[strum(serialize_all = "lowercase")] +enum DaytonaLifecycleAction { + Start, + Stop, +} + +impl DaytonaLifecycleAction { + async fn execute( + self, + client: &daytona_sdk::Client, + sandbox_name: &str, + ) -> Result<(), DaytonaError> { + match self { + Self::Start => client.start(sandbox_name).await.map(drop), + Self::Stop => client.stop(sandbox_name).await.map(drop), + } + } + + fn is_complete(self, state: Option) -> bool { + match self { + Self::Start => state == Some(SandboxState::Started), + Self::Stop => matches!(state, Some(SandboxState::Stopped | SandboxState::Destroyed)), + } + } +} + /// Sandbox that runs all operations inside a Daytona cloud sandbox. pub struct DaytonaSandbox { config: DaytonaConfig, @@ -859,117 +885,156 @@ impl DaytonaSandbox { ))) } - /// Start the sandbox, retrying while Daytona reports a lifecycle - /// transition in flight, up to `deadline`. - /// - /// Daytona rejects `start` with "state change in progress" while a - /// transition (such as an inactivity auto-stop) is still running. The - /// transition finishes on its own within seconds, so the rejection is a - /// wait-and-retry condition, not a failure. Between attempts the state is - /// re-inspected: a transition that lands on `Started` (a concurrent - /// activation won the race) needs no further start call. + async fn wait_for_stable_state( + &self, + sandbox_name: &str, + ) -> Result, DaytonaError> { + loop { + time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; + let state = self.client.get(sandbox_name).await?.state; + if !is_transitional_state(state) { + return Ok(state); + } + } + } + + async fn run_lifecycle_action( + &self, + sandbox_name: &str, + action: DaytonaLifecycleAction, + deadline: time::Instant, + ) -> crate::Result<()> { + loop { + let request = time::timeout_at(deadline, action.execute(&self.client, sandbox_name)); + match request.await { + Ok(Ok(())) => return Ok(()), + Ok(Err(source)) if is_state_change_in_progress(&source) => { + tracing::debug!( + action = %action, + sandbox = sandbox_name, + "Daytona lifecycle request rejected during state change" + ); + match time::timeout_at(deadline, self.wait_for_stable_state(sandbox_name)).await + { + Ok(Ok(state)) if action.is_complete(state) => return Ok(()), + Ok(Ok(_)) => {} + Ok(Err(wait_source)) => { + return Err(crate::Error::context( + format!( + "Failed to inspect Daytona sandbox while waiting to {action}" + ), + wait_source, + )); + } + Err(_) => { + return Err(crate::Error::context( + format!("Timed out waiting to {action} Daytona sandbox"), + source, + )); + } + } + } + Ok(Err(source)) => { + return Err(crate::Error::context( + format!("Failed to {action} Daytona sandbox"), + source, + )); + } + Err(_) => { + return Err(crate::Error::message(format!( + "Timed out waiting to {action} Daytona sandbox" + ))); + } + } + } + } + + fn start_error(&self, error: crate::Error) -> crate::Result<()> { + self.emit(SandboxEvent::StartFailed { + provider: "daytona".into(), + error: error.to_string(), + causes: error.causes(), + }); + Err(error) + } + + fn stop_error(&self, error: crate::Error) -> crate::Result<()> { + self.emit(SandboxEvent::StopFailed { + provider: "daytona".into(), + error: error.to_string(), + causes: error.causes(), + }); + Err(error) + } + async fn start_with_deadline(&self, deadline: time::Instant) -> crate::Result<()> { self.emit(SandboxEvent::StartStarted { provider: "daytona".into(), }); let start = Instant::now(); - let sandbox = self.sandbox()?; - loop { - match self.client.start(&sandbox.name).await { - Ok(_) => break, - Err(e) if is_state_change_in_progress(&e) && time::Instant::now() < deadline => { - tracing::debug!( - "Daytona start rejected while a state change is in progress; retrying" - ); - time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; - if let Ok(current) = self.client.get(&sandbox.name).await { - if current.state == Some(SandboxState::Started) { - break; - } - } - } - Err(e) => { - let err = crate::Error::context("Failed to start Daytona sandbox", e); - self.emit(SandboxEvent::StartFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - } + let result = async { + let sandbox = self.sandbox()?; + self.run_lifecycle_action(&sandbox.name, DaytonaLifecycleAction::Start, deadline) + .await?; + Self::probe_bash(sandbox).await } - if let Err(err) = Self::probe_bash(sandbox).await { - self.emit(SandboxEvent::StartFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); + .await; + if let Err(error) = result { + return self.start_error(error); } - let duration_ms = elapsed_ms(start); self.emit(SandboxEvent::StartCompleted { - provider: "daytona".into(), - duration_ms, + provider: "daytona".into(), + duration_ms: elapsed_ms(start), }); Ok(()) } - /// Stop the sandbox, retrying while Daytona reports a lifecycle - /// transition in flight, up to `deadline`. - /// - /// The in-flight transition may be the stop itself (an inactivity - /// auto-stop): between attempts the state is re-inspected, and a sandbox - /// that landed on `Stopped` or `Destroyed` needs no further stop call. async fn stop_with_deadline(&self, deadline: time::Instant) -> crate::Result<()> { self.emit(SandboxEvent::StopStarted { provider: "daytona".into(), }); let start = Instant::now(); - let sandbox = self.sandbox()?; - loop { - match self.client.stop(&sandbox.name).await { - Ok(_) => break, - Err(e) if is_state_change_in_progress(&e) && time::Instant::now() < deadline => { - tracing::debug!( - "Daytona stop rejected while a state change is in progress; retrying" - ); - time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; - if let Ok(current) = self.client.get(&sandbox.name).await { - if matches!( - current.state, - Some(SandboxState::Stopped | SandboxState::Destroyed) - ) { - break; - } - } - } - Err(e) => { - let err = crate::Error::context("Failed to stop Daytona sandbox", e); - self.emit(SandboxEvent::StopFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - } + let result = async { + let sandbox = self.sandbox()?; + self.run_lifecycle_action(&sandbox.name, DaytonaLifecycleAction::Stop, deadline) + .await + } + .await; + if let Err(error) = result { + return self.stop_error(error); } - let duration_ms = elapsed_ms(start); self.emit(SandboxEvent::StopCompleted { - provider: "daytona".into(), - duration_ms, + provider: "daytona".into(), + duration_ms: elapsed_ms(start), }); Ok(()) } } -/// Whether a Daytona API error reports a lifecycle transition in flight -/// (HTTP 400 "State change in progress" on start/stop). fn is_state_change_in_progress(err: &DaytonaError) -> bool { - err.to_string() - .to_ascii_lowercase() - .contains("state change in progress") + err.status_code() == Some(400) + && err + .message() + .to_ascii_lowercase() + .contains("state change in progress") +} + +fn is_transitional_state(state: Option) -> bool { + matches!( + state, + Some( + SandboxState::Creating + | SandboxState::Restoring + | SandboxState::Destroying + | SandboxState::Starting + | SandboxState::Stopping + | SandboxState::PendingBuild + | SandboxState::BuildingSnapshot + | SandboxState::PullingSnapshot + | SandboxState::Archiving + | SandboxState::Resizing + ) + ) } /// Detect the git remote URL and current branch from a local repository. @@ -1461,39 +1526,35 @@ impl Sandbox for DaytonaSandbox { async fn activate(&self) -> crate::Result<()> { let sandbox = self.sandbox()?; let deadline = time::Instant::now() + DAYTONA_STATE_CHANGE_TIMEOUT; - loop { - let current = self.client.get(&sandbox.name).await.map_err(|e| { + let current = time::timeout_at(deadline, self.client.get(&sandbox.name)) + .await + .map_err(|_| { + crate::Error::message("Timed out inspecting Daytona sandbox before activation") + })? + .map_err(|e| { crate::Error::context("Failed to inspect Daytona sandbox before activation", e) })?; - match current.state { - Some(SandboxState::Started) => return Ok(()), - Some(SandboxState::Starting) => { - return current - .wait_for_start(Some(DAYTONA_START_TIMEOUT)) - .await - .map_err(|e| { - crate::Error::context( - "Failed to wait for Daytona sandbox activation", - e, - ) - }); - } - // An inactivity auto-stop can be in flight when a stage - // returns after a long period with no sandbox traffic (LLM - // inference generates none). Wait out the transition and - // dispatch on whatever state it lands on. - Some(SandboxState::Stopping) => { - if time::Instant::now() >= deadline { - return Err(crate::Error::message(format!( - "Daytona sandbox stop still in progress after {}s", - DAYTONA_STATE_CHANGE_TIMEOUT.as_secs() - ))); - } - time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; - } - _ => return self.start_with_deadline(deadline).await, - } + let state = if is_transitional_state(current.state) { + time::timeout_at(deadline, self.wait_for_stable_state(&sandbox.name)) + .await + .map_err(|_| { + crate::Error::message( + "Timed out waiting for Daytona sandbox state change before activation", + ) + })? + .map_err(|e| { + crate::Error::context( + "Failed to wait for Daytona sandbox state change before activation", + e, + ) + })? + } else { + current.state + }; + if state == Some(SandboxState::Started) { + return Ok(()); } + self.start_with_deadline(deadline).await } async fn stop(&self) -> crate::Result<()> { @@ -3316,14 +3377,15 @@ mod tests { .await .expect_err("a state change that outlives the deadline should fail"); - assert!( - start_sandbox.calls_async().await >= 2, - "start should be retried while the deadline allows" + assert_eq!( + start_sandbox.calls_async().await, + 1, + "start should not be retried while the current transition is in flight" ); assert!( - err.causes() - .iter() - .any(|cause| cause.to_ascii_lowercase().contains("state change in progress")), + err.causes().iter().any(|cause| cause + .to_ascii_lowercase() + .contains("state change in progress")), "error should carry the Daytona rejection: {err}" ); } @@ -3342,6 +3404,13 @@ mod tests { 400, "Sandbox already started" ))); + assert!(!is_state_change_in_progress(&DaytonaError::api( + 500, + "Sandbox state change in progress" + ))); + assert!(!is_state_change_in_progress(&DaytonaError::general( + "Sandbox state change in progress" + ))); } #[tokio::test] From b7e3b660ffa076e4f9897c0769897a3530eca089 Mon Sep 17 00:00:00 2001 From: Release Repro Date: Thu, 20 Aug 2026 20:34:38 -0400 Subject: [PATCH 51/63] Simplify diagnostics timeout handling --- lib/apps/fabro-server/src/diagnostics.rs | 130 ++++++------------ lib/apps/fabro-server/src/server.rs | 19 ++- .../fabro-server/src/server/handler/system.rs | 12 +- lib/components/fabro-llm/src/model_test.rs | 43 +++++- .../fabro-sandbox/src/daytona/mod.rs | 89 ++++++++++-- 5 files changed, 176 insertions(+), 117 deletions(-) diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index 86f625366..9373cc81e 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -6,7 +6,7 @@ use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use fabro_auth::auth_issue_message; use fabro_llm::client::Client as LlmClient; -use fabro_llm::model_test::{ModelTestOutcome, ModelTestStatus, run_basic_model_probe}; +use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe_with_timeout}; use fabro_model::{Catalog, ProviderId}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; @@ -255,35 +255,15 @@ async fn probe_single_provider( None, ); }; - let model_id = model.id.clone(); + let model_id = model.id.to_string(); - let outcome = run_basic_model_probe(model_id.as_str(), provider.clone(), client); - provider_probe_with_timeout( - provider, - model_id.to_string(), - outcome, + let outcome = run_basic_model_probe_with_timeout( + &model_id, + &provider, + client, EXTERNAL_SERVICE_PROBE_TIMEOUT, ) - .await -} - -async fn provider_probe_with_timeout( - provider: ProviderId, - model_id: String, - probe: F, - probe_timeout: Duration, -) -> ProviderProbeResult -where - F: Future, -{ - let Ok(outcome) = timeout(probe_timeout, probe).await else { - return provider_probe_error( - provider, - Some(model_id), - probe_timeout_message(probe_timeout), - None, - ); - }; + .await; match outcome.status { ModelTestStatus::Ok => ProviderProbeResult { @@ -302,14 +282,6 @@ where } } -fn probe_timeout_message(probe_timeout: Duration) -> String { - if probe_timeout.subsec_nanos() == 0 { - format!("timeout ({}s)", probe_timeout.as_secs()) - } else { - format!("timeout ({}ms)", probe_timeout.as_millis()) - } -} - fn provider_probe_error( provider: ProviderId, model_id: Option, @@ -689,28 +661,13 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { }; }; - check_cloud_sandbox_with_probe( - || state.check_daytona_api_key(api_key), - EXTERNAL_SERVICE_PROBE_TIMEOUT, - ) - .await + let probe = state + .check_daytona_api_key_with_timeout(api_key, EXTERNAL_SERVICE_PROBE_TIMEOUT) + .await; + cloud_sandbox_probe_check(probe) } -async fn check_cloud_sandbox_with_probe(probe: F, probe_timeout: Duration) -> CheckResult -where - F: FnOnce() -> Fut, - Fut: Future>, -{ - let Ok(probe) = timeout(probe_timeout, probe()).await else { - return CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: probe_timeout_message(probe_timeout), - details: vec![CheckDetail::new("Daytona probe timed out".to_string())], - remediation: Some("Verify DAYTONA_API_KEY value and Daytona reachability".to_string()), - }; - }; - +fn cloud_sandbox_probe_check(probe: anyhow::Result) -> CheckResult { match probe { Ok(check) if check.ok() => CheckResult { name: "Cloud Sandbox".to_string(), @@ -733,13 +690,29 @@ where daytona::required_perms_display() )), }, - Err(err) => CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: "Daytona credential rejected".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], - remediation: Some("Verify DAYTONA_API_KEY value and Daytona reachability".to_string()), - }, + Err(err) => { + if let Some(timeout) = err.downcast_ref::() { + return CheckResult { + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: format!("timeout ({:?})", timeout.timeout()), + details: vec![CheckDetail::new("Daytona probe timed out".to_string())], + remediation: Some( + "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), + ), + }; + } + + CheckResult { + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: "Daytona credential rejected".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], + remediation: Some( + "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), + ), + } + } } } @@ -1090,27 +1063,6 @@ mod tests { ); } - #[tokio::test] - async fn provider_probe_reports_provider_specific_timeout() { - assert_eq!( - probe_timeout_message(EXTERNAL_SERVICE_PROBE_TIMEOUT), - "timeout (15s)" - ); - - let result = provider_probe_with_timeout( - ProviderId::new("modal"), - "modal/test-model".to_string(), - std::future::pending::(), - Duration::from_millis(1), - ) - .await; - - assert_eq!(result.provider, ProviderId::new("modal")); - assert_eq!(result.model_id.as_deref(), Some("modal/test-model")); - assert_eq!(result.status, ProviderProbeStatus::Error); - assert_eq!(result.error_message.as_deref(), Some("timeout (1ms)")); - } - #[test] fn docker_sandbox_probe_passes_when_daemon_responds() { let result = docker_sandbox_probe_check(Ok(())); @@ -1230,13 +1182,11 @@ enabled = false ); } - #[tokio::test] - async fn check_cloud_sandbox_reports_timeout() { - let result = check_cloud_sandbox_with_probe( - std::future::pending::>, - Duration::from_millis(1), - ) - .await; + #[test] + fn check_cloud_sandbox_reports_timeout() { + let result = cloud_sandbox_probe_check(Err(anyhow::Error::new( + daytona::DaytonaCredentialProbeTimeout::new(Duration::from_millis(1)), + ))); assert_eq!(result.name, "Cloud Sandbox"); assert_eq!(result.status, CheckStatus::Error); diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index 5445ad619..4976880f8 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -1455,6 +1455,15 @@ impl AppState { pub(crate) async fn check_daytona_api_key( &self, api_key: String, + ) -> anyhow::Result { + self.check_daytona_api_key_with_timeout(api_key, daytona::DAYTONA_CREDENTIAL_PROBE_TIMEOUT) + .await + } + + pub(crate) async fn check_daytona_api_key_with_timeout( + &self, + api_key: String, + probe_timeout: Duration, ) -> anyhow::Result { let base_url = self .config_env_lookup(EnvVars::DAYTONA_API_URL) @@ -1463,8 +1472,14 @@ impl AppState { let org_id = self.config_env_lookup(EnvVars::DAYTONA_ORGANIZATION_ID); let http_client = fabro_http::http_client().context("failed to build HTTP client")?; - daytona::check_daytona_api_key_with(&base_url, org_id.as_deref(), api_key, http_client) - .await + daytona::check_daytona_api_key_with_timeout( + &base_url, + org_id.as_deref(), + api_key, + http_client, + probe_timeout, + ) + .await } /// Borrow the persistent store so sibling modules can open run readers diff --git a/lib/apps/fabro-server/src/server/handler/system.rs b/lib/apps/fabro-server/src/server/handler/system.rs index a38fe33f5..d74f65944 100644 --- a/lib/apps/fabro-server/src/server/handler/system.rs +++ b/lib/apps/fabro-server/src/server/handler/system.rs @@ -778,12 +778,12 @@ mod tests { ) .await; - assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); - let body = axum::body::to_bytes(response.into_body(), usize::MAX) - .await - .expect("diagnostics timeout response body should be readable"); - let body: serde_json::Value = serde_json::from_slice(&body) - .expect("diagnostics timeout response should contain JSON"); + let body = fabro_test::expect_axum_json( + response, + StatusCode::GATEWAY_TIMEOUT, + "GET /api/v1/system/diagnostics timeout", + ) + .await; assert_eq!(body["errors"][0]["code"], "diagnostics_timeout"); assert_eq!(body["errors"][0]["detail"], "Server diagnostics timed out."); } diff --git a/lib/components/fabro-llm/src/model_test.rs b/lib/components/fabro-llm/src/model_test.rs index 6a985fb1b..de09f6fde 100644 --- a/lib/components/fabro-llm/src/model_test.rs +++ b/lib/components/fabro-llm/src/model_test.rs @@ -1,3 +1,4 @@ +use std::future::Future; use std::sync::Arc; use std::time::Duration; @@ -63,22 +64,38 @@ pub async fn run_basic_model_probe( model_id: &str, provider: impl ToString, client: Arc, +) -> ModelTestOutcome { + run_basic_model_probe_with_timeout( + model_id, + provider, + client, + Duration::from_secs(ModelTestMode::Basic.timeout_secs()), + ) + .await +} + +pub async fn run_basic_model_probe_with_timeout( + model_id: &str, + provider: impl ToString, + client: Arc, + probe_timeout: Duration, ) -> ModelTestOutcome { let params = GenerateParams::new(model_id, client) .provider(provider.to_string()) .prompt("Say OK") .max_tokens(16); - let result = time::timeout( - Duration::from_secs(ModelTestMode::Basic.timeout_secs()), - generate::generate(params), - ) - .await; + basic_model_probe_outcome(generate::generate(params), probe_timeout).await +} - match result { +async fn basic_model_probe_outcome(probe: F, probe_timeout: Duration) -> ModelTestOutcome +where + F: Future>, +{ + match time::timeout(probe_timeout, probe).await { Ok(Ok(_)) => ModelTestOutcome::ok(), Ok(Err(err)) => ModelTestOutcome::error(err.to_string()), - Err(_) => ModelTestOutcome::error("timeout (30s)"), + Err(_) => ModelTestOutcome::error(format!("timeout ({probe_timeout:?})")), } } @@ -244,6 +261,18 @@ mod tests { ); } + #[tokio::test] + async fn basic_model_probe_reports_configured_timeout() { + let outcome = basic_model_probe_outcome( + std::future::pending::>(), + Duration::from_millis(1), + ) + .await; + + assert_eq!(outcome.status, ModelTestStatus::Error); + assert_eq!(outcome.error_message.as_deref(), Some("timeout (1ms)")); + } + #[test] fn deep_test_omits_effort_for_reasoning_without_effort_controls() { let info = test_model_with(ModelFeatures { diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 6371f23d2..44d014159 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::fmt::Write; +use std::future::Future; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -62,7 +63,8 @@ pub const DEFAULT_DAYTONA_API_URL: &str = "https://app.daytona.io/api"; pub(crate) const DAYTONA_DASHBOARD_SANDBOXES_URL: &str = "https://app.daytona.io/dashboard/sandboxes"; const FABRO_SANDBOX_USER_AGENT: &str = concat!("fabro-sandbox/", env!("CARGO_PKG_VERSION")); -const DAYTONA_PROBE_TIMEOUT: Duration = Duration::from_secs(20); +pub const DAYTONA_CREDENTIAL_PROBE_TIMEOUT: Duration = Duration::from_secs(20); +const DAYTONA_BASH_SESSION_PROBE_TIMEOUT: Duration = Duration::from_secs(20); const DAYTONA_START_TIMEOUT: Duration = Duration::from_mins(1); /// Upper bound on explicit and Drop-triggered Daytona cleanup calls (session /// deletion, temporary stdin files) so a stalled REST call cannot block @@ -156,6 +158,24 @@ pub struct DaytonaKeyCheck { pub missing: Vec, } +#[derive(Debug, thiserror::Error)] +#[error("Daytona credential probe timed out after {timeout:?}")] +pub struct DaytonaCredentialProbeTimeout { + timeout: Duration, +} + +impl DaytonaCredentialProbeTimeout { + #[must_use] + pub const fn new(timeout: Duration) -> Self { + Self { timeout } + } + + #[must_use] + pub const fn timeout(&self) -> Duration { + self.timeout + } +} + impl DaytonaKeyCheck { pub fn ok(&self) -> bool { self.missing.is_empty() @@ -253,6 +273,23 @@ pub async fn check_daytona_api_key_with( org_id: Option<&str>, api_key: String, http_client: fabro_http::HttpClient, +) -> anyhow::Result { + check_daytona_api_key_with_timeout( + base_url, + org_id, + api_key, + http_client, + DAYTONA_CREDENTIAL_PROBE_TIMEOUT, + ) + .await +} + +pub async fn check_daytona_api_key_with_timeout( + base_url: &str, + org_id: Option<&str>, + api_key: String, + http_client: fabro_http::HttpClient, + probe_timeout: Duration, ) -> anyhow::Result { let work = async { let client = build_daytona_client_with( @@ -287,12 +324,21 @@ pub async fn check_daytona_api_key_with( }) }; - match time::timeout(DAYTONA_PROBE_TIMEOUT, work).await { + daytona_credential_probe_with_timeout(work, probe_timeout).await +} + +async fn daytona_credential_probe_with_timeout( + probe: F, + probe_timeout: Duration, +) -> anyhow::Result +where + F: Future>, +{ + match time::timeout(probe_timeout, probe).await { Ok(result) => result, - Err(_) => Err(anyhow::anyhow!( - "Daytona credential probe timed out after {}s", - DAYTONA_PROBE_TIMEOUT.as_secs() - )), + Err(_) => Err(anyhow::Error::new(DaytonaCredentialProbeTimeout::new( + probe_timeout, + ))), } } @@ -576,16 +622,16 @@ impl DaytonaSandbox { /// non-POSIX, and completion assertions all hold. /// /// Costs one session round trip plus a single status poll per sandbox - /// lifecycle transition. `DAYTONA_PROBE_TIMEOUT` is the outer backstop for - /// a stalled REST call; the inner [`BASH_PROBE_TIMEOUT_MS`] is the deadline - /// for the command itself. Session cleanup runs outside that deadline under - /// its own bounded timeout. + /// lifecycle transition. `DAYTONA_BASH_SESSION_PROBE_TIMEOUT` is the outer + /// backstop for a stalled REST call; the inner [`BASH_PROBE_TIMEOUT_MS`] is + /// the deadline for the command itself. Session cleanup runs outside that + /// deadline under its own bounded timeout. async fn probe_bash_session(sandbox: &daytona_sdk::Sandbox) -> crate::Result<()> { - let deadline = time::Instant::now() + DAYTONA_PROBE_TIMEOUT; + let deadline = time::Instant::now() + DAYTONA_BASH_SESSION_PROBE_TIMEOUT; let timeout_error = || { crate::Error::message(format!( "Daytona Bash session check timed out after {}s", - DAYTONA_PROBE_TIMEOUT.as_secs() + DAYTONA_BASH_SESSION_PROBE_TIMEOUT.as_secs() )) }; let mut session = match time::timeout_at(deadline, DaytonaSession::create(sandbox)).await { @@ -3358,6 +3404,25 @@ mod tests { auth.assert_async().await; } + #[tokio::test] + async fn daytona_credential_probe_reports_configured_timeout() { + let err = daytona_credential_probe_with_timeout( + std::future::pending::>(), + Duration::from_millis(1), + ) + .await + .expect_err("probe should time out"); + let timeout = err + .downcast_ref::() + .expect("timeout should preserve its type"); + + assert_eq!(timeout.timeout(), Duration::from_millis(1)); + assert_eq!( + err.to_string(), + "Daytona credential probe timed out after 1ms" + ); + } + #[tokio::test] async fn daytona_stdin_file_uploads_exact_bytes_and_is_deleted() { let server = MockServer::start_async().await; From 2b095612c8eb315d35f80ef4ac6eb728b22ddd3a Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 20:35:52 -0400 Subject: [PATCH 52/63] Address run spec persistence review findings --- .../fabro-workflow/src/operations/create.rs | 54 +++++++++-------- .../fabro-workflow/src/pipeline/persist.rs | 58 +++++++++++++------ lib/foundation/fabro-redact/src/entropy.rs | 17 ++++++ lib/foundation/fabro-test/src/lib.rs | 18 ++---- 4 files changed, 95 insertions(+), 52 deletions(-) diff --git a/lib/components/fabro-workflow/src/operations/create.rs b/lib/components/fabro-workflow/src/operations/create.rs index 3f23acd5b..a872f0186 100644 --- a/lib/components/fabro-workflow/src/operations/create.rs +++ b/lib/components/fabro-workflow/src/operations/create.rs @@ -13,10 +13,11 @@ use std::sync::Arc; use fabro_config::Storage; use fabro_graphviz::graph::{AttrValue, Graph}; use fabro_model::{Catalog, ProviderId}; -use fabro_store::Database; +use fabro_store::{Database, RunDatabase}; use fabro_template::TemplateContext; use fabro_types::{ - AutomationRef, ForkSourceRef, GitContext, ManifestPath, RunId, RunProvenance, WorkflowSettings, + AutomationRef, ForkSourceRef, GitContext, ManifestPath, RunBlobId, RunId, RunProvenance, + WorkflowSettings, }; use fabro_util::json::normalize_json_value; use tokio::task::spawn_blocking; @@ -517,24 +518,17 @@ async fn persist_created_run( .create_run(&record.run_id) .await .map_err(|err| Error::engine_with_source("failed to create run store", err))?; - let manifest_blob = match submitted_manifest_bytes { - Some(bytes) => Some(run_store.write_blob(bytes).await.map_err(store_error)?), - None => None, - }; - let definition_blob = match accepted_definition { - Some(definition) => { - let bytes = - serde_json::to_vec(definition).map_err(|err| Error::engine(err.to_string()))?; - Some(run_store.write_blob(&bytes).await.map_err(store_error)?) - } - None => None, - }; - // The spec on the run.created event is subject to secret redaction in - // stored copies; the blob keeps the exact bytes execution needs. - let spec_blob = { - let bytes = serde_json::to_vec(record).map_err(|err| Error::engine(err.to_string()))?; - Some(run_store.write_blob(&bytes).await.map_err(store_error)?) - }; + let definition_bytes = accepted_definition + .map(serde_json::to_vec) + .transpose() + .map_err(|err| Error::engine_with_source("failed to serialize run definition", err))?; + let spec_bytes = serde_json::to_vec(record) + .map_err(|err| Error::engine_with_source("failed to serialize run spec", err))?; + let (manifest_blob, definition_blob, spec_blob) = tokio::try_join!( + write_optional_blob(&run_store, submitted_manifest_bytes), + write_optional_blob(&run_store, definition_bytes.as_deref()), + async { run_store.write_blob(&spec_bytes).await.map_err(store_error) }, + )?; let title = explicit_title.unwrap_or_else(|| fabro_types::infer_run_title(record.graph.goal())); let stored = to_run_event_at( @@ -561,7 +555,7 @@ async fn persist_created_run( automation: record.automation.clone(), provenance: record.provenance.clone(), manifest_blob, - spec_blob, + spec_blob: Some(spec_blob), git: record.git.clone(), fork_source_ref: record.fork_source_ref.clone(), retried_from: None, @@ -588,8 +582,22 @@ async fn persist_created_run( .map_err(store_error) } -fn store_error(err: impl std::fmt::Display) -> Error { - Error::engine(err.to_string()) +async fn write_optional_blob( + run_store: &RunDatabase, + bytes: Option<&[u8]>, +) -> Result, Error> { + match bytes { + Some(bytes) => run_store + .write_blob(bytes) + .await + .map(Some) + .map_err(store_error), + None => Ok(None), + } +} + +fn store_error(err: impl Into) -> Error { + Error::engine_with_source("run store operation failed", err) } /// Parse, transform, and validate `dot_source`. diff --git a/lib/components/fabro-workflow/src/pipeline/persist.rs b/lib/components/fabro-workflow/src/pipeline/persist.rs index 8c224c700..303241ff5 100644 --- a/lib/components/fabro-workflow/src/pipeline/persist.rs +++ b/lib/components/fabro-workflow/src/pipeline/persist.rs @@ -65,22 +65,23 @@ async fn executable_run_spec( let bytes = run_store .read_blob(&blob_id) .await - .map_err(|err| Error::engine(err.to_string()))? + .map_err(|err| Error::engine_with_anyhow("failed to read run spec blob", err))? .ok_or_else(|| { Error::engine(format!( "run spec blob is missing from the run store: {blob_id}" )) })?; - let mut spec: RunSpec = - serde_json::from_slice(&bytes).map_err(|err| Error::Parse(err.to_string()))?; - // The event stream stays authoritative for run identity, for provenance - // (a retry rewrites it), for blob ids recorded on events after the spec - // blob was written, and for a graph source the blob does not carry. + let mut spec: RunSpec = serde_json::from_slice(&bytes) + .map_err(|err| Error::engine_with_source("run spec blob was not valid JSON", err))?; + // The event stream stays authoritative for run identity, provenance, and + // blob ids. Prefer the unredacted graph source from the blob, with the + // folded source as a compatibility fallback. spec.run_id = folded.run_id; spec.provenance = folded.provenance; spec.manifest_blob = folded.manifest_blob; spec.definition_blob = folded.definition_blob; spec.spec_blob = folded.spec_blob; + spec.fork_source_ref = folded.fork_source_ref; spec.graph_source = spec.graph_source.or(folded.graph_source); Ok(spec) } @@ -191,27 +192,24 @@ mod tests { } async fn seeded_store(record: &RunSpec, source: Option<&str>) -> RunDatabase { - seeded_store_with(record, source, true).await + seeded_store_with(record, source, Some(record)).await } async fn seeded_store_with( record: &RunSpec, source: Option<&str>, - write_spec_blob: bool, + blob_record: Option<&RunSpec>, ) -> RunDatabase { let store = memory_store(); let run_store = store.create_run(&record.run_id).await.unwrap(); - // Mirror the production producer: the unredacted spec rides a blob - // and the redacted event carries its id. - let spec_blob = if write_spec_blob { - Some( + let spec_blob = match blob_record { + Some(blob_record) => Some( run_store - .write_blob(&serde_json::to_vec(record).unwrap()) + .write_blob(&serde_json::to_vec(blob_record).unwrap()) .await .unwrap(), - ) - } else { - None + ), + None => None, }; append_event(&run_store, &record.run_id, &Event::RunCreated { run_id: record.run_id, @@ -384,7 +382,7 @@ mod tests { let mut record = sample_record(different_graph()); record.graph = graph; - let run_store = seeded_store_with(&record, Some(&source), false).await; + let run_store = seeded_store_with(&record, Some(&source), None).await; let loaded = load_from_store(&run_store.clone().into(), &run_dir) .await .unwrap(); @@ -393,6 +391,32 @@ mod tests { assert_eq!(loaded.run_spec().spec_blob, None); } + #[tokio::test] + async fn load_from_store_uses_fork_reference_from_event_fold() { + let temp = tempfile::tempdir().unwrap(); + let run_dir = temp.path().join("run"); + std::fs::create_dir_all(&run_dir).unwrap(); + let (graph, source) = graph_and_source(); + let source_record = sample_record(graph.clone()); + let mut fork_record = source_record.clone(); + fork_record.run_id = fixtures::RUN_7; + fork_record.fork_source_ref = Some(fabro_types::ForkSourceRef { + source_run_id: source_record.run_id, + checkpoint_sha: "checkpoint-sha".to_string(), + }); + + let run_store = seeded_store_with(&fork_record, Some(&source), Some(&source_record)).await; + let loaded = load_from_store(&run_store.clone().into(), &run_dir) + .await + .unwrap(); + + assert_eq!(loaded.run_spec().run_id, fork_record.run_id); + assert_eq!( + loaded.run_spec().fork_source_ref, + fork_record.fork_source_ref + ); + } + #[test] fn persist_returns_error_on_io_failure() { let temp = tempfile::tempdir().unwrap(); diff --git a/lib/foundation/fabro-redact/src/entropy.rs b/lib/foundation/fabro-redact/src/entropy.rs index 884aa3713..ec6029c3c 100644 --- a/lib/foundation/fabro-redact/src/entropy.rs +++ b/lib/foundation/fabro-redact/src/entropy.rs @@ -81,6 +81,10 @@ pub(super) fn find_entropy_regions(s: &str) -> Vec { /// qualify simply measures under the threshold; no length guard is needed. fn assignment_value_offset(token: &str) -> Option { let eq = token.find('=')?; + let value = &token[eq + 1..]; + if value.is_empty() || value.starts_with('=') { + return None; + } let name = &token[..eq]; let mut chars = name.chars(); let first = chars.next()?; @@ -135,6 +139,19 @@ mod tests { assert_eq!(regions[0].end, input.len()); } + #[test] + fn regions_find_padded_base64_tokens() { + for input in [ + "WxFhjC5EAnh30M0JIe0Wa58Xb1BYf8kedTTdKUbbd9Y=", + "AbCdEfGhIjKlMnOpQrStUvWxYz0123456789ABCDEF==", + ] { + assert_eq!(find_entropy_regions(input), vec![Region { + start: 0, + end: input.len(), + }]); + } + } + #[test] fn regions_empty_for_json_escape_sequence() { // "controller.go\nmodel.go" — the regex could match across the \n boundary diff --git a/lib/foundation/fabro-test/src/lib.rs b/lib/foundation/fabro-test/src/lib.rs index eec273d32..5c53f9ba2 100644 --- a/lib/foundation/fabro-test/src/lib.rs +++ b/lib/foundation/fabro-test/src/lib.rs @@ -1955,18 +1955,12 @@ pub fn json_snapshot_filters(mut filters: Vec<(String, String)>) -> Vec<(String, r#""id": "[EVENT_ID]""#.to_string(), )); filters = json_elapsed_ms_snapshot_filters(filters); - filters.push(( - r#""manifest_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""manifest_blob": "[BLOB_ID]""#.to_string(), - )); - filters.push(( - r#""definition_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""definition_blob": "[BLOB_ID]""#.to_string(), - )); - filters.push(( - r#""spec_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""spec_blob": "[BLOB_ID]""#.to_string(), - )); + for field in ["manifest_blob", "definition_blob", "spec_blob"] { + filters.push(( + format!(r#""{field}":\s*"[0-9a-f]{{64}}""#), + format!(r#""{field}": "[BLOB_ID]""#), + )); + } filters.push(( r#""run_dir":\s*"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]""#.to_string(), r#""run_dir": "[RUN_DIR]""#.to_string(), From a64b65b88c1b6d33e65116cc28d428cc7e1c1951 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 20:50:41 -0400 Subject: [PATCH 53/63] Update blob hash CLI snapshot --- lib/apps/fabro-cli/tests/it/cmd/attach.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/apps/fabro-cli/tests/it/cmd/attach.rs b/lib/apps/fabro-cli/tests/it/cmd/attach.rs index b2b3904ee..9f388c14b 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/attach.rs @@ -1012,7 +1012,7 @@ fn attach_json_errors_without_prompting_for_human_input() { } }, "source_directory": "[TEMP_DIR]", - "spec_blob": "[BLOB_ID]", + "spec_blob": "[BLOB_HASH]", "title": "Wait for approval", "web_url": "http://localhost:3000/runs/[ULID]", "workflow_slug": "human-gate", From 78cb0d134841515c9b2d5458ae8d4b11cdb1589c Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 21:59:47 -0400 Subject: [PATCH 54/63] Clean up git push retry handling --- .../fabro-github/src/token_source.rs | 7 +- .../fabro-sandbox/src/daytona/mod.rs | 36 +-- lib/components/fabro-sandbox/src/docker.rs | 34 +-- lib/components/fabro-sandbox/src/git_retry.rs | 69 ++++-- lib/components/fabro-sandbox/src/lib.rs | 4 +- .../fabro-sandbox/src/push_credentials.rs | 86 ++++--- lib/components/fabro-sandbox/src/sandbox.rs | 226 +++++++++++++----- lib/components/fabro-workflow/src/error.rs | 30 ++- lib/components/fabro-workflow/src/event.rs | 2 +- .../fabro-workflow/src/event/convert.rs | 33 ++- .../fabro-workflow/src/event/events.rs | 5 +- .../fabro-workflow/src/lifecycle/git.rs | 40 ++-- .../fabro-workflow/src/pipeline/finalize.rs | 13 +- .../fabro-workflow/src/pipeline/publish.rs | 144 +++++------ .../fabro-workflow/src/run_metadata.rs | 120 ++++++---- .../fabro-types/src/run_event/misc.rs | 56 ++++- 16 files changed, 543 insertions(+), 362 deletions(-) diff --git a/lib/components/fabro-github/src/token_source.rs b/lib/components/fabro-github/src/token_source.rs index 11743e077..8c3bc7fc2 100644 --- a/lib/components/fabro-github/src/token_source.rs +++ b/lib/components/fabro-github/src/token_source.rs @@ -36,7 +36,10 @@ pub const REFRESH_MARGIN: Duration = Duration::from_mins(10); /// credentials (a PAT, or a pre-minted installation token) carry no /// `minted_at`, so token age is undefined for them and they are never /// treated as freshly minted. -#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, strum::Display, +)] +#[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum TokenProvenance { /// This resolve minted the token. @@ -55,7 +58,7 @@ pub enum TokenProvenance { /// Non-secret description of the token a resolve returned. Shared by the /// source, refresh outcomes, logs, and events. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct TokenSnapshot { /// Increments per mint; 0 for `Static`. pub generation: u64, diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 7337d9cf0..b698b08f9 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -1085,15 +1085,9 @@ impl Sandbox for DaytonaSandbox { })?), None => None, }; - // The clone call site maps its mint knowledge onto the - // credential context: a token minted for this clone is - // FreshApp; a static credential cannot become valid by - // waiting. - let clone_credential_context = match &resolved_token { - Some(token) if !token.snapshot.is_static() => CredentialContext::FreshApp, - Some(_) => CredentialContext::Static, - None => CredentialContext::None, - }; + let clone_credential_context = CredentialContext::from_snapshot( + resolved_token.as_ref().map(|token| &token.snapshot), + ); let (username, password) = match &resolved_token { Some(token) => ( Some("x-access-token".to_string()), @@ -1164,7 +1158,7 @@ impl Sandbox for DaytonaSandbox { })?; let clone_plan = git_retry::RetryPlan::clone_default(None); - let clone_result = git_retry::retry_git( + let clone_result = git_retry::retry_clone( SandboxProviderKind::Daytona, "clone", &clone_plan, @@ -1562,26 +1556,8 @@ impl Sandbox for DaytonaSandbox { return Ok(RefreshOutcome::none()); // no authenticated origin — nothing to refresh }; self.push_credentials - .refresh(origin_url, |auth_url| async move { - let cmd = format!( - "git -c maintenance.auto=0 remote set-url origin {}", - shell_quote(auth_url.as_raw_url().as_str()), - ); - let result = self - .exec_command(&cmd, 10_000, None, None, None) - .await - .map_err(|_| { - crate::Error::message( - "Failed to refresh push credentials: set_url_exec_failed", - ) - })?; - if !result.is_success() { - return Err(result.into_exec_error_with_redactor( - "git remote set-url origin (refresh push credentials)", - |s| redact_auth_url(s, Some(&auth_url)), - )); - } - Ok(()) + .refresh(origin_url, |auth_url| { + push_credentials::set_auth_url_via_exec(self, auth_url) }) .await } diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index 5232487f4..90d12d3c8 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -764,11 +764,8 @@ impl DockerSandbox { // The clone call site maps its mint knowledge onto the credential // context: a token minted for this clone is FreshApp; a static // credential cannot become valid by waiting. - let clone_credential_context = match &resolved_token { - Some(token) if !token.snapshot.is_static() => CredentialContext::FreshApp, - Some(_) => CredentialContext::Static, - None => CredentialContext::None, - }; + let clone_credential_context = + CredentialContext::from_snapshot(resolved_token.as_ref().map(|token| &token.snapshot)); let auth_url = match &resolved_token { Some(token) => Some( @@ -814,7 +811,7 @@ impl DockerSandbox { let command = git_clone_command(clone_url, branch.as_deref(), &layout.primary_repo_path); let clone_deadline = time::Instant::now() + GIT_CLONE_TIMEOUT; let clone_plan = git_retry::RetryPlan::clone_default(Some(clone_deadline)); - let clone_result = git_retry::retry_git( + let clone_result = git_retry::retry_clone( SandboxProviderKind::Docker, "clone", &clone_plan, @@ -1407,13 +1404,7 @@ fn classify_docker_clone_result( result: &ExecResult, cred: CredentialContext, ) -> Option { - let stderr = git_retry::classify_message(&result.stderr, cred); - match stderr { - git_retry::GitMessageClass::Unknown => { - git_retry::classify_message(&result.stdout, cred).retry_reason() - } - class => class.retry_reason(), - } + git_retry::classify_output(&result.stderr, &result.stdout, cred).retry_reason() } fn host_config(config: &DockerSandboxOptions) -> HostConfig { @@ -2224,21 +2215,8 @@ impl Sandbox for DockerSandbox { return Ok(RefreshOutcome::none()); }; self.push_credentials - .refresh(origin_url, |auth_url| async move { - let command = format!( - "git -c maintenance.auto=0 remote set-url origin {}", - shell_quote(auth_url.as_raw_url().as_str()) - ); - let result = self - .docker_exec_shell(&command, 10_000, Some(self.working_directory()), None, None) - .await?; - if !result.is_success() { - return Err(result.into_exec_error_with_redactor( - "git remote set-url origin (refresh push credentials)", - |s| redact_auth_url(s, Some(&auth_url)), - )); - } - Ok(()) + .refresh(origin_url, |auth_url| { + push_credentials::set_auth_url_via_exec(self, auth_url) }) .await } diff --git a/lib/components/fabro-sandbox/src/git_retry.rs b/lib/components/fabro-sandbox/src/git_retry.rs index 5e767abdb..7b16cc256 100644 --- a/lib/components/fabro-sandbox/src/git_retry.rs +++ b/lib/components/fabro-sandbox/src/git_retry.rs @@ -22,6 +22,7 @@ use fabro_github::token_source::TokenSnapshot; #[cfg(test)] use fabro_github::token_source::{REFRESH_MARGIN, TokenProvenance}; use fabro_types::SandboxProviderKind; +pub use fabro_types::run_event::GitPushRetryReason as GitRetryReason; use fabro_util::backoff::BackoffPolicy; use tokio::time; @@ -30,17 +31,6 @@ use tokio::time; /// occasionally tens of seconds). pub(crate) const REPLICATION_HORIZON: Duration = Duration::from_mins(1); -/// Why a failed git attempt is worth repeating. -#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] -#[strum(serialize_all = "snake_case")] -pub enum GitRetryReason { - /// A recently minted installation token has not reached the GitHub edge - /// cache site serving this operation yet. - TokenReplication, - /// The operation failed on infrastructure, unrelated to credentials. - TransientInfra, -} - /// What a git failure message tells us about retry safety. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum GitMessageClass { @@ -145,6 +135,10 @@ pub(crate) fn matches_auth_failure_hints(message: &str) -> bool { .any(|hint| lower.contains(hint)) } +pub(crate) fn output_matches_auth_failure_hints(stderr: &str, stdout: &str) -> bool { + matches_auth_failure_hints(stderr) || matches_auth_failure_hints(stdout) +} + /// Classify a failed git operation by its rendered message. /// /// `cred` gates the reading of 404/auth-failure messages: a fresh App token @@ -179,6 +173,19 @@ pub(crate) fn classify_message(message: &str, cred: CredentialContext) -> GitMes GitMessageClass::Unknown } +pub(crate) fn classify_output( + stderr: &str, + stdout: &str, + cred: CredentialContext, +) -> GitMessageClass { + let by_stderr = classify_message(stderr, cred); + if by_stderr == GitMessageClass::Unknown { + classify_message(stdout, cred) + } else { + by_stderr + } +} + /// Classify a rendered git failure message, returning the retry reason when /// the failure is transient for these credentials. `None` means the failure /// is permanent or unrecognized. @@ -292,15 +299,30 @@ impl RetryPlan { (None, remaining) => remaining, } } + + pub(crate) fn retry_delay( + &self, + attempt_number: u32, + deadline: Option, + ) -> Option { + let delay = self.backoff.delay_for_attempt(attempt_number); + if deadline.is_some_and(|deadline| { + delay >= deadline.saturating_duration_since(time::Instant::now()) + }) { + None + } else { + Some(delay) + } + } } -/// Run a git operation, repeating it while the failure looks transient. +/// Run a clone operation, repeating it while the failure looks transient. /// /// `attempt` receives the 1-based attempt number. `classify` decides whether /// an error is worth repeating; `None` returns it to the caller untouched. /// A retry starts only when its backoff fits before the plan's effective /// deadline. The final error is returned as-is. -pub(crate) async fn retry_git( +pub(crate) async fn retry_clone( provider: SandboxProviderKind, op: &str, plan: &RetryPlan, @@ -321,12 +343,9 @@ where let Some(reason) = classify(&err) else { return Err(err); }; - let delay = plan.backoff.delay_for_attempt(attempt_number); - if deadline.is_some_and(|deadline| { - delay >= deadline.saturating_duration_since(time::Instant::now()) - }) { + let Some(delay) = plan.retry_delay(attempt_number, deadline) else { return Err(err); - } + }; // The failure text can carry git stderr, so log the category // rather than the message. The caller still reports the full // error if the attempts run out. @@ -576,7 +595,7 @@ mod tests { async fn first_success_runs_one_attempt() { let attempts = Attempts::default(); - let result = retry_git( + let result = retry_clone( SandboxProviderKind::Docker, "clone", &RetryPlan::clone_default(None), @@ -596,7 +615,7 @@ mod tests { async fn retries_until_a_later_attempt_succeeds() { let attempts = Attempts::default(); - let result = retry_git( + let result = retry_clone( SandboxProviderKind::Docker, "clone", &RetryPlan::clone_default(None), @@ -622,7 +641,7 @@ mod tests { async fn exhausted_attempts_return_the_final_error() { let attempts = Attempts::default(); - let result = retry_git( + let result = retry_clone( SandboxProviderKind::Docker, "clone", &RetryPlan::clone_default(None), @@ -646,7 +665,7 @@ mod tests { async fn unretryable_failure_stops_immediately() { let attempts = Attempts::default(); - let result = retry_git( + let result = retry_clone( SandboxProviderKind::Docker, "clone", &RetryPlan::clone_default(None), @@ -673,7 +692,7 @@ mod tests { let attempts = Attempts::default(); let deadline = time::Instant::now() + Duration::from_secs(2); - let result = retry_git( + let result = retry_clone( SandboxProviderKind::Docker, "clone", &RetryPlan::clone_default(Some(deadline)), @@ -696,7 +715,7 @@ mod tests { async fn unbounded_plan_runs_all_attempts() { let attempts = Attempts::default(); - let result = retry_git( + let result = retry_clone( SandboxProviderKind::Daytona, "clone", &RetryPlan::clone_default(None), @@ -723,7 +742,7 @@ mod tests { outer_deadline: None, }; - let result = retry_git( + let result = retry_clone( SandboxProviderKind::Docker, "push", &plan, diff --git a/lib/components/fabro-sandbox/src/lib.rs b/lib/components/fabro-sandbox/src/lib.rs index 5777beccf..fb442e9fb 100644 --- a/lib/components/fabro-sandbox/src/lib.rs +++ b/lib/components/fabro-sandbox/src/lib.rs @@ -9,7 +9,7 @@ pub mod sandbox_spec; #[cfg(any(feature = "docker", feature = "daytona"))] mod clone_source; -pub mod git_retry; +mod git_retry; #[cfg(any(feature = "docker", feature = "daytona", test))] mod managed_labels; @@ -43,7 +43,7 @@ pub use fabro_github::token_source::{ InstallationTokenSource, ResolvedToken, TokenProvenance, TokenSnapshot, }; pub use fabro_types::{RunSandboxInstance, SandboxProviderKind}; -pub use git_retry::{CredentialContext, GitRetryReason, RetryPlan}; +pub use git_retry::{CredentialContext, GitRetryReason, RetryPlan, classify_failure}; pub use local::LocalSandbox; #[cfg(feature = "daytona")] pub use provider::daytona::DaytonaSandboxProvider; diff --git a/lib/components/fabro-sandbox/src/push_credentials.rs b/lib/components/fabro-sandbox/src/push_credentials.rs index 5ec3cc48b..81569e7c5 100644 --- a/lib/components/fabro-sandbox/src/push_credentials.rs +++ b/lib/components/fabro-sandbox/src/push_credentials.rs @@ -13,6 +13,7 @@ use std::sync::Arc; use fabro_github::GitHubCredentials; use fabro_github::token_source::{InstallationTokenSource, ResolvedToken, TokenSnapshot}; use fabro_redact::DisplaySafeUrl; +pub use fabro_types::run_event::GitCredentialRefreshError as RefreshErrorKind; use tokio::sync::{Mutex, MutexGuard}; use crate::redact; @@ -151,18 +152,6 @@ impl PushCredentialState { } } -/// Which refresh step failed while a push held the credential lease. -#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] -#[strum(serialize_all = "snake_case")] -pub enum RefreshErrorKind { - /// Minting a replacement token failed; the push proceeded with the last - /// embedded token. - Mint, - /// Rewriting `origin` with the resolved token failed; the push proceeded - /// with the last embedded token. - SetUrl, -} - /// What [`CredentialLease::ensure_embedded`] did for one push attempt. #[derive(Debug, Clone, Copy)] pub(crate) struct EnsureOutcome { @@ -187,11 +176,14 @@ pub(crate) struct EnsureOutcome { /// embed. The token source's refresh margin exceeds every push plan's elapsed /// bound, so the pinned token always outlives the operation. pub(crate) struct CredentialLease<'a> { - source: Option<&'a InstallationTokenSource>, + source: Option<&'a InstallationTokenSource>, /// Embed-mutex guard: the last successfully embedded token. - embedded: MutexGuard<'a, Option>, + embedded: MutexGuard<'a, Option>, /// The operation's single successful resolve. - target: Option, + target: Option, + /// Skip an immediate duplicate resolve after lease acquisition already + /// failed. A later push attempt can retry after backoff. + defer_resolve_once: bool, } impl PushCredentialState { @@ -210,6 +202,7 @@ impl PushCredentialState { source: None, embedded, target: None, + defer_resolve_once: false, }); }; match source.resolve().await { @@ -217,6 +210,7 @@ impl PushCredentialState { source: Some(source), embedded, target: Some(resolved), + defer_resolve_once: false, }), Err(err) => { if let Some(prev) = embedded.as_ref() { @@ -231,6 +225,7 @@ impl PushCredentialState { source: Some(source), embedded, target: None, + defer_resolve_once: true, }) } else { tracing::warn!( @@ -266,16 +261,18 @@ impl CredentialLease<'_> { sandbox: &dyn crate::Sandbox, origin_url: &str, force: bool, - ) -> EnsureOutcome { + ) -> crate::Result { let Some(source) = self.source else { - return EnsureOutcome { + return Ok(EnsureOutcome { action: RemoteCredentialAction::None, token: None, refresh_error: None, - }; + }); }; - let mut refresh_error = None; - if self.target.is_none() { + let mut refresh_error = self.defer_resolve_once.then_some(RefreshErrorKind::Mint); + if self.defer_resolve_once { + self.defer_resolve_once = false; + } else if self.target.is_none() { match source.resolve().await { Ok(resolved) => self.target = Some(resolved), Err(err) => { @@ -291,43 +288,50 @@ impl CredentialLease<'_> { // Managed credentials with nothing resolved or embedded: // acquisition fails before any attempt runs, so pushes never see // this state. - return EnsureOutcome { + return Ok(EnsureOutcome { action: RemoteCredentialAction::None, token: None, refresh_error, - }; + }); }; let embedded_generation = self .embedded .as_ref() .map(|token| token.snapshot.generation); if !force && embedded_generation == Some(desired.snapshot.generation) { - return EnsureOutcome { + return Ok(EnsureOutcome { action: RemoteCredentialAction::Unchanged, token: Some(desired.snapshot), refresh_error, - }; + }); } match set_url_via_exec(sandbox, origin_url, &desired).await { Ok(()) => { let snapshot = desired.snapshot; *self.embedded = Some(desired); - EnsureOutcome { + Ok(EnsureOutcome { action: RemoteCredentialAction::Embedded, token: Some(snapshot), refresh_error, - } + }) } Err(err) => { + if matches!( + &err, + crate::Error::Exec { result, .. } + if result.termination != fabro_types::CommandTermination::Exited + ) { + return Err(err); + } tracing::warn!( error = %crate::display_for_log(&err), "embedding push credentials in origin failed; pushing with the last embedded token" ); - EnsureOutcome { + Ok(EnsureOutcome { action: RemoteCredentialAction::Unchanged, token: self.snapshot(), refresh_error: Some(RefreshErrorKind::SetUrl), - } + }) } } } @@ -342,8 +346,18 @@ async fn set_url_via_exec( ) -> crate::Result<()> { let auth_url = fabro_github::embed_token_in_url(origin_url, token.token.expose()).map_err(|err| { - crate::Error::message(format!("Failed to build authenticated origin URL: {err:#}")) + crate::Error::context( + "Failed to build authenticated origin URL", + RedactedSetUrlError(fabro_redact::redact_string(&format!("{err:#}"))), + ) })?; + set_auth_url_via_exec(sandbox, auth_url).await +} + +pub(crate) async fn set_auth_url_via_exec( + sandbox: &dyn crate::Sandbox, + auth_url: DisplaySafeUrl, +) -> crate::Result<()> { let command = format!( "git -c maintenance.auto=0 remote set-url origin {}", crate::shell_quote(auth_url.as_raw_url().as_str()) @@ -351,18 +365,26 @@ async fn set_url_via_exec( let result = sandbox .exec_command(&command, 10_000, None, None, None) .await - .map_err(|_| { - crate::Error::message("Failed to refresh push credentials: set_url_exec_failed") + .map_err(|err| { + let message = redact::redact_auth_url(&crate::display_for_log(&err), Some(&auth_url)); + crate::Error::context( + "Failed to refresh push credentials: set_url_exec_failed", + RedactedSetUrlError(message), + ) })?; if !result.is_success() { return Err(result.into_exec_error_with_redactor( - "git remote set-url origin (push credential lease)", + "git remote set-url origin (refresh push credentials)", |s| redact::redact_auth_url(s, Some(&auth_url)), )); } Ok(()) } +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +struct RedactedSetUrlError(String); + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/lib/components/fabro-sandbox/src/sandbox.rs b/lib/components/fabro-sandbox/src/sandbox.rs index d5adfc701..496df21d1 100644 --- a/lib/components/fabro-sandbox/src/sandbox.rs +++ b/lib/components/fabro-sandbox/src/sandbox.rs @@ -8,6 +8,7 @@ use std::time::Duration; use async_trait::async_trait; use fabro_github::token_source::{InstallationTokenSource, TokenSnapshot}; +pub use fabro_types::run_event::GitCredentialAction as RemoteCredentialAction; use fabro_types::{CommandOutputStream, CommandTermination}; use fabro_util::shell; use fabro_util::workspace_glob::WorkspaceGlob; @@ -18,7 +19,7 @@ use tokio::task::JoinHandle; use tokio::time; use tokio_util::sync::CancellationToken; -use crate::git_retry::{self, CredentialContext, GitMessageClass, GitRetryReason, RetryPlan}; +use crate::git_retry::{self, CredentialContext, GitRetryReason, RetryPlan}; use crate::push_credentials::{CredentialLease, PushCredentialState, RefreshErrorKind}; /// Git command prefix that disables background maintenance. @@ -1027,24 +1028,6 @@ pub struct GrepOptions { pub max_results: Option, } -/// What [`Sandbox::refresh_push_credentials`] did to the origin remote. -/// -/// Distinct from what the token *is* — the two are independent facts. A token -/// minted by another consumer and embedded here for the first time is an -/// `Embedded` action carrying a `Reused` provenance. -#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display)] -#[strum(serialize_all = "snake_case")] -pub enum RemoteCredentialAction { - /// `set-url` ran with a different generation than last embedded. - Embedded, - /// The resolved generation matched the last embedded one; `set-url` was - /// skipped. - Unchanged, - /// No managed credentials to embed (no clone, no authenticated origin, or - /// no GitHub credentials). - None, -} - /// Outcome of [`Sandbox::refresh_push_credentials`]: what this call did to the /// remote, and the non-secret description of the token embedded in it. /// `token` is `None` only when `action` is [`RemoteCredentialAction::None`]. @@ -1562,7 +1545,7 @@ pub(crate) async fn fetch_source_run_ref( /// One push attempt inside a retried push operation. Runtime detail only — /// the durable serialized shape lives in `fabro-types` and the workflow layer /// owns the conversion. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct PushAttempt { /// 1-based attempt number within this operation. pub attempt: u32, @@ -1590,34 +1573,22 @@ pub struct PushReport { /// A failed push operation: the final typed error plus the attempt history. /// The error type stays the safety boundary for output tails. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("git push failed")] pub struct PushError { pub report: PushReport, + #[source] pub error: crate::Error, } -impl std::fmt::Display for PushError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.error.fmt(f) - } -} - -impl std::error::Error for PushError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.error) - } -} - /// Classify a failed push attempt by the failure's rendered output. fn classify_push_error(error: &crate::Error, cred: CredentialContext) -> Option { let class = match error { + crate::Error::Exec { result, .. } if result.termination != CommandTermination::Exited => { + return None; + } crate::Error::Exec { result, .. } => { - let by_stderr = git_retry::classify_message(&result.stderr, cred); - if by_stderr == GitMessageClass::Unknown { - git_retry::classify_message(&result.stdout, cred) - } else { - by_stderr - } + git_retry::classify_output(&result.stderr, &result.stdout, cred) } other => git_retry::classify_message(&crate::display_for_log(other), cred), }; @@ -1629,8 +1600,7 @@ fn classify_push_error(error: &crate::Error, cred: CredentialContext) -> Option< fn push_failure_looks_auth_shaped(error: &crate::Error) -> bool { match error { crate::Error::Exec { result, .. } => { - git_retry::matches_auth_failure_hints(&result.stderr) - || git_retry::matches_auth_failure_hints(&result.stdout) + git_retry::output_matches_auth_failure_hints(&result.stderr, &result.stdout) } other => git_retry::matches_auth_failure_hints(&crate::display_for_log(other)), } @@ -1652,12 +1622,26 @@ pub(crate) async fn git_push_via_exec( use CredentialContext; use CredentialLease; + let start = time::Instant::now(); + let deadline = plan.effective_deadline(start); + // The lease pins one token generation and owns the embed mutex for the // whole operation; no concurrent refresh can re-embed mid-operation, and // no attempt can cross the refresh margin and restart the replication // clock. let mut lease: Option<(CredentialLease<'_>, &str)> = match credentials { - Some((state, origin_url)) => match state.lease().await { + Some((state, origin_url)) => match match deadline { + Some(deadline) => match time::timeout_at(deadline, state.lease()).await { + Ok(result) => result, + Err(_) => { + return Err(push_deadline_error( + Vec::new(), + "while acquiring credentials", + )); + } + }, + None => state.lease().await, + } { Ok(lease) => Some((lease, origin_url)), Err(error) => { return Err(PushError { @@ -1669,8 +1653,6 @@ pub(crate) async fn git_push_via_exec( None => None, }; - let start = time::Instant::now(); - let deadline = plan.effective_deadline(start); let mut attempts: Vec = Vec::new(); let mut force_reembed = false; let mut drift_repaired = false; @@ -1680,21 +1662,46 @@ pub(crate) async fn git_push_via_exec( loop { let attempt_number = u32::try_from(attempts.len()).unwrap_or(u32::MAX) + 1; let started_at = chrono::Utc::now(); + let attempt_timeout = plan + .attempt_timeout(deadline) + .unwrap_or(Duration::from_mins(1)); + if attempt_timeout.is_zero() { + return Err(push_deadline_error(attempts, "before the next attempt")); + } + let attempt_deadline = time::Instant::now() + attempt_timeout; let (token, credential_action, refresh_error) = match lease.as_mut() { Some((lease, origin_url)) => { - let ensured = lease - .ensure_embedded(sandbox, origin_url, force_reembed) - .await; + let ensured = match time::timeout_at( + attempt_deadline, + lease.ensure_embedded(sandbox, origin_url, force_reembed), + ) + .await + { + Ok(Ok(ensured)) => ensured, + Ok(Err(error)) => { + return Err(PushError { + report: PushReport { attempts }, + error, + }); + } + Err(_) => { + return Err(push_deadline_error( + attempts, + "while refreshing credentials", + )); + } + }; force_reembed = false; (ensured.token, Some(ensured.action), ensured.refresh_error) } None => (None, None, None), }; - let timeout = plan - .attempt_timeout(deadline) - .unwrap_or(Duration::from_mins(1)); - let timeout_ms = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX); + let remaining = attempt_deadline.saturating_duration_since(time::Instant::now()); + let timeout_ms = u64::try_from(remaining.as_millis()).unwrap_or(u64::MAX); + if timeout_ms == 0 { + return Err(push_deadline_error(attempts, "before running git push")); + } let push_result = match sandbox .exec_command(&cmd, timeout_ms, None, None, None) .await @@ -1753,15 +1760,12 @@ pub(crate) async fn git_push_via_exec( error, }); }; - let delay = plan.backoff.delay_for_attempt(attempt_number); - if deadline.is_some_and(|deadline| { - delay >= deadline.saturating_duration_since(time::Instant::now()) - }) { + let Some(delay) = plan.retry_delay(attempt_number, deadline) else { return Err(PushError { report: PushReport { attempts }, error, }); - } + }; // The failure text can carry git stderr, so log the category // rather than the message. tracing::warn!( @@ -1780,6 +1784,13 @@ pub(crate) async fn git_push_via_exec( } } +fn push_deadline_error(attempts: Vec, stage: &str) -> PushError { + PushError { + report: PushReport { attempts }, + error: crate::Error::message(format!("Git push retry deadline expired {stage}")), + } +} + #[cfg(test)] mod push_tests { use std::collections::VecDeque; @@ -1820,6 +1831,16 @@ mod push_tests { } } + fn timed_out_exec() -> ExecResult { + ExecResult { + stdout: String::new(), + stderr: "Command timed out".to_string(), + exit_code: None, + termination: CommandTermination::TimedOut, + duration_ms: 60_000, + } + } + /// Sandbox stub that scripts `git push` results and records the exec /// commands the push driver runs. `git remote set-url` execs succeed /// unless scripted otherwise. @@ -1998,6 +2019,19 @@ mod push_tests { } } + struct SlowMinter; + + #[async_trait] + impl InstallationTokenMinter for SlowMinter { + async fn mint(&self) -> anyhow::Result { + time::sleep(Duration::from_secs(2)).await; + Ok(InstallationToken { + token: "ghs_slow".to_string(), + expires_at: Utc::now() + chrono::Duration::hours(1), + }) + } + } + fn minting_state( script: Vec, ) -> (PushCredentialState, std::sync::Arc) { @@ -2165,9 +2199,8 @@ mod push_tests { async fn mint_failure_falls_back_to_the_clone_token() { let (state, minter) = minting_state(vec![ MintAction::Token("ghs_clone", chrono::Duration::minutes(5)), - // The clone token is inside the margin, so the lease acquisition - // re-mints — and fails. So does the attempt-level retry. - MintAction::Error("mint failed"), + // The clone token is inside the margin, so lease acquisition + // re-mints and fails. MintAction::Error("mint failed"), ]); seed_clone_token(&state).await; @@ -2182,7 +2215,7 @@ mod push_tests { .await .expect("push proceeds with the still-valid clone token"); - assert_eq!(minter.calls(), 3); + assert_eq!(minter.calls(), 2); let attempt = &report.attempts[0]; assert!(attempt.success); assert_eq!(attempt.refresh_error, Some(RefreshErrorKind::Mint)); @@ -2224,7 +2257,6 @@ mod push_tests { let (state, minter) = minting_state(vec![ MintAction::Token("ghs_gen1", chrono::Duration::minutes(5)), MintAction::Error("mint failed"), - MintAction::Error("mint failed"), MintAction::Token("ghs_gen2", chrono::Duration::minutes(60)), ]); seed_clone_token(&state).await; @@ -2242,7 +2274,7 @@ mod push_tests { .await .expect("late mint should recover the push"); - assert_eq!(minter.calls(), 4); + assert_eq!(minter.calls(), 3); let first = &report.attempts[0]; assert_eq!(first.refresh_error, Some(RefreshErrorKind::Mint)); assert_eq!(first.token.unwrap().generation, 1); @@ -2307,6 +2339,29 @@ mod push_tests { assert!(second.success); } + #[tokio::test(start_paused = true)] + async fn timed_out_set_url_stops_before_push_while_it_may_still_run() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_gen1", chrono::Duration::minutes(5)), + MintAction::Token("ghs_gen2", chrono::Duration::minutes(60)), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![]).with_set_url_results(vec![timed_out_exec()]); + + let push_error = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect_err("a timed-out set-url can still rewrite origin later"); + + assert_eq!(minter.calls(), 2); + assert!(push_error.report.attempts.is_empty()); + assert_eq!(sandbox.push_count(), 0); + } + /// Remote drift: agent code rewrote `origin`, so the push fails on auth /// even though the tracked generation looks current. The first /// auth-shaped failure earns one forced re-embed of the pinned token. @@ -2375,6 +2430,53 @@ mod push_tests { assert_eq!(push_error.report.attempts.len(), 1); assert_eq!(push_error.report.attempts[0].retry_reason, None); } + + #[tokio::test(start_paused = true)] + async fn timed_out_push_is_not_retried_while_the_remote_process_may_still_run() { + let sandbox = ScriptedGitSandbox::new(vec![timed_out_exec()]); + + let push_error = git_push_via_exec(&sandbox, None, REFSPEC, &RetryPlan::publish_push()) + .await + .expect_err("an unconfirmed timeout must fail without another push"); + + assert_eq!(sandbox.push_count(), 1); + assert_eq!(push_error.report.attempts.len(), 1); + assert_eq!(push_error.report.attempts[0].retry_reason, None); + } + + #[tokio::test(start_paused = true)] + async fn retry_deadline_includes_credential_lease_acquisition() { + let source = InstallationTokenSource::with_minter( + "fabro-testing/repo".to_string(), + Box::new(SlowMinter), + ); + let state = PushCredentialState::new(Some(source)); + let sandbox = ScriptedGitSandbox::new(vec![]); + let mut plan = RetryPlan::checkpoint_push(); + plan.max_elapsed = Some(Duration::from_secs(1)); + + let push_error = git_push_via_exec(&sandbox, Some((&state, ORIGIN)), REFSPEC, &plan) + .await + .expect_err("credential acquisition must stop at the operation deadline"); + + assert!(push_error.report.attempts.is_empty()); + assert_eq!(sandbox.push_count(), 0); + assert!(push_error.error.to_string().contains("deadline expired")); + } + + #[tokio::test(start_paused = true)] + async fn expired_retry_deadline_does_not_launch_a_zero_timeout_push() { + let sandbox = ScriptedGitSandbox::new(vec![]); + let mut plan = RetryPlan::checkpoint_push(); + plan.max_elapsed = Some(Duration::ZERO); + + let push_error = git_push_via_exec(&sandbox, None, REFSPEC, &plan) + .await + .expect_err("an expired operation must stop before exec"); + + assert!(push_error.report.attempts.is_empty()); + assert_eq!(sandbox.push_count(), 0); + } } #[cfg(test)] diff --git a/lib/components/fabro-workflow/src/error.rs b/lib/components/fabro-workflow/src/error.rs index 4d7add9eb..ea6656d16 100644 --- a/lib/components/fabro-workflow/src/error.rs +++ b/lib/components/fabro-workflow/src/error.rs @@ -371,17 +371,29 @@ impl Error { message: impl Into, source: impl Into, exec_output_tail: Option, + ) -> Self { + Self::stage_with_source_details(stage, message, source, None, exec_output_tail, Vec::new()) + } + + fn stage_with_source_details( + stage: ErrorStage, + message: impl Into, + source: impl Into, + failure_class: Option, + exec_output_tail: Option, + extra_causes: Vec, ) -> Self { let message = message.into(); let source = SharedError::new(source.into()); - let failure_class = - classify_failure_reason(&render_with_causes(&message, &collect_chain(&source))); + let failure_class = failure_class.unwrap_or_else(|| { + classify_failure_reason(&render_with_causes(&message, &collect_chain(&source))) + }); Self::Stage { stage, message, failure_class, exec_output_tail, - extra_causes: Vec::new(), + extra_causes, source: Some(source), } } @@ -470,14 +482,14 @@ impl Error { exec_output_tail: Option, extra_causes: Vec, ) -> Self { - Self::Stage { - stage: ErrorStage::Publish, - message: message.into(), - failure_class, + Self::stage_with_source_details( + ErrorStage::Publish, + message, + source, + Some(failure_class), exec_output_tail, extra_causes, - source: Some(SharedError::new(source.into())), - } + ) } #[must_use] diff --git a/lib/components/fabro-workflow/src/event.rs b/lib/components/fabro-workflow/src/event.rs index 19c61c43d..a5c1f583e 100644 --- a/lib/components/fabro-workflow/src/event.rs +++ b/lib/components/fabro-workflow/src/event.rs @@ -10,7 +10,7 @@ mod test_support; pub use fabro_types::{EventBody, RunNoticeCode, RunNoticeLevel}; -pub use self::convert::{git_push_attempt_props, to_run_event, to_run_event_at}; +pub use self::convert::{to_run_event, to_run_event_at}; pub use self::emitter::Emitter; pub use self::events::Event; pub use self::names::event_name; diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index ded494571..2405f5d8a 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -30,7 +30,7 @@ fn stage_status_from_string(status: &str) -> StageOutcome { /// events: the token snapshot flattens into the three flat `token_*` fields /// (a nested provenance enum never appears in stored events), and the retry /// classifier's verdict becomes `classified_reason`. -pub fn git_push_attempt_props( +fn git_push_attempt_props( attempts: &[fabro_sandbox::PushAttempt], ) -> Vec { attempts @@ -39,16 +39,24 @@ pub fn git_push_attempt_props( attempt: attempt.attempt, started_at: attempt.started_at, success: attempt.success, - classified_reason: attempt.retry_reason.map(|reason| reason.to_string()), + classified_reason: attempt.retry_reason, exec_output_tail: attempt.exec_output_tail.clone(), token_generation: attempt.token.map(|token| token.generation), - token_provenance: attempt.token.map(|token| token.provenance.to_string()), + token_provenance: attempt.token.map(|token| match token.provenance { + fabro_sandbox::TokenProvenance::Minted { .. } => { + fabro_types::GitTokenProvenance::Minted + } + fabro_sandbox::TokenProvenance::Reused { .. } => { + fabro_types::GitTokenProvenance::Reused + } + fabro_sandbox::TokenProvenance::Static => fabro_types::GitTokenProvenance::Static, + }), token_age_ms: attempt .token .and_then(|token| token.age_at(attempt.started_at)) .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)), - credential_action: attempt.credential_action.map(|action| action.to_string()), - refresh_error: attempt.refresh_error.map(|kind| kind.to_string()), + credential_action: attempt.credential_action, + refresh_error: attempt.refresh_error, }) .collect() } @@ -555,7 +563,7 @@ fn event_body_from_event(event: &Event) -> EventBody { branch: branch.clone(), success: *success, exec_output_tail: exec_output_tail.clone(), - attempts: attempts.clone(), + attempts: git_push_attempt_props(attempts), }), Event::GitFetch { branch, success } => EventBody::GitFetch(fabro_types::GitFetchProps { branch: branch.clone(), @@ -2212,7 +2220,7 @@ mod tests { let started_at = Utc::now(); let minted_at = started_at - chrono::Duration::milliseconds(180); let expires_at = started_at + chrono::Duration::minutes(60); - let attempts = git_push_attempt_props(&[ + let runtime_attempts = vec![ fabro_sandbox::PushAttempt { attempt: 1, started_at, @@ -2247,13 +2255,14 @@ mod tests { credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), refresh_error: Some(fabro_sandbox::RefreshErrorKind::SetUrl), }, - ]); + ]; + let expected_attempts = git_push_attempt_props(&runtime_attempts); let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { branch: "fabro/run/01M0DH033P2XSTHAGVBHG6922F".to_string(), success: false, exec_output_tail: Some(exec_tail()), - attempts: attempts.clone(), + attempts: runtime_attempts, }); let json = serde_json::to_value(&stored).unwrap(); @@ -2275,7 +2284,7 @@ mod tests { match round_tripped.body { EventBody::GitPush(props) => { assert!(!props.success); - assert_eq!(props.attempts, attempts); + assert_eq!(props.attempts, expected_attempts); } other => panic!("expected GitPush body, got {other:?}"), } @@ -2283,7 +2292,7 @@ mod tests { #[test] fn successful_single_attempt_push_omits_failure_fields() { - let attempts = git_push_attempt_props(&[fabro_sandbox::PushAttempt { + let attempts = vec![fabro_sandbox::PushAttempt { attempt: 1, started_at: Utc::now(), success: true, @@ -2295,7 +2304,7 @@ mod tests { }), credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), refresh_error: None, - }]); + }]; let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { branch: "fabro/run/run-1".to_string(), success: true, diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index 7d18ec911..90a966b12 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -432,10 +432,9 @@ pub enum Event { success: bool, #[serde(default, skip_serializing_if = "Option::is_none")] exec_output_tail: Option, - /// Per-attempt history of the push operation, already projected to - /// the durable shape by the emit site. + /// Per-attempt history of the push operation. #[serde(default, skip_serializing_if = "Vec::is_empty")] - attempts: Vec, + attempts: Vec, }, GitFetch { branch: String, diff --git a/lib/components/fabro-workflow/src/lifecycle/git.rs b/lib/components/fabro-workflow/src/lifecycle/git.rs index a744d0ba0..5396f6d9f 100644 --- a/lib/components/fabro-workflow/src/lifecycle/git.rs +++ b/lib/components/fabro-workflow/src/lifecycle/git.rs @@ -8,22 +8,20 @@ use fabro_core::lifecycle::RunLifecycle; use fabro_core::outcome::NodeResult; use fabro_core::state::ExecutionState; use fabro_dump::RunDump; -use fabro_sandbox::git_retry; -use fabro_types::run_event::{ - GitPushAttemptProps, MetadataSnapshotFailureKind, MetadataSnapshotPhase, -}; +use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; use fabro_types::{CheckpointRecord, DiffSummary, RunDiff, RunId}; use fabro_util::error::collect_causes; use fabro_util::time::elapsed_ms; use crate::artifact; -use crate::event::{ - Emitter, Event, RunNoticeCode, RunNoticeLevel, StageScope, git_push_attempt_props, -}; +use crate::event::{Emitter, Event, RunNoticeCode, RunNoticeLevel, StageScope}; use crate::graph::{WorkflowGraph, WorkflowNode}; use crate::lifecycle::event::stage_scope_for; use crate::outcome::BilledModelUsage; -use crate::run_metadata::{MetadataSnapshot, RunMetadataRuntime, RunMetadataWriterHandle}; +use crate::run_metadata::{ + MetadataSnapshot, RunMetadataRuntime, RunMetadataWriterHandle, + metadata_push_failure_is_transient, +}; use crate::run_options::RunOptions; use crate::runtime_store::RunStoreHandle; use crate::sandbox_git::{ @@ -77,8 +75,7 @@ pub(crate) struct PushResult { pub branch: String, pub success: bool, pub exec_output_tail: Option, - /// Per-attempt history, already projected to the durable event shape. - pub attempts: Vec, + pub attempts: Vec, } /// Push a run branch to its remote counterpart. @@ -97,18 +94,6 @@ pub(crate) async fn push_run_branch( .await } -/// Whether a metadata push failure leaves the writer eligible for re-probing -/// at later checkpoints. Only push failures with retryable classifications -/// (replication lag on a fresh token, transient infrastructure) qualify; -/// everything else keeps the permanent latch. -fn metadata_push_failure_is_transient( - detail: &str, - token: Option<&fabro_sandbox::TokenSnapshot>, -) -> bool { - let cred = fabro_sandbox::CredentialContext::from_snapshot(token); - git_retry::classify_failure(detail, cred).is_some() -} - /// Sub-lifecycle responsible for git operations (checkpoint commits, pushes, /// diffs). pub(crate) struct GitLifecycle { @@ -232,7 +217,9 @@ impl RunLifecycle for GitLifecycle { None, ); let shadow_sha = if let Some(meta_branch) = self.metadata_branch().map(str::to_string) { - if self.metadata_writer.is_none() || self.metadata_runtime.metadata_suspended() { + if self.metadata_writer.is_none() + || self.metadata_runtime.metadata_checkpoint_suspended() + { None } else { let phase = MetadataSnapshotPhase::Checkpoint; @@ -380,7 +367,7 @@ impl RunLifecycle for GitLifecycle { branch: branch.clone(), success: push_ok, exec_output_tail, - attempts: git_push_attempt_props(&attempts), + attempts, }); } } @@ -489,7 +476,10 @@ impl GitLifecycle { message: &str, scope: Option<&StageScope>, ) -> Option { - if self.metadata_runtime.metadata_suspended() { + if self.metadata_runtime.metadata_suspended() + || (phase == MetadataSnapshotPhase::Checkpoint + && self.metadata_runtime.metadata_checkpoint_suspended()) + { return None; } let writer = self.metadata_writer.as_ref()?; diff --git a/lib/components/fabro-workflow/src/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index f30bbc27c..b3653999b 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -4,7 +4,6 @@ use std::time::Instant; use fabro_dump::RunDump; use fabro_hooks::{HookContext, HookEvent}; -use fabro_sandbox::git_retry; use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; use fabro_types::{BilledTokenCounts, DiffSummary, EventBody, RunFailure, RunProjection}; use fabro_util::error::collect_causes; @@ -15,7 +14,7 @@ use crate::error::{Error, run_failure_from_error, run_failure_from_outcome_failu use crate::event::{Event, RunNoticeCode, RunNoticeLevel}; use crate::outcome::{Outcome, StageOutcome}; use crate::records::{Checkpoint, Conclusion, StageSummary}; -use crate::run_metadata::MetadataSnapshot; +use crate::run_metadata::{MetadataSnapshot, metadata_push_failure_is_transient}; use crate::run_options::RunOptions; use crate::run_status::{FailureReason, RunStatus, SuccessReason}; use crate::runtime_store::RunStoreHandle; @@ -382,16 +381,6 @@ fn emit_metadata_snapshot_failed( }); } -/// Whether a metadata push failure leaves the writer eligible for re-probing. -/// See `lifecycle::git`: only retryable push classifications qualify. -fn metadata_push_failure_is_transient( - detail: &str, - token: Option<&fabro_sandbox::TokenSnapshot>, -) -> bool { - let cred = fabro_sandbox::CredentialContext::from_snapshot(token); - git_retry::classify_failure(detail, cred).is_some() -} - fn emit_metadata_warning( services: &RunServices, code: RunNoticeCode, diff --git a/lib/components/fabro-workflow/src/pipeline/publish.rs b/lib/components/fabro-workflow/src/pipeline/publish.rs index 2191f7a7e..42010e133 100644 --- a/lib/components/fabro-workflow/src/pipeline/publish.rs +++ b/lib/components/fabro-workflow/src/pipeline/publish.rs @@ -2,12 +2,11 @@ use std::fmt::Write as _; use std::sync::Arc; use fabro_types::ExecOutputTail; -use fabro_types::run_event::GitPushAttemptProps; use super::pull_request::{AutoMergeOptions, OpenPullRequestRequest, open_pull_request}; use super::types::{Concluded, PublishOptions, PublishOutcome, Published}; use crate::error::{Error, FailureCategory, classify_failure_reason}; -use crate::event::{Event, git_push_attempt_props}; +use crate::event::Event; use crate::lifecycle::git::push_run_branch; /// PUBLISH phase: push the final run commit and, when configured, open a pull @@ -49,9 +48,9 @@ pub async fn publish(concluded: Concluded, options: &PublishOptions) -> Publishe /// exec output tail. fn publish_push_error( run_branch: &str, - push_error: fabro_sandbox::PushError, + push_error: fabro_sandbox::Error, exec_output_tail: Option, - attempts: &[GitPushAttemptProps], + attempts: &[fabro_sandbox::PushAttempt], last_successful_push_at: Option>, ) -> Error { let message = match last_successful_push_at { @@ -61,16 +60,11 @@ fn publish_push_error( ), None => format!("failed to push run branch '{run_branch}'"), }; - let failure_class = match push_error - .report - .attempts - .last() - .and_then(|attempt| attempt.retry_reason) - { + let failure_class = match attempts.last().and_then(|attempt| attempt.retry_reason) { Some(_) => FailureCategory::TransientInfra, None => classify_failure_reason(&format!( "{message}: {}", - fabro_sandbox::display_for_log(&push_error.error) + fabro_sandbox::display_for_log(&push_error) )), }; let causes = attempts.iter().map(push_attempt_cause).collect(); @@ -84,14 +78,13 @@ fn publish_push_error( } /// One bounded line per push attempt for the failure detail. -fn push_attempt_cause(attempt: &GitPushAttemptProps) -> String { +fn push_attempt_cause(attempt: &fabro_sandbox::PushAttempt) -> String { let outcome = if attempt.success { - "succeeded" + "succeeded".to_string() } else { attempt - .classified_reason - .as_deref() - .unwrap_or("unclassified") + .retry_reason + .map_or_else(|| "unclassified".to_string(), |reason| reason.to_string()) }; let mut line = format!( "push attempt {} at {}: {outcome}", @@ -100,10 +93,14 @@ fn push_attempt_cause(attempt: &GitPushAttemptProps) -> String { .started_at .to_rfc3339_opts(chrono::SecondsFormat::Millis, true) ); - if let Some(age_ms) = attempt.token_age_ms { + if let Some(age_ms) = attempt + .token + .and_then(|token| token.age_at(attempt.started_at)) + .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)) + { let _ = write!(line, " (token age {age_ms}ms)"); } - if let Some(refresh_error) = &attempt.refresh_error { + if let Some(refresh_error) = attempt.refresh_error { let _ = write!(line, ", refresh error: {refresh_error}"); } line @@ -243,14 +240,14 @@ impl Concluded { branch: run_branch.to_string(), success: true, exec_output_tail: None, - attempts: git_push_attempt_props(&report.attempts), + attempts: report.attempts, }); Ok(()) } Err(push_error) => { - let exec_output_tail = - fabro_sandbox::default_redacted_output_tail(&push_error.error); - let attempts = git_push_attempt_props(&push_error.report.attempts); + let fabro_sandbox::PushError { report, error } = push_error; + let exec_output_tail = fabro_sandbox::default_redacted_output_tail(&error); + let attempts = report.attempts; self.services.emitter.emit(&Event::GitPush { branch: run_branch.to_string(), success: false, @@ -259,7 +256,7 @@ impl Concluded { }); Err(publish_push_error( run_branch, - push_error, + error, exec_output_tail, &attempts, self.services.sandbox_git.last_successful_push_at(), @@ -280,35 +277,40 @@ impl Concluded { #[cfg(test)] mod tests { use chrono::Utc; - use fabro_types::run_event::GitPushAttemptProps; use super::*; use crate::error::FailureCategory; - fn attempt_props( + fn push_attempt( attempt: u32, - classified_reason: Option<&str>, + retry_reason: Option, token_age_ms: Option, - refresh_error: Option<&str>, - ) -> GitPushAttemptProps { - GitPushAttemptProps { + refresh_error: Option, + ) -> fabro_sandbox::PushAttempt { + let started_at = Utc::now(); + fabro_sandbox::PushAttempt { attempt, - started_at: Utc::now(), + started_at, success: false, - classified_reason: classified_reason.map(str::to_string), + retry_reason, exec_output_tail: None, - token_generation: Some(14), - token_provenance: Some("minted".to_string()), - token_age_ms, - credential_action: Some("unchanged".to_string()), - refresh_error: refresh_error.map(str::to_string), + token: token_age_ms.map(|age_ms| fabro_sandbox::TokenSnapshot { + generation: 14, + provenance: fabro_sandbox::TokenProvenance::Minted { + minted_at: started_at + - chrono::Duration::milliseconds(i64::try_from(age_ms).unwrap()), + expires_at: started_at + chrono::Duration::hours(1), + }, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), + refresh_error, } } - fn push_error_with_reasons( + fn push_attempts_with_reasons( reasons: &[Option], - ) -> fabro_sandbox::PushError { - let attempts = reasons + ) -> Vec { + reasons .iter() .enumerate() .map(|(index, reason)| fabro_sandbox::PushAttempt { @@ -321,11 +323,11 @@ mod tests { credential_action: None, refresh_error: None, }) - .collect(); - fabro_sandbox::PushError { - report: fabro_sandbox::PushReport { attempts }, - error: fabro_sandbox::Error::message("remote: Repository not found."), - } + .collect() + } + + fn push_source_error() -> fabro_sandbox::Error { + fabro_sandbox::Error::message("remote: Repository not found.") } /// Exhausted retries on a retryable classification are transient @@ -334,28 +336,20 @@ mod tests { /// configuration change. #[test] fn exhausted_transient_retries_classify_as_transient_infra() { - let error = publish_push_error( - "fabro/run/test", - push_error_with_reasons(&[ - Some(fabro_sandbox::GitRetryReason::TokenReplication), - Some(fabro_sandbox::GitRetryReason::TokenReplication), - ]), - None, - &[], - None, - ); + let attempts = push_attempts_with_reasons(&[ + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(fabro_sandbox::GitRetryReason::TokenReplication), + ]); + let error = + publish_push_error("fabro/run/test", push_source_error(), None, &attempts, None); assert_eq!(error.failure_category(), FailureCategory::TransientInfra); } #[test] fn permanently_classified_push_falls_back_to_message_sniffing() { - let error = publish_push_error( - "fabro/run/test", - push_error_with_reasons(&[None]), - None, - &[], - None, - ); + let attempts = push_attempts_with_reasons(&[None]); + let error = + publish_push_error("fabro/run/test", push_source_error(), None, &attempts, None); // "Repository not found." carries no transient hint for the // heuristic, so the fallback stays deterministic. assert_eq!(error.failure_category(), FailureCategory::Deterministic); @@ -364,16 +358,23 @@ mod tests { #[test] fn failure_detail_renders_one_cause_line_per_attempt() { let attempts = vec![ - attempt_props(1, Some("token_replication"), Some(180), None), - attempt_props(2, Some("token_replication"), Some(3320), Some("set_url")), + push_attempt( + 1, + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(180), + None, + ), + push_attempt( + 2, + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(3320), + Some(fabro_sandbox::RefreshErrorKind::SetUrl), + ), ]; let last_push = Utc::now() - chrono::Duration::seconds(67); let error = publish_push_error( "fabro/run/test", - push_error_with_reasons(&[ - Some(fabro_sandbox::GitRetryReason::TokenReplication), - Some(fabro_sandbox::GitRetryReason::TokenReplication), - ]), + push_source_error(), None, &attempts, Some(last_push), @@ -403,5 +404,14 @@ mod tests { attempt_lines[1].contains("refresh error: set_url"), "{attempt_lines:?}" ); + assert_eq!( + detail + .causes + .iter() + .filter(|cause| cause.as_str() == "remote: Repository not found.") + .count(), + 1, + "the source chain must not repeat the inner push error" + ); } } diff --git a/lib/components/fabro-workflow/src/run_metadata.rs b/lib/components/fabro-workflow/src/run_metadata.rs index 6f18632da..154973522 100644 --- a/lib/components/fabro-workflow/src/run_metadata.rs +++ b/lib/components/fabro-workflow/src/run_metadata.rs @@ -1,16 +1,17 @@ use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; +use std::time::Duration; use async_trait::async_trait; use fabro_checkpoint::git::{FileMode, Store, TreeEntries}; use fabro_dump::RunDump; -use fabro_github::token_source::{InstallationTokenSource, TokenSnapshot}; +use fabro_github::token_source::{InstallationTokenSource, ResolvedToken, TokenSnapshot}; use git2::{ Cred, Direction, ErrorClass, ErrorCode, FetchOptions, Oid, PushOptions, RemoteCallbacks, Repository, Signature, }; use tokio::task::{self, JoinError}; +use tokio::time; use crate::git::{GitAuthor, META_BRANCH_PREFIX}; use crate::run_options::RunOptions; @@ -19,6 +20,14 @@ pub(crate) fn metadata_branch_name(run_id: &str) -> String { format!("{META_BRANCH_PREFIX}{run_id}") } +pub(crate) fn metadata_push_failure_is_transient( + detail: &str, + token: Option<&TokenSnapshot>, +) -> bool { + let credentials = fabro_sandbox::CredentialContext::from_snapshot(token); + fabro_sandbox::classify_failure(detail, credentials).is_some() +} + #[derive(Debug, thiserror::Error)] pub(crate) enum RunMetadataError { #[error("metadata writer initialization failed: {0}")] @@ -52,21 +61,23 @@ pub(crate) struct MetadataSnapshot { pub token: Option, } +const METADATA_REPROBE_COOLDOWN: Duration = Duration::from_secs(30); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum MetadataRuntimeState { + Healthy, + TransientDegraded { next_probe_at: time::Instant }, + PermanentlyDegraded, +} + pub(crate) struct RunMetadataRuntime { - degraded: AtomicBool, - /// A transiently degraded writer stays eligible for one snapshot attempt - /// at each subsequent checkpoint; a permanent failure latches snapshots - /// off for the rest of the run. - reprobe_eligible: AtomicBool, - warning_emitted: AtomicBool, + state: Mutex, } impl RunMetadataRuntime { pub(crate) fn new() -> Self { Self { - degraded: AtomicBool::new(false), - reprobe_eligible: AtomicBool::new(false), - warning_emitted: AtomicBool::new(false), + state: Mutex::new(MetadataRuntimeState::Healthy), } } @@ -78,34 +89,53 @@ impl RunMetadataRuntime { /// upgraded by a later transient failure. Returns whether the caller /// should emit the degradation warning (once per degradation). pub(crate) fn mark_metadata_degraded(&self, transient: bool) -> bool { - let was_degraded = self.degraded.swap(true, Ordering::SeqCst); - if was_degraded { - if !transient { - self.reprobe_eligible.store(false, Ordering::SeqCst); + let mut state = self.state.lock().expect("metadata runtime mutex poisoned"); + let should_warn = matches!(*state, MetadataRuntimeState::Healthy); + *state = match (*state, transient) { + (MetadataRuntimeState::PermanentlyDegraded, _) | (_, false) => { + MetadataRuntimeState::PermanentlyDegraded } - } else { - self.reprobe_eligible.store(transient, Ordering::SeqCst); - } - !self.warning_emitted.swap(true, Ordering::SeqCst) + (_, true) => MetadataRuntimeState::TransientDegraded { + next_probe_at: time::Instant::now() + METADATA_REPROBE_COOLDOWN, + }, + }; + should_warn } /// A successful snapshot clears the degraded state and re-arms the /// warning, so a later independent failure warns again instead of /// failing silently. pub(crate) fn clear_metadata_degraded(&self) { - self.degraded.store(false, Ordering::SeqCst); - self.reprobe_eligible.store(false, Ordering::SeqCst); - self.warning_emitted.store(false, Ordering::SeqCst); + let mut state = self.state.lock().expect("metadata runtime mutex poisoned"); + if !matches!(*state, MetadataRuntimeState::Healthy) { + *state = MetadataRuntimeState::Healthy; + } } - /// Whether snapshot writes should be skipped: degraded with no re-probe - /// eligibility. + /// Whether all later snapshot writes must be skipped. pub(crate) fn metadata_suspended(&self) -> bool { - self.degraded.load(Ordering::SeqCst) && !self.reprobe_eligible.load(Ordering::SeqCst) + matches!( + *self.state.lock().expect("metadata runtime mutex poisoned"), + MetadataRuntimeState::PermanentlyDegraded + ) + } + + /// Whether a checkpoint should defer its transient-failure re-probe. + pub(crate) fn metadata_checkpoint_suspended(&self) -> bool { + match *self.state.lock().expect("metadata runtime mutex poisoned") { + MetadataRuntimeState::Healthy => false, + MetadataRuntimeState::TransientDegraded { next_probe_at } => { + time::Instant::now() < next_probe_at + } + MetadataRuntimeState::PermanentlyDegraded => true, + } } pub(crate) fn metadata_degraded(&self) -> bool { - self.degraded.load(Ordering::SeqCst) + !matches!( + *self.state.lock().expect("metadata runtime mutex poisoned"), + MetadataRuntimeState::Healthy + ) } } @@ -115,16 +145,9 @@ impl Default for RunMetadataRuntime { } } -/// A resolved metadata push token: the secret plus the non-secret snapshot -/// used to classify push failures against the credential context. -pub(crate) struct MetadataToken { - pub secret: String, - pub snapshot: Option, -} - #[async_trait] pub(crate) trait AuthProvider: Send + Sync { - async fn token(&self) -> Result, RunMetadataError>; + async fn token(&self) -> Result, RunMetadataError>; } struct GitHubAuthProvider { @@ -139,16 +162,11 @@ impl GitHubAuthProvider { #[async_trait] impl AuthProvider for GitHubAuthProvider { - async fn token(&self) -> Result, RunMetadataError> { + async fn token(&self) -> Result, RunMetadataError> { self.source .resolve() .await - .map(|resolved| { - Some(MetadataToken { - secret: resolved.token.expose().to_owned(), - snapshot: Some(resolved.snapshot), - }) - }) + .map(Some) .map_err(RunMetadataError::TokenMint) } } @@ -159,7 +177,7 @@ struct NoAuth; #[cfg(test)] #[async_trait] impl AuthProvider for NoAuth { - async fn token(&self) -> Result, RunMetadataError> { + async fn token(&self) -> Result, RunMetadataError> { Ok(None) } } @@ -231,8 +249,7 @@ impl RunMetadataWriterHandle { message: &str, ) -> Result { let token = self.auth.token().await?; - let token_snapshot = token.as_ref().and_then(|token| token.snapshot); - let secret = token.map(|token| token.secret); + let token_snapshot = token.as_ref().map(|token| token.snapshot); let entries = dump .git_entries() .map_err(RunMetadataError::DumpSerialize)?; @@ -241,7 +258,11 @@ impl RunMetadataWriterHandle { task::spawn_blocking(move || { let mut guard = writer.lock().expect("metadata writer mutex poisoned"); - guard.write_snapshot_blocking(&entries, &message, secret.as_deref()) + guard.write_snapshot_blocking( + &entries, + &message, + token.as_ref().map(|token| token.token.expose()), + ) }) .await .map_err(RunMetadataError::Join)? @@ -1070,15 +1091,18 @@ mod tests { assert!(build_metadata_writer(&options, None).unwrap().is_none()); } - #[test] - fn transient_degradation_stays_eligible_for_reprobe() { + #[tokio::test(start_paused = true)] + async fn transient_degradation_reprobes_after_a_cooldown() { let runtime = RunMetadataRuntime::new(); assert!(runtime.mark_metadata_degraded(true), "first failure warns"); assert!(runtime.metadata_degraded()); assert!( !runtime.metadata_suspended(), - "a transiently degraded writer re-probes at later checkpoints" + "a final snapshot can still re-probe" ); + assert!(runtime.metadata_checkpoint_suspended()); + time::advance(METADATA_REPROBE_COOLDOWN).await; + assert!(!runtime.metadata_checkpoint_suspended()); assert!( !runtime.mark_metadata_degraded(true), "repeat failures do not warn again" diff --git a/lib/foundation/fabro-types/src/run_event/misc.rs b/lib/foundation/fabro-types/src/run_event/misc.rs index 9643f9fb1..9e5600287 100644 --- a/lib/foundation/fabro-types/src/run_event/misc.rs +++ b/lib/foundation/fabro-types/src/run_event/misc.rs @@ -112,6 +112,54 @@ pub struct GitCommitProps { pub sha: String, } +/// Why a failed git push attempt is safe to retry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitPushRetryReason { + /// A recently minted token may not have reached every GitHub git endpoint. + TokenReplication, + /// The failure came from transient network or service infrastructure. + TransientInfra, +} + +/// Non-secret origin of the token used by a git push attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitTokenProvenance { + /// The token source minted the token for this resolve. + Minted, + /// The token source reused an earlier mint. + Reused, + /// The credential cannot be refreshed by Fabro. + Static, +} + +/// What credential preparation changed before a git push attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitCredentialAction { + /// Fabro wrote a token generation into the remote URL. + Embedded, + /// The remote already tracked the selected token generation. + Unchanged, + /// No managed credential was available. + None, +} + +/// Which credential preparation step failed before a git push attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitCredentialRefreshError { + /// Token resolution or minting failed. + Mint, + /// Rewriting the remote URL failed. + SetUrl, +} + /// One attempt of a retried git push, nested inside [`GitPushProps`]. /// /// The durable projection of the sandbox layer's runtime attempt record. @@ -127,7 +175,7 @@ pub struct GitPushAttemptProps { pub started_at: chrono::DateTime, pub success: bool, #[serde(default, skip_serializing_if = "Option::is_none")] - pub classified_reason: Option, + pub classified_reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub exec_output_tail: Option, /// Generation of the token embedded during this attempt (0 for static @@ -136,17 +184,17 @@ pub struct GitPushAttemptProps { pub token_generation: Option, /// `minted`, `reused`, or `static`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub token_provenance: Option, + pub token_provenance: Option, /// Token age at the attempt; absent for static credentials. #[serde(default, skip_serializing_if = "Option::is_none")] pub token_age_ms: Option, /// What the credential refresh did to the remote this attempt: /// `embedded`, `unchanged`, or `none`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub credential_action: Option, + pub credential_action: Option, /// A credential `mint` or `set_url` failure this attempt pushed through. #[serde(default, skip_serializing_if = "Option::is_none")] - pub refresh_error: Option, + pub refresh_error: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] From f8879d13c465b32277b71439106445179a15ddb7 Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Fri, 21 Aug 2026 02:31:24 +0000 Subject: [PATCH 55/63] Bump version to 0.332.0-nightly.0 --- Cargo.lock | 104 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15bb36035..6708e85b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2675,7 +2675,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2690,7 +2690,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -2713,7 +2713,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2723,7 +2723,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2742,7 +2742,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2757,7 +2757,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2799,7 +2799,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2810,7 +2810,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "fabro-api", @@ -2831,7 +2831,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2879,7 +2879,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2897,7 +2897,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2913,7 +2913,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2935,7 +2935,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2943,7 +2943,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "cc", "libc", @@ -2952,7 +2952,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2968,7 +2968,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3012,7 +3012,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3107,7 +3107,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3129,18 +3129,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" [[package]] name = "fabro-store" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3170,7 +3170,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3196,7 +3196,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3210,7 +3210,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3235,7 +3235,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3256,7 +3256,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3270,7 +3270,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3293,7 +3293,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3316,7 +3316,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3329,7 +3329,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3365,7 +3365,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3435,7 +3435,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8546,7 +8546,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "axum", "base64", @@ -8565,7 +8565,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index bb339e93f..4233d8c9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.331.0-nightly.0" +version = "0.332.0-nightly.0" license = "MIT" [workspace.dependencies] From e89f03b31675492c770365fe432467c01b4d19bd Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 06:21:33 -0400 Subject: [PATCH 56/63] Fix flaky run id vs variable timestamp assertion RunId is a ULID, so its embedded timestamp is truncated to whole milliseconds, while Variable.updated_at comes from Utc::now() with sub-millisecond precision. When the variable write and the run creation landed in the same millisecond, the run id compared as earlier and the assertion failed. Truncate the variable timestamp to milliseconds so both sides use the same precision. Co-Authored-By: Claude Fable 5 --- lib/apps/fabro-server/src/server/tests.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index b63923e79..dc656c32e 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -10,7 +10,7 @@ use std::sync::{Arc as StdArc, Mutex as StdMutex}; use async_zip::base::read::mem::ZipFileReader; use axum::body::Body; use axum::http::{Method, Request, header}; -use chrono::{Duration as ChronoDuration, Utc}; +use chrono::{Duration as ChronoDuration, SubsecRound as _, Utc}; use fabro_automation::{AutomationId, AutomationTarget}; use fabro_config::bind::Bind; use fabro_config::{ @@ -4069,7 +4069,9 @@ async fn create_run_from_manifest_resolves_generated_id_after_variable_snapshot( let body = response_json!(response, StatusCode::CREATED).await; let run_id = body["id"].as_str().unwrap().parse::().unwrap(); - assert!(run_id.created_at() >= variable.updated_at); + // RunId is a ULID whose timestamp only has millisecond precision, so + // truncate the variable timestamp to milliseconds before comparing. + assert!(run_id.created_at() >= variable.updated_at.trunc_subsecs(3)); } #[tokio::test] From a8aba809506e16a024776fea0855c00979a5bb6d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 06:21:40 -0400 Subject: [PATCH 57/63] Pin CI Rust toolchain to 1.97.1 Rust 1.98.0 (released 2026-08-20) passes --fix-cortex-a53-843419 to the linker for aarch64-unknown-linux-musl, which the zig cc wrapper used by cargo-zigbuild rejects, breaking the release build for that target. Pin all workflows that installed unpinned stable to 1.97.1 until the zig toolchain handles the new flag. The nightly-2026-04-14 fmt/clippy toolchains are unchanged. Co-Authored-By: Claude Fable 5 --- .github/workflows/nightly.yml | 2 ++ .github/workflows/release.yml | 1 + .github/workflows/rust.yml | 6 ++++++ .github/workflows/typescript.yml | 2 ++ 4 files changed, 11 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5dcc0ce3c..4140f576b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -54,6 +54,8 @@ jobs: - name: Set up Rust if: steps.skip.outputs.skip != 'true' uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest if: steps.skip.outputs.skip != 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75b216f91..522d1a311 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,7 @@ jobs: - name: Set up Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: + toolchain: 1.97.1 targets: ${{ matrix.target }} - name: Set up zig diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 12ba1b9cc..b3686f872 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -101,6 +101,8 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: cache-on-failure: true @@ -116,6 +118,8 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: cache-on-failure: true @@ -140,6 +144,8 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: cache-on-failure: true diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 064c38c3d..450b88d97 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -73,5 +73,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - run: bun install --frozen-lockfile - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - run: cargo --locked dev build -- --locked -p fabro-cli --release - run: wc -c < target/release/fabro From 103cbb419eed7e26d8e656dacf7ba23bdcda2447 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 06:43:38 -0400 Subject: [PATCH 58/63] Pin Bun to 1.3.14 in CI and release workflows setup-bun installed the latest Bun at run time, so every job floated to new Bun releases the day they shipped. Bun bundles the SPA embedded in release binaries, so an unvetted Bun release could break or silently change shipped artifacts. Pin to 1.3.14, the version the last green nightly used, and hold off on the day-old 1.4.0 until it has soaked. Co-Authored-By: Claude Fable 5 --- .github/workflows/nightly.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/typescript.yml | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 4140f576b..b5469bf9f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,6 +45,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 if: steps.skip.outputs.skip != 'true' with: + bun-version: 1.3.14 no-cache: true - name: Install bun deps (for SPA verify) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 522d1a311..a643070f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: + bun-version: 1.3.14 no-cache: true - name: Install bun deps diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 450b88d97..f2086669e 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -44,6 +44,8 @@ jobs: with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 - run: bun install --frozen-lockfile - run: cd apps/fabro-web && bun run typecheck - run: cd lib/packages/fabro-api-client && bun run typecheck @@ -58,6 +60,8 @@ jobs: with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 - run: bun install --frozen-lockfile - run: cd apps/fabro-web && bun run test @@ -71,6 +75,8 @@ jobs: with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 - run: bun install --frozen-lockfile - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: From e179fd02d083aee12e05c8fc8d009d02e289e29f Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 06:43:52 -0400 Subject: [PATCH 59/63] Pin release workflow runners to ubuntu-24.04 The release, docker, and Homebrew jobs ran on ubuntu-latest, which migrates across Ubuntu major versions on GitHub's schedule. Pin to ubuntu-24.04, the image ubuntu-latest resolved to in the last green release run, matching the explicit runner labels used elsewhere. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a643070f0..6900c4dea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,7 +138,7 @@ jobs: release: name: Release needs: compile - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write steps: @@ -166,7 +166,7 @@ jobs: docker: name: Docker image needs: compile - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write @@ -259,7 +259,7 @@ jobs: name: Update Homebrew Formula needs: release if: ${{ !contains(github.ref_name, '-') }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release permissions: contents: read @@ -308,7 +308,7 @@ jobs: name: Update Homebrew Nightly Formula needs: release if: ${{ contains(github.ref_name, '-') }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release permissions: contents: read From 346e81f4003496e77afa960a3807bc2062de4f21 Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Fri, 21 Aug 2026 11:40:10 +0000 Subject: [PATCH 60/63] Bump version to 0.332.0-nightly.1 --- Cargo.lock | 104 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6708e85b2..800846e58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" [[package]] name = "fabro-checkpoint" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -2675,7 +2675,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "fabro-types", @@ -2690,7 +2690,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "fabro-agent", @@ -2713,7 +2713,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-static", "http 1.4.0", @@ -2723,7 +2723,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "base64", @@ -2742,7 +2742,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "dialoguer", @@ -2757,7 +2757,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -2799,7 +2799,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "clap", "fabro-options-metadata", @@ -2810,7 +2810,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "fabro-api", @@ -2831,7 +2831,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "axum", @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2879,7 +2879,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "fabro-db", @@ -2897,7 +2897,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-static", "http 1.4.0", @@ -2913,7 +2913,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "axum", @@ -2935,7 +2935,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "serde", "serde_json", @@ -2943,7 +2943,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "cc", "libc", @@ -2952,7 +2952,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "aho-corasick", "ref-cast", @@ -2968,7 +2968,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3012,7 +3012,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3107,7 +3107,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-http", "fabro-interview", @@ -3129,18 +3129,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" [[package]] name = "fabro-store" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "bytes", @@ -3170,7 +3170,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "base64", @@ -3196,7 +3196,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "fabro-types", @@ -3210,7 +3210,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -3235,7 +3235,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3256,7 +3256,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3270,7 +3270,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "clap", @@ -3293,7 +3293,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "console 0.15.11", @@ -3316,7 +3316,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3329,7 +3329,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -3365,7 +3365,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -3435,7 +3435,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8546,7 +8546,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "axum", "base64", @@ -8565,7 +8565,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index 4233d8c9b..4a6437546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.332.0-nightly.0" +version = "0.332.0-nightly.1" license = "MIT" [workspace.dependencies] From ded92a215d9530cddf5ca61f5329f722527521da Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 10:37:04 -0400 Subject: [PATCH 61/63] Update Venice model catalog --- docs/public/core-concepts/models.mdx | 6 + .../fabro-llm/src/adapter_registry.rs | 92 +------- .../src/catalog/providers/venice.toml | 214 ++++++++++++++++-- 3 files changed, 206 insertions(+), 106 deletions(-) diff --git a/docs/public/core-concepts/models.mdx b/docs/public/core-concepts/models.mdx index df78987d7..e3bd5249e 100644 --- a/docs/public/core-concepts/models.mdx +++ b/docs/public/core-concepts/models.mdx @@ -59,13 +59,18 @@ Fabro performs this selection once when creating a run and persists the chosen p | `gemini-3.1-flash-lite` | gemini | `gemini-flash-lite`, `gemini-3.1-flash-lite-preview` | 1M | $0.25 / $1.50 | 200 tok/s | | `kimi-k2.5` | moonshot | | 262K | $0.60 / $3.00 | 50 tok/s | | `kimi-k3` | moonshot | `kimi` | 1M | $3.00 / $15.00 | n/a | +| `kimi-k3-fast` | venice | `kimi-fast` | 1M | $4.50 / $22.50 | n/a | | `deepseek-v4-flash` | deepseek | `deepseek`, `deepseek-v4`, `deepseek-flash` | 1,048,576 | $0.14 / $0.28 | n/a | | `deepseek-v4-pro` | deepseek | | 1,048,576 | $0.435 / $0.87 | n/a | +| `grok-4.6` | venice | `grok`, `grok46`, `grok-46` | 500K | $2.27 / $6.80 | n/a | | `laguna-s-2.1` | poolside | `laguna`, `laguna-s` | 1M | $0.10 / $0.20 | n/a | | `laguna-xs-2.1` | poolside | `laguna-xs` | 262K | $0.10 / $0.20 | n/a | | `glm-5.2` | zai | `glm`, `glm5`, `glm52`, `glm5.2` | 1M | $1.40 / $4.40 | n/a | +| `glm-5.3` | venice | `glm`, `glm5`, `glm53`, `glm5.3`, `glm-5-3` | 1M | $1.75 / $5.50 | n/a | | `minimax-m2.5` | minimax | `minimax` | 197K | $0.30 / $1.20 | 45 tok/s | | `mercury-2` | inception | `mercury` | 131K | $0.25 / $0.75 | 1000 tok/s | +| `qwen3.8-max` | venice | `qwen`, `qwen-max`, `qwen3.8`, `qwen-3.8`, `qwen38`, `qwen-3.8-max`, `qwen38-max` | 1M | $2.50 / $7.50 | n/a | +| `qwen3.8-27b` | venice | `qwen-27b`, `qwen-3.8-27b`, `qwen38-27b` | 262K | $0.45 / $3.20 | n/a | Each provider requires its own API key. Server-backed workflows read provider credentials from the server vault (for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPSEEK_API_KEY`, or `POOLSIDE_API_KEY` set with `fabro secret set` or `fabro provider login`). Standalone SDK/CLI flows can opt into env-backed credential sources explicitly. See the [Quick Start](/getting-started/quick-start) for setup. @@ -232,6 +237,7 @@ When no model or provider is specified, Fabro chooses the default offering on th | `moonshot` | `kimi-k3` | | `poolside` | `laguna-s-2.1` | | `zai` | `glm-5.2` | +| `venice` | `deepseek-v4-flash` | | `minimax` | `minimax-m2.5` | | `inception` | `mercury-2` | diff --git a/lib/components/fabro-llm/src/adapter_registry.rs b/lib/components/fabro-llm/src/adapter_registry.rs index a306c846b..60ad8ec42 100644 --- a/lib/components/fabro-llm/src/adapter_registry.rs +++ b/lib/components/fabro-llm/src/adapter_registry.rs @@ -269,91 +269,19 @@ mod tests { .unwrap_or_else(|error| panic!("built-in model '{selector}' should resolve: {error}")) } - /// One row of the route-equivalence table: model id plus the - /// `(deployment_id, transport, codec, billing_policy, agent_profile)` - /// tuple it must resolve to. - type RouteRow = ( - &'static str, - &'static str, - AdapterKind, - CodecKind, - BillingPolicy, - AgentProfileKind, - ); - - /// The compat mapping as an executable table: every built-in catalog - /// model resolves to exactly this tuple. Adding or rerouting a built-in - /// model means updating this table deliberately. #[test] - fn builtin_catalog_route_equivalence_table() { - use AdapterKind as T; - use AgentProfileKind as P; - use BillingPolicy as B; - use CodecKind as C; - - #[rustfmt::skip] - let expected: &[RouteRow] = &[ - // model id deployment_id transport codec billing profile - ("claude-fable-5", "claude-fable-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Claude5), - ("claude-haiku-4-5", "claude-haiku-4-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-4-6", "claude-opus-4-6", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-4-7", "claude-opus-4-7", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-4-8", "claude-opus-4-8", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-5", "claude-opus-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Claude5), - ("claude-sonnet-4-5", "claude-sonnet-4-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-sonnet-4-6", "claude-sonnet-4-6", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-sonnet-5", "claude-sonnet-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Claude5), - ("deepseek-v4-flash", "deepseek-v4-flash", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("deepseek-v4-pro", "deepseek-v4-pro", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("gemini-3-flash-preview", "gemini-3-flash-preview", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.1-flash-lite", "gemini-3.1-flash-lite", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.1-pro-preview", "gemini-3.1-pro-preview", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.1-pro-preview-customtools", "gemini-3.1-pro-preview-customtools", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.5-flash", "gemini-3.5-flash", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("glm-4.7", "glm-4.7", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("glm-5.2", "glm-5.2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("gpt-5.4", "gpt-5.4", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.4-mini", "gpt-5.4-mini", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.4-pro", "gpt-5.4-pro", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.5", "gpt-5.5", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.5-pro", "gpt-5.5-pro", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.6-luna", "gpt-5.6-luna", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::Gpt56), - ("gpt-5.6-sol", "gpt-5.6-sol", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::Gpt56), - ("gpt-5.6-terra", "gpt-5.6-terra", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::Gpt56), - ("kimi-k2.5", "kimi-k2.5", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::Kimi), - ("kimi-k3", "kimi-k3", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::Kimi), - ("laguna-s-2.1", "poolside/laguna-s-2.1", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("laguna-xs-2.1", "poolside/laguna-xs-2.1", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("mercury-2", "mercury-2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("minimax-m2.5", "minimax-m2.5", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("venice-uncensored-1-2", "venice-uncensored-1-2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("venice-uncensored-role-play", "venice-uncensored-role-play", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ]; - + fn every_builtin_catalog_offering_resolves() { let catalog = Catalog::builtin(); - let mut model_ids: Vec<&str> = catalog - .list(None) - .iter() - .map(|model| model.id.as_str()) - .collect(); - model_ids.sort_unstable(); - let mut expected_ids: Vec<&str> = expected.iter().map(|row| row.0).collect(); - expected_ids.sort_unstable(); - assert_eq!( - model_ids, expected_ids, - "route-equivalence table must cover every built-in model row" - ); - - for (model_id, deployment_id, transport, codec, billing_policy, agent_profile) in expected { - let model = select_from_all(catalog, model_id); - let route = resolve_route(catalog, model) - .unwrap_or_else(|| panic!("built-in model '{model_id}' should resolve")); - assert_eq!(route.deployment_id, *deployment_id, "{model_id}"); - assert_eq!(route.transport, *transport, "{model_id}"); - assert_eq!(route.codec, *codec, "{model_id}"); - assert_eq!(route.billing_policy, *billing_policy, "{model_id}"); - assert_eq!(route.agent_profile, *agent_profile, "{model_id}"); + for model in catalog.list(None) { + let route = resolve_route(catalog, model).unwrap_or_else(|| { + panic!( + "built-in offering '{}/{}' should resolve", + model.provider, model.id + ) + }); + assert_eq!(route.provider, model.provider); + assert!(!route.deployment_id.is_empty()); } } diff --git a/lib/foundation/fabro-model/src/catalog/providers/venice.toml b/lib/foundation/fabro-model/src/catalog/providers/venice.toml index dc97c4ab4..9791591e0 100644 --- a/lib/foundation/fabro-model/src/catalog/providers/venice.toml +++ b/lib/foundation/fabro-model/src/catalog/providers/venice.toml @@ -1,46 +1,212 @@ +# Model IDs, capabilities, contexts, and prices are from Venice's published +# model catalog, verified 2026-08-21: +# https://github.com/veniceai/api-docs/blob/59a300b1d036c0c0acc0e5f75c0ab0dd07c40c1c/data/static-models.json + [providers.venice] display_name = "Venice" adapter = "openai_compatible" base_url = "https://api.venice.ai/api/v1" priority = 35 aliases = ["venice-ai"] +billing_policy = "openai" [providers.venice.auth] credentials = ["env:VENICE_API_KEY", "vault:VENICE_API_KEY"] -[providers.venice.models."venice-uncensored-1-2"] -display_name = "Venice Uncensored 1.2" -family = "venice-uncensored" +[providers.venice.models."kimi-k3"] +display_name = "Kimi K3" +family = "kimi-k3" +agent_profile = "kimi" +aliases = ["kimi"] + +[providers.venice.models."kimi-k3".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."kimi-k3".features] +tools = true +vision = true +reasoning = true +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."kimi-k3".costs] +input_cost_per_mtok = 3.75 +output_cost_per_mtok = 18.75 +cache_input_cost_per_mtok = 0.375 + +[providers.venice.models."kimi-k3-fast"] +api_id = "kimi-k3-fast-api" +display_name = "Kimi K3 Fast" +family = "kimi-k3" +agent_profile = "kimi" +aliases = ["kimi-fast"] + +[providers.venice.models."kimi-k3-fast".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."kimi-k3-fast".features] +tools = true +vision = true +reasoning = true +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."kimi-k3-fast".costs] +input_cost_per_mtok = 4.5 +output_cost_per_mtok = 22.5 +cache_input_cost_per_mtok = 0.45 + +[providers.venice.models."grok-4.6"] +api_id = "grok-4-6" +display_name = "Grok 4.6" +family = "grok-4" +aliases = ["grok", "grok46", "grok-46"] + +[providers.venice.models."grok-4.6".limits] +context_window = 500000 +max_output = 32000 + +[providers.venice.models."grok-4.6".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true +prompt_cache = true + +[providers.venice.models."grok-4.6".controls] +reasoning_effort = ["low", "medium", "high", "xhigh"] + +[providers.venice.models."grok-4.6".costs] +input_cost_per_mtok = 2.27 +output_cost_per_mtok = 6.8 +cache_input_cost_per_mtok = 0.57 + +[providers.venice.models."glm-5.3"] +api_id = "z-ai-glm-5-3" +display_name = "GLM 5.3" +family = "glm-5" +aliases = ["glm", "glm5", "glm53", "glm5.3", "glm-5-3"] + +[providers.venice.models."glm-5.3".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."glm-5.3".features] +tools = true +vision = false +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true +prompt_cache = true + +[providers.venice.models."glm-5.3".controls] +reasoning_effort = ["low", "high", "max"] + +[providers.venice.models."glm-5.3".costs] +input_cost_per_mtok = 1.75 +output_cost_per_mtok = 5.5 +cache_input_cost_per_mtok = 0.325 + +[providers.venice.models."deepseek-v4-flash"] +api_id = "deepseek-v4-flash-0731" +display_name = "DeepSeek V4 Flash" +family = "deepseek-v4" +agent_profile = "openai" default = true -aliases = ["venice-uncensored", "vu"] +aliases = ["deepseek-v4", "deepseek", "deepseek-flash"] -[providers.venice.models."venice-uncensored-1-2".limits] -context_window = 128000 -max_output = 8192 +[providers.venice.models."deepseek-v4-flash".limits] +context_window = 1000000 +max_output = 32768 -[providers.venice.models."venice-uncensored-1-2".features] +[providers.venice.models."deepseek-v4-flash".features] +tools = true +vision = false +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."deepseek-v4-flash".controls] +reasoning_effort = ["low", "high", "max"] + +[providers.venice.models."deepseek-v4-flash".costs] +input_cost_per_mtok = 0.175 +output_cost_per_mtok = 0.35 +cache_input_cost_per_mtok = 0.035 + +[providers.venice.models."deepseek-v4-pro"] +api_id = "deepseek-v4-pro-0813" +display_name = "DeepSeek V4 Pro" +family = "deepseek-v4" +agent_profile = "openai" +aliases = ["deepseek-pro"] + +[providers.venice.models."deepseek-v4-pro".limits] +context_window = 1000000 +max_output = 32768 + +[providers.venice.models."deepseek-v4-pro".features] +tools = true +vision = false +reasoning = true +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."deepseek-v4-pro".costs] +input_cost_per_mtok = 1.65 +output_cost_per_mtok = 4.95 +cache_input_cost_per_mtok = 0.165 + +[providers.venice.models."qwen3.8-max"] +api_id = "qwen-3-8-max" +display_name = "Qwen 3.8 Max" +family = "qwen3" +aliases = ["qwen", "qwen-max", "qwen3.8", "qwen-3.8", "qwen38", "qwen-3.8-max", "qwen38-max"] + +[providers.venice.models."qwen3.8-max".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."qwen3.8-max".features] tools = true vision = true -reasoning = false +reasoning = true +reasoning_by_default = true +prompt_cache = true -[providers.venice.models."venice-uncensored-1-2".costs] -input_cost_per_mtok = 0.2 -output_cost_per_mtok = 0.9 +[providers.venice.models."qwen3.8-max".costs] +input_cost_per_mtok = 2.5 +output_cost_per_mtok = 7.5 +cache_input_cost_per_mtok = 0.3125 -[providers.venice.models."venice-uncensored-role-play"] -display_name = "Venice Uncensored Role Play" -family = "venice-uncensored" -aliases = ["venice-roleplay", "vrp"] +[providers.venice.models."qwen3.8-27b"] +api_id = "qwen-3-8-27b" +display_name = "Qwen 3.8 27B" +family = "qwen3.8" +aliases = ["qwen-27b", "qwen-3.8-27b", "qwen38-27b"] -[providers.venice.models."venice-uncensored-role-play".limits] -context_window = 128000 -max_output = 4096 +[providers.venice.models."qwen3.8-27b".limits] +context_window = 262144 +max_output = 131072 -[providers.venice.models."venice-uncensored-role-play".features] +[providers.venice.models."qwen3.8-27b".features] tools = true vision = true -reasoning = false +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true -[providers.venice.models."venice-uncensored-role-play".costs] -input_cost_per_mtok = 0.5 -output_cost_per_mtok = 2.0 +[providers.venice.models."qwen3.8-27b".controls] +reasoning_effort = ["low", "medium", "xhigh"] + +[providers.venice.models."qwen3.8-27b".costs] +input_cost_per_mtok = 0.45 +output_cost_per_mtok = 3.2 From ff1ca976c3397428b882836b1ec50654761d025a Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 10:48:03 -0400 Subject: [PATCH 62/63] Document Venice model integration --- docs/public/core-concepts/models.mdx | 4 + docs/public/docs.json | 1 + docs/public/integrations/venice.mdx | 125 +++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 docs/public/integrations/venice.mdx diff --git a/docs/public/core-concepts/models.mdx b/docs/public/core-concepts/models.mdx index e3bd5249e..d471e18fd 100644 --- a/docs/public/core-concepts/models.mdx +++ b/docs/public/core-concepts/models.mdx @@ -174,6 +174,10 @@ Provider `billing_policy` defaults from `adapter` and controls usage-cost estima Provider fields in configuration, APIs, and model routing are provider ID strings. Built-in names like `anthropic`, `openai`, and `gemini` still work, but custom IDs like `proxy` work anywhere a provider ID is accepted. +### Venice + +Fabro ships a built-in [Venice](/integrations/venice) provider with a curated catalog of Venice-hosted Kimi, Grok, GLM, DeepSeek, and Qwen models. Store its API key with `fabro provider login --provider venice`. Pin `provider = "venice"` when a shared model slug must use Venice instead of a higher-priority direct provider. + ### Poolside Fabro ships a built-in [Poolside](/integrations/poolside) provider for Laguna S 2.1 and Laguna XS 2.1 over Poolside's OpenAI-compatible API. Store a direct API key with `fabro provider login --provider poolside`. The same model slugs are also available through the opt-in OpenRouter provider; its vendor-namespaced strings remain provider-only `api_id` values. diff --git a/docs/public/docs.json b/docs/public/docs.json index faf0d0f84..284e5e4cb 100644 --- a/docs/public/docs.json +++ b/docs/public/docs.json @@ -97,6 +97,7 @@ "integrations/litellm", "integrations/bedrock", "integrations/deepseek", + "integrations/venice", "integrations/poolside", "integrations/openrouter", "integrations/modal", diff --git a/docs/public/integrations/venice.mdx b/docs/public/integrations/venice.mdx new file mode 100644 index 000000000..f172ccb34 --- /dev/null +++ b/docs/public/integrations/venice.mdx @@ -0,0 +1,125 @@ +--- +title: "Venice" +description: "Run Kimi, Grok, GLM, DeepSeek, and Qwen models through Venice" +--- + +[Venice](https://venice.ai/) provides an OpenAI-compatible API for hosted text models. Fabro enables the `venice` provider in its built-in catalog and maps stable Fabro model slugs to Venice's API model IDs. + +## Prerequisites + +- A Venice account +- An inference API key from [venice.ai/settings/api](https://venice.ai/settings/api) +- A running Fabro server + +## Configure credentials + +Store the API key in the target Fabro server vault: + +```bash +fabro provider login --provider venice + +# For a non-default remote server: +fabro provider login --server https://your-fabro.example --provider venice + +# Or set the vault token directly: +fabro secret set VENICE_API_KEY +fabro secret --server https://your-fabro.example set VENICE_API_KEY +``` + +Standalone SDK usage outside a Fabro server can use an env-backed credential source explicitly: + +```bash +export VENICE_API_KEY= +``` + +Fabro sends bearer-authenticated Chat Completions requests to `https://api.venice.ai/api/v1`. + +## Included models + +| Fabro model slug | Venice API ID | Context | Max output | Role and aliases | +|---|---|---:|---:|---| +| `kimi-k3` | `kimi-k3` | 1,000,000 | 131,072 | Alias `kimi` | +| `kimi-k3-fast` | `kimi-k3-fast-api` | 1,000,000 | 131,072 | Alias `kimi-fast` | +| `grok-4.6` | `grok-4-6` | 500,000 | 32,000 | Aliases `grok`, `grok46`, `grok-46` | +| `glm-5.3` | `z-ai-glm-5-3` | 1,000,000 | 131,072 | Aliases `glm`, `glm5`, `glm53`, `glm5.3`, `glm-5-3` | +| `deepseek-v4-flash` | `deepseek-v4-flash-0731` | 1,000,000 | 32,768 | Provider default; aliases `deepseek`, `deepseek-v4`, `deepseek-flash` | +| `deepseek-v4-pro` | `deepseek-v4-pro-0813` | 1,000,000 | 32,768 | Alias `deepseek-pro` | +| `qwen3.8-max` | `qwen-3-8-max` | 1,000,000 | 131,072 | Aliases `qwen`, `qwen-max`, `qwen3.8`, `qwen-3.8`, `qwen38`, `qwen-3.8-max`, `qwen38-max` | +| `qwen3.8-27b` | `qwen-3-8-27b` | 262,144 | 131,072 | Aliases `qwen-27b`, `qwen-3.8-27b`, `qwen38-27b` | + +Venice API IDs are also valid provider-scoped selectors. Fabro persists the stable Fabro slug and the selected provider when it creates a run. + +## Select Venice explicitly + +Some Venice models use the same stable slugs as direct providers. An unqualified selector chooses the highest-priority ready provider. For example, `deepseek` can select the direct DeepSeek provider when both API keys are configured. + +Pin Venice when the run must use Venice: + +```bash +fabro model list --provider venice +fabro model test --provider venice --model deepseek-v4-flash --deep +fabro run workflow.fabro --provider venice --model deepseek-v4-flash +``` + +In a workflow stylesheet: + +```dot title="workflow.fabro" +digraph Example { + graph [ + model_stylesheet=" + * { provider: venice; model: deepseek-v4-flash; } + .complex { provider: venice; model: qwen; } + .fast { provider: venice; model: kimi-fast; } + " + ] + + start [shape=Mdiamond, label="Start"] + work [label="Implement", class="complex"] + check [label="Check", class="fast"] + exit [shape=Msquare, label="Exit"] + + start -> work -> check -> exit +} +``` + +The generic Qwen aliases `qwen` and `qwen3.8` select Qwen 3.8 Max. Use a size-specific alias such as `qwen-27b` to select Qwen 3.8 27B. + +## Capabilities and reasoning + +All included models support tool calling and reasoning. Kimi K3, Kimi K3 Fast, Grok 4.6, Qwen 3.8 Max, and Qwen 3.8 27B also accept image input. + +Fabro exposes native reasoning-effort controls only when Venice supports them: + +| Model | Reasoning effort values | +|---|---| +| `grok-4.6` | `low`, `medium`, `high`, `xhigh` | +| `glm-5.3` | `low`, `high`, `max` | +| `deepseek-v4-flash` | `low`, `high`, `max` | +| `qwen3.8-27b` | `low`, `medium`, `xhigh` | + +The other models reason by default but do not expose a Venice reasoning-effort control. Fabro omits sampling parameters for Kimi and DeepSeek because those routes do not use them with their configured reasoning behavior. + +## Pricing and prompt caching + +The built-in catalog uses Venice's published prices per million tokens: + +| Model | Uncached input | Cache hit | Output | +|---|---:|---:|---:| +| `kimi-k3` | $3.75 | $0.375 | $18.75 | +| `kimi-k3-fast` | $4.50 | $0.45 | $22.50 | +| `grok-4.6` | $2.27 | $0.57 | $6.80 | +| `glm-5.3` | $1.75 | $0.325 | $5.50 | +| `deepseek-v4-flash` | $0.175 | $0.035 | $0.35 | +| `deepseek-v4-pro` | $1.65 | $0.165 | $4.95 | +| `qwen3.8-max` | $2.50 | $0.3125 | $7.50 | +| `qwen3.8-27b` | $0.45 | n/a | $3.20 | + +Fabro reports cached input separately when Venice returns cache usage for the selected model. Prices and model availability can change upstream; use `fabro model list --provider venice` to inspect the catalog shipped with your Fabro version and the [Venice model catalog](https://docs.venice.ai/models/overview) for the current upstream service. + +## Troubleshooting + +**"No credential was found for provider 'venice'"** — Store `VENICE_API_KEY` in the server vault with `fabro provider login --provider venice`. Pass `--server` when configuring a remote Fabro server. + +**A shared model used another provider** — Pin Venice with `--provider venice` or `provider: venice` in the workflow stylesheet. Unqualified selectors use provider priority. + +**A Venice API model ID is rejected without a provider** — Use the stable Fabro slug for portable selection, or qualify the API ID with the provider, such as `venice:qwen-3-8-max`. From db1faf02ec345fd690a86457b80812260f0b17ea Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 11:19:28 -0400 Subject: [PATCH 63/63] Fix catalog dispatch invariant for shared models --- lib/components/fabro-llm/src/client.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/components/fabro-llm/src/client.rs b/lib/components/fabro-llm/src/client.rs index fc2802be1..59cfa421d 100644 --- a/lib/components/fabro-llm/src/client.rs +++ b/lib/components/fabro-llm/src/client.rs @@ -2037,17 +2037,20 @@ reasoning = false client } - /// Live-dispatch counterpart of the adapter_registry route-equivalence - /// table: for every built-in model, `resolve_provider` lands on the same - /// provider the resolved route names. + /// For every built-in model selector, live dispatch and catalog selection + /// choose the same provider from the same ready-provider set. #[tokio::test] async fn dispatch_agrees_with_resolve_route_for_every_builtin_model() { let catalog = catalog_with(""); let client = client_with_all_catalog_providers(&catalog).await; + let ready_providers = catalog.all_provider_ids(); for model in catalog.list(None) { - let route = adapter_registry::resolve_route(&catalog, model) - .expect("built-in model should resolve to a route"); + let selected = catalog + .select(model.id.as_str(), None, &ready_providers) + .expect("built-in model should be selectable"); + let route = adapter_registry::resolve_route(&catalog, selected) + .expect("selected built-in model should resolve to a route"); let mut request = test_request(); request.model = model.id.to_string();