From 9f13611e832b6edf4cc103e7ed7839b80e090a4f Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 4 Aug 2026 13:59:48 -0400 Subject: [PATCH 01/62] Extract working-tree collection from manifest assembly --- lib/apps/fabro-cli/tests/it/cmd/validate.rs | 3 +- lib/components/fabro-manifest/src/lib.rs | 857 ++++----- .../fabro-manifest/src/working_tree.rs | 1703 +++++++++++++++++ 3 files changed, 2094 insertions(+), 469 deletions(-) create mode 100644 lib/components/fabro-manifest/src/working_tree.rs diff --git a/lib/apps/fabro-cli/tests/it/cmd/validate.rs b/lib/apps/fabro-cli/tests/it/cmd/validate.rs index f82f5dc9e..dd469c4a5 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/validate.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/validate.rs @@ -314,7 +314,8 @@ fn validate_reports_missing_template_dependency() { exit_code: 1 ----- stdout ----- ----- stderr ----- - × failed to discover template dependencies: missing template dependency `missing.tpl.md` from `[FIXTURES]/templates/missing_dependency/workflow.fabro` + × failed to discover template dependencies + ╰─▶ missing template dependency `missing.tpl.md` from `[FIXTURES]/templates/missing_dependency/workflow.fabro` "); } diff --git a/lib/components/fabro-manifest/src/lib.rs b/lib/components/fabro-manifest/src/lib.rs index eb1cc0dad..6cd29c2f9 100644 --- a/lib/components/fabro-manifest/src/lib.rs +++ b/lib/components/fabro-manifest/src/lib.rs @@ -3,33 +3,33 @@ reason = "CLI manifest builder: sync file I/O building install manifests" )] -use std::collections::{HashMap, HashSet}; +mod working_tree; + +use std::collections::HashMap; use std::path::{Component, Path, PathBuf}; -use std::sync::Arc; use anyhow::{Context, Result, anyhow}; use fabro_api::types; use fabro_config::project::{self, WorkflowLocation, discover_project_config}; use fabro_config::run::{resolve_run_goal_from_layer, resolve_run_goal_from_namespace}; use fabro_config::{ - CliLayer, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer, - EnvironmentLifecycleLayer, MergeMap, ReplaceMap, RunEnvironmentLayer, RunExecutionLayer, - RunGoalLayer, RunLayer, RunModelLayer, SettingsLayer, WorkflowSettingsBuilder, + CliLayer, EnvironmentLayer, EnvironmentLifecycleLayer, MergeMap, ReplaceMap, + RunEnvironmentLayer, RunExecutionLayer, RunGoalLayer, RunLayer, RunModelLayer, + WorkflowSettingsBuilder, }; use fabro_graphviz::graph::AttrValue; use fabro_graphviz::parser; -use fabro_template::{ - BundleTemplateStore, FilesystemTemplateStore, RecordingTemplateStore, TemplateContext, - TemplateRenderMode, TemplateSource, discover_static_dependency_closure, render_source, -}; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{ApprovalMode, ResolvedGoalSource, ResolvedRunGoal, RunMode}; use fabro_types::{DirtyStatus, GitContext, ManifestPath, WorkflowSettings}; use fabro_workflow::git::{ GitSyncStatus, branch_needs_push, head_sha, push_branch_noninteractive, sync_status, }; -use fabro_workflow::static_reference::{ - AttributeScope, ReferenceKind, reference_kind_for_attribute, +use fabro_workflow::static_reference::ReferenceKind; + +use crate::working_tree::{ + CollectWorkingTreeInput, CollectedDocument, CollectedFileReferenceType, CollectedSourceInput, + CollectedWorkingTree, }; #[derive(Debug, Default)] @@ -127,20 +127,6 @@ pub fn build_sparse_run_overrides(input: RunOverrideInput<'_>) -> Option { - cwd: &'a Path, - inputs: HashMap, - workflows: HashMap, - visited_workflows: HashSet, -} - -#[derive(Clone)] -struct WorkflowScanInput { - absolute_dot_path: PathBuf, - dot_path: ManifestPath, - source: String, -} - pub fn build_run_manifest(input: ManifestBuildInput) -> Result { let root_location = WorkflowLocation::resolve(&input.workflow, &input.cwd)?; if root_location.toml.is_none() && !root_location.graph.is_file() { @@ -155,8 +141,10 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { .map(|path| { let source = std::fs::read_to_string(path) .with_context(|| format!("Failed to read {}", path.display()))?; - let manifest_path = manifest_path_from_absolute(path, &input.cwd)?; - Ok::<_, anyhow::Error>((path.clone(), manifest_path, source)) + Ok::<_, anyhow::Error>(CollectedSourceInput { + access_path: path.clone(), + source, + }) }) .transpose()?; @@ -186,47 +174,27 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { .context("failed to resolve manifest settings")?; workflow_settings.run.inputs.extend(input.input_overrides); let target_path = root_location.graph.clone(); - let target_manifest_path = manifest_path_from_absolute(&target_path, &input.cwd)?; - let target_key = target_manifest_path.to_string(); - - let mut context = CollectContext { - cwd: &input.cwd, - inputs: workflow_settings.run.inputs.clone(), - workflows: HashMap::new(), - visited_workflows: HashSet::new(), - }; - collect_workflow_entry(&mut context, &input.workflow, &input.cwd)?; - if let Some((_, config_path, source)) = project_config_source.as_ref() { - let workflow = context - .workflows - .get_mut(&target_key) - .ok_or_else(|| anyhow!("root workflow missing from manifest bundle"))?; - collect_config_dockerfile(context.cwd, config_path, source, &mut workflow.files)?; - } - - let root_source = context - .workflows - .get(&target_key) - .map(|workflow| workflow.source.clone()) - .ok_or_else(|| anyhow!("root workflow missing from manifest bundle"))?; - - let mut configs = Vec::new(); - if let Some((path, _, source)) = project_config_source { - configs.push(types::ManifestConfig { - path: Some(path.display().to_string()), - source: Some(source), - type_: types::ManifestConfigType::Project, - }); - } - if let Some(path) = input.user_settings_path.filter(|p| p.is_file()) { - let source = std::fs::read_to_string(&path) - .with_context(|| format!("Failed to read {}", path.display()))?; - configs.push(types::ManifestConfig { - path: Some(path.display().to_string()), - source: Some(source), - type_: types::ManifestConfigType::User, - }); - } + let user_config_source = input + .user_settings_path + .as_ref() + .filter(|path| path.is_file()) + .map(|path| { + let source = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + Ok::<_, anyhow::Error>(CollectedSourceInput { + access_path: path.clone(), + source, + }) + }) + .transpose()?; + let collected = working_tree::collect_working_tree(CollectWorkingTreeInput { + cwd: &input.cwd, + root_location, + inputs: &workflow_settings.run.inputs, + project_config: project_config_source, + user_config: user_config_source, + })?; + let assembled = assemble_current_manifest(&collected, &input.cwd)?; let working_directory = project::resolve_working_directory_from_run(&workflow_settings.run, &input.cwd); @@ -234,7 +202,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { let goal = resolve_manifest_goal( input.run_overrides.as_ref(), &workflow_settings, - &root_source, + &assembled.root_source, &target_path, &working_directory, )?; @@ -246,423 +214,118 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { Ok(BuiltManifest { manifest: types::RunManifest { args, - configs, cwd: input.cwd.display().to_string(), git, goal, parent_id: None, title: None, - target: types::ManifestTarget { path: target_key }, + target: types::ManifestTarget { + path: assembled.target_key, + }, version: 1, - workflows: context.workflows, + workflows: assembled.workflows, + configs: assembled.configs, }, target_path, }) } -fn collect_workflow_entry( - context: &mut CollectContext<'_>, - workflow: &Path, - resolve_from: &Path, -) -> Result<()> { - let normalized_workflow = if workflow.extension().is_some() && workflow.is_relative() { - normalize_absolute_path(resolve_from, &workflow.to_string_lossy()).ok_or_else(|| { - anyhow!( - "unsupported manifest workflow reference: {}", - workflow.display() - ) - })? - } else { - workflow.to_path_buf() - }; - let location = WorkflowLocation::resolve(&normalized_workflow, resolve_from)?; - let dot_path = manifest_path_from_absolute(&location.graph, context.cwd)?; - let dot_key = dot_path.to_string(); - if !context.visited_workflows.insert(dot_key.clone()) { - return Ok(()); - } - - let source = std::fs::read_to_string(&location.graph) - .with_context(|| format!("Failed to read {}", location.graph.display()))?; - let config = if let Some(workflow_toml_path) = location.toml.as_ref() { - Some(types::ManifestWorkflowConfig { - path: manifest_path_from_absolute(workflow_toml_path, context.cwd)?.to_string(), - source: std::fs::read_to_string(workflow_toml_path) - .with_context(|| format!("Failed to read {}", workflow_toml_path.display()))?, - }) - } else { - None - }; - - let scan = WorkflowScanInput { - absolute_dot_path: location.graph, - dot_path, - source: source.clone(), - }; - let mut files = HashMap::new(); - let mut visited_imports = HashSet::new(); - if let Some(config) = config.as_ref() { - let config_path = ManifestPath::from_wire(&config.path) - .ok_or_else(|| anyhow!("invalid manifest workflow config path: {}", config.path))?; - collect_config_dockerfile(context.cwd, &config_path, &config.source, &mut files)?; - } - collect_workflow_files(context, &scan, &mut files, &mut visited_imports)?; - - context.workflows.insert(dot_key, types::ManifestWorkflow { - config, - files, - source, - }); - - Ok(()) +struct AssembledCurrentManifest { + target_key: String, + root_source: String, + workflows: HashMap, + configs: Vec, } -fn collect_workflow_files( - context: &mut CollectContext<'_>, - workflow: &WorkflowScanInput, - files: &mut HashMap, - visited_imports: &mut HashSet, -) -> Result<()> { - let graph = parser::parse(&workflow.source).map_err(|err| { - anyhow!( - "Failed to parse {}: {err}", - workflow.absolute_dot_path.display() - ) - })?; - let workflow_base_dir = workflow - .absolute_dot_path - .parent() - .unwrap_or_else(|| Path::new(".")); - let workflow_template_root = manifest_parent_or_dot(&workflow.dot_path)?; - - if let Some(goal_ref) = graph.attrs.get("goal").and_then(AttrValue::as_str) { - if goal_ref.starts_with('@') { - let bundled = collect_bundled_file( - files, - workflow_base_dir, - context.cwd, - goal_ref.trim_start_matches('@'), - types::ManifestFileRefType::FileInline, - manifest_attr_reference_kind(AttributeScope::Graph, "goal", goal_ref)?, - Some(workflow.dot_path.clone()), - )?; - let source = std::fs::read_to_string(&bundled.absolute_path) - .with_context(|| format!("Failed to read {}", bundled.absolute_path.display()))?; - let template_root = - template_root_for_bundled_file(&bundled.path, &workflow_template_root)?; - collect_template_include_files( - files, - context.cwd, - TemplateSource::new(bundled.path.clone(), template_root, source), - Some(&bundled.path), - &context.inputs, - )?; - } else { - collect_template_include_files( - files, - context.cwd, - TemplateSource::new( - workflow.dot_path.clone(), - workflow_template_root.clone(), - goal_ref.to_owned(), - ), - Some(&workflow.dot_path), - &context.inputs, - )?; - } - } - - for node in graph.nodes.values() { - if let Some(prompt_ref) = node.attrs.get("prompt").and_then(AttrValue::as_str) { - if !prompt_ref.starts_with('@') { - collect_template_include_files( - files, - context.cwd, - TemplateSource::new( - workflow.dot_path.clone(), - workflow_template_root.clone(), - prompt_ref.to_owned(), - ), - Some(&workflow.dot_path), - &context.inputs, - )?; - } - } - - for (name, value) in &node.attrs { - let Some(value) = value.as_str() else { - continue; - }; - let Some(ReferenceKind::FileInline) = - reference_kind_for_attribute(AttributeScope::Node, name, value) - else { - continue; - }; - let reference = value.strip_prefix('@').ok_or_else(|| { - anyhow!("file inline reference must start with '@': {name}={value}") - })?; - let bundled = collect_bundled_file( - files, - workflow_base_dir, - context.cwd, - reference, - types::ManifestFileRefType::FileInline, - ReferenceKind::FileInline, - Some(workflow.dot_path.clone()), - )?; - - if name == "prompt" { - let source = - std::fs::read_to_string(&bundled.absolute_path).with_context(|| { - format!("Failed to read {}", bundled.absolute_path.display()) - })?; - let template_root = - template_root_for_bundled_file(&bundled.path, &workflow_template_root)?; - collect_template_include_files( - files, - context.cwd, - TemplateSource::new(bundled.path.clone(), template_root, source), - Some(&bundled.path), - &context.inputs, - )?; - } - } - - if let Some(import_ref) = node.attrs.get("import").and_then(AttrValue::as_str) { - let imported = collect_bundled_file( - files, - workflow_base_dir, - context.cwd, - import_ref, - types::ManifestFileRefType::Import, - manifest_attr_reference_kind(AttributeScope::Node, "import", import_ref)?, - Some(workflow.dot_path.clone()), - )?; - let import_key = imported.path.to_string(); - if visited_imports.insert(import_key) { - let imported_source = std::fs::read_to_string(&imported.absolute_path) - .with_context(|| { - format!("Failed to read {}", imported.absolute_path.display()) - })?; - let imported_scan = WorkflowScanInput { - absolute_dot_path: imported.absolute_path, - dot_path: imported.path, - source: imported_source, - }; - collect_workflow_files(context, &imported_scan, files, visited_imports)?; - } - } - - if let Some(child_ref) = node - .attrs - .get("stack.child_workflow") - .and_then(AttrValue::as_str) - { - manifest_attr_reference_kind(AttributeScope::Node, "stack.child_workflow", child_ref)? - .validate(child_ref) - .map_err(anyhow::Error::new)?; - collect_workflow_entry(context, Path::new(child_ref), workflow_base_dir)?; - } - } - - Ok(()) -} - -fn collect_template_include_files( - files: &mut HashMap, +fn assemble_current_manifest( + collected: &CollectedWorkingTree, cwd: &Path, - source: TemplateSource, - from: Option<&ManifestPath>, - inputs: &HashMap, -) -> Result<()> { - let source_path = source.path.clone(); - let store = FilesystemTemplateStore::new(cwd.to_path_buf()); - let closure = discover_static_dependency_closure([source], &store) - .map_err(|err| anyhow!("failed to discover template dependencies: {err}"))?; - verify_recorded_template_dependencies(&source_path, &closure, files, from, inputs)?; - - for (path, source) in closure.sources { - if path == source_path { - continue; - } - let key = path.to_string(); - files - .entry(key) - .or_insert_with(|| types::ManifestFileEntry { - content: source.content, +) -> Result { + let mut workflows = HashMap::new(); + for workflow in collected.workflows().values() { + let graph = workflow.graph(); + let graph_key = manifest_path_from_absolute(graph.access_path(), cwd)?.to_string(); + let config = workflow + .config() + .map(|config| { + Ok::<_, anyhow::Error>(types::ManifestWorkflowConfig { + path: manifest_path_from_absolute(config.access_path(), cwd)?.to_string(), + source: config.source().to_owned(), + }) + }) + .transpose()?; + let mut files = HashMap::new(); + for file in workflow.files().values() { + let document = file.document(); + let reference = file.reference(); + let key = manifest_path_from_absolute(document.access_path(), cwd)?.to_string(); + let from = reference + .source_access_path() + .map(|path| manifest_path_from_absolute(path, cwd).map(|path| path.to_string())) + .transpose()?; + let type_ = match reference.type_() { + CollectedFileReferenceType::FileInline => types::ManifestFileRefType::FileInline, + CollectedFileReferenceType::Import => types::ManifestFileRefType::Import, + CollectedFileReferenceType::Dockerfile => types::ManifestFileRefType::Dockerfile, + }; + files.insert(key, types::ManifestFileEntry { + content: document.source().to_owned(), ref_: types::ManifestFileRef { - from: from.map(std::string::ToString::to_string), - original: path.to_string(), - type_: types::ManifestFileRefType::FileInline, + from, + original: reference.original().to_owned(), + type_, }, }); - } - Ok(()) -} - -fn template_root_for_bundled_file( - path: &ManifestPath, - workflow_template_root: &ManifestPath, -) -> Result { - if manifest_path_is_within_root(path, workflow_template_root) { - Ok(workflow_template_root.clone()) - } else { - manifest_parent_or_dot(path) - } -} - -fn manifest_path_is_within_root(path: &ManifestPath, root: &ManifestPath) -> bool { - if root.as_path().as_os_str().is_empty() { - return !matches!( - path.as_path().components().next(), - Some(Component::ParentDir) - ); - } - path.starts_with(root) -} - -fn verify_recorded_template_dependencies( - source_path: &ManifestPath, - closure: &fabro_template::TemplateDependencyClosure, - files: &HashMap, - from: Option<&ManifestPath>, - inputs: &HashMap, -) -> Result<()> { - let Some(source) = closure.sources.get(source_path) else { - return Ok(()); - }; - let mut bundled_files = closure - .sources - .iter() - .map(|(path, source)| (path.clone(), source.content.clone())) - .collect::>(); - for (path, entry) in files { - if let Some(path) = ManifestPath::from_wire(path) { - bundled_files.insert(path, entry.content.clone()); } - } - let allowed = bundled_files.keys().cloned().collect(); - let store = - RecordingTemplateStore::with_allowed(BundleTemplateStore::new(bundled_files), allowed); - let ctx = TemplateContext::for_input_scan(inputs.clone()); - render_source(source, &ctx, Arc::new(store), TemplateRenderMode::Lenient).with_context( - || { - let from = - from.map_or_else(|| source_path.to_string(), std::string::ToString::to_string); - format!("failed to verify template dependencies for {from}") - }, - )?; - Ok(()) -} - -fn manifest_attr_reference_kind( - scope: AttributeScope, - key: &str, - value: &str, -) -> Result { - reference_kind_for_attribute(scope, key, value) - .ok_or_else(|| anyhow!("unsupported manifest reference attribute: {key}={value}")) -} - -fn collect_config_dockerfile( - cwd: &Path, - config_path: &ManifestPath, - source: &str, - files: &mut HashMap, -) -> Result<()> { - let layer = source - .parse::() - .context("Failed to parse run config TOML")?; - let absolute_config_path = cwd.join(config_path.as_path()); - let base_dir = absolute_config_path - .parent() - .unwrap_or_else(|| Path::new(".")); - - for environment in layer.environments.values() { - collect_environment_dockerfile( + workflows.insert(graph_key, types::ManifestWorkflow { + source: graph.source().to_owned(), + config, files, - base_dir, - cwd, - config_path, - environment.image.as_ref(), - )?; - } - if let Some(run_environment) = layer.run.as_ref().and_then(|run| run.environment.as_ref()) { - collect_environment_dockerfile( - files, - base_dir, - cwd, - config_path, - run_environment.image.as_ref(), - )?; - } - Ok(()) -} - -fn collect_environment_dockerfile( - files: &mut HashMap, - base_dir: &Path, - cwd: &Path, - config_path: &ManifestPath, - image: Option<&EnvironmentImageLayer>, -) -> Result<()> { - let dockerfile = image.and_then(|image| image.dockerfile.as_ref()); - let Some(EnvironmentDockerfileLayer::Path { path }) = dockerfile else { - return Ok(()); - }; - collect_bundled_file( - files, - base_dir, - cwd, - path, - types::ManifestFileRefType::Dockerfile, - ReferenceKind::Dockerfile, - Some(config_path.clone()), - )?; - Ok(()) -} - -struct BundledFile { - absolute_path: PathBuf, - path: ManifestPath, -} - -fn collect_bundled_file( - files: &mut HashMap, - base_dir: &Path, - cwd: &Path, - reference: &str, - ref_type: types::ManifestFileRefType, - reference_kind: ReferenceKind, - from: Option, -) -> Result { - reference_kind - .validate(reference) - .map_err(anyhow::Error::new)?; - - let absolute_path = normalize_absolute_path(base_dir, reference) - .ok_or_else(|| anyhow!("unsupported manifest reference: {reference}"))?; - let path = manifest_path_from_absolute(&absolute_path, cwd)?; - let key = path.to_string(); - if !files.contains_key(&key) { - let content = std::fs::read_to_string(&absolute_path) - .with_context(|| format!("Failed to read {}", absolute_path.display()))?; - files.insert(key.clone(), types::ManifestFileEntry { - content, - ref_: types::ManifestFileRef { - from: from.map(|value| value.to_string()), - original: reference.to_string(), - type_: ref_type, - }, }); } - Ok(BundledFile { - absolute_path, - path, + let target_key = manifest_path_from_absolute( + collected + .workflows() + .get(collected.entrypoint()) + .ok_or_else(|| anyhow!("root workflow missing from collected working tree"))? + .graph() + .access_path(), + cwd, + )? + .to_string(); + let root_source = workflows + .get(&target_key) + .map(|workflow| workflow.source.clone()) + .ok_or_else(|| anyhow!("root workflow missing from manifest bundle"))?; + + let mut configs = Vec::new(); + if let Some(config) = collected.project_config() { + configs.push(manifest_config(config, types::ManifestConfigType::Project)); + } + if let Some(config) = collected.user_config() { + configs.push(manifest_config(config, types::ManifestConfigType::User)); + } + + Ok(AssembledCurrentManifest { + target_key, + root_source, + workflows, + configs, }) } +fn manifest_config( + document: &CollectedDocument, + type_: types::ManifestConfigType, +) -> types::ManifestConfig { + types::ManifestConfig { + path: Some(document.access_path().display().to_string()), + source: Some(document.source().to_owned()), + type_, + } +} + fn resolve_manifest_goal( run_overrides: Option<&RunLayer>, settings: &WorkflowSettings, @@ -854,12 +517,6 @@ fn manifest_path_from_absolute(path: &Path, cwd: &Path) -> Result .ok_or_else(|| anyhow!("Failed to compute manifest path for {}", path.display())) } -fn manifest_parent_or_dot(path: &ManifestPath) -> Result { - let parent = path.parent_or_dot().to_string_lossy(); - ManifestPath::from_wire(&parent) - .ok_or_else(|| anyhow!("invalid manifest parent path for {path}: {parent}")) -} - pub fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool { args.auto_approve.is_none() && args.dry_run.is_none() @@ -932,6 +589,269 @@ mod tests { assert_eq!(schema.content, schema_source); } + #[test] + fn build_manifest_characterizes_the_complete_legacy_projection() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("project"); + let root = project.join(".fabro/workflows/root"); + let child = project.join(".fabro/workflows/child"); + let user_config_path = temp.path().join("home/.fabro/config.toml"); + let project_config = r#"_version = 1 + +[environments.project] +provider = "docker" + +[environments.project.image] +dockerfile = { path = "Project.Dockerfile" } +"#; + let root_config = r#"_version = 1 + +[workflow] +graph = "workflow.fabro" +"#; + let child_config = root_config; + let user_config = "_version = 1\n"; + let root_graph = r#"digraph Root { + graph [goal="@goals/goal.md"] + start [shape=Mdiamond] + prompt [prompt="@prompts/plan.md"] + schema [type="agent", prompt="schema", output_schema="@schemas/output.json"] + imported [import="imports/shared.fabro"] + child [shape=house, stack.child_workflow="../child/workflow.fabro"] + exit [shape=Msquare] + start -> prompt -> schema -> imported -> child -> exit + }"#; + let child_graph = + "digraph Child { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }"; + let imported_graph = r#"digraph Shared { + start [shape=Mdiamond] + shared [prompt="@../prompts/shared.md"] + exit [shape=Msquare] + start -> shared -> exit + }"#; + let plan_prompt = "{% include \"partial.md\" %}\n{% from \"helpers.md\" import render %}"; + let helpers = "{% macro render() %}{% include \"deep.md\" %}{% endmacro %}"; + let output_schema = r#"{"type":"object"}"#; + let write = |path: &Path, source: &str| { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + std::fs::write(path, source).unwrap(); + }; + write(&project.join(".fabro/project.toml"), project_config); + write(&project.join(".fabro/Project.Dockerfile"), "FROM project\n"); + write(&user_config_path, user_config); + write(&root.join("workflow.toml"), root_config); + write(&root.join("workflow.fabro"), root_graph); + write(&root.join("goals/goal.md"), "ship it\n"); + write(&root.join("prompts/plan.md"), plan_prompt); + write(&root.join("prompts/partial.md"), "partial\n"); + write(&root.join("prompts/helpers.md"), helpers); + write(&root.join("prompts/deep.md"), "deep\n"); + write(&root.join("prompts/shared.md"), "shared\n"); + write(&root.join("schemas/output.json"), output_schema); + write(&root.join("imports/shared.fabro"), imported_graph); + write(&child.join("workflow.toml"), child_config); + write(&child.join("workflow.fabro"), child_graph); + + let built = build_run_manifest(ManifestBuildInput { + workflow: PathBuf::from(".fabro/workflows/root/workflow.toml"), + cwd: project.clone(), + input_overrides: HashMap::from([("feature".to_owned(), toml::Value::Boolean(true))]), + args: Some(types::ManifestArgs { + dry_run: Some(true), + input: vec!["feature=true".to_owned()], + label: vec!["suite=characterization".to_owned()], + ..types::ManifestArgs::default() + }), + environment_defaults: test_environment_defaults(), + user_settings_path: Some(user_config_path), + ..ManifestBuildInput::default() + }) + .unwrap(); + + let mut actual = serde_json::to_value(&built.manifest).unwrap(); + actual["cwd"] = serde_json::json!(""); + actual["configs"][0]["path"] = serde_json::json!(""); + actual["configs"][1]["path"] = serde_json::json!(""); + let file = |content: &str, from: &str, original: &str, type_: &str| { + serde_json::json!({ + "content": content, + "ref": { + "from": from, + "original": original, + "type": type_, + } + }) + }; + + assert_eq!( + actual, + serde_json::json!({ + "args": { + "dry_run": true, + "input": ["feature=true"], + "label": ["suite=characterization"], + }, + "configs": [ + { + "path": "", + "source": project_config, + "type": "project", + }, + { + "path": "", + "source": user_config, + "type": "user", + }, + ], + "cwd": "", + "goal": { "type": "graph", "text": "ship it\n" }, + "target": { "path": ".fabro/workflows/root/workflow.fabro" }, + "version": 1, + "workflows": { + ".fabro/workflows/child/workflow.fabro": { + "config": { + "path": ".fabro/workflows/child/workflow.toml", + "source": child_config, + }, + "source": child_graph, + }, + ".fabro/workflows/root/workflow.fabro": { + "config": { + "path": ".fabro/workflows/root/workflow.toml", + "source": root_config, + }, + "files": { + ".fabro/Project.Dockerfile": file( + "FROM project\n", + ".fabro/project.toml", + "Project.Dockerfile", + "dockerfile", + ), + ".fabro/workflows/root/goals/goal.md": file( + "ship it\n", + ".fabro/workflows/root/workflow.fabro", + "goals/goal.md", + "file_inline", + ), + ".fabro/workflows/root/imports/shared.fabro": file( + imported_graph, + ".fabro/workflows/root/workflow.fabro", + "imports/shared.fabro", + "import", + ), + ".fabro/workflows/root/prompts/deep.md": file( + "deep\n", + ".fabro/workflows/root/workflow.fabro", + ".fabro/workflows/root/prompts/deep.md", + "file_inline", + ), + ".fabro/workflows/root/prompts/helpers.md": file( + helpers, + ".fabro/workflows/root/workflow.fabro", + ".fabro/workflows/root/prompts/helpers.md", + "file_inline", + ), + ".fabro/workflows/root/prompts/partial.md": file( + "partial\n", + ".fabro/workflows/root/workflow.fabro", + ".fabro/workflows/root/prompts/partial.md", + "file_inline", + ), + ".fabro/workflows/root/prompts/plan.md": file( + plan_prompt, + ".fabro/workflows/root/workflow.fabro", + "prompts/plan.md", + "file_inline", + ), + ".fabro/workflows/root/prompts/shared.md": file( + "shared\n", + ".fabro/workflows/root/imports/shared.fabro", + "../prompts/shared.md", + "file_inline", + ), + ".fabro/workflows/root/schemas/output.json": file( + output_schema, + ".fabro/workflows/root/workflow.fabro", + "schemas/output.json", + "file_inline", + ), + }, + "source": root_graph, + }, + }, + }) + ); + } + + #[test] + fn build_manifest_keeps_legacy_parent_paths_for_external_siblings() { + let temp = tempfile::tempdir().unwrap(); + let cwd = temp.path().join("checkout"); + let root = temp.path().join("user/workflows/root"); + let child = temp.path().join("user/workflows/child"); + std::fs::create_dir_all(&cwd).unwrap(); + for directory in [&root, &child] { + std::fs::create_dir_all(directory.join("prompts")).unwrap(); + std::fs::write( + directory.join("workflow.toml"), + "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n", + ) + .unwrap(); + } + std::fs::write( + root.join("workflow.fabro"), + r#"digraph Root { + start [shape=Mdiamond] + prompt [prompt="@prompts/root.md"] + child [shape=house, stack.child_workflow="../child/workflow.fabro"] + exit [shape=Msquare] + start -> prompt -> child -> exit + }"#, + ) + .unwrap(); + std::fs::write(root.join("prompts/root.md"), "root prompt\n").unwrap(); + std::fs::write( + child.join("workflow.fabro"), + r#"digraph Child { + start [shape=Mdiamond] + prompt [prompt="@prompts/child.md"] + exit [shape=Msquare] + start -> prompt -> exit + }"#, + ) + .unwrap(); + std::fs::write(child.join("prompts/child.md"), "child prompt\n").unwrap(); + + let built = build_run_manifest(ManifestBuildInput { + workflow: root.join("workflow.fabro"), + cwd, + environment_defaults: test_environment_defaults(), + ..ManifestBuildInput::default() + }) + .unwrap(); + + let root_key = "../user/workflows/root/workflow.fabro"; + let child_key = "../user/workflows/child/workflow.fabro"; + assert_eq!(built.manifest.target.path, root_key); + let root_workflow = &built.manifest.workflows[root_key]; + assert_eq!( + root_workflow.config.as_ref().unwrap().path, + "../user/workflows/root/workflow.toml" + ); + let root_prompt = &root_workflow.files["../user/workflows/root/prompts/root.md"]; + assert_eq!(root_prompt.ref_.from.as_deref(), Some(root_key)); + assert_eq!(root_prompt.ref_.original, "prompts/root.md"); + let child_workflow = &built.manifest.workflows[child_key]; + assert_eq!( + child_workflow.config.as_ref().unwrap().path, + "../user/workflows/child/workflow.toml" + ); + let child_prompt = &child_workflow.files["../user/workflows/child/prompts/child.md"]; + assert_eq!(child_prompt.ref_.from.as_deref(), Some(child_key)); + } + #[test] fn build_run_overrides_sets_common_cli_and_mcp_layers() { let overrides = build_run_overrides(RunOverrideInput { @@ -1290,7 +1210,8 @@ mod tests { .unwrap_err(); assert!( - err.to_string().contains("dynamic template dependency"), + err.chain() + .any(|cause| cause.to_string().contains("dynamic template dependency")), "unexpected error: {err:#}" ); } diff --git a/lib/components/fabro-manifest/src/working_tree.rs b/lib/components/fabro-manifest/src/working_tree.rs new file mode 100644 index 000000000..3b9dbed95 --- /dev/null +++ b/lib/components/fabro-manifest/src/working_tree.rs @@ -0,0 +1,1703 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fmt; +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context as _, Result, anyhow, bail}; +use fabro_config::project::WorkflowLocation; +use fabro_config::{EnvironmentDockerfileLayer, EnvironmentImageLayer, SettingsLayer}; +use fabro_graphviz::graph::AttrValue; +use fabro_graphviz::parser; +use fabro_template::{ + BundleTemplateStore, FilesystemTemplateStore, RecordingTemplateStore, TemplateContext, + TemplateDependencyClosure, TemplateRenderMode, TemplateSource, + discover_static_dependency_closure, render_source, +}; +use fabro_types::ManifestPath; +use fabro_workflow::static_reference::{ + AttributeScope, ReferenceKind, reference_kind_for_attribute, +}; + +use super::{manifest_path_from_absolute, normalize_absolute_path}; + +pub(super) struct CollectWorkingTreeInput<'a> { + pub(super) cwd: &'a Path, + pub(super) root_location: WorkflowLocation, + pub(super) inputs: &'a HashMap, + pub(super) project_config: Option, + pub(super) user_config: Option, +} + +pub(super) struct CollectedSourceInput { + pub(super) access_path: PathBuf, + pub(super) source: String, +} + +#[derive(Clone, Debug)] +pub(super) struct CollectedWorkingTree { + entrypoint: CollectedPath, + workflows: BTreeMap, + project_config: Option, + user_config: Option, +} + +impl CollectedWorkingTree { + pub(super) fn entrypoint(&self) -> &CollectedPath { + &self.entrypoint + } + + pub(super) fn workflows(&self) -> &BTreeMap { + &self.workflows + } + + pub(super) fn project_config(&self) -> Option<&CollectedDocument> { + self.project_config.as_ref() + } + + pub(super) fn user_config(&self) -> Option<&CollectedDocument> { + self.user_config.as_ref() + } +} + +#[derive(Clone, Debug)] +pub(super) struct CollectedWorkflow { + graph: CollectedDocument, + config: Option, + files: BTreeMap, +} + +impl CollectedWorkflow { + pub(super) fn graph(&self) -> &CollectedDocument { + &self.graph + } + + pub(super) fn config(&self) -> Option<&CollectedDocument> { + self.config.as_ref() + } + + pub(super) fn files(&self) -> &BTreeMap { + &self.files + } +} + +#[derive(Clone, Debug)] +pub(super) struct CollectedDocument { + access_path: PathBuf, + path: CollectedPath, + source: String, +} + +impl CollectedDocument { + pub(super) fn access_path(&self) -> &Path { + &self.access_path + } + + pub(super) fn source(&self) -> &str { + &self.source + } +} + +#[derive(Clone, Debug)] +pub(super) struct CollectedFile { + document: CollectedDocument, + reference: CollectedFileReference, +} + +impl CollectedFile { + pub(super) fn document(&self) -> &CollectedDocument { + &self.document + } + + pub(super) fn reference(&self) -> &CollectedFileReference { + &self.reference + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum CollectedFileReferenceType { + FileInline, + Import, + Dockerfile, +} + +#[derive(Clone, Debug)] +pub(super) struct CollectedFileReference { + type_: CollectedFileReferenceType, + original: String, + from_access_path: Option, +} + +impl CollectedFileReference { + pub(super) fn type_(&self) -> CollectedFileReferenceType { + self.type_ + } + + pub(super) fn original(&self) -> &str { + &self.original + } + + pub(super) fn source_access_path(&self) -> Option<&Path> { + self.from_access_path.as_deref() + } +} + +/// A canonical virtual coordinate inside one collected working-tree closure. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub(super) struct CollectedPath(String); + +impl CollectedPath { + fn try_new(path: impl AsRef) -> Result { + let path = path.as_ref(); + let value = path + .to_str() + .ok_or_else(|| anyhow!("collected path is not valid UTF-8"))?; + + if value.is_empty() { + bail!("collected path must not be empty"); + } + if path.is_absolute() { + bail!("collected path must be relative: {value}"); + } + if value.contains('\\') { + bail!("collected path must use forward slashes: {value}"); + } + if value.chars().any(char::is_control) { + bail!("collected path contains a control character"); + } + let bytes = value.as_bytes(); + if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { + bail!("collected path must not use a Windows drive prefix: {value}"); + } + if value + .split('/') + .any(|segment| segment.is_empty() || segment == "." || segment == "..") + { + bail!("collected path contains an empty, dot, or parent component: {value}"); + } + + Ok(Self(value.to_owned())) + } + + pub(super) fn as_str(&self) -> &str { + &self.0 + } + + #[cfg(test)] + fn as_path(&self) -> &Path { + Path::new(&self.0) + } +} + +impl fmt::Display for CollectedPath { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +enum ComponentRole { + Workflow, + ProjectConfig, + UserConfig, +} + +impl ComponentRole { + const fn label(self) -> &'static str { + match self { + Self::Workflow => "workflow", + Self::ProjectConfig => "project_config", + Self::UserConfig => "user_config", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +struct DocumentId(usize); + +#[derive(Clone, Debug)] +struct DraftDocument { + access_path: PathBuf, + provisional_path: PathBuf, + component: ComponentRole, + source: String, +} + +#[derive(Clone, Debug)] +struct DraftFileReference { + type_: CollectedFileReferenceType, + original: String, + from_document: Option, +} + +#[derive(Clone, Debug)] +struct DraftFile { + document: DocumentId, + reference: DraftFileReference, +} + +#[derive(Clone, Debug)] +struct DraftWorkflow { + graph: DocumentId, + config: Option, + files: BTreeMap, +} + +struct CollectionDraft<'a> { + cwd: PathBuf, + inputs: &'a HashMap, + documents: Vec, + document_ids: HashMap<(PathBuf, ComponentRole, PathBuf), DocumentId>, + workflows: BTreeMap, + visited_workflows: HashMap, +} + +impl<'a> CollectionDraft<'a> { + fn new(cwd: &Path, inputs: &'a HashMap) -> Result { + Ok(Self { + cwd: normalized_absolute_access_path(cwd)?, + inputs, + documents: Vec::new(), + document_ids: HashMap::new(), + workflows: BTreeMap::new(), + visited_workflows: HashMap::new(), + }) + } + + fn insert_document( + &mut self, + access_path: &Path, + provisional_path: PathBuf, + component: ComponentRole, + source: String, + ) -> DocumentId { + let access_path = lexically_normalize_access_path(access_path); + let key = (access_path.clone(), component, provisional_path.clone()); + if let Some(document) = self.document_ids.get(&key) { + return *document; + } + + let document = DocumentId(self.documents.len()); + self.documents.push(DraftDocument { + access_path, + provisional_path, + component, + source, + }); + self.document_ids.insert(key, document); + document + } + + fn document(&self, document: DocumentId) -> &DraftDocument { + &self.documents[document.0] + } + + fn collect_workflow_location( + &mut self, + location: &WorkflowLocation, + provisional_graph_path: PathBuf, + ) -> Result { + let graph_access_path = normalized_absolute_access_path(&location.graph)?; + let graph_manifest_path = manifest_path_from_absolute(&graph_access_path, &self.cwd)?; + let graph_key = graph_manifest_path.to_string(); + if let Some(document) = self.visited_workflows.get(&graph_key) { + return Ok(*document); + } + + let graph_source = std::fs::read_to_string(&graph_access_path) + .with_context(|| format!("Failed to read {}", graph_access_path.display()))?; + let graph = self.insert_document( + &graph_access_path, + provisional_graph_path, + ComponentRole::Workflow, + graph_source, + ); + self.visited_workflows.insert(graph_key.clone(), graph); + + let config = location + .toml + .as_ref() + .map(|config_path| { + let access_path = normalized_absolute_access_path(config_path)?; + let source = std::fs::read_to_string(&access_path) + .with_context(|| format!("Failed to read {}", access_path.display()))?; + let file_name = access_path.file_name().ok_or_else(|| { + anyhow!( + "workflow config has no file name: {}", + access_path.display() + ) + })?; + let provisional_path = virtual_sibling_path( + &self.document(graph).provisional_path, + Path::new(file_name), + )?; + Ok::<_, anyhow::Error>(self.insert_document( + &access_path, + provisional_path, + ComponentRole::Workflow, + source, + )) + }) + .transpose()?; + + let mut workflow = DraftWorkflow { + graph, + config, + files: BTreeMap::new(), + }; + if let Some(config) = config { + self.collect_config_dockerfile(config, &mut workflow.files)?; + } + self.collect_workflow_files(graph, &mut workflow.files, &mut HashSet::new())?; + self.workflows.insert(graph_key, workflow); + + Ok(graph) + } + + fn collect_workflow_entry( + &mut self, + workflow: &Path, + resolve_from: &Path, + provisional_graph_path: PathBuf, + ) -> Result { + let normalized_workflow = if workflow.extension().is_some() && workflow.is_relative() { + normalize_absolute_path(resolve_from, &workflow.to_string_lossy()).ok_or_else(|| { + anyhow!( + "unsupported manifest workflow reference: {}", + workflow.display() + ) + })? + } else { + workflow.to_path_buf() + }; + let location = WorkflowLocation::resolve(&normalized_workflow, resolve_from)?; + self.collect_workflow_location(&location, provisional_graph_path) + } + + fn collect_workflow_files( + &mut self, + graph_document_id: DocumentId, + files: &mut BTreeMap, + visited_imports: &mut HashSet, + ) -> Result<()> { + let graph_document = self.document(graph_document_id).clone(); + let graph = parser::parse(&graph_document.source) + .with_context(|| format!("Failed to parse {}", graph_document.access_path.display()))?; + let workflow_base_dir = graph_document + .access_path + .parent() + .unwrap_or_else(|| Path::new(".")); + let graph_manifest_path = + manifest_path_from_absolute(&graph_document.access_path, &self.cwd)?; + let workflow_template_root = manifest_parent_or_dot(&graph_manifest_path)?; + + if let Some(goal_reference) = graph.attrs.get("goal").and_then(AttrValue::as_str) { + if let Some(reference) = goal_reference.strip_prefix('@') { + let bundled = self.collect_bundled_file( + files, + workflow_base_dir, + reference, + CollectedFileReferenceType::FileInline, + manifest_attr_reference_kind(AttributeScope::Graph, "goal", goal_reference)?, + graph_document_id, + )?; + let source = self.document(bundled).source.clone(); + let bundled_manifest_path = + manifest_path_from_absolute(&self.document(bundled).access_path, &self.cwd)?; + let template_root = template_root_for_bundled_file( + &bundled_manifest_path, + &workflow_template_root, + )?; + self.collect_template_include_files( + files, + TemplateSource::new(bundled_manifest_path, template_root, source), + bundled, + graph_document_id, + )?; + } else { + self.collect_template_include_files( + files, + TemplateSource::new( + graph_manifest_path.clone(), + workflow_template_root.clone(), + goal_reference.to_owned(), + ), + graph_document_id, + graph_document_id, + )?; + } + } + + let mut nodes = graph.nodes.values().collect::>(); + nodes.sort_by(|left, right| left.id.cmp(&right.id)); + for node in nodes { + if let Some(prompt_reference) = node.attrs.get("prompt").and_then(AttrValue::as_str) { + if !prompt_reference.starts_with('@') { + self.collect_template_include_files( + files, + TemplateSource::new( + graph_manifest_path.clone(), + workflow_template_root.clone(), + prompt_reference.to_owned(), + ), + graph_document_id, + graph_document_id, + )?; + } + } + + let mut attributes = node.attrs.iter().collect::>(); + attributes.sort_by_key(|(name, _)| *name); + for (name, value) in attributes { + let Some(value) = value.as_str() else { + continue; + }; + let Some(ReferenceKind::FileInline) = + reference_kind_for_attribute(AttributeScope::Node, name, value) + else { + continue; + }; + let reference = value.strip_prefix('@').ok_or_else(|| { + anyhow!("file inline reference must start with '@': {name}={value}") + })?; + let bundled = self.collect_bundled_file( + files, + workflow_base_dir, + reference, + CollectedFileReferenceType::FileInline, + ReferenceKind::FileInline, + graph_document_id, + )?; + + if name == "prompt" { + let source = self.document(bundled).source.clone(); + let bundled_manifest_path = manifest_path_from_absolute( + &self.document(bundled).access_path, + &self.cwd, + )?; + let template_root = template_root_for_bundled_file( + &bundled_manifest_path, + &workflow_template_root, + )?; + self.collect_template_include_files( + files, + TemplateSource::new(bundled_manifest_path, template_root, source), + bundled, + graph_document_id, + )?; + } + } + + if let Some(import_reference) = node.attrs.get("import").and_then(AttrValue::as_str) { + let imported = self.collect_bundled_file( + files, + workflow_base_dir, + import_reference, + CollectedFileReferenceType::Import, + manifest_attr_reference_kind(AttributeScope::Node, "import", import_reference)?, + graph_document_id, + )?; + let import_key = + manifest_path_from_absolute(&self.document(imported).access_path, &self.cwd)? + .to_string(); + if visited_imports.insert(import_key) { + self.collect_workflow_files(imported, files, visited_imports)?; + } + } + + if let Some(child_reference) = node + .attrs + .get("stack.child_workflow") + .and_then(AttrValue::as_str) + { + manifest_attr_reference_kind( + AttributeScope::Node, + "stack.child_workflow", + child_reference, + )? + .validate(child_reference) + .map_err(anyhow::Error::new)?; + let child_provisional_path = virtual_reference_path( + self.document(graph_document_id) + .provisional_path + .parent() + .unwrap_or_else(|| Path::new(".")), + child_reference, + )?; + self.collect_workflow_entry( + Path::new(child_reference), + workflow_base_dir, + child_provisional_path, + )?; + } + } + + Ok(()) + } + + fn collect_template_include_files( + &mut self, + files: &mut BTreeMap, + source: TemplateSource, + source_document: DocumentId, + from_document: DocumentId, + ) -> Result<()> { + let source_path = source.path.clone(); + let stable_root = stable_template_root(self.document(source_document), &source)?; + let store = FilesystemTemplateStore::new(self.cwd.clone()); + let closure = discover_static_dependency_closure([source], &store) + .context("failed to discover template dependencies")?; + self.verify_recorded_template_dependencies(&source_path, &closure, files, from_document)?; + + let mut sources = closure.sources.into_iter().collect::>(); + sources.sort_by_key(|(path, _)| path.to_string()); + for (path, source) in sources { + if path == source_path { + continue; + } + let relative = path + .as_path() + .strip_prefix(source.root.as_path()) + .map_err(|_| { + anyhow!( + "template path {path} is outside its logical root {}", + source.root + ) + })?; + let provisional_path = normalize_relative_path(&stable_root.join(relative))?; + let key = path.to_string(); + if let Some(existing) = files.get(&key) { + let existing_path = &self.document(existing.document).provisional_path; + if existing_path != &provisional_path { + bail!( + "collected file has conflicting logical coordinates `{}` and `{}`", + existing_path.display(), + provisional_path.display() + ); + } + continue; + } + + let access_path = lexically_normalize_access_path(&self.cwd.join(path.as_path())); + let document = self.insert_document( + &access_path, + provisional_path, + ComponentRole::Workflow, + source.content, + ); + files.insert(key.clone(), DraftFile { + document, + reference: DraftFileReference { + type_: CollectedFileReferenceType::FileInline, + original: key, + from_document: Some(from_document), + }, + }); + } + Ok(()) + } + + fn verify_recorded_template_dependencies( + &self, + source_path: &ManifestPath, + closure: &TemplateDependencyClosure, + files: &BTreeMap, + from_document: DocumentId, + ) -> Result<()> { + let Some(source) = closure.sources.get(source_path) else { + return Ok(()); + }; + let mut bundled_files = closure + .sources + .iter() + .map(|(path, source)| (path.clone(), source.content.clone())) + .collect::>(); + for file in files.values() { + let document = self.document(file.document); + let path = manifest_path_from_absolute(&document.access_path, &self.cwd)?; + bundled_files.insert(path, document.source.clone()); + } + let allowed = bundled_files.keys().cloned().collect(); + let store = + RecordingTemplateStore::with_allowed(BundleTemplateStore::new(bundled_files), allowed); + let context = TemplateContext::for_input_scan(self.inputs.clone()); + render_source( + source, + &context, + Arc::new(store), + TemplateRenderMode::Lenient, + ) + .with_context(|| { + let from = + manifest_path_from_absolute(&self.document(from_document).access_path, &self.cwd) + .map_or_else(|_| source_path.to_string(), |path| path.to_string()); + format!("failed to verify template dependencies for {from}") + })?; + Ok(()) + } + + fn collect_config_dockerfile( + &mut self, + config: DocumentId, + files: &mut BTreeMap, + ) -> Result<()> { + let config_document = self.document(config).clone(); + let layer = config_document + .source + .parse::() + .context("Failed to parse run config TOML")?; + let base_dir = config_document + .access_path + .parent() + .unwrap_or_else(|| Path::new(".")); + + let mut environments = layer.environments.iter().collect::>(); + environments.sort_by_key(|(name, _)| *name); + for (_, environment) in environments { + self.collect_environment_dockerfile( + files, + base_dir, + config, + environment.image.as_ref(), + )?; + } + if let Some(run_environment) = layer.run.as_ref().and_then(|run| run.environment.as_ref()) { + self.collect_environment_dockerfile( + files, + base_dir, + config, + run_environment.image.as_ref(), + )?; + } + Ok(()) + } + + fn collect_environment_dockerfile( + &mut self, + files: &mut BTreeMap, + base_dir: &Path, + config: DocumentId, + image: Option<&EnvironmentImageLayer>, + ) -> Result<()> { + let dockerfile = image.and_then(|image| image.dockerfile.as_ref()); + let Some(EnvironmentDockerfileLayer::Path { path }) = dockerfile else { + return Ok(()); + }; + self.collect_bundled_file( + files, + base_dir, + path, + CollectedFileReferenceType::Dockerfile, + ReferenceKind::Dockerfile, + config, + )?; + Ok(()) + } + + fn collect_bundled_file( + &mut self, + files: &mut BTreeMap, + base_dir: &Path, + reference: &str, + reference_type: CollectedFileReferenceType, + reference_kind: ReferenceKind, + from_document: DocumentId, + ) -> Result { + reference_kind + .validate(reference) + .map_err(anyhow::Error::new)?; + + let access_path = normalize_absolute_path(base_dir, reference) + .ok_or_else(|| anyhow!("unsupported manifest reference: {reference}"))?; + let access_path = lexically_normalize_access_path(&access_path); + let manifest_path = manifest_path_from_absolute(&access_path, &self.cwd)?; + let key = manifest_path.to_string(); + let provisional_path = virtual_reference_path( + self.document(from_document) + .provisional_path + .parent() + .unwrap_or_else(|| Path::new(".")), + reference, + )?; + + if let Some(existing) = files.get(&key) { + let existing_path = &self.document(existing.document).provisional_path; + if existing_path != &provisional_path { + bail!( + "collected file has conflicting logical coordinates `{}` and `{}`", + existing_path.display(), + provisional_path.display() + ); + } + return Ok(existing.document); + } + + let source = std::fs::read_to_string(&access_path) + .with_context(|| format!("Failed to read {}", access_path.display()))?; + let document = self.insert_document( + &access_path, + provisional_path, + self.document(from_document).component, + source, + ); + files.insert(key, DraftFile { + document, + reference: DraftFileReference { + type_: reference_type, + original: reference.to_owned(), + from_document: Some(from_document), + }, + }); + Ok(document) + } + + fn finish( + self, + entrypoint: DocumentId, + project_config: Option, + user_config: Option, + ) -> Result { + let documents = CollectionNamespace::finalize(self.documents)?; + let mut workflows = BTreeMap::new(); + for workflow in self.workflows.into_values() { + let graph = documents[workflow.graph.0].clone(); + let config = workflow + .config + .map(|document| documents[document.0].clone()); + let mut files = BTreeMap::new(); + for file in workflow.files.into_values() { + let document = documents[file.document.0].clone(); + let from_access_path = file + .reference + .from_document + .map(|from| documents[from.0].access_path.clone()); + files.insert(document.path.clone(), CollectedFile { + document, + reference: CollectedFileReference { + type_: file.reference.type_, + original: file.reference.original, + from_access_path, + }, + }); + } + workflows.insert(graph.path.clone(), CollectedWorkflow { + graph, + config, + files, + }); + } + + Ok(CollectedWorkingTree { + entrypoint: documents[entrypoint.0].path.clone(), + workflows, + project_config: project_config.map(|document| documents[document.0].clone()), + user_config: user_config.map(|document| documents[document.0].clone()), + }) + } +} + +struct CollectionNamespace { + physical_to_virtual: BTreeMap, + virtual_to_physical: BTreeMap, +} + +impl CollectionNamespace { + fn finalize(drafts: Vec) -> Result> { + let mut deficits = BTreeMap::::new(); + for draft in &drafts { + let deficit = leading_parent_count(&draft.provisional_path); + deficits + .entry(draft.component) + .and_modify(|current| *current = (*current).max(deficit)) + .or_insert(deficit); + } + + let paths = drafts + .iter() + .map(|draft| { + finalize_component_path( + &draft.provisional_path, + draft.component, + deficits.get(&draft.component).copied().unwrap_or_default(), + ) + }) + .collect::>>()?; + + let mut order = (0..drafts.len()).collect::>(); + order.sort_by(|left, right| { + paths[*left] + .cmp(&paths[*right]) + .then_with(|| drafts[*left].access_path.cmp(&drafts[*right].access_path)) + }); + + let mut namespace = Self { + physical_to_virtual: BTreeMap::new(), + virtual_to_physical: BTreeMap::new(), + }; + for index in order { + let physical = + std::fs::canonicalize(&drafts[index].access_path).with_context(|| { + format!( + "failed to identify collected file {}", + drafts[index].access_path.display() + ) + })?; + namespace.register(physical, paths[index].clone())?; + } + + Ok(drafts + .into_iter() + .zip(paths) + .map(|(draft, path)| CollectedDocument { + access_path: draft.access_path, + path, + source: draft.source, + }) + .collect()) + } + + fn register(&mut self, physical: PathBuf, path: CollectedPath) -> Result<()> { + if let Some(existing) = self.physical_to_virtual.get(&physical) { + if existing != &path { + bail!( + "one physical file has conflicting collected coordinates `{existing}` and `{path}`" + ); + } + } + if let Some(existing) = self.virtual_to_physical.get(&path) { + if existing != &physical { + bail!("collected coordinate `{path}` maps to multiple physical files"); + } + } + + self.physical_to_virtual + .insert(physical.clone(), path.clone()); + self.virtual_to_physical.insert(path, physical); + Ok(()) + } +} + +pub(super) fn collect_working_tree( + input: CollectWorkingTreeInput<'_>, +) -> Result { + let mut draft = CollectionDraft::new(input.cwd, input.inputs)?; + let root_graph_access_path = normalized_absolute_access_path(&input.root_location.graph)?; + let root_graph_path = + seed_component_path(&root_graph_access_path, &draft.cwd, ComponentRole::Workflow)?; + + let project_config = input + .project_config + .map(|config| { + let access_path = normalized_absolute_access_path(&config.access_path)?; + let provisional_path = + seed_component_path(&access_path, &draft.cwd, ComponentRole::ProjectConfig)?; + Ok::<_, anyhow::Error>(draft.insert_document( + &access_path, + provisional_path, + ComponentRole::ProjectConfig, + config.source, + )) + }) + .transpose()?; + let user_config = input + .user_config + .map(|config| { + let access_path = normalized_absolute_access_path(&config.access_path)?; + let provisional_path = + seed_component_path(&access_path, &draft.cwd, ComponentRole::UserConfig)?; + Ok::<_, anyhow::Error>(draft.insert_document( + &access_path, + provisional_path, + ComponentRole::UserConfig, + config.source, + )) + }) + .transpose()?; + + let entrypoint = draft.collect_workflow_location(&input.root_location, root_graph_path)?; + if let Some(project_config) = project_config { + let root_key = + manifest_path_from_absolute(&draft.document(entrypoint).access_path, &draft.cwd)? + .to_string(); + let mut root = draft + .workflows + .remove(&root_key) + .ok_or_else(|| anyhow!("root workflow missing from collected working tree"))?; + draft.collect_config_dockerfile(project_config, &mut root.files)?; + draft.workflows.insert(root_key, root); + } + + draft.finish(entrypoint, project_config, user_config) +} + +fn seed_component_path( + access_path: &Path, + cwd: &Path, + component: ComponentRole, +) -> Result { + if !matches!(component, ComponentRole::UserConfig) { + if let Ok(relative) = access_path.strip_prefix(cwd) { + let relative = normalize_relative_path(relative)?; + if leading_parent_count(&relative) == 0 && !relative.as_os_str().is_empty() { + return Ok(relative); + } + } + } + + let file_name = access_path + .file_name() + .ok_or_else(|| anyhow!("collected root has no file name: {}", access_path.display()))?; + let root = match component { + ComponentRole::Workflow => PathBuf::from("_fabro_external/entrypoint"), + ComponentRole::ProjectConfig => PathBuf::from("_fabro_external/project_config"), + ComponentRole::UserConfig => PathBuf::from("_fabro_external/user_config"), + }; + normalize_relative_path(&root.join(file_name)) +} + +fn stable_template_root(document: &DraftDocument, source: &TemplateSource) -> Result { + let relative = source + .path + .as_path() + .strip_prefix(source.root.as_path()) + .map_err(|_| { + anyhow!( + "template source {} is outside its logical root {}", + source.path, + source.root + ) + })?; + let mut root = document.provisional_path.clone(); + for component in relative.components() { + if matches!(component, Component::Normal(_)) && !root.pop() { + bail!( + "template source {} cannot be placed under its collected root", + source.path + ); + } + } + Ok(root) +} + +fn finalize_component_path( + provisional: &Path, + component: ComponentRole, + deficit: usize, +) -> Result { + let path = if deficit == 0 { + normalize_relative_path(provisional)? + } else { + let mut prefix = PathBuf::from("_fabro_rebased"); + prefix.push(component.label()); + for _ in 0..deficit { + prefix.push("anchor"); + } + normalize_relative_path(&prefix.join(provisional))? + }; + CollectedPath::try_new(virtual_path_to_wire(&path)?) +} + +fn virtual_path_to_wire(path: &Path) -> Result { + let mut segments = Vec::new(); + for component in path.components() { + let Component::Normal(segment) = component else { + bail!("collected path is not finalized: {}", path.display()); + }; + segments.push( + segment + .to_str() + .ok_or_else(|| anyhow!("collected path is not valid UTF-8"))?, + ); + } + Ok(segments.join("/")) +} + +fn virtual_sibling_path(path: &Path, sibling: &Path) -> Result { + let parent = path.parent().unwrap_or_else(|| Path::new(".")); + normalize_relative_path(&parent.join(sibling)) +} + +fn virtual_reference_path(base: &Path, reference: &str) -> Result { + let reference_path = Path::new(reference); + if reference_path.is_absolute() || reference.starts_with('~') { + bail!("unsupported collected reference: {reference}"); + } + normalize_relative_path(&base.join(reference_path)) +} + +fn normalize_relative_path(path: &Path) -> Result { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::CurDir => {} + Component::Normal(part) => normalized.push(part), + Component::ParentDir => { + if normalized.file_name().is_some() { + normalized.pop(); + } else { + normalized.push(".."); + } + } + Component::RootDir | Component::Prefix(_) => { + bail!("collected path must be relative: {}", path.display()); + } + } + } + Ok(normalized) +} + +fn leading_parent_count(path: &Path) -> usize { + path.components() + .take_while(|component| matches!(component, Component::ParentDir)) + .count() +} + +fn lexically_normalize_access_path(path: &Path) -> PathBuf { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::CurDir => {} + Component::Normal(part) => normalized.push(part), + Component::ParentDir => { + normalized.pop(); + } + Component::RootDir => normalized.push(Path::new("/")), + Component::Prefix(prefix) => normalized.push(prefix.as_os_str()), + } + } + normalized +} + +fn normalized_absolute_access_path(path: &Path) -> Result { + let absolute = std::path::absolute(path) + .with_context(|| format!("failed to make collected path absolute: {}", path.display()))?; + Ok(lexically_normalize_access_path(&absolute)) +} + +fn manifest_parent_or_dot(path: &ManifestPath) -> Result { + let parent = path.parent_or_dot().to_string_lossy(); + ManifestPath::from_wire(&parent) + .ok_or_else(|| anyhow!("invalid manifest parent path for {path}: {parent}")) +} + +fn template_root_for_bundled_file( + path: &ManifestPath, + workflow_template_root: &ManifestPath, +) -> Result { + if manifest_path_is_within_root(path, workflow_template_root) { + Ok(workflow_template_root.clone()) + } else { + manifest_parent_or_dot(path) + } +} + +fn manifest_path_is_within_root(path: &ManifestPath, root: &ManifestPath) -> bool { + if root.as_path().as_os_str().is_empty() { + return !matches!( + path.as_path().components().next(), + Some(Component::ParentDir) + ); + } + path.starts_with(root) +} + +fn manifest_attr_reference_kind( + scope: AttributeScope, + key: &str, + value: &str, +) -> Result { + reference_kind_for_attribute(scope, key, value) + .ok_or_else(|| anyhow!("unsupported manifest reference attribute: {key}={value}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn write_file(path: &Path, source: &str) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("fixture directory should be created"); + } + std::fs::write(path, source).expect("fixture file should be written"); + } + + fn collect_graph(cwd: &Path, graph: &Path) -> Result { + let inputs = HashMap::new(); + let root_location = WorkflowLocation::resolve(graph, cwd)?; + collect_working_tree(CollectWorkingTreeInput { + cwd, + root_location, + inputs: &inputs, + project_config: None, + user_config: None, + }) + } + + fn logical_contents(tree: &CollectedWorkingTree) -> BTreeMap { + let mut contents = BTreeMap::new(); + if let Some(config) = tree.project_config() { + contents.insert(config.path.to_string(), config.source.clone()); + } + if let Some(config) = tree.user_config() { + contents.insert(config.path.to_string(), config.source.clone()); + } + for workflow in tree.workflows().values() { + contents.insert( + workflow.graph.path.to_string(), + workflow.graph.source.clone(), + ); + if let Some(config) = workflow.config() { + contents.insert(config.path.to_string(), config.source.clone()); + } + for file in workflow.files().values() { + contents.insert(file.document.path.to_string(), file.document.source.clone()); + } + } + contents + } + + fn logical_provenance( + tree: &CollectedWorkingTree, + ) -> BTreeMap)> { + let mut paths_by_access = HashMap::new(); + if let Some(config) = tree.project_config() { + paths_by_access.insert(config.access_path.clone(), config.path.to_string()); + } + if let Some(config) = tree.user_config() { + paths_by_access.insert(config.access_path.clone(), config.path.to_string()); + } + for workflow in tree.workflows().values() { + paths_by_access.insert( + workflow.graph.access_path.clone(), + workflow.graph.path.to_string(), + ); + if let Some(config) = workflow.config() { + paths_by_access.insert(config.access_path.clone(), config.path.to_string()); + } + for file in workflow.files().values() { + paths_by_access.insert( + file.document.access_path.clone(), + file.document.path.to_string(), + ); + } + } + + let mut provenance = BTreeMap::new(); + for workflow in tree.workflows().values() { + for file in workflow.files().values() { + let from = file.reference.source_access_path().map(|access_path| { + paths_by_access + .get(access_path) + .expect("reference source should be collected") + .clone() + }); + provenance.insert( + file.document.path.to_string(), + ( + file.reference.type_(), + file.reference.original.clone(), + from, + ), + ); + } + } + provenance + } + + #[test] + fn collected_path_rejects_parent_components() { + assert!(CollectedPath::try_new("../prompt.md").is_err()); + } + + #[test] + fn collected_path_rejects_non_canonical_forms() { + for value in ["", ".", "a/./b", "a//b", "a/../b", "C:/a", "a\\b", "a\nb"] { + assert!(CollectedPath::try_new(value).is_err(), "accepted {value:?}"); + } + } + + #[test] + fn component_rebase_is_uniform_and_preserves_relative_relationships() { + let root = Path::new("_fabro_external/entrypoint/workflow.fabro"); + let sibling = virtual_reference_path( + root.parent().expect("entrypoint should have a parent"), + "../../../sibling/workflow.fabro", + ) + .expect("reference should normalize"); + let deficit = leading_parent_count(&sibling); + + let rebased_root = finalize_component_path(root, ComponentRole::Workflow, deficit) + .expect("root should finalize"); + let rebased_sibling = finalize_component_path(&sibling, ComponentRole::Workflow, deficit) + .expect("sibling should finalize"); + let resolved = virtual_reference_path( + rebased_root + .as_path() + .parent() + .expect("rebased root should have a parent"), + "../../../sibling/workflow.fabro", + ) + .expect("rebased reference should normalize"); + + assert_eq!(resolved, rebased_sibling.as_path()); + assert!(!rebased_root.as_str().contains("..")); + assert!(!rebased_sibling.as_str().contains("..")); + } + + #[test] + fn collector_captures_complete_workflow_and_config_closure() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let project = temp.path().join("project"); + let root = project.join(".fabro/workflows/root"); + let child = project.join(".fabro/workflows/child"); + let project_config_path = project.join(".fabro/project.toml"); + let user_config_path = temp.path().join("home/.fabro/config.toml"); + let project_config = r#"_version = 1 + +[environments.project] +provider = "docker" + +[environments.project.image] +dockerfile = { path = "Project.Dockerfile" } +"#; + let workflow_config = r#"_version = 1 + +[workflow] +graph = "workflow.fabro" + +[environments.workflow] +provider = "docker" + +[environments.workflow.image] +dockerfile = { path = "Dockerfile" } +"#; + let user_config = "_version = 1\n"; + write_file(&project_config_path, project_config); + write_file(&project.join(".fabro/Project.Dockerfile"), "FROM project\n"); + write_file(&user_config_path, user_config); + write_file(&root.join("workflow.toml"), workflow_config); + write_file(&root.join("Dockerfile"), "FROM workflow\n"); + write_file( + &root.join("workflow.fabro"), + r#"digraph Root { + start [shape=Mdiamond] + prompt [prompt="@prompts/plan.md"] + imported [import="imports/shared.fabro"] + child [shape=house, stack.child_workflow="../child/workflow.fabro"] + exit [shape=Msquare] + start -> prompt -> imported -> child -> exit + }"#, + ); + write_file( + &root.join("prompts/plan.md"), + r#"{% include "partial.md" %}"#, + ); + write_file(&root.join("prompts/partial.md"), "partial\n"); + write_file( + &root.join("imports/shared.fabro"), + r#"digraph Shared { + start [shape=Mdiamond] + shared [prompt="@../prompts/shared.md"] + exit [shape=Msquare] + start -> shared -> exit + }"#, + ); + write_file(&root.join("prompts/shared.md"), "shared\n"); + write_file(&child.join("workflow.toml"), workflow_config); + write_file(&child.join("Dockerfile"), "FROM child\n"); + write_file( + &child.join("workflow.fabro"), + "digraph Child { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", + ); + + let inputs = HashMap::new(); + let tree = collect_working_tree(CollectWorkingTreeInput { + cwd: &project, + root_location: WorkflowLocation::resolve(&root.join("workflow.toml"), &project) + .expect("root workflow should resolve"), + inputs: &inputs, + project_config: Some(CollectedSourceInput { + access_path: project_config_path, + source: project_config.to_owned(), + }), + user_config: Some(CollectedSourceInput { + access_path: user_config_path, + source: user_config.to_owned(), + }), + }) + .expect("working tree should collect"); + + assert_eq!( + tree.entrypoint().as_str(), + ".fabro/workflows/root/workflow.fabro" + ); + assert_eq!(tree.workflows().len(), 2); + assert_eq!( + logical_contents(&tree) + .keys() + .map(String::as_str) + .collect::>(), + vec![ + ".fabro/Project.Dockerfile", + ".fabro/project.toml", + ".fabro/workflows/child/Dockerfile", + ".fabro/workflows/child/workflow.fabro", + ".fabro/workflows/child/workflow.toml", + ".fabro/workflows/root/Dockerfile", + ".fabro/workflows/root/imports/shared.fabro", + ".fabro/workflows/root/prompts/partial.md", + ".fabro/workflows/root/prompts/plan.md", + ".fabro/workflows/root/prompts/shared.md", + ".fabro/workflows/root/workflow.fabro", + ".fabro/workflows/root/workflow.toml", + "_fabro_external/user_config/config.toml", + ] + ); + } + + #[test] + fn external_collection_is_stable_when_the_checkout_moves() { + fn fixture(parent: &Path) -> (PathBuf, PathBuf) { + let cwd = parent.join("checkout"); + let workflow = parent.join("catalog/root/workflow.fabro"); + std::fs::create_dir_all(&cwd).expect("checkout should be created"); + write_file( + &workflow, + r#"digraph Root { + start [shape=Mdiamond] + work [prompt="@prompts/plan.md"] + exit [shape=Msquare] + start -> work -> exit + }"#, + ); + write_file(&parent.join("catalog/root/prompts/plan.md"), "plan\n"); + (cwd, workflow) + } + + let first = tempfile::tempdir().expect("first temp directory should be created"); + let second = tempfile::tempdir().expect("second temp directory should be created"); + let (first_cwd, first_workflow) = fixture(first.path()); + let (second_cwd, second_workflow) = fixture(second.path()); + + let first_tree = + collect_graph(&first_cwd, &first_workflow).expect("first working tree should collect"); + let second_tree = collect_graph(&second_cwd, &second_workflow) + .expect("second working tree should collect"); + + assert_eq!(first_tree.entrypoint(), second_tree.entrypoint()); + assert_eq!( + logical_contents(&first_tree), + logical_contents(&second_tree) + ); + assert_eq!( + logical_provenance(&first_tree), + logical_provenance(&second_tree) + ); + assert_eq!( + first_tree.entrypoint().as_str(), + "_fabro_external/entrypoint/workflow.fabro" + ); + } + + #[test] + fn external_sibling_workflow_reference_resolves_in_stable_namespace() { + fn fixture(parent: &Path) -> (PathBuf, PathBuf) { + let cwd = parent.join("checkout"); + let root_dir = parent.join("user/workflows/root"); + let child_dir = parent.join("user/workflows/child"); + let root = root_dir.join("workflow.fabro"); + std::fs::create_dir_all(&cwd).expect("checkout should be created"); + write_file( + &root, + r#"digraph Root { + start [shape=Mdiamond] + prompt [prompt="@prompts/root.md"] + imported [import="imports/root.fabro"] + child [shape=house, stack.child_workflow="../child/workflow.fabro"] + exit [shape=Msquare] + start -> prompt -> imported -> child -> exit + }"#, + ); + write_file( + &root_dir.join("prompts/root.md"), + r#"{% include "root-partial.md" %}"#, + ); + write_file(&root_dir.join("prompts/root-partial.md"), "root partial\n"); + write_file( + &root_dir.join("imports/root.fabro"), + r#"digraph Import { + start [shape=Mdiamond] + work [prompt="@../prompts/root-import.md"] + exit [shape=Msquare] + start -> work -> exit + }"#, + ); + write_file(&root_dir.join("prompts/root-import.md"), "root import\n"); + write_file( + &child_dir.join("workflow.fabro"), + r#"digraph Child { + start [shape=Mdiamond] + prompt [prompt="@prompts/child.md"] + imported [import="imports/child.fabro"] + exit [shape=Msquare] + start -> prompt -> imported -> exit + }"#, + ); + write_file( + &child_dir.join("prompts/child.md"), + r#"{% include "child-partial.md" %}"#, + ); + write_file( + &child_dir.join("prompts/child-partial.md"), + "child partial\n", + ); + write_file( + &child_dir.join("imports/child.fabro"), + r#"digraph Import { + start [shape=Mdiamond] + work [prompt="@../prompts/child-import.md"] + exit [shape=Msquare] + start -> work -> exit + }"#, + ); + write_file(&child_dir.join("prompts/child-import.md"), "child import\n"); + (cwd, root) + } + + let first = tempfile::tempdir().expect("first temp directory should be created"); + let second = tempfile::tempdir().expect("second temp directory should be created"); + let (first_cwd, first_root) = fixture(first.path()); + let (second_cwd, second_root) = fixture(second.path()); + let tree = + collect_graph(&first_cwd, &first_root).expect("first working tree should collect"); + let moved = + collect_graph(&second_cwd, &second_root).expect("moved working tree should collect"); + let entrypoint = tree.entrypoint(); + let resolved = virtual_reference_path( + entrypoint + .as_path() + .parent() + .expect("entrypoint should have a parent"), + "../child/workflow.fabro", + ) + .expect("child reference should resolve"); + + assert_eq!(resolved, Path::new("_fabro_external/child/workflow.fabro")); + assert!(tree.workflows().contains_key( + &CollectedPath::try_new(resolved).expect("child path should be canonical") + )); + assert_eq!(logical_contents(&tree), logical_contents(&moved)); + assert_eq!(logical_provenance(&tree), logical_provenance(&moved)); + assert_eq!(tree.entrypoint(), moved.entrypoint()); + for path in logical_contents(&tree).into_keys() { + assert!(!path.contains(first.path().file_name().unwrap().to_string_lossy().as_ref())); + CollectedPath::try_new(path).expect("every collected coordinate should be canonical"); + } + } + + #[test] + fn repeated_references_collect_one_file() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let cwd = temp.path(); + let graph = cwd.join("workflow.fabro"); + write_file( + &graph, + r#"digraph Root { + start [shape=Mdiamond] + first [prompt="@prompt.md"] + second [prompt="@prompt.md"] + exit [shape=Msquare] + start -> first -> second -> exit + }"#, + ); + write_file(&cwd.join("prompt.md"), "prompt\n"); + + let tree = collect_graph(cwd, &graph).expect("working tree should collect"); + let root = tree + .workflows() + .get(tree.entrypoint()) + .expect("root workflow should be present"); + + assert_eq!(root.files().len(), 1); + let assembled = + crate::assemble_current_manifest(&tree, cwd).expect("legacy manifest should assemble"); + assert_eq!(assembled.workflows["workflow.fabro"].files.len(), 1); + } + + #[cfg(unix)] + #[test] + fn collector_rejects_one_physical_file_with_two_coordinates() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let cwd = temp.path(); + let graph = cwd.join("workflow.fabro"); + write_file( + &graph, + r#"digraph Root { + start [shape=Mdiamond] + first [prompt="@first.md"] + second [prompt="@second.md"] + exit [shape=Msquare] + start -> first -> second -> exit + }"#, + ); + write_file(&cwd.join("actual.md"), "prompt\n"); + std::os::unix::fs::symlink("actual.md", cwd.join("first.md")) + .expect("first symlink should be created"); + std::os::unix::fs::symlink("actual.md", cwd.join("second.md")) + .expect("second symlink should be created"); + + let error = collect_graph(cwd, &graph).expect_err("alias should be rejected"); + + assert!( + error + .to_string() + .contains("one physical file has conflicting collected coordinates"), + "unexpected error: {error:#}" + ); + assert!(error.to_string().contains("first.md")); + assert!(error.to_string().contains("second.md")); + } + + #[test] + fn namespace_rejects_two_physical_files_at_one_coordinate() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let first = temp.path().join("first.md"); + let second = temp.path().join("second.md"); + write_file(&first, "first\n"); + write_file(&second, "second\n"); + let draft = |access_path| DraftDocument { + access_path, + provisional_path: PathBuf::from("shared.md"), + component: ComponentRole::Workflow, + source: String::new(), + }; + + let error = CollectionNamespace::finalize(vec![draft(first), draft(second)]) + .expect_err("virtual collision should be rejected"); + + assert!( + error + .to_string() + .contains("collected coordinate `shared.md` maps to multiple physical files"), + "unexpected error: {error:#}" + ); + } + + #[test] + fn namespace_identity_errors_keep_the_io_error_in_the_source_chain() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let draft = DraftDocument { + access_path: temp.path().join("missing.md"), + provisional_path: PathBuf::from("missing.md"), + component: ComponentRole::Workflow, + source: String::new(), + }; + + let error = CollectionNamespace::finalize(vec![draft]) + .expect_err("missing physical identity should fail"); + + assert!( + error + .chain() + .any(|cause| cause.downcast_ref::().is_some()), + "unexpected error chain: {error:#}" + ); + } + + #[test] + fn read_errors_keep_the_io_error_in_the_source_chain() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let cwd = temp.path(); + let graph = cwd.join("workflow.fabro"); + write_file( + &graph, + r#"digraph Root { + start [shape=Mdiamond] + work [prompt="@missing.md"] + exit [shape=Msquare] + start -> work -> exit + }"#, + ); + + let error = collect_graph(cwd, &graph).expect_err("missing file should fail"); + + assert!( + error + .chain() + .any(|cause| cause.downcast_ref::().is_some()), + "unexpected error chain: {error:#}" + ); + } + + #[test] + fn collector_does_not_push_an_ahead_branch() { + fn commit_all(repository: &git2::Repository, message: &str) -> git2::Oid { + let mut index = repository.index().expect("index should open"); + index + .add_all(["*"], git2::IndexAddOption::DEFAULT, None) + .expect("fixture files should be staged"); + index.write().expect("index should be written"); + let tree_id = index.write_tree().expect("tree should be written"); + let tree = repository.find_tree(tree_id).expect("tree should exist"); + let signature = git2::Signature::now("Fabro Test", "fabro@example.com") + .expect("signature should be valid"); + let parents = repository + .head() + .ok() + .and_then(|head| head.target()) + .map(|oid| { + repository + .find_commit(oid) + .expect("parent commit should exist") + }); + let parent_refs = parents.iter().collect::>(); + repository + .commit( + Some("refs/heads/main"), + &signature, + &signature, + message, + &tree, + &parent_refs, + ) + .expect("commit should be created") + } + + let temp = tempfile::tempdir().expect("temp directory should be created"); + let origin_path = temp.path().join("origin.git"); + let checkout = temp.path().join("checkout"); + let origin = + git2::Repository::init_bare(&origin_path).expect("bare origin should be initialized"); + let repository = git2::Repository::init(&checkout).expect("checkout should be initialized"); + repository + .set_head("refs/heads/main") + .expect("main should be selected"); + write_file( + &checkout.join("workflow.fabro"), + "digraph Root { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", + ); + let first_commit = commit_all(&repository, "initial"); + let mut remote = repository + .remote( + "origin", + origin_path.to_str().expect("origin path should be UTF-8"), + ) + .expect("origin should be configured"); + remote + .push(&["refs/heads/main:refs/heads/main"], None) + .expect("initial commit should be pushed"); + drop(remote); + write_file(&checkout.join("README.md"), "ahead\n"); + let ahead_commit = commit_all(&repository, "ahead"); + assert_ne!(first_commit, ahead_commit); + + collect_graph(&checkout, &checkout.join("workflow.fabro")) + .expect("working tree should collect"); + + let origin_commit = origin + .find_reference("refs/heads/main") + .expect("origin main should exist") + .target() + .expect("origin main should point to a commit"); + assert_eq!(origin_commit, first_commit); + } +} From 0e703a7770cbf67f9eea1652c60bb1f963660c88 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 4 Aug 2026 14:45:28 -0400 Subject: [PATCH 02/62] Simplify working-tree collector and manifest assembly Apply cleanup review findings on the collector extraction: - Deduplicate the lexical path-normalization loop: normalize_absolute_path now delegates to lexically_normalize_access_path, and it plus manifest_path_from_absolute live in working_tree.rs so the module dependency points one way (projection -> collector). Drop the redundant re-normalization in collect_bundled_file. - Extract collect_bundled_template_includes to replace the copy-pasted goal/prompt template-closure sequence, seed_config_document for the duplicated config seeding, and read_source_input for the duplicated config reader closures (with the user-settings is_file check hoisted). - Replace ~100 lines of trivial getters on the Collected* output structs with pub(super) fields; keep the CollectedPath newtype encapsulated. - Assemble the manifest by value, moving collected sources into the wire types instead of deep-copying every file a second time; drop two full DraftDocument clones that only satisfied the borrow checker; stop recomputing manifest paths per file in template-dependency verification. - Resolve the root workflow once in assemble_current_manifest, removing an unreachable duplicate error path; flatten single-use CollectionNamespace into a finalize_documents free function. No behavior change; fabro-manifest tests, clippy, and fmt pass. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-manifest/src/lib.rs | 143 +++--- .../fabro-manifest/src/working_tree.rs | 408 +++++++----------- 2 files changed, 214 insertions(+), 337 deletions(-) diff --git a/lib/components/fabro-manifest/src/lib.rs b/lib/components/fabro-manifest/src/lib.rs index 6cd29c2f9..8dc010510 100644 --- a/lib/components/fabro-manifest/src/lib.rs +++ b/lib/components/fabro-manifest/src/lib.rs @@ -6,7 +6,7 @@ mod working_tree; use std::collections::HashMap; -use std::path::{Component, Path, PathBuf}; +use std::path::{Path, PathBuf}; use anyhow::{Context, Result, anyhow}; use fabro_api::types; @@ -21,7 +21,7 @@ use fabro_graphviz::graph::AttrValue; use fabro_graphviz::parser; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{ApprovalMode, ResolvedGoalSource, ResolvedRunGoal, RunMode}; -use fabro_types::{DirtyStatus, GitContext, ManifestPath, WorkflowSettings}; +use fabro_types::{DirtyStatus, GitContext, WorkflowSettings}; use fabro_workflow::git::{ GitSyncStatus, branch_needs_push, head_sha, push_branch_noninteractive, sync_status, }; @@ -29,7 +29,7 @@ use fabro_workflow::static_reference::ReferenceKind; use crate::working_tree::{ CollectWorkingTreeInput, CollectedDocument, CollectedFileReferenceType, CollectedSourceInput, - CollectedWorkingTree, + CollectedWorkingTree, manifest_path_from_absolute, normalize_absolute_path, }; #[derive(Debug, Default)] @@ -137,16 +137,13 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { } let project_config = discover_project_config(&root_location.dir)?; let project_config_source = project_config - .as_ref() - .map(|path| { - let source = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read {}", path.display()))?; - Ok::<_, anyhow::Error>(CollectedSourceInput { - access_path: path.clone(), - source, - }) - }) + .as_deref() + .map(read_source_input) .transpose()?; + let user_settings_path = input + .user_settings_path + .as_deref() + .filter(|path| path.is_file()); let mut workflow_settings_builder = WorkflowSettingsBuilder::new() .server_manifest_defaults(RunLayer::default(), input.environment_defaults.clone()); @@ -162,11 +159,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { if let Some(path) = project_config.as_ref() { workflow_settings_builder = workflow_settings_builder.project_file(path)?; } - if let Some(path) = input - .user_settings_path - .as_ref() - .filter(|path| path.is_file()) - { + if let Some(path) = user_settings_path { workflow_settings_builder = workflow_settings_builder.user_file(path)?; } let mut workflow_settings = workflow_settings_builder @@ -174,19 +167,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { .context("failed to resolve manifest settings")?; workflow_settings.run.inputs.extend(input.input_overrides); let target_path = root_location.graph.clone(); - let user_config_source = input - .user_settings_path - .as_ref() - .filter(|path| path.is_file()) - .map(|path| { - let source = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read {}", path.display()))?; - Ok::<_, anyhow::Error>(CollectedSourceInput { - access_path: path.clone(), - source, - }) - }) - .transpose()?; + let user_config_source = user_settings_path.map(read_source_input).transpose()?; let collected = working_tree::collect_working_tree(CollectWorkingTreeInput { cwd: &input.cwd, root_location, @@ -194,7 +175,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { project_config: project_config_source, user_config: user_config_source, })?; - let assembled = assemble_current_manifest(&collected, &input.cwd)?; + let assembled = assemble_current_manifest(collected, &input.cwd)?; let working_directory = project::resolve_working_directory_from_run(&workflow_settings.run, &input.cwd); @@ -238,72 +219,63 @@ struct AssembledCurrentManifest { } fn assemble_current_manifest( - collected: &CollectedWorkingTree, + collected: CollectedWorkingTree, cwd: &Path, ) -> Result { + let root = collected + .workflows + .get(&collected.entrypoint) + .ok_or_else(|| anyhow!("root workflow missing from collected working tree"))?; + let target_key = manifest_path_from_absolute(&root.graph.access_path, cwd)?.to_string(); + let root_source = root.graph.source.clone(); + let mut workflows = HashMap::new(); - for workflow in collected.workflows().values() { - let graph = workflow.graph(); - let graph_key = manifest_path_from_absolute(graph.access_path(), cwd)?.to_string(); + for workflow in collected.workflows.into_values() { + let graph_key = manifest_path_from_absolute(&workflow.graph.access_path, cwd)?.to_string(); let config = workflow - .config() + .config .map(|config| { Ok::<_, anyhow::Error>(types::ManifestWorkflowConfig { - path: manifest_path_from_absolute(config.access_path(), cwd)?.to_string(), - source: config.source().to_owned(), + path: manifest_path_from_absolute(&config.access_path, cwd)?.to_string(), + source: config.source, }) }) .transpose()?; let mut files = HashMap::new(); - for file in workflow.files().values() { - let document = file.document(); - let reference = file.reference(); - let key = manifest_path_from_absolute(document.access_path(), cwd)?.to_string(); - let from = reference - .source_access_path() + for file in workflow.files.into_values() { + let key = manifest_path_from_absolute(&file.document.access_path, cwd)?.to_string(); + let from = file + .reference + .from_access_path + .as_deref() .map(|path| manifest_path_from_absolute(path, cwd).map(|path| path.to_string())) .transpose()?; - let type_ = match reference.type_() { + let type_ = match file.reference.type_ { CollectedFileReferenceType::FileInline => types::ManifestFileRefType::FileInline, CollectedFileReferenceType::Import => types::ManifestFileRefType::Import, CollectedFileReferenceType::Dockerfile => types::ManifestFileRefType::Dockerfile, }; files.insert(key, types::ManifestFileEntry { - content: document.source().to_owned(), + content: file.document.source, ref_: types::ManifestFileRef { from, - original: reference.original().to_owned(), + original: file.reference.original, type_, }, }); } workflows.insert(graph_key, types::ManifestWorkflow { - source: graph.source().to_owned(), + source: workflow.graph.source, config, files, }); } - let target_key = manifest_path_from_absolute( - collected - .workflows() - .get(collected.entrypoint()) - .ok_or_else(|| anyhow!("root workflow missing from collected working tree"))? - .graph() - .access_path(), - cwd, - )? - .to_string(); - let root_source = workflows - .get(&target_key) - .map(|workflow| workflow.source.clone()) - .ok_or_else(|| anyhow!("root workflow missing from manifest bundle"))?; - let mut configs = Vec::new(); - if let Some(config) = collected.project_config() { + if let Some(config) = collected.project_config { configs.push(manifest_config(config, types::ManifestConfigType::Project)); } - if let Some(config) = collected.user_config() { + if let Some(config) = collected.user_config { configs.push(manifest_config(config, types::ManifestConfigType::User)); } @@ -316,16 +288,25 @@ fn assemble_current_manifest( } fn manifest_config( - document: &CollectedDocument, + document: CollectedDocument, type_: types::ManifestConfigType, ) -> types::ManifestConfig { types::ManifestConfig { - path: Some(document.access_path().display().to_string()), - source: Some(document.source().to_owned()), + path: Some(document.access_path.display().to_string()), + source: Some(document.source), type_, } } +fn read_source_input(path: &Path) -> Result { + let source = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + Ok(CollectedSourceInput { + access_path: path.to_path_buf(), + source, + }) +} + fn resolve_manifest_goal( run_overrides: Option<&RunLayer>, settings: &WorkflowSettings, @@ -491,32 +472,6 @@ fn push_manifest_branch_best_effort( let _ = push_branch_noninteractive(repo_path, "origin", branch); } -fn normalize_absolute_path(base_dir: &Path, reference: &str) -> Option { - let path = Path::new(reference); - if path.is_absolute() || reference.starts_with('~') { - return None; - } - - let mut normalized = PathBuf::new(); - for component in base_dir.join(path).components() { - match component { - Component::CurDir => {} - Component::Normal(part) => normalized.push(part), - Component::ParentDir => { - normalized.pop(); - } - Component::RootDir => normalized.push(Path::new("/")), - Component::Prefix(prefix) => normalized.push(prefix.as_os_str()), - } - } - Some(normalized) -} - -fn manifest_path_from_absolute(path: &Path, cwd: &Path) -> Result { - ManifestPath::from_absolute(path, cwd) - .ok_or_else(|| anyhow!("Failed to compute manifest path for {}", path.display())) -} - pub fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool { args.auto_approve.is_none() && args.dry_run.is_none() diff --git a/lib/components/fabro-manifest/src/working_tree.rs b/lib/components/fabro-manifest/src/working_tree.rs index 3b9dbed95..b2af413c4 100644 --- a/lib/components/fabro-manifest/src/working_tree.rs +++ b/lib/components/fabro-manifest/src/working_tree.rs @@ -18,8 +18,6 @@ use fabro_workflow::static_reference::{ AttributeScope, ReferenceKind, reference_kind_for_attribute, }; -use super::{manifest_path_from_absolute, normalize_absolute_path}; - pub(super) struct CollectWorkingTreeInput<'a> { pub(super) cwd: &'a Path, pub(super) root_location: WorkflowLocation, @@ -35,82 +33,30 @@ pub(super) struct CollectedSourceInput { #[derive(Clone, Debug)] pub(super) struct CollectedWorkingTree { - entrypoint: CollectedPath, - workflows: BTreeMap, - project_config: Option, - user_config: Option, -} - -impl CollectedWorkingTree { - pub(super) fn entrypoint(&self) -> &CollectedPath { - &self.entrypoint - } - - pub(super) fn workflows(&self) -> &BTreeMap { - &self.workflows - } - - pub(super) fn project_config(&self) -> Option<&CollectedDocument> { - self.project_config.as_ref() - } - - pub(super) fn user_config(&self) -> Option<&CollectedDocument> { - self.user_config.as_ref() - } + pub(super) entrypoint: CollectedPath, + pub(super) workflows: BTreeMap, + pub(super) project_config: Option, + pub(super) user_config: Option, } #[derive(Clone, Debug)] pub(super) struct CollectedWorkflow { - graph: CollectedDocument, - config: Option, - files: BTreeMap, -} - -impl CollectedWorkflow { - pub(super) fn graph(&self) -> &CollectedDocument { - &self.graph - } - - pub(super) fn config(&self) -> Option<&CollectedDocument> { - self.config.as_ref() - } - - pub(super) fn files(&self) -> &BTreeMap { - &self.files - } + pub(super) graph: CollectedDocument, + pub(super) config: Option, + pub(super) files: BTreeMap, } #[derive(Clone, Debug)] pub(super) struct CollectedDocument { - access_path: PathBuf, - path: CollectedPath, - source: String, -} - -impl CollectedDocument { - pub(super) fn access_path(&self) -> &Path { - &self.access_path - } - - pub(super) fn source(&self) -> &str { - &self.source - } + pub(super) access_path: PathBuf, + pub(super) path: CollectedPath, + pub(super) source: String, } #[derive(Clone, Debug)] pub(super) struct CollectedFile { - document: CollectedDocument, - reference: CollectedFileReference, -} - -impl CollectedFile { - pub(super) fn document(&self) -> &CollectedDocument { - &self.document - } - - pub(super) fn reference(&self) -> &CollectedFileReference { - &self.reference - } + pub(super) document: CollectedDocument, + pub(super) reference: CollectedFileReference, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -122,23 +68,9 @@ pub(super) enum CollectedFileReferenceType { #[derive(Clone, Debug)] pub(super) struct CollectedFileReference { - type_: CollectedFileReferenceType, - original: String, - from_access_path: Option, -} - -impl CollectedFileReference { - pub(super) fn type_(&self) -> CollectedFileReferenceType { - self.type_ - } - - pub(super) fn original(&self) -> &str { - &self.original - } - - pub(super) fn source_access_path(&self) -> Option<&Path> { - self.from_access_path.as_deref() - } + pub(super) type_: CollectedFileReferenceType, + pub(super) original: String, + pub(super) from_access_path: Option, } /// A canonical virtual coordinate inside one collected working-tree closure. @@ -379,15 +311,15 @@ impl<'a> CollectionDraft<'a> { files: &mut BTreeMap, visited_imports: &mut HashSet, ) -> Result<()> { - let graph_document = self.document(graph_document_id).clone(); - let graph = parser::parse(&graph_document.source) - .with_context(|| format!("Failed to parse {}", graph_document.access_path.display()))?; - let workflow_base_dir = graph_document - .access_path - .parent() - .unwrap_or_else(|| Path::new(".")); - let graph_manifest_path = - manifest_path_from_absolute(&graph_document.access_path, &self.cwd)?; + let graph = parser::parse(&self.document(graph_document_id).source).with_context(|| { + format!( + "Failed to parse {}", + self.document(graph_document_id).access_path.display() + ) + })?; + let graph_access_path = self.document(graph_document_id).access_path.clone(); + let workflow_base_dir = graph_access_path.parent().unwrap_or_else(|| Path::new(".")); + let graph_manifest_path = manifest_path_from_absolute(&graph_access_path, &self.cwd)?; let workflow_template_root = manifest_parent_or_dot(&graph_manifest_path)?; if let Some(goal_reference) = graph.attrs.get("goal").and_then(AttrValue::as_str) { @@ -400,17 +332,10 @@ impl<'a> CollectionDraft<'a> { manifest_attr_reference_kind(AttributeScope::Graph, "goal", goal_reference)?, graph_document_id, )?; - let source = self.document(bundled).source.clone(); - let bundled_manifest_path = - manifest_path_from_absolute(&self.document(bundled).access_path, &self.cwd)?; - let template_root = template_root_for_bundled_file( - &bundled_manifest_path, - &workflow_template_root, - )?; - self.collect_template_include_files( + self.collect_bundled_template_includes( files, - TemplateSource::new(bundled_manifest_path, template_root, source), bundled, + &workflow_template_root, graph_document_id, )?; } else { @@ -469,19 +394,10 @@ impl<'a> CollectionDraft<'a> { )?; if name == "prompt" { - let source = self.document(bundled).source.clone(); - let bundled_manifest_path = manifest_path_from_absolute( - &self.document(bundled).access_path, - &self.cwd, - )?; - let template_root = template_root_for_bundled_file( - &bundled_manifest_path, - &workflow_template_root, - )?; - self.collect_template_include_files( + self.collect_bundled_template_includes( files, - TemplateSource::new(bundled_manifest_path, template_root, source), bundled, + &workflow_template_root, graph_document_id, )?; } @@ -534,6 +450,28 @@ impl<'a> CollectionDraft<'a> { Ok(()) } + /// Collects the template dependency closure of an already-bundled + /// `@`-referenced file (a goal or prompt document). + fn collect_bundled_template_includes( + &mut self, + files: &mut BTreeMap, + bundled: DocumentId, + workflow_template_root: &ManifestPath, + from_document: DocumentId, + ) -> Result<()> { + let document = self.document(bundled); + let source = document.source.clone(); + let bundled_manifest_path = manifest_path_from_absolute(&document.access_path, &self.cwd)?; + let template_root = + template_root_for_bundled_file(&bundled_manifest_path, workflow_template_root)?; + self.collect_template_include_files( + files, + TemplateSource::new(bundled_manifest_path, template_root, source), + bundled, + from_document, + ) + } + fn collect_template_include_files( &mut self, files: &mut BTreeMap, @@ -611,10 +549,10 @@ impl<'a> CollectionDraft<'a> { .iter() .map(|(path, source)| (path.clone(), source.content.clone())) .collect::>(); - for file in files.values() { - let document = self.document(file.document); - let path = manifest_path_from_absolute(&document.access_path, &self.cwd)?; - bundled_files.insert(path, document.source.clone()); + for (key, file) in files { + let path = ManifestPath::from_wire(key) + .ok_or_else(|| anyhow!("invalid collected file key: {key}"))?; + bundled_files.insert(path, self.document(file.document).source.clone()); } let allowed = bundled_files.keys().cloned().collect(); let store = @@ -640,13 +578,13 @@ impl<'a> CollectionDraft<'a> { config: DocumentId, files: &mut BTreeMap, ) -> Result<()> { - let config_document = self.document(config).clone(); - let layer = config_document + let layer = self + .document(config) .source .parse::() .context("Failed to parse run config TOML")?; - let base_dir = config_document - .access_path + let config_access_path = self.document(config).access_path.clone(); + let base_dir = config_access_path .parent() .unwrap_or_else(|| Path::new(".")); @@ -708,7 +646,6 @@ impl<'a> CollectionDraft<'a> { let access_path = normalize_absolute_path(base_dir, reference) .ok_or_else(|| anyhow!("unsupported manifest reference: {reference}"))?; - let access_path = lexically_normalize_access_path(&access_path); let manifest_path = manifest_path_from_absolute(&access_path, &self.cwd)?; let key = manifest_path.to_string(); let provisional_path = virtual_reference_path( @@ -756,7 +693,7 @@ impl<'a> CollectionDraft<'a> { project_config: Option, user_config: Option, ) -> Result { - let documents = CollectionNamespace::finalize(self.documents)?; + let documents = finalize_documents(self.documents)?; let mut workflows = BTreeMap::new(); for workflow in self.workflows.into_values() { let graph = documents[workflow.graph.0].clone(); @@ -795,85 +732,71 @@ impl<'a> CollectionDraft<'a> { } } -struct CollectionNamespace { - physical_to_virtual: BTreeMap, - virtual_to_physical: BTreeMap, -} - -impl CollectionNamespace { - fn finalize(drafts: Vec) -> Result> { - let mut deficits = BTreeMap::::new(); - for draft in &drafts { - let deficit = leading_parent_count(&draft.provisional_path); - deficits - .entry(draft.component) - .and_modify(|current| *current = (*current).max(deficit)) - .or_insert(deficit); - } - - let paths = drafts - .iter() - .map(|draft| { - finalize_component_path( - &draft.provisional_path, - draft.component, - deficits.get(&draft.component).copied().unwrap_or_default(), - ) - }) - .collect::>>()?; - - let mut order = (0..drafts.len()).collect::>(); - order.sort_by(|left, right| { - paths[*left] - .cmp(&paths[*right]) - .then_with(|| drafts[*left].access_path.cmp(&drafts[*right].access_path)) - }); - - let mut namespace = Self { - physical_to_virtual: BTreeMap::new(), - virtual_to_physical: BTreeMap::new(), - }; - for index in order { - let physical = - std::fs::canonicalize(&drafts[index].access_path).with_context(|| { - format!( - "failed to identify collected file {}", - drafts[index].access_path.display() - ) - })?; - namespace.register(physical, paths[index].clone())?; - } - - Ok(drafts - .into_iter() - .zip(paths) - .map(|(draft, path)| CollectedDocument { - access_path: draft.access_path, - path, - source: draft.source, - }) - .collect()) +/// Finalizes draft documents into collected documents, rejecting conflicting +/// physical aliases and virtual-coordinate collisions. +fn finalize_documents(drafts: Vec) -> Result> { + let mut deficits = BTreeMap::::new(); + for draft in &drafts { + let deficit = leading_parent_count(&draft.provisional_path); + deficits + .entry(draft.component) + .and_modify(|current| *current = (*current).max(deficit)) + .or_insert(deficit); } - fn register(&mut self, physical: PathBuf, path: CollectedPath) -> Result<()> { - if let Some(existing) = self.physical_to_virtual.get(&physical) { - if existing != &path { + let paths = drafts + .iter() + .map(|draft| { + finalize_component_path( + &draft.provisional_path, + draft.component, + deficits.get(&draft.component).copied().unwrap_or_default(), + ) + }) + .collect::>>()?; + + let mut order = (0..drafts.len()).collect::>(); + order.sort_by(|left, right| { + paths[*left] + .cmp(&paths[*right]) + .then_with(|| drafts[*left].access_path.cmp(&drafts[*right].access_path)) + }); + + let mut physical_to_virtual = BTreeMap::::new(); + let mut virtual_to_physical = BTreeMap::::new(); + for index in order { + let physical = std::fs::canonicalize(&drafts[index].access_path).with_context(|| { + format!( + "failed to identify collected file {}", + drafts[index].access_path.display() + ) + })?; + let path = &paths[index]; + if let Some(existing) = physical_to_virtual.get(&physical) { + if existing != path { bail!( "one physical file has conflicting collected coordinates `{existing}` and `{path}`" ); } } - if let Some(existing) = self.virtual_to_physical.get(&path) { + if let Some(existing) = virtual_to_physical.get(path) { if existing != &physical { bail!("collected coordinate `{path}` maps to multiple physical files"); } } - - self.physical_to_virtual - .insert(physical.clone(), path.clone()); - self.virtual_to_physical.insert(path, physical); - Ok(()) + physical_to_virtual.insert(physical.clone(), path.clone()); + virtual_to_physical.insert(path.clone(), physical); } + + Ok(drafts + .into_iter() + .zip(paths) + .map(|(draft, path)| CollectedDocument { + access_path: draft.access_path, + path, + source: draft.source, + }) + .collect()) } pub(super) fn collect_working_tree( @@ -886,31 +809,11 @@ pub(super) fn collect_working_tree( let project_config = input .project_config - .map(|config| { - let access_path = normalized_absolute_access_path(&config.access_path)?; - let provisional_path = - seed_component_path(&access_path, &draft.cwd, ComponentRole::ProjectConfig)?; - Ok::<_, anyhow::Error>(draft.insert_document( - &access_path, - provisional_path, - ComponentRole::ProjectConfig, - config.source, - )) - }) + .map(|config| seed_config_document(&mut draft, config, ComponentRole::ProjectConfig)) .transpose()?; let user_config = input .user_config - .map(|config| { - let access_path = normalized_absolute_access_path(&config.access_path)?; - let provisional_path = - seed_component_path(&access_path, &draft.cwd, ComponentRole::UserConfig)?; - Ok::<_, anyhow::Error>(draft.insert_document( - &access_path, - provisional_path, - ComponentRole::UserConfig, - config.source, - )) - }) + .map(|config| seed_config_document(&mut draft, config, ComponentRole::UserConfig)) .transpose()?; let entrypoint = draft.collect_workflow_location(&input.root_location, root_graph_path)?; @@ -929,6 +832,16 @@ pub(super) fn collect_working_tree( draft.finish(entrypoint, project_config, user_config) } +fn seed_config_document( + draft: &mut CollectionDraft<'_>, + config: CollectedSourceInput, + role: ComponentRole, +) -> Result { + let access_path = normalized_absolute_access_path(&config.access_path)?; + let provisional_path = seed_component_path(&access_path, &draft.cwd, role)?; + Ok(draft.insert_document(&access_path, provisional_path, role, config.source)) +} + fn seed_component_path( access_path: &Path, cwd: &Path, @@ -1073,6 +986,19 @@ fn normalized_absolute_access_path(path: &Path) -> Result { Ok(lexically_normalize_access_path(&absolute)) } +pub(super) fn normalize_absolute_path(base_dir: &Path, reference: &str) -> Option { + let path = Path::new(reference); + if path.is_absolute() || reference.starts_with('~') { + return None; + } + Some(lexically_normalize_access_path(&base_dir.join(path))) +} + +pub(super) fn manifest_path_from_absolute(path: &Path, cwd: &Path) -> Result { + ManifestPath::from_absolute(path, cwd) + .ok_or_else(|| anyhow!("Failed to compute manifest path for {}", path.display())) +} + fn manifest_parent_or_dot(path: &ManifestPath) -> Result { let parent = path.parent_or_dot().to_string_lossy(); ManifestPath::from_wire(&parent) @@ -1134,21 +1060,21 @@ mod tests { fn logical_contents(tree: &CollectedWorkingTree) -> BTreeMap { let mut contents = BTreeMap::new(); - if let Some(config) = tree.project_config() { + if let Some(config) = &tree.project_config { contents.insert(config.path.to_string(), config.source.clone()); } - if let Some(config) = tree.user_config() { + if let Some(config) = &tree.user_config { contents.insert(config.path.to_string(), config.source.clone()); } - for workflow in tree.workflows().values() { + for workflow in tree.workflows.values() { contents.insert( workflow.graph.path.to_string(), workflow.graph.source.clone(), ); - if let Some(config) = workflow.config() { + if let Some(config) = &workflow.config { contents.insert(config.path.to_string(), config.source.clone()); } - for file in workflow.files().values() { + for file in workflow.files.values() { contents.insert(file.document.path.to_string(), file.document.source.clone()); } } @@ -1159,21 +1085,21 @@ mod tests { tree: &CollectedWorkingTree, ) -> BTreeMap)> { let mut paths_by_access = HashMap::new(); - if let Some(config) = tree.project_config() { + if let Some(config) = &tree.project_config { paths_by_access.insert(config.access_path.clone(), config.path.to_string()); } - if let Some(config) = tree.user_config() { + if let Some(config) = &tree.user_config { paths_by_access.insert(config.access_path.clone(), config.path.to_string()); } - for workflow in tree.workflows().values() { + for workflow in tree.workflows.values() { paths_by_access.insert( workflow.graph.access_path.clone(), workflow.graph.path.to_string(), ); - if let Some(config) = workflow.config() { + if let Some(config) = &workflow.config { paths_by_access.insert(config.access_path.clone(), config.path.to_string()); } - for file in workflow.files().values() { + for file in workflow.files.values() { paths_by_access.insert( file.document.access_path.clone(), file.document.path.to_string(), @@ -1182,9 +1108,9 @@ mod tests { } let mut provenance = BTreeMap::new(); - for workflow in tree.workflows().values() { - for file in workflow.files().values() { - let from = file.reference.source_access_path().map(|access_path| { + for workflow in tree.workflows.values() { + for file in workflow.files.values() { + let from = file.reference.from_access_path.as_ref().map(|access_path| { paths_by_access .get(access_path) .expect("reference source should be collected") @@ -1192,11 +1118,7 @@ mod tests { }); provenance.insert( file.document.path.to_string(), - ( - file.reference.type_(), - file.reference.original.clone(), - from, - ), + (file.reference.type_, file.reference.original.clone(), from), ); } } @@ -1327,10 +1249,10 @@ dockerfile = { path = "Dockerfile" } .expect("working tree should collect"); assert_eq!( - tree.entrypoint().as_str(), + tree.entrypoint.as_str(), ".fabro/workflows/root/workflow.fabro" ); - assert_eq!(tree.workflows().len(), 2); + assert_eq!(tree.workflows.len(), 2); assert_eq!( logical_contents(&tree) .keys() @@ -1383,7 +1305,7 @@ dockerfile = { path = "Dockerfile" } let second_tree = collect_graph(&second_cwd, &second_workflow) .expect("second working tree should collect"); - assert_eq!(first_tree.entrypoint(), second_tree.entrypoint()); + assert_eq!(first_tree.entrypoint, second_tree.entrypoint); assert_eq!( logical_contents(&first_tree), logical_contents(&second_tree) @@ -1393,7 +1315,7 @@ dockerfile = { path = "Dockerfile" } logical_provenance(&second_tree) ); assert_eq!( - first_tree.entrypoint().as_str(), + first_tree.entrypoint.as_str(), "_fabro_external/entrypoint/workflow.fabro" ); } @@ -1471,7 +1393,7 @@ dockerfile = { path = "Dockerfile" } collect_graph(&first_cwd, &first_root).expect("first working tree should collect"); let moved = collect_graph(&second_cwd, &second_root).expect("moved working tree should collect"); - let entrypoint = tree.entrypoint(); + let entrypoint = &tree.entrypoint; let resolved = virtual_reference_path( entrypoint .as_path() @@ -1482,12 +1404,12 @@ dockerfile = { path = "Dockerfile" } .expect("child reference should resolve"); assert_eq!(resolved, Path::new("_fabro_external/child/workflow.fabro")); - assert!(tree.workflows().contains_key( + assert!(tree.workflows.contains_key( &CollectedPath::try_new(resolved).expect("child path should be canonical") )); assert_eq!(logical_contents(&tree), logical_contents(&moved)); assert_eq!(logical_provenance(&tree), logical_provenance(&moved)); - assert_eq!(tree.entrypoint(), moved.entrypoint()); + assert_eq!(tree.entrypoint, moved.entrypoint); for path in logical_contents(&tree).into_keys() { assert!(!path.contains(first.path().file_name().unwrap().to_string_lossy().as_ref())); CollectedPath::try_new(path).expect("every collected coordinate should be canonical"); @@ -1513,13 +1435,13 @@ dockerfile = { path = "Dockerfile" } let tree = collect_graph(cwd, &graph).expect("working tree should collect"); let root = tree - .workflows() - .get(tree.entrypoint()) + .workflows + .get(&tree.entrypoint) .expect("root workflow should be present"); - assert_eq!(root.files().len(), 1); + assert_eq!(root.files.len(), 1); let assembled = - crate::assemble_current_manifest(&tree, cwd).expect("legacy manifest should assemble"); + crate::assemble_current_manifest(tree, cwd).expect("legacy manifest should assemble"); assert_eq!(assembled.workflows["workflow.fabro"].files.len(), 1); } @@ -1571,7 +1493,7 @@ dockerfile = { path = "Dockerfile" } source: String::new(), }; - let error = CollectionNamespace::finalize(vec![draft(first), draft(second)]) + let error = finalize_documents(vec![draft(first), draft(second)]) .expect_err("virtual collision should be rejected"); assert!( @@ -1592,8 +1514,8 @@ dockerfile = { path = "Dockerfile" } source: String::new(), }; - let error = CollectionNamespace::finalize(vec![draft]) - .expect_err("missing physical identity should fail"); + let error = + finalize_documents(vec![draft]).expect_err("missing physical identity should fail"); assert!( error From afd06bf56051a521df89392338a95875c3240194 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 4 Aug 2026 14:54:56 -0400 Subject: [PATCH 03/62] Rename collector to workflow bundle --- lib/components/fabro-manifest/src/lib.rs | 15 +-- .../{working_tree.rs => workflow_bundle.rs} | 100 +++++++++--------- 2 files changed, 58 insertions(+), 57 deletions(-) rename lib/components/fabro-manifest/src/{working_tree.rs => workflow_bundle.rs} (95%) diff --git a/lib/components/fabro-manifest/src/lib.rs b/lib/components/fabro-manifest/src/lib.rs index 8dc010510..762265d13 100644 --- a/lib/components/fabro-manifest/src/lib.rs +++ b/lib/components/fabro-manifest/src/lib.rs @@ -3,7 +3,7 @@ reason = "CLI manifest builder: sync file I/O building install manifests" )] -mod working_tree; +mod workflow_bundle; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -27,9 +27,10 @@ use fabro_workflow::git::{ }; use fabro_workflow::static_reference::ReferenceKind; -use crate::working_tree::{ - CollectWorkingTreeInput, CollectedDocument, CollectedFileReferenceType, CollectedSourceInput, - CollectedWorkingTree, manifest_path_from_absolute, normalize_absolute_path, +use crate::workflow_bundle::{ + CollectWorkflowBundleInput, CollectedDocument, CollectedFileReferenceType, + CollectedSourceInput, CollectedWorkflowBundle, manifest_path_from_absolute, + normalize_absolute_path, }; #[derive(Debug, Default)] @@ -168,7 +169,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { workflow_settings.run.inputs.extend(input.input_overrides); let target_path = root_location.graph.clone(); let user_config_source = user_settings_path.map(read_source_input).transpose()?; - let collected = working_tree::collect_working_tree(CollectWorkingTreeInput { + let collected = workflow_bundle::collect_workflow_bundle(CollectWorkflowBundleInput { cwd: &input.cwd, root_location, inputs: &workflow_settings.run.inputs, @@ -219,13 +220,13 @@ struct AssembledCurrentManifest { } fn assemble_current_manifest( - collected: CollectedWorkingTree, + collected: CollectedWorkflowBundle, cwd: &Path, ) -> Result { let root = collected .workflows .get(&collected.entrypoint) - .ok_or_else(|| anyhow!("root workflow missing from collected working tree"))?; + .ok_or_else(|| anyhow!("root workflow missing from collected workflow bundle"))?; let target_key = manifest_path_from_absolute(&root.graph.access_path, cwd)?.to_string(); let root_source = root.graph.source.clone(); diff --git a/lib/components/fabro-manifest/src/working_tree.rs b/lib/components/fabro-manifest/src/workflow_bundle.rs similarity index 95% rename from lib/components/fabro-manifest/src/working_tree.rs rename to lib/components/fabro-manifest/src/workflow_bundle.rs index b2af413c4..3256f7f6e 100644 --- a/lib/components/fabro-manifest/src/working_tree.rs +++ b/lib/components/fabro-manifest/src/workflow_bundle.rs @@ -18,7 +18,7 @@ use fabro_workflow::static_reference::{ AttributeScope, ReferenceKind, reference_kind_for_attribute, }; -pub(super) struct CollectWorkingTreeInput<'a> { +pub(super) struct CollectWorkflowBundleInput<'a> { pub(super) cwd: &'a Path, pub(super) root_location: WorkflowLocation, pub(super) inputs: &'a HashMap, @@ -32,7 +32,7 @@ pub(super) struct CollectedSourceInput { } #[derive(Clone, Debug)] -pub(super) struct CollectedWorkingTree { +pub(super) struct CollectedWorkflowBundle { pub(super) entrypoint: CollectedPath, pub(super) workflows: BTreeMap, pub(super) project_config: Option, @@ -73,7 +73,7 @@ pub(super) struct CollectedFileReference { pub(super) from_access_path: Option, } -/// A canonical virtual coordinate inside one collected working-tree closure. +/// A canonical virtual coordinate inside one collected workflow bundle. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub(super) struct CollectedPath(String); @@ -692,7 +692,7 @@ impl<'a> CollectionDraft<'a> { entrypoint: DocumentId, project_config: Option, user_config: Option, - ) -> Result { + ) -> Result { let documents = finalize_documents(self.documents)?; let mut workflows = BTreeMap::new(); for workflow in self.workflows.into_values() { @@ -723,7 +723,7 @@ impl<'a> CollectionDraft<'a> { }); } - Ok(CollectedWorkingTree { + Ok(CollectedWorkflowBundle { entrypoint: documents[entrypoint.0].path.clone(), workflows, project_config: project_config.map(|document| documents[document.0].clone()), @@ -799,9 +799,9 @@ fn finalize_documents(drafts: Vec) -> Result, -) -> Result { +pub(super) fn collect_workflow_bundle( + input: CollectWorkflowBundleInput<'_>, +) -> Result { let mut draft = CollectionDraft::new(input.cwd, input.inputs)?; let root_graph_access_path = normalized_absolute_access_path(&input.root_location.graph)?; let root_graph_path = @@ -824,7 +824,7 @@ pub(super) fn collect_working_tree( let mut root = draft .workflows .remove(&root_key) - .ok_or_else(|| anyhow!("root workflow missing from collected working tree"))?; + .ok_or_else(|| anyhow!("root workflow missing from collected workflow bundle"))?; draft.collect_config_dockerfile(project_config, &mut root.files)?; draft.workflows.insert(root_key, root); } @@ -1046,10 +1046,10 @@ mod tests { std::fs::write(path, source).expect("fixture file should be written"); } - fn collect_graph(cwd: &Path, graph: &Path) -> Result { + fn collect_graph(cwd: &Path, graph: &Path) -> Result { let inputs = HashMap::new(); let root_location = WorkflowLocation::resolve(graph, cwd)?; - collect_working_tree(CollectWorkingTreeInput { + collect_workflow_bundle(CollectWorkflowBundleInput { cwd, root_location, inputs: &inputs, @@ -1058,15 +1058,15 @@ mod tests { }) } - fn logical_contents(tree: &CollectedWorkingTree) -> BTreeMap { + fn logical_contents(bundle: &CollectedWorkflowBundle) -> BTreeMap { let mut contents = BTreeMap::new(); - if let Some(config) = &tree.project_config { + if let Some(config) = &bundle.project_config { contents.insert(config.path.to_string(), config.source.clone()); } - if let Some(config) = &tree.user_config { + if let Some(config) = &bundle.user_config { contents.insert(config.path.to_string(), config.source.clone()); } - for workflow in tree.workflows.values() { + for workflow in bundle.workflows.values() { contents.insert( workflow.graph.path.to_string(), workflow.graph.source.clone(), @@ -1082,16 +1082,16 @@ mod tests { } fn logical_provenance( - tree: &CollectedWorkingTree, + bundle: &CollectedWorkflowBundle, ) -> BTreeMap)> { let mut paths_by_access = HashMap::new(); - if let Some(config) = &tree.project_config { + if let Some(config) = &bundle.project_config { paths_by_access.insert(config.access_path.clone(), config.path.to_string()); } - if let Some(config) = &tree.user_config { + if let Some(config) = &bundle.user_config { paths_by_access.insert(config.access_path.clone(), config.path.to_string()); } - for workflow in tree.workflows.values() { + for workflow in bundle.workflows.values() { paths_by_access.insert( workflow.graph.access_path.clone(), workflow.graph.path.to_string(), @@ -1108,7 +1108,7 @@ mod tests { } let mut provenance = BTreeMap::new(); - for workflow in tree.workflows.values() { + for workflow in bundle.workflows.values() { for file in workflow.files.values() { let from = file.reference.from_access_path.as_ref().map(|access_path| { paths_by_access @@ -1166,7 +1166,7 @@ mod tests { } #[test] - fn collector_captures_complete_workflow_and_config_closure() { + fn collector_captures_complete_workflow_bundle() { let temp = tempfile::tempdir().expect("temp directory should be created"); let project = temp.path().join("project"); let root = project.join(".fabro/workflows/root"); @@ -1232,7 +1232,7 @@ dockerfile = { path = "Dockerfile" } ); let inputs = HashMap::new(); - let tree = collect_working_tree(CollectWorkingTreeInput { + let bundle = collect_workflow_bundle(CollectWorkflowBundleInput { cwd: &project, root_location: WorkflowLocation::resolve(&root.join("workflow.toml"), &project) .expect("root workflow should resolve"), @@ -1246,15 +1246,15 @@ dockerfile = { path = "Dockerfile" } source: user_config.to_owned(), }), }) - .expect("working tree should collect"); + .expect("workflow bundle should collect"); assert_eq!( - tree.entrypoint.as_str(), + bundle.entrypoint.as_str(), ".fabro/workflows/root/workflow.fabro" ); - assert_eq!(tree.workflows.len(), 2); + assert_eq!(bundle.workflows.len(), 2); assert_eq!( - logical_contents(&tree) + logical_contents(&bundle) .keys() .map(String::as_str) .collect::>(), @@ -1300,22 +1300,22 @@ dockerfile = { path = "Dockerfile" } let (first_cwd, first_workflow) = fixture(first.path()); let (second_cwd, second_workflow) = fixture(second.path()); - let first_tree = - collect_graph(&first_cwd, &first_workflow).expect("first working tree should collect"); - let second_tree = collect_graph(&second_cwd, &second_workflow) - .expect("second working tree should collect"); + let first_bundle = collect_graph(&first_cwd, &first_workflow) + .expect("first workflow bundle should collect"); + let second_bundle = collect_graph(&second_cwd, &second_workflow) + .expect("second workflow bundle should collect"); - assert_eq!(first_tree.entrypoint, second_tree.entrypoint); + assert_eq!(first_bundle.entrypoint, second_bundle.entrypoint); assert_eq!( - logical_contents(&first_tree), - logical_contents(&second_tree) + logical_contents(&first_bundle), + logical_contents(&second_bundle) ); assert_eq!( - logical_provenance(&first_tree), - logical_provenance(&second_tree) + logical_provenance(&first_bundle), + logical_provenance(&second_bundle) ); assert_eq!( - first_tree.entrypoint.as_str(), + first_bundle.entrypoint.as_str(), "_fabro_external/entrypoint/workflow.fabro" ); } @@ -1389,11 +1389,11 @@ dockerfile = { path = "Dockerfile" } let second = tempfile::tempdir().expect("second temp directory should be created"); let (first_cwd, first_root) = fixture(first.path()); let (second_cwd, second_root) = fixture(second.path()); - let tree = - collect_graph(&first_cwd, &first_root).expect("first working tree should collect"); + let bundle = + collect_graph(&first_cwd, &first_root).expect("first workflow bundle should collect"); let moved = - collect_graph(&second_cwd, &second_root).expect("moved working tree should collect"); - let entrypoint = &tree.entrypoint; + collect_graph(&second_cwd, &second_root).expect("moved workflow bundle should collect"); + let entrypoint = &bundle.entrypoint; let resolved = virtual_reference_path( entrypoint .as_path() @@ -1404,13 +1404,13 @@ dockerfile = { path = "Dockerfile" } .expect("child reference should resolve"); assert_eq!(resolved, Path::new("_fabro_external/child/workflow.fabro")); - assert!(tree.workflows.contains_key( + assert!(bundle.workflows.contains_key( &CollectedPath::try_new(resolved).expect("child path should be canonical") )); - assert_eq!(logical_contents(&tree), logical_contents(&moved)); - assert_eq!(logical_provenance(&tree), logical_provenance(&moved)); - assert_eq!(tree.entrypoint, moved.entrypoint); - for path in logical_contents(&tree).into_keys() { + assert_eq!(logical_contents(&bundle), logical_contents(&moved)); + assert_eq!(logical_provenance(&bundle), logical_provenance(&moved)); + assert_eq!(bundle.entrypoint, moved.entrypoint); + for path in logical_contents(&bundle).into_keys() { assert!(!path.contains(first.path().file_name().unwrap().to_string_lossy().as_ref())); CollectedPath::try_new(path).expect("every collected coordinate should be canonical"); } @@ -1433,15 +1433,15 @@ dockerfile = { path = "Dockerfile" } ); write_file(&cwd.join("prompt.md"), "prompt\n"); - let tree = collect_graph(cwd, &graph).expect("working tree should collect"); - let root = tree + let bundle = collect_graph(cwd, &graph).expect("workflow bundle should collect"); + let root = bundle .workflows - .get(&tree.entrypoint) + .get(&bundle.entrypoint) .expect("root workflow should be present"); assert_eq!(root.files.len(), 1); let assembled = - crate::assemble_current_manifest(tree, cwd).expect("legacy manifest should assemble"); + crate::assemble_current_manifest(bundle, cwd).expect("legacy manifest should assemble"); assert_eq!(assembled.workflows["workflow.fabro"].files.len(), 1); } @@ -1613,7 +1613,7 @@ dockerfile = { path = "Dockerfile" } assert_ne!(first_commit, ahead_commit); collect_graph(&checkout, &checkout.join("workflow.fabro")) - .expect("working tree should collect"); + .expect("workflow bundle should collect"); let origin_commit = origin .find_reference("refs/heads/main") From 2ff54bfc1b37f7c5225fc9bc3da2fd73a31918a9 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 4 Aug 2026 15:41:50 -0400 Subject: [PATCH 04/62] Model bundle collection as workflow bundler --- .../fabro-manifest/src/workflow_bundle.rs | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/lib/components/fabro-manifest/src/workflow_bundle.rs b/lib/components/fabro-manifest/src/workflow_bundle.rs index 3256f7f6e..4c10a86c1 100644 --- a/lib/components/fabro-manifest/src/workflow_bundle.rs +++ b/lib/components/fabro-manifest/src/workflow_bundle.rs @@ -174,7 +174,7 @@ struct DraftWorkflow { files: BTreeMap, } -struct CollectionDraft<'a> { +struct WorkflowBundler<'a> { cwd: PathBuf, inputs: &'a HashMap, documents: Vec, @@ -183,8 +183,8 @@ struct CollectionDraft<'a> { visited_workflows: HashMap, } -impl<'a> CollectionDraft<'a> { - fn new(cwd: &Path, inputs: &'a HashMap) -> Result { +impl<'a> WorkflowBundler<'a> { + fn try_new(cwd: &Path, inputs: &'a HashMap) -> Result { Ok(Self { cwd: normalized_absolute_access_path(cwd)?, inputs, @@ -687,6 +687,16 @@ impl<'a> CollectionDraft<'a> { Ok(document) } + fn seed_config_document( + &mut self, + config: CollectedSourceInput, + role: ComponentRole, + ) -> Result { + let access_path = normalized_absolute_access_path(&config.access_path)?; + let provisional_path = seed_component_path(&access_path, &self.cwd, role)?; + Ok(self.insert_document(&access_path, provisional_path, role, config.source)) + } + fn finish( self, entrypoint: DocumentId, @@ -802,44 +812,37 @@ fn finalize_documents(drafts: Vec) -> Result, ) -> Result { - let mut draft = CollectionDraft::new(input.cwd, input.inputs)?; + let mut bundler = WorkflowBundler::try_new(input.cwd, input.inputs)?; let root_graph_access_path = normalized_absolute_access_path(&input.root_location.graph)?; - let root_graph_path = - seed_component_path(&root_graph_access_path, &draft.cwd, ComponentRole::Workflow)?; + let root_graph_path = seed_component_path( + &root_graph_access_path, + &bundler.cwd, + ComponentRole::Workflow, + )?; let project_config = input .project_config - .map(|config| seed_config_document(&mut draft, config, ComponentRole::ProjectConfig)) + .map(|config| bundler.seed_config_document(config, ComponentRole::ProjectConfig)) .transpose()?; let user_config = input .user_config - .map(|config| seed_config_document(&mut draft, config, ComponentRole::UserConfig)) + .map(|config| bundler.seed_config_document(config, ComponentRole::UserConfig)) .transpose()?; - let entrypoint = draft.collect_workflow_location(&input.root_location, root_graph_path)?; + let entrypoint = bundler.collect_workflow_location(&input.root_location, root_graph_path)?; if let Some(project_config) = project_config { let root_key = - manifest_path_from_absolute(&draft.document(entrypoint).access_path, &draft.cwd)? + manifest_path_from_absolute(&bundler.document(entrypoint).access_path, &bundler.cwd)? .to_string(); - let mut root = draft + let mut root = bundler .workflows .remove(&root_key) .ok_or_else(|| anyhow!("root workflow missing from collected workflow bundle"))?; - draft.collect_config_dockerfile(project_config, &mut root.files)?; - draft.workflows.insert(root_key, root); + bundler.collect_config_dockerfile(project_config, &mut root.files)?; + bundler.workflows.insert(root_key, root); } - draft.finish(entrypoint, project_config, user_config) -} - -fn seed_config_document( - draft: &mut CollectionDraft<'_>, - config: CollectedSourceInput, - role: ComponentRole, -) -> Result { - let access_path = normalized_absolute_access_path(&config.access_path)?; - let provisional_path = seed_component_path(&access_path, &draft.cwd, role)?; - Ok(draft.insert_document(&access_path, provisional_path, role, config.source)) + bundler.finish(entrypoint, project_config, user_config) } fn seed_component_path( From 8c95011fbf74452310ddca46d7e75a54941b9fb8 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 4 Aug 2026 15:49:48 -0400 Subject: [PATCH 05/62] Rename bundler intermediates as records --- .../fabro-manifest/src/workflow_bundle.rs | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/lib/components/fabro-manifest/src/workflow_bundle.rs b/lib/components/fabro-manifest/src/workflow_bundle.rs index 4c10a86c1..6c7d5c0d1 100644 --- a/lib/components/fabro-manifest/src/workflow_bundle.rs +++ b/lib/components/fabro-manifest/src/workflow_bundle.rs @@ -147,7 +147,7 @@ impl ComponentRole { struct DocumentId(usize); #[derive(Clone, Debug)] -struct DraftDocument { +struct DocumentRecord { access_path: PathBuf, provisional_path: PathBuf, component: ComponentRole, @@ -155,31 +155,31 @@ struct DraftDocument { } #[derive(Clone, Debug)] -struct DraftFileReference { +struct FileReferenceRecord { type_: CollectedFileReferenceType, original: String, from_document: Option, } #[derive(Clone, Debug)] -struct DraftFile { +struct FileRecord { document: DocumentId, - reference: DraftFileReference, + reference: FileReferenceRecord, } #[derive(Clone, Debug)] -struct DraftWorkflow { +struct WorkflowRecord { graph: DocumentId, config: Option, - files: BTreeMap, + files: BTreeMap, } struct WorkflowBundler<'a> { cwd: PathBuf, inputs: &'a HashMap, - documents: Vec, + documents: Vec, document_ids: HashMap<(PathBuf, ComponentRole, PathBuf), DocumentId>, - workflows: BTreeMap, + workflows: BTreeMap, visited_workflows: HashMap, } @@ -209,7 +209,7 @@ impl<'a> WorkflowBundler<'a> { } let document = DocumentId(self.documents.len()); - self.documents.push(DraftDocument { + self.documents.push(DocumentRecord { access_path, provisional_path, component, @@ -219,7 +219,7 @@ impl<'a> WorkflowBundler<'a> { document } - fn document(&self, document: DocumentId) -> &DraftDocument { + fn document(&self, document: DocumentId) -> &DocumentRecord { &self.documents[document.0] } @@ -271,7 +271,7 @@ impl<'a> WorkflowBundler<'a> { }) .transpose()?; - let mut workflow = DraftWorkflow { + let mut workflow = WorkflowRecord { graph, config, files: BTreeMap::new(), @@ -308,7 +308,7 @@ impl<'a> WorkflowBundler<'a> { fn collect_workflow_files( &mut self, graph_document_id: DocumentId, - files: &mut BTreeMap, + files: &mut BTreeMap, visited_imports: &mut HashSet, ) -> Result<()> { let graph = parser::parse(&self.document(graph_document_id).source).with_context(|| { @@ -454,7 +454,7 @@ impl<'a> WorkflowBundler<'a> { /// `@`-referenced file (a goal or prompt document). fn collect_bundled_template_includes( &mut self, - files: &mut BTreeMap, + files: &mut BTreeMap, bundled: DocumentId, workflow_template_root: &ManifestPath, from_document: DocumentId, @@ -474,7 +474,7 @@ impl<'a> WorkflowBundler<'a> { fn collect_template_include_files( &mut self, - files: &mut BTreeMap, + files: &mut BTreeMap, source: TemplateSource, source_document: DocumentId, from_document: DocumentId, @@ -522,9 +522,9 @@ impl<'a> WorkflowBundler<'a> { ComponentRole::Workflow, source.content, ); - files.insert(key.clone(), DraftFile { + files.insert(key.clone(), FileRecord { document, - reference: DraftFileReference { + reference: FileReferenceRecord { type_: CollectedFileReferenceType::FileInline, original: key, from_document: Some(from_document), @@ -538,7 +538,7 @@ impl<'a> WorkflowBundler<'a> { &self, source_path: &ManifestPath, closure: &TemplateDependencyClosure, - files: &BTreeMap, + files: &BTreeMap, from_document: DocumentId, ) -> Result<()> { let Some(source) = closure.sources.get(source_path) else { @@ -576,7 +576,7 @@ impl<'a> WorkflowBundler<'a> { fn collect_config_dockerfile( &mut self, config: DocumentId, - files: &mut BTreeMap, + files: &mut BTreeMap, ) -> Result<()> { let layer = self .document(config) @@ -611,7 +611,7 @@ impl<'a> WorkflowBundler<'a> { fn collect_environment_dockerfile( &mut self, - files: &mut BTreeMap, + files: &mut BTreeMap, base_dir: &Path, config: DocumentId, image: Option<&EnvironmentImageLayer>, @@ -633,7 +633,7 @@ impl<'a> WorkflowBundler<'a> { fn collect_bundled_file( &mut self, - files: &mut BTreeMap, + files: &mut BTreeMap, base_dir: &Path, reference: &str, reference_type: CollectedFileReferenceType, @@ -676,9 +676,9 @@ impl<'a> WorkflowBundler<'a> { self.document(from_document).component, source, ); - files.insert(key, DraftFile { + files.insert(key, FileRecord { document, - reference: DraftFileReference { + reference: FileReferenceRecord { type_: reference_type, original: reference.to_owned(), from_document: Some(from_document), @@ -742,43 +742,43 @@ impl<'a> WorkflowBundler<'a> { } } -/// Finalizes draft documents into collected documents, rejecting conflicting +/// Finalizes document records into collected documents, rejecting conflicting /// physical aliases and virtual-coordinate collisions. -fn finalize_documents(drafts: Vec) -> Result> { +fn finalize_documents(records: Vec) -> Result> { let mut deficits = BTreeMap::::new(); - for draft in &drafts { - let deficit = leading_parent_count(&draft.provisional_path); + for record in &records { + let deficit = leading_parent_count(&record.provisional_path); deficits - .entry(draft.component) + .entry(record.component) .and_modify(|current| *current = (*current).max(deficit)) .or_insert(deficit); } - let paths = drafts + let paths = records .iter() - .map(|draft| { + .map(|record| { finalize_component_path( - &draft.provisional_path, - draft.component, - deficits.get(&draft.component).copied().unwrap_or_default(), + &record.provisional_path, + record.component, + deficits.get(&record.component).copied().unwrap_or_default(), ) }) .collect::>>()?; - let mut order = (0..drafts.len()).collect::>(); + let mut order = (0..records.len()).collect::>(); order.sort_by(|left, right| { paths[*left] .cmp(&paths[*right]) - .then_with(|| drafts[*left].access_path.cmp(&drafts[*right].access_path)) + .then_with(|| records[*left].access_path.cmp(&records[*right].access_path)) }); let mut physical_to_virtual = BTreeMap::::new(); let mut virtual_to_physical = BTreeMap::::new(); for index in order { - let physical = std::fs::canonicalize(&drafts[index].access_path).with_context(|| { + let physical = std::fs::canonicalize(&records[index].access_path).with_context(|| { format!( "failed to identify collected file {}", - drafts[index].access_path.display() + records[index].access_path.display() ) })?; let path = &paths[index]; @@ -798,13 +798,13 @@ fn finalize_documents(drafts: Vec) -> Result Result { +fn stable_template_root(document: &DocumentRecord, source: &TemplateSource) -> Result { let relative = source .path .as_path() @@ -1489,14 +1489,14 @@ dockerfile = { path = "Dockerfile" } let second = temp.path().join("second.md"); write_file(&first, "first\n"); write_file(&second, "second\n"); - let draft = |access_path| DraftDocument { + let record = |access_path| DocumentRecord { access_path, provisional_path: PathBuf::from("shared.md"), component: ComponentRole::Workflow, source: String::new(), }; - let error = finalize_documents(vec![draft(first), draft(second)]) + let error = finalize_documents(vec![record(first), record(second)]) .expect_err("virtual collision should be rejected"); assert!( @@ -1510,7 +1510,7 @@ dockerfile = { path = "Dockerfile" } #[test] fn namespace_identity_errors_keep_the_io_error_in_the_source_chain() { let temp = tempfile::tempdir().expect("temp directory should be created"); - let draft = DraftDocument { + let record = DocumentRecord { access_path: temp.path().join("missing.md"), provisional_path: PathBuf::from("missing.md"), component: ComponentRole::Workflow, @@ -1518,7 +1518,7 @@ dockerfile = { path = "Dockerfile" } }; let error = - finalize_documents(vec![draft]).expect_err("missing physical identity should fail"); + finalize_documents(vec![record]).expect_err("missing physical identity should fail"); assert!( error From 227e5204002132659ebdf6a3ec07127d5de923de Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 6 Aug 2026 16:21:10 -0400 Subject: [PATCH 06/62] Simplify workflow bundling extraction --- lib/components/fabro-manifest/src/lib.rs | 239 +-- .../fabro-manifest/src/workflow_bundle.rs | 1628 ----------------- .../fabro-manifest/src/workflow_bundler.rs | 618 +++++++ 3 files changed, 717 insertions(+), 1768 deletions(-) delete mode 100644 lib/components/fabro-manifest/src/workflow_bundle.rs create mode 100644 lib/components/fabro-manifest/src/workflow_bundler.rs diff --git a/lib/components/fabro-manifest/src/lib.rs b/lib/components/fabro-manifest/src/lib.rs index 762265d13..eff4ff2fc 100644 --- a/lib/components/fabro-manifest/src/lib.rs +++ b/lib/components/fabro-manifest/src/lib.rs @@ -3,10 +3,10 @@ reason = "CLI manifest builder: sync file I/O building install manifests" )] -mod workflow_bundle; +mod workflow_bundler; use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use anyhow::{Context, Result, anyhow}; use fabro_api::types; @@ -21,17 +21,13 @@ use fabro_graphviz::graph::AttrValue; use fabro_graphviz::parser; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{ApprovalMode, ResolvedGoalSource, ResolvedRunGoal, RunMode}; -use fabro_types::{DirtyStatus, GitContext, WorkflowSettings}; +use fabro_types::{DirtyStatus, GitContext, ManifestPath, WorkflowSettings}; use fabro_workflow::git::{ GitSyncStatus, branch_needs_push, head_sha, push_branch_noninteractive, sync_status, }; use fabro_workflow::static_reference::ReferenceKind; -use crate::workflow_bundle::{ - CollectWorkflowBundleInput, CollectedDocument, CollectedFileReferenceType, - CollectedSourceInput, CollectedWorkflowBundle, manifest_path_from_absolute, - normalize_absolute_path, -}; +use crate::workflow_bundler::WorkflowBundler; #[derive(Debug, Default)] pub struct ManifestBuildInput { @@ -138,13 +134,14 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { } let project_config = discover_project_config(&root_location.dir)?; let project_config_source = project_config - .as_deref() - .map(read_source_input) + .as_ref() + .map(|path| { + let source = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + let manifest_path = manifest_path_from_absolute(path, &input.cwd)?; + Ok::<_, anyhow::Error>((path.clone(), manifest_path, source)) + }) .transpose()?; - let user_settings_path = input - .user_settings_path - .as_deref() - .filter(|path| path.is_file()); let mut workflow_settings_builder = WorkflowSettingsBuilder::new() .server_manifest_defaults(RunLayer::default(), input.environment_defaults.clone()); @@ -160,7 +157,11 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { if let Some(path) = project_config.as_ref() { workflow_settings_builder = workflow_settings_builder.project_file(path)?; } - if let Some(path) = user_settings_path { + if let Some(path) = input + .user_settings_path + .as_ref() + .filter(|path| path.is_file()) + { workflow_settings_builder = workflow_settings_builder.user_file(path)?; } let mut workflow_settings = workflow_settings_builder @@ -168,15 +169,35 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { .context("failed to resolve manifest settings")?; workflow_settings.run.inputs.extend(input.input_overrides); let target_path = root_location.graph.clone(); - let user_config_source = user_settings_path.map(read_source_input).transpose()?; - let collected = workflow_bundle::collect_workflow_bundle(CollectWorkflowBundleInput { - cwd: &input.cwd, - root_location, - inputs: &workflow_settings.run.inputs, - project_config: project_config_source, - user_config: user_config_source, - })?; - let assembled = assemble_current_manifest(collected, &input.cwd)?; + let target_manifest_path = manifest_path_from_absolute(&target_path, &input.cwd)?; + let target_key = target_manifest_path.to_string(); + let project_config_input = project_config_source + .as_ref() + .map(|(_, path, source)| (path, source.as_str())); + let workflows = WorkflowBundler::new(&input.cwd, &workflow_settings.run.inputs) + .bundle(&root_location, project_config_input)?; + let root_source = workflows + .get(&target_key) + .map(|workflow| workflow.source.clone()) + .ok_or_else(|| anyhow!("root workflow missing from manifest bundle"))?; + + let mut configs = Vec::new(); + if let Some((path, _, source)) = project_config_source { + configs.push(types::ManifestConfig { + path: Some(path.display().to_string()), + source: Some(source), + type_: types::ManifestConfigType::Project, + }); + } + if let Some(path) = input.user_settings_path.filter(|path| path.is_file()) { + let source = std::fs::read_to_string(&path) + .with_context(|| format!("Failed to read {}", path.display()))?; + configs.push(types::ManifestConfig { + path: Some(path.display().to_string()), + source: Some(source), + type_: types::ManifestConfigType::User, + }); + } let working_directory = project::resolve_working_directory_from_run(&workflow_settings.run, &input.cwd); @@ -184,7 +205,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { let goal = resolve_manifest_goal( input.run_overrides.as_ref(), &workflow_settings, - &assembled.root_source, + &root_source, &target_path, &working_directory, )?; @@ -196,118 +217,20 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { Ok(BuiltManifest { manifest: types::RunManifest { args, + configs, cwd: input.cwd.display().to_string(), git, goal, parent_id: None, title: None, - target: types::ManifestTarget { - path: assembled.target_key, - }, + target: types::ManifestTarget { path: target_key }, version: 1, - workflows: assembled.workflows, - configs: assembled.configs, + workflows, }, target_path, }) } -struct AssembledCurrentManifest { - target_key: String, - root_source: String, - workflows: HashMap, - configs: Vec, -} - -fn assemble_current_manifest( - collected: CollectedWorkflowBundle, - cwd: &Path, -) -> Result { - let root = collected - .workflows - .get(&collected.entrypoint) - .ok_or_else(|| anyhow!("root workflow missing from collected workflow bundle"))?; - let target_key = manifest_path_from_absolute(&root.graph.access_path, cwd)?.to_string(); - let root_source = root.graph.source.clone(); - - let mut workflows = HashMap::new(); - for workflow in collected.workflows.into_values() { - let graph_key = manifest_path_from_absolute(&workflow.graph.access_path, cwd)?.to_string(); - let config = workflow - .config - .map(|config| { - Ok::<_, anyhow::Error>(types::ManifestWorkflowConfig { - path: manifest_path_from_absolute(&config.access_path, cwd)?.to_string(), - source: config.source, - }) - }) - .transpose()?; - let mut files = HashMap::new(); - for file in workflow.files.into_values() { - let key = manifest_path_from_absolute(&file.document.access_path, cwd)?.to_string(); - let from = file - .reference - .from_access_path - .as_deref() - .map(|path| manifest_path_from_absolute(path, cwd).map(|path| path.to_string())) - .transpose()?; - let type_ = match file.reference.type_ { - CollectedFileReferenceType::FileInline => types::ManifestFileRefType::FileInline, - CollectedFileReferenceType::Import => types::ManifestFileRefType::Import, - CollectedFileReferenceType::Dockerfile => types::ManifestFileRefType::Dockerfile, - }; - files.insert(key, types::ManifestFileEntry { - content: file.document.source, - ref_: types::ManifestFileRef { - from, - original: file.reference.original, - type_, - }, - }); - } - workflows.insert(graph_key, types::ManifestWorkflow { - source: workflow.graph.source, - config, - files, - }); - } - - let mut configs = Vec::new(); - if let Some(config) = collected.project_config { - configs.push(manifest_config(config, types::ManifestConfigType::Project)); - } - if let Some(config) = collected.user_config { - configs.push(manifest_config(config, types::ManifestConfigType::User)); - } - - Ok(AssembledCurrentManifest { - target_key, - root_source, - workflows, - configs, - }) -} - -fn manifest_config( - document: CollectedDocument, - type_: types::ManifestConfigType, -) -> types::ManifestConfig { - types::ManifestConfig { - path: Some(document.access_path.display().to_string()), - source: Some(document.source), - type_, - } -} - -fn read_source_input(path: &Path) -> Result { - let source = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read {}", path.display()))?; - Ok(CollectedSourceInput { - access_path: path.to_path_buf(), - source, - }) -} - fn resolve_manifest_goal( run_overrides: Option<&RunLayer>, settings: &WorkflowSettings, @@ -473,6 +396,32 @@ fn push_manifest_branch_best_effort( let _ = push_branch_noninteractive(repo_path, "origin", branch); } +fn normalize_absolute_path(base_dir: &Path, reference: &str) -> Option { + let path = Path::new(reference); + if path.is_absolute() || reference.starts_with('~') { + return None; + } + + let mut normalized = PathBuf::new(); + for component in base_dir.join(path).components() { + match component { + Component::CurDir => {} + Component::Normal(part) => normalized.push(part), + Component::ParentDir => { + normalized.pop(); + } + Component::RootDir => normalized.push(Path::new("/")), + Component::Prefix(prefix) => normalized.push(prefix.as_os_str()), + } + } + Some(normalized) +} + +fn manifest_path_from_absolute(path: &Path, cwd: &Path) -> Result { + ManifestPath::from_absolute(path, cwd) + .ok_or_else(|| anyhow!("Failed to compute manifest path for {}", path.display())) +} + pub fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool { args.auto_approve.is_none() && args.dry_run.is_none() @@ -699,19 +648,19 @@ graph = "workflow.fabro" ), ".fabro/workflows/root/prompts/deep.md": file( "deep\n", - ".fabro/workflows/root/workflow.fabro", + ".fabro/workflows/root/prompts/plan.md", ".fabro/workflows/root/prompts/deep.md", "file_inline", ), ".fabro/workflows/root/prompts/helpers.md": file( helpers, - ".fabro/workflows/root/workflow.fabro", + ".fabro/workflows/root/prompts/plan.md", ".fabro/workflows/root/prompts/helpers.md", "file_inline", ), ".fabro/workflows/root/prompts/partial.md": file( "partial\n", - ".fabro/workflows/root/workflow.fabro", + ".fabro/workflows/root/prompts/plan.md", ".fabro/workflows/root/prompts/partial.md", "file_inline", ), @@ -1057,17 +1006,26 @@ graph = "workflow.fabro" .unwrap(); let root = &built.manifest.workflows[".fabro/workflows/demo/workflow.fabro"]; - assert!( - root.files - .contains_key(".fabro/workflows/demo/prompts/goal.tpl.md") + assert_eq!( + root.files[".fabro/workflows/demo/prompts/goal.tpl.md"] + .ref_ + .from + .as_deref(), + Some(".fabro/workflows/demo/prompts/goal.md") ); - assert!( - root.files - .contains_key(".fabro/workflows/demo/prompts/plan.tpl.md") + assert_eq!( + root.files[".fabro/workflows/demo/prompts/plan.tpl.md"] + .ref_ + .from + .as_deref(), + Some(".fabro/workflows/demo/prompts/plan.md") ); - assert!( - root.files - .contains_key(".fabro/workflows/demo/inline.tpl.md") + assert_eq!( + root.files[".fabro/workflows/demo/inline.tpl.md"] + .ref_ + .from + .as_deref(), + Some(".fabro/workflows/demo/workflow.fabro") ); } @@ -1166,8 +1124,9 @@ graph = "workflow.fabro" .unwrap_err(); assert!( - err.chain() - .any(|cause| cause.to_string().contains("dynamic template dependency")), + err.chain().any(|cause| cause + .downcast_ref::() + .is_some()), "unexpected error: {err:#}" ); } diff --git a/lib/components/fabro-manifest/src/workflow_bundle.rs b/lib/components/fabro-manifest/src/workflow_bundle.rs deleted file mode 100644 index 6c7d5c0d1..000000000 --- a/lib/components/fabro-manifest/src/workflow_bundle.rs +++ /dev/null @@ -1,1628 +0,0 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::fmt; -use std::path::{Component, Path, PathBuf}; -use std::sync::Arc; - -use anyhow::{Context as _, Result, anyhow, bail}; -use fabro_config::project::WorkflowLocation; -use fabro_config::{EnvironmentDockerfileLayer, EnvironmentImageLayer, SettingsLayer}; -use fabro_graphviz::graph::AttrValue; -use fabro_graphviz::parser; -use fabro_template::{ - BundleTemplateStore, FilesystemTemplateStore, RecordingTemplateStore, TemplateContext, - TemplateDependencyClosure, TemplateRenderMode, TemplateSource, - discover_static_dependency_closure, render_source, -}; -use fabro_types::ManifestPath; -use fabro_workflow::static_reference::{ - AttributeScope, ReferenceKind, reference_kind_for_attribute, -}; - -pub(super) struct CollectWorkflowBundleInput<'a> { - pub(super) cwd: &'a Path, - pub(super) root_location: WorkflowLocation, - pub(super) inputs: &'a HashMap, - pub(super) project_config: Option, - pub(super) user_config: Option, -} - -pub(super) struct CollectedSourceInput { - pub(super) access_path: PathBuf, - pub(super) source: String, -} - -#[derive(Clone, Debug)] -pub(super) struct CollectedWorkflowBundle { - pub(super) entrypoint: CollectedPath, - pub(super) workflows: BTreeMap, - pub(super) project_config: Option, - pub(super) user_config: Option, -} - -#[derive(Clone, Debug)] -pub(super) struct CollectedWorkflow { - pub(super) graph: CollectedDocument, - pub(super) config: Option, - pub(super) files: BTreeMap, -} - -#[derive(Clone, Debug)] -pub(super) struct CollectedDocument { - pub(super) access_path: PathBuf, - pub(super) path: CollectedPath, - pub(super) source: String, -} - -#[derive(Clone, Debug)] -pub(super) struct CollectedFile { - pub(super) document: CollectedDocument, - pub(super) reference: CollectedFileReference, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum CollectedFileReferenceType { - FileInline, - Import, - Dockerfile, -} - -#[derive(Clone, Debug)] -pub(super) struct CollectedFileReference { - pub(super) type_: CollectedFileReferenceType, - pub(super) original: String, - pub(super) from_access_path: Option, -} - -/// A canonical virtual coordinate inside one collected workflow bundle. -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub(super) struct CollectedPath(String); - -impl CollectedPath { - fn try_new(path: impl AsRef) -> Result { - let path = path.as_ref(); - let value = path - .to_str() - .ok_or_else(|| anyhow!("collected path is not valid UTF-8"))?; - - if value.is_empty() { - bail!("collected path must not be empty"); - } - if path.is_absolute() { - bail!("collected path must be relative: {value}"); - } - if value.contains('\\') { - bail!("collected path must use forward slashes: {value}"); - } - if value.chars().any(char::is_control) { - bail!("collected path contains a control character"); - } - let bytes = value.as_bytes(); - if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { - bail!("collected path must not use a Windows drive prefix: {value}"); - } - if value - .split('/') - .any(|segment| segment.is_empty() || segment == "." || segment == "..") - { - bail!("collected path contains an empty, dot, or parent component: {value}"); - } - - Ok(Self(value.to_owned())) - } - - pub(super) fn as_str(&self) -> &str { - &self.0 - } - - #[cfg(test)] - fn as_path(&self) -> &Path { - Path::new(&self.0) - } -} - -impl fmt::Display for CollectedPath { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(self.as_str()) - } -} - -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -enum ComponentRole { - Workflow, - ProjectConfig, - UserConfig, -} - -impl ComponentRole { - const fn label(self) -> &'static str { - match self { - Self::Workflow => "workflow", - Self::ProjectConfig => "project_config", - Self::UserConfig => "user_config", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -struct DocumentId(usize); - -#[derive(Clone, Debug)] -struct DocumentRecord { - access_path: PathBuf, - provisional_path: PathBuf, - component: ComponentRole, - source: String, -} - -#[derive(Clone, Debug)] -struct FileReferenceRecord { - type_: CollectedFileReferenceType, - original: String, - from_document: Option, -} - -#[derive(Clone, Debug)] -struct FileRecord { - document: DocumentId, - reference: FileReferenceRecord, -} - -#[derive(Clone, Debug)] -struct WorkflowRecord { - graph: DocumentId, - config: Option, - files: BTreeMap, -} - -struct WorkflowBundler<'a> { - cwd: PathBuf, - inputs: &'a HashMap, - documents: Vec, - document_ids: HashMap<(PathBuf, ComponentRole, PathBuf), DocumentId>, - workflows: BTreeMap, - visited_workflows: HashMap, -} - -impl<'a> WorkflowBundler<'a> { - fn try_new(cwd: &Path, inputs: &'a HashMap) -> Result { - Ok(Self { - cwd: normalized_absolute_access_path(cwd)?, - inputs, - documents: Vec::new(), - document_ids: HashMap::new(), - workflows: BTreeMap::new(), - visited_workflows: HashMap::new(), - }) - } - - fn insert_document( - &mut self, - access_path: &Path, - provisional_path: PathBuf, - component: ComponentRole, - source: String, - ) -> DocumentId { - let access_path = lexically_normalize_access_path(access_path); - let key = (access_path.clone(), component, provisional_path.clone()); - if let Some(document) = self.document_ids.get(&key) { - return *document; - } - - let document = DocumentId(self.documents.len()); - self.documents.push(DocumentRecord { - access_path, - provisional_path, - component, - source, - }); - self.document_ids.insert(key, document); - document - } - - fn document(&self, document: DocumentId) -> &DocumentRecord { - &self.documents[document.0] - } - - fn collect_workflow_location( - &mut self, - location: &WorkflowLocation, - provisional_graph_path: PathBuf, - ) -> Result { - let graph_access_path = normalized_absolute_access_path(&location.graph)?; - let graph_manifest_path = manifest_path_from_absolute(&graph_access_path, &self.cwd)?; - let graph_key = graph_manifest_path.to_string(); - if let Some(document) = self.visited_workflows.get(&graph_key) { - return Ok(*document); - } - - let graph_source = std::fs::read_to_string(&graph_access_path) - .with_context(|| format!("Failed to read {}", graph_access_path.display()))?; - let graph = self.insert_document( - &graph_access_path, - provisional_graph_path, - ComponentRole::Workflow, - graph_source, - ); - self.visited_workflows.insert(graph_key.clone(), graph); - - let config = location - .toml - .as_ref() - .map(|config_path| { - let access_path = normalized_absolute_access_path(config_path)?; - let source = std::fs::read_to_string(&access_path) - .with_context(|| format!("Failed to read {}", access_path.display()))?; - let file_name = access_path.file_name().ok_or_else(|| { - anyhow!( - "workflow config has no file name: {}", - access_path.display() - ) - })?; - let provisional_path = virtual_sibling_path( - &self.document(graph).provisional_path, - Path::new(file_name), - )?; - Ok::<_, anyhow::Error>(self.insert_document( - &access_path, - provisional_path, - ComponentRole::Workflow, - source, - )) - }) - .transpose()?; - - let mut workflow = WorkflowRecord { - graph, - config, - files: BTreeMap::new(), - }; - if let Some(config) = config { - self.collect_config_dockerfile(config, &mut workflow.files)?; - } - self.collect_workflow_files(graph, &mut workflow.files, &mut HashSet::new())?; - self.workflows.insert(graph_key, workflow); - - Ok(graph) - } - - fn collect_workflow_entry( - &mut self, - workflow: &Path, - resolve_from: &Path, - provisional_graph_path: PathBuf, - ) -> Result { - let normalized_workflow = if workflow.extension().is_some() && workflow.is_relative() { - normalize_absolute_path(resolve_from, &workflow.to_string_lossy()).ok_or_else(|| { - anyhow!( - "unsupported manifest workflow reference: {}", - workflow.display() - ) - })? - } else { - workflow.to_path_buf() - }; - let location = WorkflowLocation::resolve(&normalized_workflow, resolve_from)?; - self.collect_workflow_location(&location, provisional_graph_path) - } - - fn collect_workflow_files( - &mut self, - graph_document_id: DocumentId, - files: &mut BTreeMap, - visited_imports: &mut HashSet, - ) -> Result<()> { - let graph = parser::parse(&self.document(graph_document_id).source).with_context(|| { - format!( - "Failed to parse {}", - self.document(graph_document_id).access_path.display() - ) - })?; - let graph_access_path = self.document(graph_document_id).access_path.clone(); - let workflow_base_dir = graph_access_path.parent().unwrap_or_else(|| Path::new(".")); - let graph_manifest_path = manifest_path_from_absolute(&graph_access_path, &self.cwd)?; - let workflow_template_root = manifest_parent_or_dot(&graph_manifest_path)?; - - if let Some(goal_reference) = graph.attrs.get("goal").and_then(AttrValue::as_str) { - if let Some(reference) = goal_reference.strip_prefix('@') { - let bundled = self.collect_bundled_file( - files, - workflow_base_dir, - reference, - CollectedFileReferenceType::FileInline, - manifest_attr_reference_kind(AttributeScope::Graph, "goal", goal_reference)?, - graph_document_id, - )?; - self.collect_bundled_template_includes( - files, - bundled, - &workflow_template_root, - graph_document_id, - )?; - } else { - self.collect_template_include_files( - files, - TemplateSource::new( - graph_manifest_path.clone(), - workflow_template_root.clone(), - goal_reference.to_owned(), - ), - graph_document_id, - graph_document_id, - )?; - } - } - - let mut nodes = graph.nodes.values().collect::>(); - nodes.sort_by(|left, right| left.id.cmp(&right.id)); - for node in nodes { - if let Some(prompt_reference) = node.attrs.get("prompt").and_then(AttrValue::as_str) { - if !prompt_reference.starts_with('@') { - self.collect_template_include_files( - files, - TemplateSource::new( - graph_manifest_path.clone(), - workflow_template_root.clone(), - prompt_reference.to_owned(), - ), - graph_document_id, - graph_document_id, - )?; - } - } - - let mut attributes = node.attrs.iter().collect::>(); - attributes.sort_by_key(|(name, _)| *name); - for (name, value) in attributes { - let Some(value) = value.as_str() else { - continue; - }; - let Some(ReferenceKind::FileInline) = - reference_kind_for_attribute(AttributeScope::Node, name, value) - else { - continue; - }; - let reference = value.strip_prefix('@').ok_or_else(|| { - anyhow!("file inline reference must start with '@': {name}={value}") - })?; - let bundled = self.collect_bundled_file( - files, - workflow_base_dir, - reference, - CollectedFileReferenceType::FileInline, - ReferenceKind::FileInline, - graph_document_id, - )?; - - if name == "prompt" { - self.collect_bundled_template_includes( - files, - bundled, - &workflow_template_root, - graph_document_id, - )?; - } - } - - if let Some(import_reference) = node.attrs.get("import").and_then(AttrValue::as_str) { - let imported = self.collect_bundled_file( - files, - workflow_base_dir, - import_reference, - CollectedFileReferenceType::Import, - manifest_attr_reference_kind(AttributeScope::Node, "import", import_reference)?, - graph_document_id, - )?; - let import_key = - manifest_path_from_absolute(&self.document(imported).access_path, &self.cwd)? - .to_string(); - if visited_imports.insert(import_key) { - self.collect_workflow_files(imported, files, visited_imports)?; - } - } - - if let Some(child_reference) = node - .attrs - .get("stack.child_workflow") - .and_then(AttrValue::as_str) - { - manifest_attr_reference_kind( - AttributeScope::Node, - "stack.child_workflow", - child_reference, - )? - .validate(child_reference) - .map_err(anyhow::Error::new)?; - let child_provisional_path = virtual_reference_path( - self.document(graph_document_id) - .provisional_path - .parent() - .unwrap_or_else(|| Path::new(".")), - child_reference, - )?; - self.collect_workflow_entry( - Path::new(child_reference), - workflow_base_dir, - child_provisional_path, - )?; - } - } - - Ok(()) - } - - /// Collects the template dependency closure of an already-bundled - /// `@`-referenced file (a goal or prompt document). - fn collect_bundled_template_includes( - &mut self, - files: &mut BTreeMap, - bundled: DocumentId, - workflow_template_root: &ManifestPath, - from_document: DocumentId, - ) -> Result<()> { - let document = self.document(bundled); - let source = document.source.clone(); - let bundled_manifest_path = manifest_path_from_absolute(&document.access_path, &self.cwd)?; - let template_root = - template_root_for_bundled_file(&bundled_manifest_path, workflow_template_root)?; - self.collect_template_include_files( - files, - TemplateSource::new(bundled_manifest_path, template_root, source), - bundled, - from_document, - ) - } - - fn collect_template_include_files( - &mut self, - files: &mut BTreeMap, - source: TemplateSource, - source_document: DocumentId, - from_document: DocumentId, - ) -> Result<()> { - let source_path = source.path.clone(); - let stable_root = stable_template_root(self.document(source_document), &source)?; - let store = FilesystemTemplateStore::new(self.cwd.clone()); - let closure = discover_static_dependency_closure([source], &store) - .context("failed to discover template dependencies")?; - self.verify_recorded_template_dependencies(&source_path, &closure, files, from_document)?; - - let mut sources = closure.sources.into_iter().collect::>(); - sources.sort_by_key(|(path, _)| path.to_string()); - for (path, source) in sources { - if path == source_path { - continue; - } - let relative = path - .as_path() - .strip_prefix(source.root.as_path()) - .map_err(|_| { - anyhow!( - "template path {path} is outside its logical root {}", - source.root - ) - })?; - let provisional_path = normalize_relative_path(&stable_root.join(relative))?; - let key = path.to_string(); - if let Some(existing) = files.get(&key) { - let existing_path = &self.document(existing.document).provisional_path; - if existing_path != &provisional_path { - bail!( - "collected file has conflicting logical coordinates `{}` and `{}`", - existing_path.display(), - provisional_path.display() - ); - } - continue; - } - - let access_path = lexically_normalize_access_path(&self.cwd.join(path.as_path())); - let document = self.insert_document( - &access_path, - provisional_path, - ComponentRole::Workflow, - source.content, - ); - files.insert(key.clone(), FileRecord { - document, - reference: FileReferenceRecord { - type_: CollectedFileReferenceType::FileInline, - original: key, - from_document: Some(from_document), - }, - }); - } - Ok(()) - } - - fn verify_recorded_template_dependencies( - &self, - source_path: &ManifestPath, - closure: &TemplateDependencyClosure, - files: &BTreeMap, - from_document: DocumentId, - ) -> Result<()> { - let Some(source) = closure.sources.get(source_path) else { - return Ok(()); - }; - let mut bundled_files = closure - .sources - .iter() - .map(|(path, source)| (path.clone(), source.content.clone())) - .collect::>(); - for (key, file) in files { - let path = ManifestPath::from_wire(key) - .ok_or_else(|| anyhow!("invalid collected file key: {key}"))?; - bundled_files.insert(path, self.document(file.document).source.clone()); - } - let allowed = bundled_files.keys().cloned().collect(); - let store = - RecordingTemplateStore::with_allowed(BundleTemplateStore::new(bundled_files), allowed); - let context = TemplateContext::for_input_scan(self.inputs.clone()); - render_source( - source, - &context, - Arc::new(store), - TemplateRenderMode::Lenient, - ) - .with_context(|| { - let from = - manifest_path_from_absolute(&self.document(from_document).access_path, &self.cwd) - .map_or_else(|_| source_path.to_string(), |path| path.to_string()); - format!("failed to verify template dependencies for {from}") - })?; - Ok(()) - } - - fn collect_config_dockerfile( - &mut self, - config: DocumentId, - files: &mut BTreeMap, - ) -> Result<()> { - let layer = self - .document(config) - .source - .parse::() - .context("Failed to parse run config TOML")?; - let config_access_path = self.document(config).access_path.clone(); - let base_dir = config_access_path - .parent() - .unwrap_or_else(|| Path::new(".")); - - let mut environments = layer.environments.iter().collect::>(); - environments.sort_by_key(|(name, _)| *name); - for (_, environment) in environments { - self.collect_environment_dockerfile( - files, - base_dir, - config, - environment.image.as_ref(), - )?; - } - if let Some(run_environment) = layer.run.as_ref().and_then(|run| run.environment.as_ref()) { - self.collect_environment_dockerfile( - files, - base_dir, - config, - run_environment.image.as_ref(), - )?; - } - Ok(()) - } - - fn collect_environment_dockerfile( - &mut self, - files: &mut BTreeMap, - base_dir: &Path, - config: DocumentId, - image: Option<&EnvironmentImageLayer>, - ) -> Result<()> { - let dockerfile = image.and_then(|image| image.dockerfile.as_ref()); - let Some(EnvironmentDockerfileLayer::Path { path }) = dockerfile else { - return Ok(()); - }; - self.collect_bundled_file( - files, - base_dir, - path, - CollectedFileReferenceType::Dockerfile, - ReferenceKind::Dockerfile, - config, - )?; - Ok(()) - } - - fn collect_bundled_file( - &mut self, - files: &mut BTreeMap, - base_dir: &Path, - reference: &str, - reference_type: CollectedFileReferenceType, - reference_kind: ReferenceKind, - from_document: DocumentId, - ) -> Result { - reference_kind - .validate(reference) - .map_err(anyhow::Error::new)?; - - let access_path = normalize_absolute_path(base_dir, reference) - .ok_or_else(|| anyhow!("unsupported manifest reference: {reference}"))?; - let manifest_path = manifest_path_from_absolute(&access_path, &self.cwd)?; - let key = manifest_path.to_string(); - let provisional_path = virtual_reference_path( - self.document(from_document) - .provisional_path - .parent() - .unwrap_or_else(|| Path::new(".")), - reference, - )?; - - if let Some(existing) = files.get(&key) { - let existing_path = &self.document(existing.document).provisional_path; - if existing_path != &provisional_path { - bail!( - "collected file has conflicting logical coordinates `{}` and `{}`", - existing_path.display(), - provisional_path.display() - ); - } - return Ok(existing.document); - } - - let source = std::fs::read_to_string(&access_path) - .with_context(|| format!("Failed to read {}", access_path.display()))?; - let document = self.insert_document( - &access_path, - provisional_path, - self.document(from_document).component, - source, - ); - files.insert(key, FileRecord { - document, - reference: FileReferenceRecord { - type_: reference_type, - original: reference.to_owned(), - from_document: Some(from_document), - }, - }); - Ok(document) - } - - fn seed_config_document( - &mut self, - config: CollectedSourceInput, - role: ComponentRole, - ) -> Result { - let access_path = normalized_absolute_access_path(&config.access_path)?; - let provisional_path = seed_component_path(&access_path, &self.cwd, role)?; - Ok(self.insert_document(&access_path, provisional_path, role, config.source)) - } - - fn finish( - self, - entrypoint: DocumentId, - project_config: Option, - user_config: Option, - ) -> Result { - let documents = finalize_documents(self.documents)?; - let mut workflows = BTreeMap::new(); - for workflow in self.workflows.into_values() { - let graph = documents[workflow.graph.0].clone(); - let config = workflow - .config - .map(|document| documents[document.0].clone()); - let mut files = BTreeMap::new(); - for file in workflow.files.into_values() { - let document = documents[file.document.0].clone(); - let from_access_path = file - .reference - .from_document - .map(|from| documents[from.0].access_path.clone()); - files.insert(document.path.clone(), CollectedFile { - document, - reference: CollectedFileReference { - type_: file.reference.type_, - original: file.reference.original, - from_access_path, - }, - }); - } - workflows.insert(graph.path.clone(), CollectedWorkflow { - graph, - config, - files, - }); - } - - Ok(CollectedWorkflowBundle { - entrypoint: documents[entrypoint.0].path.clone(), - workflows, - project_config: project_config.map(|document| documents[document.0].clone()), - user_config: user_config.map(|document| documents[document.0].clone()), - }) - } -} - -/// Finalizes document records into collected documents, rejecting conflicting -/// physical aliases and virtual-coordinate collisions. -fn finalize_documents(records: Vec) -> Result> { - let mut deficits = BTreeMap::::new(); - for record in &records { - let deficit = leading_parent_count(&record.provisional_path); - deficits - .entry(record.component) - .and_modify(|current| *current = (*current).max(deficit)) - .or_insert(deficit); - } - - let paths = records - .iter() - .map(|record| { - finalize_component_path( - &record.provisional_path, - record.component, - deficits.get(&record.component).copied().unwrap_or_default(), - ) - }) - .collect::>>()?; - - let mut order = (0..records.len()).collect::>(); - order.sort_by(|left, right| { - paths[*left] - .cmp(&paths[*right]) - .then_with(|| records[*left].access_path.cmp(&records[*right].access_path)) - }); - - let mut physical_to_virtual = BTreeMap::::new(); - let mut virtual_to_physical = BTreeMap::::new(); - for index in order { - let physical = std::fs::canonicalize(&records[index].access_path).with_context(|| { - format!( - "failed to identify collected file {}", - records[index].access_path.display() - ) - })?; - let path = &paths[index]; - if let Some(existing) = physical_to_virtual.get(&physical) { - if existing != path { - bail!( - "one physical file has conflicting collected coordinates `{existing}` and `{path}`" - ); - } - } - if let Some(existing) = virtual_to_physical.get(path) { - if existing != &physical { - bail!("collected coordinate `{path}` maps to multiple physical files"); - } - } - physical_to_virtual.insert(physical.clone(), path.clone()); - virtual_to_physical.insert(path.clone(), physical); - } - - Ok(records - .into_iter() - .zip(paths) - .map(|(record, path)| CollectedDocument { - access_path: record.access_path, - path, - source: record.source, - }) - .collect()) -} - -pub(super) fn collect_workflow_bundle( - input: CollectWorkflowBundleInput<'_>, -) -> Result { - let mut bundler = WorkflowBundler::try_new(input.cwd, input.inputs)?; - let root_graph_access_path = normalized_absolute_access_path(&input.root_location.graph)?; - let root_graph_path = seed_component_path( - &root_graph_access_path, - &bundler.cwd, - ComponentRole::Workflow, - )?; - - let project_config = input - .project_config - .map(|config| bundler.seed_config_document(config, ComponentRole::ProjectConfig)) - .transpose()?; - let user_config = input - .user_config - .map(|config| bundler.seed_config_document(config, ComponentRole::UserConfig)) - .transpose()?; - - let entrypoint = bundler.collect_workflow_location(&input.root_location, root_graph_path)?; - if let Some(project_config) = project_config { - let root_key = - manifest_path_from_absolute(&bundler.document(entrypoint).access_path, &bundler.cwd)? - .to_string(); - let mut root = bundler - .workflows - .remove(&root_key) - .ok_or_else(|| anyhow!("root workflow missing from collected workflow bundle"))?; - bundler.collect_config_dockerfile(project_config, &mut root.files)?; - bundler.workflows.insert(root_key, root); - } - - bundler.finish(entrypoint, project_config, user_config) -} - -fn seed_component_path( - access_path: &Path, - cwd: &Path, - component: ComponentRole, -) -> Result { - if !matches!(component, ComponentRole::UserConfig) { - if let Ok(relative) = access_path.strip_prefix(cwd) { - let relative = normalize_relative_path(relative)?; - if leading_parent_count(&relative) == 0 && !relative.as_os_str().is_empty() { - return Ok(relative); - } - } - } - - let file_name = access_path - .file_name() - .ok_or_else(|| anyhow!("collected root has no file name: {}", access_path.display()))?; - let root = match component { - ComponentRole::Workflow => PathBuf::from("_fabro_external/entrypoint"), - ComponentRole::ProjectConfig => PathBuf::from("_fabro_external/project_config"), - ComponentRole::UserConfig => PathBuf::from("_fabro_external/user_config"), - }; - normalize_relative_path(&root.join(file_name)) -} - -fn stable_template_root(document: &DocumentRecord, source: &TemplateSource) -> Result { - let relative = source - .path - .as_path() - .strip_prefix(source.root.as_path()) - .map_err(|_| { - anyhow!( - "template source {} is outside its logical root {}", - source.path, - source.root - ) - })?; - let mut root = document.provisional_path.clone(); - for component in relative.components() { - if matches!(component, Component::Normal(_)) && !root.pop() { - bail!( - "template source {} cannot be placed under its collected root", - source.path - ); - } - } - Ok(root) -} - -fn finalize_component_path( - provisional: &Path, - component: ComponentRole, - deficit: usize, -) -> Result { - let path = if deficit == 0 { - normalize_relative_path(provisional)? - } else { - let mut prefix = PathBuf::from("_fabro_rebased"); - prefix.push(component.label()); - for _ in 0..deficit { - prefix.push("anchor"); - } - normalize_relative_path(&prefix.join(provisional))? - }; - CollectedPath::try_new(virtual_path_to_wire(&path)?) -} - -fn virtual_path_to_wire(path: &Path) -> Result { - let mut segments = Vec::new(); - for component in path.components() { - let Component::Normal(segment) = component else { - bail!("collected path is not finalized: {}", path.display()); - }; - segments.push( - segment - .to_str() - .ok_or_else(|| anyhow!("collected path is not valid UTF-8"))?, - ); - } - Ok(segments.join("/")) -} - -fn virtual_sibling_path(path: &Path, sibling: &Path) -> Result { - let parent = path.parent().unwrap_or_else(|| Path::new(".")); - normalize_relative_path(&parent.join(sibling)) -} - -fn virtual_reference_path(base: &Path, reference: &str) -> Result { - let reference_path = Path::new(reference); - if reference_path.is_absolute() || reference.starts_with('~') { - bail!("unsupported collected reference: {reference}"); - } - normalize_relative_path(&base.join(reference_path)) -} - -fn normalize_relative_path(path: &Path) -> Result { - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - Component::CurDir => {} - Component::Normal(part) => normalized.push(part), - Component::ParentDir => { - if normalized.file_name().is_some() { - normalized.pop(); - } else { - normalized.push(".."); - } - } - Component::RootDir | Component::Prefix(_) => { - bail!("collected path must be relative: {}", path.display()); - } - } - } - Ok(normalized) -} - -fn leading_parent_count(path: &Path) -> usize { - path.components() - .take_while(|component| matches!(component, Component::ParentDir)) - .count() -} - -fn lexically_normalize_access_path(path: &Path) -> PathBuf { - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - Component::CurDir => {} - Component::Normal(part) => normalized.push(part), - Component::ParentDir => { - normalized.pop(); - } - Component::RootDir => normalized.push(Path::new("/")), - Component::Prefix(prefix) => normalized.push(prefix.as_os_str()), - } - } - normalized -} - -fn normalized_absolute_access_path(path: &Path) -> Result { - let absolute = std::path::absolute(path) - .with_context(|| format!("failed to make collected path absolute: {}", path.display()))?; - Ok(lexically_normalize_access_path(&absolute)) -} - -pub(super) fn normalize_absolute_path(base_dir: &Path, reference: &str) -> Option { - let path = Path::new(reference); - if path.is_absolute() || reference.starts_with('~') { - return None; - } - Some(lexically_normalize_access_path(&base_dir.join(path))) -} - -pub(super) fn manifest_path_from_absolute(path: &Path, cwd: &Path) -> Result { - ManifestPath::from_absolute(path, cwd) - .ok_or_else(|| anyhow!("Failed to compute manifest path for {}", path.display())) -} - -fn manifest_parent_or_dot(path: &ManifestPath) -> Result { - let parent = path.parent_or_dot().to_string_lossy(); - ManifestPath::from_wire(&parent) - .ok_or_else(|| anyhow!("invalid manifest parent path for {path}: {parent}")) -} - -fn template_root_for_bundled_file( - path: &ManifestPath, - workflow_template_root: &ManifestPath, -) -> Result { - if manifest_path_is_within_root(path, workflow_template_root) { - Ok(workflow_template_root.clone()) - } else { - manifest_parent_or_dot(path) - } -} - -fn manifest_path_is_within_root(path: &ManifestPath, root: &ManifestPath) -> bool { - if root.as_path().as_os_str().is_empty() { - return !matches!( - path.as_path().components().next(), - Some(Component::ParentDir) - ); - } - path.starts_with(root) -} - -fn manifest_attr_reference_kind( - scope: AttributeScope, - key: &str, - value: &str, -) -> Result { - reference_kind_for_attribute(scope, key, value) - .ok_or_else(|| anyhow!("unsupported manifest reference attribute: {key}={value}")) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn write_file(path: &Path, source: &str) { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).expect("fixture directory should be created"); - } - std::fs::write(path, source).expect("fixture file should be written"); - } - - fn collect_graph(cwd: &Path, graph: &Path) -> Result { - let inputs = HashMap::new(); - let root_location = WorkflowLocation::resolve(graph, cwd)?; - collect_workflow_bundle(CollectWorkflowBundleInput { - cwd, - root_location, - inputs: &inputs, - project_config: None, - user_config: None, - }) - } - - fn logical_contents(bundle: &CollectedWorkflowBundle) -> BTreeMap { - let mut contents = BTreeMap::new(); - if let Some(config) = &bundle.project_config { - contents.insert(config.path.to_string(), config.source.clone()); - } - if let Some(config) = &bundle.user_config { - contents.insert(config.path.to_string(), config.source.clone()); - } - for workflow in bundle.workflows.values() { - contents.insert( - workflow.graph.path.to_string(), - workflow.graph.source.clone(), - ); - if let Some(config) = &workflow.config { - contents.insert(config.path.to_string(), config.source.clone()); - } - for file in workflow.files.values() { - contents.insert(file.document.path.to_string(), file.document.source.clone()); - } - } - contents - } - - fn logical_provenance( - bundle: &CollectedWorkflowBundle, - ) -> BTreeMap)> { - let mut paths_by_access = HashMap::new(); - if let Some(config) = &bundle.project_config { - paths_by_access.insert(config.access_path.clone(), config.path.to_string()); - } - if let Some(config) = &bundle.user_config { - paths_by_access.insert(config.access_path.clone(), config.path.to_string()); - } - for workflow in bundle.workflows.values() { - paths_by_access.insert( - workflow.graph.access_path.clone(), - workflow.graph.path.to_string(), - ); - if let Some(config) = &workflow.config { - paths_by_access.insert(config.access_path.clone(), config.path.to_string()); - } - for file in workflow.files.values() { - paths_by_access.insert( - file.document.access_path.clone(), - file.document.path.to_string(), - ); - } - } - - let mut provenance = BTreeMap::new(); - for workflow in bundle.workflows.values() { - for file in workflow.files.values() { - let from = file.reference.from_access_path.as_ref().map(|access_path| { - paths_by_access - .get(access_path) - .expect("reference source should be collected") - .clone() - }); - provenance.insert( - file.document.path.to_string(), - (file.reference.type_, file.reference.original.clone(), from), - ); - } - } - provenance - } - - #[test] - fn collected_path_rejects_parent_components() { - assert!(CollectedPath::try_new("../prompt.md").is_err()); - } - - #[test] - fn collected_path_rejects_non_canonical_forms() { - for value in ["", ".", "a/./b", "a//b", "a/../b", "C:/a", "a\\b", "a\nb"] { - assert!(CollectedPath::try_new(value).is_err(), "accepted {value:?}"); - } - } - - #[test] - fn component_rebase_is_uniform_and_preserves_relative_relationships() { - let root = Path::new("_fabro_external/entrypoint/workflow.fabro"); - let sibling = virtual_reference_path( - root.parent().expect("entrypoint should have a parent"), - "../../../sibling/workflow.fabro", - ) - .expect("reference should normalize"); - let deficit = leading_parent_count(&sibling); - - let rebased_root = finalize_component_path(root, ComponentRole::Workflow, deficit) - .expect("root should finalize"); - let rebased_sibling = finalize_component_path(&sibling, ComponentRole::Workflow, deficit) - .expect("sibling should finalize"); - let resolved = virtual_reference_path( - rebased_root - .as_path() - .parent() - .expect("rebased root should have a parent"), - "../../../sibling/workflow.fabro", - ) - .expect("rebased reference should normalize"); - - assert_eq!(resolved, rebased_sibling.as_path()); - assert!(!rebased_root.as_str().contains("..")); - assert!(!rebased_sibling.as_str().contains("..")); - } - - #[test] - fn collector_captures_complete_workflow_bundle() { - let temp = tempfile::tempdir().expect("temp directory should be created"); - let project = temp.path().join("project"); - let root = project.join(".fabro/workflows/root"); - let child = project.join(".fabro/workflows/child"); - let project_config_path = project.join(".fabro/project.toml"); - let user_config_path = temp.path().join("home/.fabro/config.toml"); - let project_config = r#"_version = 1 - -[environments.project] -provider = "docker" - -[environments.project.image] -dockerfile = { path = "Project.Dockerfile" } -"#; - let workflow_config = r#"_version = 1 - -[workflow] -graph = "workflow.fabro" - -[environments.workflow] -provider = "docker" - -[environments.workflow.image] -dockerfile = { path = "Dockerfile" } -"#; - let user_config = "_version = 1\n"; - write_file(&project_config_path, project_config); - write_file(&project.join(".fabro/Project.Dockerfile"), "FROM project\n"); - write_file(&user_config_path, user_config); - write_file(&root.join("workflow.toml"), workflow_config); - write_file(&root.join("Dockerfile"), "FROM workflow\n"); - write_file( - &root.join("workflow.fabro"), - r#"digraph Root { - start [shape=Mdiamond] - prompt [prompt="@prompts/plan.md"] - imported [import="imports/shared.fabro"] - child [shape=house, stack.child_workflow="../child/workflow.fabro"] - exit [shape=Msquare] - start -> prompt -> imported -> child -> exit - }"#, - ); - write_file( - &root.join("prompts/plan.md"), - r#"{% include "partial.md" %}"#, - ); - write_file(&root.join("prompts/partial.md"), "partial\n"); - write_file( - &root.join("imports/shared.fabro"), - r#"digraph Shared { - start [shape=Mdiamond] - shared [prompt="@../prompts/shared.md"] - exit [shape=Msquare] - start -> shared -> exit - }"#, - ); - write_file(&root.join("prompts/shared.md"), "shared\n"); - write_file(&child.join("workflow.toml"), workflow_config); - write_file(&child.join("Dockerfile"), "FROM child\n"); - write_file( - &child.join("workflow.fabro"), - "digraph Child { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", - ); - - let inputs = HashMap::new(); - let bundle = collect_workflow_bundle(CollectWorkflowBundleInput { - cwd: &project, - root_location: WorkflowLocation::resolve(&root.join("workflow.toml"), &project) - .expect("root workflow should resolve"), - inputs: &inputs, - project_config: Some(CollectedSourceInput { - access_path: project_config_path, - source: project_config.to_owned(), - }), - user_config: Some(CollectedSourceInput { - access_path: user_config_path, - source: user_config.to_owned(), - }), - }) - .expect("workflow bundle should collect"); - - assert_eq!( - bundle.entrypoint.as_str(), - ".fabro/workflows/root/workflow.fabro" - ); - assert_eq!(bundle.workflows.len(), 2); - assert_eq!( - logical_contents(&bundle) - .keys() - .map(String::as_str) - .collect::>(), - vec![ - ".fabro/Project.Dockerfile", - ".fabro/project.toml", - ".fabro/workflows/child/Dockerfile", - ".fabro/workflows/child/workflow.fabro", - ".fabro/workflows/child/workflow.toml", - ".fabro/workflows/root/Dockerfile", - ".fabro/workflows/root/imports/shared.fabro", - ".fabro/workflows/root/prompts/partial.md", - ".fabro/workflows/root/prompts/plan.md", - ".fabro/workflows/root/prompts/shared.md", - ".fabro/workflows/root/workflow.fabro", - ".fabro/workflows/root/workflow.toml", - "_fabro_external/user_config/config.toml", - ] - ); - } - - #[test] - fn external_collection_is_stable_when_the_checkout_moves() { - fn fixture(parent: &Path) -> (PathBuf, PathBuf) { - let cwd = parent.join("checkout"); - let workflow = parent.join("catalog/root/workflow.fabro"); - std::fs::create_dir_all(&cwd).expect("checkout should be created"); - write_file( - &workflow, - r#"digraph Root { - start [shape=Mdiamond] - work [prompt="@prompts/plan.md"] - exit [shape=Msquare] - start -> work -> exit - }"#, - ); - write_file(&parent.join("catalog/root/prompts/plan.md"), "plan\n"); - (cwd, workflow) - } - - let first = tempfile::tempdir().expect("first temp directory should be created"); - let second = tempfile::tempdir().expect("second temp directory should be created"); - let (first_cwd, first_workflow) = fixture(first.path()); - let (second_cwd, second_workflow) = fixture(second.path()); - - let first_bundle = collect_graph(&first_cwd, &first_workflow) - .expect("first workflow bundle should collect"); - let second_bundle = collect_graph(&second_cwd, &second_workflow) - .expect("second workflow bundle should collect"); - - assert_eq!(first_bundle.entrypoint, second_bundle.entrypoint); - assert_eq!( - logical_contents(&first_bundle), - logical_contents(&second_bundle) - ); - assert_eq!( - logical_provenance(&first_bundle), - logical_provenance(&second_bundle) - ); - assert_eq!( - first_bundle.entrypoint.as_str(), - "_fabro_external/entrypoint/workflow.fabro" - ); - } - - #[test] - fn external_sibling_workflow_reference_resolves_in_stable_namespace() { - fn fixture(parent: &Path) -> (PathBuf, PathBuf) { - let cwd = parent.join("checkout"); - let root_dir = parent.join("user/workflows/root"); - let child_dir = parent.join("user/workflows/child"); - let root = root_dir.join("workflow.fabro"); - std::fs::create_dir_all(&cwd).expect("checkout should be created"); - write_file( - &root, - r#"digraph Root { - start [shape=Mdiamond] - prompt [prompt="@prompts/root.md"] - imported [import="imports/root.fabro"] - child [shape=house, stack.child_workflow="../child/workflow.fabro"] - exit [shape=Msquare] - start -> prompt -> imported -> child -> exit - }"#, - ); - write_file( - &root_dir.join("prompts/root.md"), - r#"{% include "root-partial.md" %}"#, - ); - write_file(&root_dir.join("prompts/root-partial.md"), "root partial\n"); - write_file( - &root_dir.join("imports/root.fabro"), - r#"digraph Import { - start [shape=Mdiamond] - work [prompt="@../prompts/root-import.md"] - exit [shape=Msquare] - start -> work -> exit - }"#, - ); - write_file(&root_dir.join("prompts/root-import.md"), "root import\n"); - write_file( - &child_dir.join("workflow.fabro"), - r#"digraph Child { - start [shape=Mdiamond] - prompt [prompt="@prompts/child.md"] - imported [import="imports/child.fabro"] - exit [shape=Msquare] - start -> prompt -> imported -> exit - }"#, - ); - write_file( - &child_dir.join("prompts/child.md"), - r#"{% include "child-partial.md" %}"#, - ); - write_file( - &child_dir.join("prompts/child-partial.md"), - "child partial\n", - ); - write_file( - &child_dir.join("imports/child.fabro"), - r#"digraph Import { - start [shape=Mdiamond] - work [prompt="@../prompts/child-import.md"] - exit [shape=Msquare] - start -> work -> exit - }"#, - ); - write_file(&child_dir.join("prompts/child-import.md"), "child import\n"); - (cwd, root) - } - - let first = tempfile::tempdir().expect("first temp directory should be created"); - let second = tempfile::tempdir().expect("second temp directory should be created"); - let (first_cwd, first_root) = fixture(first.path()); - let (second_cwd, second_root) = fixture(second.path()); - let bundle = - collect_graph(&first_cwd, &first_root).expect("first workflow bundle should collect"); - let moved = - collect_graph(&second_cwd, &second_root).expect("moved workflow bundle should collect"); - let entrypoint = &bundle.entrypoint; - let resolved = virtual_reference_path( - entrypoint - .as_path() - .parent() - .expect("entrypoint should have a parent"), - "../child/workflow.fabro", - ) - .expect("child reference should resolve"); - - assert_eq!(resolved, Path::new("_fabro_external/child/workflow.fabro")); - assert!(bundle.workflows.contains_key( - &CollectedPath::try_new(resolved).expect("child path should be canonical") - )); - assert_eq!(logical_contents(&bundle), logical_contents(&moved)); - assert_eq!(logical_provenance(&bundle), logical_provenance(&moved)); - assert_eq!(bundle.entrypoint, moved.entrypoint); - for path in logical_contents(&bundle).into_keys() { - assert!(!path.contains(first.path().file_name().unwrap().to_string_lossy().as_ref())); - CollectedPath::try_new(path).expect("every collected coordinate should be canonical"); - } - } - - #[test] - fn repeated_references_collect_one_file() { - let temp = tempfile::tempdir().expect("temp directory should be created"); - let cwd = temp.path(); - let graph = cwd.join("workflow.fabro"); - write_file( - &graph, - r#"digraph Root { - start [shape=Mdiamond] - first [prompt="@prompt.md"] - second [prompt="@prompt.md"] - exit [shape=Msquare] - start -> first -> second -> exit - }"#, - ); - write_file(&cwd.join("prompt.md"), "prompt\n"); - - let bundle = collect_graph(cwd, &graph).expect("workflow bundle should collect"); - let root = bundle - .workflows - .get(&bundle.entrypoint) - .expect("root workflow should be present"); - - assert_eq!(root.files.len(), 1); - let assembled = - crate::assemble_current_manifest(bundle, cwd).expect("legacy manifest should assemble"); - assert_eq!(assembled.workflows["workflow.fabro"].files.len(), 1); - } - - #[cfg(unix)] - #[test] - fn collector_rejects_one_physical_file_with_two_coordinates() { - let temp = tempfile::tempdir().expect("temp directory should be created"); - let cwd = temp.path(); - let graph = cwd.join("workflow.fabro"); - write_file( - &graph, - r#"digraph Root { - start [shape=Mdiamond] - first [prompt="@first.md"] - second [prompt="@second.md"] - exit [shape=Msquare] - start -> first -> second -> exit - }"#, - ); - write_file(&cwd.join("actual.md"), "prompt\n"); - std::os::unix::fs::symlink("actual.md", cwd.join("first.md")) - .expect("first symlink should be created"); - std::os::unix::fs::symlink("actual.md", cwd.join("second.md")) - .expect("second symlink should be created"); - - let error = collect_graph(cwd, &graph).expect_err("alias should be rejected"); - - assert!( - error - .to_string() - .contains("one physical file has conflicting collected coordinates"), - "unexpected error: {error:#}" - ); - assert!(error.to_string().contains("first.md")); - assert!(error.to_string().contains("second.md")); - } - - #[test] - fn namespace_rejects_two_physical_files_at_one_coordinate() { - let temp = tempfile::tempdir().expect("temp directory should be created"); - let first = temp.path().join("first.md"); - let second = temp.path().join("second.md"); - write_file(&first, "first\n"); - write_file(&second, "second\n"); - let record = |access_path| DocumentRecord { - access_path, - provisional_path: PathBuf::from("shared.md"), - component: ComponentRole::Workflow, - source: String::new(), - }; - - let error = finalize_documents(vec![record(first), record(second)]) - .expect_err("virtual collision should be rejected"); - - assert!( - error - .to_string() - .contains("collected coordinate `shared.md` maps to multiple physical files"), - "unexpected error: {error:#}" - ); - } - - #[test] - fn namespace_identity_errors_keep_the_io_error_in_the_source_chain() { - let temp = tempfile::tempdir().expect("temp directory should be created"); - let record = DocumentRecord { - access_path: temp.path().join("missing.md"), - provisional_path: PathBuf::from("missing.md"), - component: ComponentRole::Workflow, - source: String::new(), - }; - - let error = - finalize_documents(vec![record]).expect_err("missing physical identity should fail"); - - assert!( - error - .chain() - .any(|cause| cause.downcast_ref::().is_some()), - "unexpected error chain: {error:#}" - ); - } - - #[test] - fn read_errors_keep_the_io_error_in_the_source_chain() { - let temp = tempfile::tempdir().expect("temp directory should be created"); - let cwd = temp.path(); - let graph = cwd.join("workflow.fabro"); - write_file( - &graph, - r#"digraph Root { - start [shape=Mdiamond] - work [prompt="@missing.md"] - exit [shape=Msquare] - start -> work -> exit - }"#, - ); - - let error = collect_graph(cwd, &graph).expect_err("missing file should fail"); - - assert!( - error - .chain() - .any(|cause| cause.downcast_ref::().is_some()), - "unexpected error chain: {error:#}" - ); - } - - #[test] - fn collector_does_not_push_an_ahead_branch() { - fn commit_all(repository: &git2::Repository, message: &str) -> git2::Oid { - let mut index = repository.index().expect("index should open"); - index - .add_all(["*"], git2::IndexAddOption::DEFAULT, None) - .expect("fixture files should be staged"); - index.write().expect("index should be written"); - let tree_id = index.write_tree().expect("tree should be written"); - let tree = repository.find_tree(tree_id).expect("tree should exist"); - let signature = git2::Signature::now("Fabro Test", "fabro@example.com") - .expect("signature should be valid"); - let parents = repository - .head() - .ok() - .and_then(|head| head.target()) - .map(|oid| { - repository - .find_commit(oid) - .expect("parent commit should exist") - }); - let parent_refs = parents.iter().collect::>(); - repository - .commit( - Some("refs/heads/main"), - &signature, - &signature, - message, - &tree, - &parent_refs, - ) - .expect("commit should be created") - } - - let temp = tempfile::tempdir().expect("temp directory should be created"); - let origin_path = temp.path().join("origin.git"); - let checkout = temp.path().join("checkout"); - let origin = - git2::Repository::init_bare(&origin_path).expect("bare origin should be initialized"); - let repository = git2::Repository::init(&checkout).expect("checkout should be initialized"); - repository - .set_head("refs/heads/main") - .expect("main should be selected"); - write_file( - &checkout.join("workflow.fabro"), - "digraph Root { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", - ); - let first_commit = commit_all(&repository, "initial"); - let mut remote = repository - .remote( - "origin", - origin_path.to_str().expect("origin path should be UTF-8"), - ) - .expect("origin should be configured"); - remote - .push(&["refs/heads/main:refs/heads/main"], None) - .expect("initial commit should be pushed"); - drop(remote); - write_file(&checkout.join("README.md"), "ahead\n"); - let ahead_commit = commit_all(&repository, "ahead"); - assert_ne!(first_commit, ahead_commit); - - collect_graph(&checkout, &checkout.join("workflow.fabro")) - .expect("workflow bundle should collect"); - - let origin_commit = origin - .find_reference("refs/heads/main") - .expect("origin main should exist") - .target() - .expect("origin main should point to a commit"); - assert_eq!(origin_commit, first_commit); - } -} diff --git a/lib/components/fabro-manifest/src/workflow_bundler.rs b/lib/components/fabro-manifest/src/workflow_bundler.rs new file mode 100644 index 000000000..15d0a3d20 --- /dev/null +++ b/lib/components/fabro-manifest/src/workflow_bundler.rs @@ -0,0 +1,618 @@ +use std::collections::{HashMap, HashSet}; +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context as _, Result, anyhow}; +use fabro_api::types; +use fabro_config::project::WorkflowLocation; +use fabro_config::{EnvironmentDockerfileLayer, EnvironmentImageLayer, SettingsLayer}; +use fabro_graphviz::graph::AttrValue; +use fabro_graphviz::parser; +use fabro_template::{ + BundleTemplateStore, FilesystemTemplateStore, RecordingTemplateStore, TemplateContext, + TemplateDependencyClosure, TemplateRenderMode, TemplateSource, +}; +use fabro_types::ManifestPath; +use fabro_workflow::static_reference::{self, AttributeScope, ReferenceKind}; + +use crate::{manifest_path_from_absolute, normalize_absolute_path}; + +pub(super) struct WorkflowBundler<'a> { + cwd: &'a Path, + inputs: &'a HashMap, + workflows: HashMap, + visited_workflows: HashSet, +} + +impl<'a> WorkflowBundler<'a> { + pub(super) fn new(cwd: &'a Path, inputs: &'a HashMap) -> Self { + Self { + cwd, + inputs, + workflows: HashMap::new(), + visited_workflows: HashSet::new(), + } + } + + pub(super) fn bundle( + mut self, + root_location: &WorkflowLocation, + project_config: Option<(&ManifestPath, &str)>, + ) -> Result> { + self.collect_workflow_location(root_location)?; + + if let Some((config_path, source)) = project_config { + let root_key = manifest_path_from_absolute(&root_location.graph, self.cwd)?.to_string(); + let mut root = self + .workflows + .remove(&root_key) + .ok_or_else(|| anyhow!("root workflow missing from manifest bundle"))?; + self.collect_config_dockerfile(config_path, source, &mut root.files)?; + self.workflows.insert(root_key, root); + } + + Ok(self.workflows) + } + + fn collect_workflow_location(&mut self, location: &WorkflowLocation) -> Result<()> { + let dot_path = manifest_path_from_absolute(&location.graph, self.cwd)?; + let dot_key = dot_path.to_string(); + if !self.visited_workflows.insert(dot_key.clone()) { + return Ok(()); + } + + let source = std::fs::read_to_string(&location.graph) + .with_context(|| format!("Failed to read {}", location.graph.display()))?; + let config = if let Some(workflow_toml_path) = location.toml.as_ref() { + Some(types::ManifestWorkflowConfig { + path: manifest_path_from_absolute(workflow_toml_path, self.cwd)?.to_string(), + source: std::fs::read_to_string(workflow_toml_path) + .with_context(|| format!("Failed to read {}", workflow_toml_path.display()))?, + }) + } else { + None + }; + + let scan = WorkflowScanInput { + absolute_dot_path: location.graph.clone(), + dot_path, + source: source.clone(), + }; + let mut files = HashMap::new(); + let mut visited_imports = HashSet::new(); + if let Some(config) = config.as_ref() { + let config_path = ManifestPath::from_wire(&config.path) + .ok_or_else(|| anyhow!("invalid manifest workflow config path: {}", config.path))?; + self.collect_config_dockerfile(&config_path, &config.source, &mut files)?; + } + self.collect_workflow_files(&scan, &mut files, &mut visited_imports)?; + + self.workflows.insert(dot_key, types::ManifestWorkflow { + config, + files, + source, + }); + + Ok(()) + } + + fn collect_workflow_entry(&mut self, workflow: &Path, resolve_from: &Path) -> Result<()> { + let normalized_workflow = if workflow.extension().is_some() && workflow.is_relative() { + normalize_absolute_path(resolve_from, &workflow.to_string_lossy()).ok_or_else(|| { + anyhow!( + "unsupported manifest workflow reference: {}", + workflow.display() + ) + })? + } else { + workflow.to_path_buf() + }; + let location = WorkflowLocation::resolve(&normalized_workflow, resolve_from)?; + self.collect_workflow_location(&location) + } + + fn collect_workflow_files( + &mut self, + workflow: &WorkflowScanInput, + files: &mut HashMap, + visited_imports: &mut HashSet, + ) -> Result<()> { + let graph = parser::parse(&workflow.source) + .with_context(|| format!("Failed to parse {}", workflow.absolute_dot_path.display()))?; + let workflow_base_dir = workflow + .absolute_dot_path + .parent() + .unwrap_or_else(|| Path::new(".")); + let workflow_template_root = manifest_parent_or_dot(&workflow.dot_path)?; + + if let Some(goal_ref) = graph.attrs.get("goal").and_then(AttrValue::as_str) { + if goal_ref.starts_with('@') { + let bundled = self.collect_bundled_file( + files, + workflow_base_dir, + goal_ref.trim_start_matches('@'), + types::ManifestFileRefType::FileInline, + manifest_attr_reference_kind(AttributeScope::Graph, "goal", goal_ref)?, + Some(workflow.dot_path.clone()), + )?; + self.collect_bundled_template_includes(files, &bundled, &workflow_template_root)?; + } else { + self.collect_template_include_files( + files, + TemplateSource::new( + workflow.dot_path.clone(), + workflow_template_root.clone(), + goal_ref.to_owned(), + ), + Some(&workflow.dot_path), + )?; + } + } + + for node in graph.nodes.values() { + if let Some(prompt_ref) = node.attrs.get("prompt").and_then(AttrValue::as_str) { + if !prompt_ref.starts_with('@') { + self.collect_template_include_files( + files, + TemplateSource::new( + workflow.dot_path.clone(), + workflow_template_root.clone(), + prompt_ref.to_owned(), + ), + Some(&workflow.dot_path), + )?; + } + } + + for (name, value) in &node.attrs { + let Some(value) = value.as_str() else { + continue; + }; + let Some(ReferenceKind::FileInline) = + static_reference::reference_kind_for_attribute( + AttributeScope::Node, + name, + value, + ) + else { + continue; + }; + let reference = value.strip_prefix('@').ok_or_else(|| { + anyhow!("file inline reference must start with '@': {name}={value}") + })?; + let bundled = self.collect_bundled_file( + files, + workflow_base_dir, + reference, + types::ManifestFileRefType::FileInline, + ReferenceKind::FileInline, + Some(workflow.dot_path.clone()), + )?; + + if name == "prompt" { + self.collect_bundled_template_includes( + files, + &bundled, + &workflow_template_root, + )?; + } + } + + if let Some(import_ref) = node.attrs.get("import").and_then(AttrValue::as_str) { + let imported = self.collect_bundled_file( + files, + workflow_base_dir, + import_ref, + types::ManifestFileRefType::Import, + manifest_attr_reference_kind(AttributeScope::Node, "import", import_ref)?, + Some(workflow.dot_path.clone()), + )?; + let import_key = imported.path.to_string(); + if visited_imports.insert(import_key) { + let imported_source = std::fs::read_to_string(&imported.absolute_path) + .with_context(|| { + format!("Failed to read {}", imported.absolute_path.display()) + })?; + let imported_scan = WorkflowScanInput { + absolute_dot_path: imported.absolute_path, + dot_path: imported.path, + source: imported_source, + }; + self.collect_workflow_files(&imported_scan, files, visited_imports)?; + } + } + + if let Some(child_ref) = node + .attrs + .get("stack.child_workflow") + .and_then(AttrValue::as_str) + { + manifest_attr_reference_kind( + AttributeScope::Node, + "stack.child_workflow", + child_ref, + )? + .validate(child_ref) + .map_err(anyhow::Error::new)?; + self.collect_workflow_entry(Path::new(child_ref), workflow_base_dir)?; + } + } + + Ok(()) + } + + fn collect_bundled_template_includes( + &self, + files: &mut HashMap, + bundled: &BundledFile, + workflow_template_root: &ManifestPath, + ) -> Result<()> { + let source = std::fs::read_to_string(&bundled.absolute_path) + .with_context(|| format!("Failed to read {}", bundled.absolute_path.display()))?; + let template_root = template_root_for_bundled_file(&bundled.path, workflow_template_root)?; + self.collect_template_include_files( + files, + TemplateSource::new(bundled.path.clone(), template_root, source), + Some(&bundled.path), + ) + } + + fn collect_template_include_files( + &self, + files: &mut HashMap, + source: TemplateSource, + from: Option<&ManifestPath>, + ) -> Result<()> { + let source_path = source.path.clone(); + let store = FilesystemTemplateStore::new(self.cwd.to_path_buf()); + let closure = fabro_template::discover_static_dependency_closure([source], &store) + .context("failed to discover template dependencies")?; + self.verify_recorded_template_dependencies(&source_path, &closure, files, from)?; + + for (path, source) in closure.sources { + if path == source_path { + continue; + } + let key = path.to_string(); + files + .entry(key) + .or_insert_with(|| types::ManifestFileEntry { + content: source.content, + ref_: types::ManifestFileRef { + from: from.map(std::string::ToString::to_string), + original: path.to_string(), + type_: types::ManifestFileRefType::FileInline, + }, + }); + } + Ok(()) + } + + fn verify_recorded_template_dependencies( + &self, + source_path: &ManifestPath, + closure: &TemplateDependencyClosure, + files: &HashMap, + from: Option<&ManifestPath>, + ) -> Result<()> { + let Some(source) = closure.sources.get(source_path) else { + return Ok(()); + }; + let mut bundled_files = closure + .sources + .iter() + .map(|(path, source)| (path.clone(), source.content.clone())) + .collect::>(); + for (path, entry) in files { + if let Some(path) = ManifestPath::from_wire(path) { + bundled_files.insert(path, entry.content.clone()); + } + } + let allowed = bundled_files.keys().cloned().collect(); + let store = + RecordingTemplateStore::with_allowed(BundleTemplateStore::new(bundled_files), allowed); + let context = TemplateContext::for_input_scan(self.inputs.clone()); + fabro_template::render_source( + source, + &context, + Arc::new(store), + TemplateRenderMode::Lenient, + ) + .with_context(|| { + let from = + from.map_or_else(|| source_path.to_string(), std::string::ToString::to_string); + format!("failed to verify template dependencies for {from}") + })?; + Ok(()) + } + + fn collect_config_dockerfile( + &self, + config_path: &ManifestPath, + source: &str, + files: &mut HashMap, + ) -> Result<()> { + let layer = source + .parse::() + .context("Failed to parse run config TOML")?; + let absolute_config_path = self.cwd.join(config_path.as_path()); + let base_dir = absolute_config_path + .parent() + .unwrap_or_else(|| Path::new(".")); + + for environment in layer.environments.values() { + self.collect_environment_dockerfile( + files, + base_dir, + config_path, + environment.image.as_ref(), + )?; + } + if let Some(run_environment) = layer.run.as_ref().and_then(|run| run.environment.as_ref()) { + self.collect_environment_dockerfile( + files, + base_dir, + config_path, + run_environment.image.as_ref(), + )?; + } + Ok(()) + } + + fn collect_environment_dockerfile( + &self, + files: &mut HashMap, + base_dir: &Path, + config_path: &ManifestPath, + image: Option<&EnvironmentImageLayer>, + ) -> Result<()> { + let dockerfile = image.and_then(|image| image.dockerfile.as_ref()); + let Some(EnvironmentDockerfileLayer::Path { path }) = dockerfile else { + return Ok(()); + }; + self.collect_bundled_file( + files, + base_dir, + path, + types::ManifestFileRefType::Dockerfile, + ReferenceKind::Dockerfile, + Some(config_path.clone()), + )?; + Ok(()) + } + + fn collect_bundled_file( + &self, + files: &mut HashMap, + base_dir: &Path, + reference: &str, + ref_type: types::ManifestFileRefType, + reference_kind: ReferenceKind, + from: Option, + ) -> Result { + reference_kind + .validate(reference) + .map_err(anyhow::Error::new)?; + + let absolute_path = normalize_absolute_path(base_dir, reference) + .ok_or_else(|| anyhow!("unsupported manifest reference: {reference}"))?; + let path = manifest_path_from_absolute(&absolute_path, self.cwd)?; + let key = path.to_string(); + if !files.contains_key(&key) { + let content = std::fs::read_to_string(&absolute_path) + .with_context(|| format!("Failed to read {}", absolute_path.display()))?; + files.insert(key.clone(), types::ManifestFileEntry { + content, + ref_: types::ManifestFileRef { + from: from.map(|value| value.to_string()), + original: reference.to_owned(), + type_: ref_type, + }, + }); + } + + Ok(BundledFile { + absolute_path, + path, + }) + } +} + +#[derive(Clone)] +struct WorkflowScanInput { + absolute_dot_path: PathBuf, + dot_path: ManifestPath, + source: String, +} + +struct BundledFile { + absolute_path: PathBuf, + path: ManifestPath, +} + +fn manifest_parent_or_dot(path: &ManifestPath) -> Result { + let parent = path.parent_or_dot().to_string_lossy(); + ManifestPath::from_wire(&parent) + .ok_or_else(|| anyhow!("invalid manifest parent path for {path}: {parent}")) +} + +fn template_root_for_bundled_file( + path: &ManifestPath, + workflow_template_root: &ManifestPath, +) -> Result { + if manifest_path_is_within_root(path, workflow_template_root) { + Ok(workflow_template_root.clone()) + } else { + manifest_parent_or_dot(path) + } +} + +fn manifest_path_is_within_root(path: &ManifestPath, root: &ManifestPath) -> bool { + if root.as_path().as_os_str().is_empty() { + return !matches!( + path.as_path().components().next(), + Some(Component::ParentDir) + ); + } + path.starts_with(root) +} + +fn manifest_attr_reference_kind( + scope: AttributeScope, + key: &str, + value: &str, +) -> Result { + static_reference::reference_kind_for_attribute(scope, key, value) + .ok_or_else(|| anyhow!("unsupported manifest reference attribute: {key}={value}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn write_file(path: &Path, source: &str) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("fixture directory should be created"); + } + std::fs::write(path, source).expect("fixture file should be written"); + } + + fn bundle_graph(cwd: &Path, graph: &Path) -> Result> { + let inputs = HashMap::new(); + let root_location = WorkflowLocation::resolve(graph, cwd)?; + WorkflowBundler::new(cwd, &inputs).bundle(&root_location, None) + } + + #[test] + fn repeated_references_collect_one_file() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let graph = temp.path().join("workflow.fabro"); + write_file( + &graph, + r#"digraph Root { + start [shape=Mdiamond] + first [prompt="@prompt.md"] + second [prompt="@prompt.md"] + exit [shape=Msquare] + start -> first -> second -> exit + }"#, + ); + write_file(&temp.path().join("prompt.md"), "prompt\n"); + + let workflows = bundle_graph(temp.path(), &graph).expect("workflow should bundle"); + + assert_eq!(workflows["workflow.fabro"].files.len(), 1); + } + + #[test] + fn parse_errors_keep_the_graphviz_error_in_the_source_chain() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let graph = temp.path().join("workflow.fabro"); + write_file(&graph, "not a graph"); + + let error = bundle_graph(temp.path(), &graph).expect_err("invalid graph should fail"); + + assert!( + error + .chain() + .any(|cause| cause.downcast_ref::().is_some()), + "unexpected error chain: {error:#}" + ); + } + + #[test] + fn read_errors_keep_the_io_error_in_the_source_chain() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let graph = temp.path().join("workflow.fabro"); + write_file( + &graph, + r#"digraph Root { + start [shape=Mdiamond] + work [prompt="@missing.md"] + exit [shape=Msquare] + start -> work -> exit + }"#, + ); + + let error = bundle_graph(temp.path(), &graph).expect_err("missing file should fail"); + + assert!( + error + .chain() + .any(|cause| cause.downcast_ref::().is_some()), + "unexpected error chain: {error:#}" + ); + } + + #[test] + fn bundler_does_not_push_an_ahead_branch() { + fn commit_all(repository: &git2::Repository, message: &str) -> git2::Oid { + let mut index = repository.index().expect("index should open"); + index + .add_all(["*"], git2::IndexAddOption::DEFAULT, None) + .expect("fixture files should be staged"); + index.write().expect("index should be written"); + let tree_id = index.write_tree().expect("tree should be written"); + let tree = repository.find_tree(tree_id).expect("tree should exist"); + let signature = git2::Signature::now("Fabro Test", "fabro@example.com") + .expect("signature should be valid"); + let parents = repository + .head() + .ok() + .and_then(|head| head.target()) + .map(|oid| { + repository + .find_commit(oid) + .expect("parent commit should exist") + }); + let parent_refs = parents.iter().collect::>(); + repository + .commit( + Some("refs/heads/main"), + &signature, + &signature, + message, + &tree, + &parent_refs, + ) + .expect("commit should be created") + } + + let temp = tempfile::tempdir().expect("temp directory should be created"); + let origin_path = temp.path().join("origin.git"); + let checkout = temp.path().join("checkout"); + let origin = + git2::Repository::init_bare(&origin_path).expect("bare origin should be initialized"); + let repository = git2::Repository::init(&checkout).expect("checkout should be initialized"); + repository + .set_head("refs/heads/main") + .expect("main should be selected"); + write_file( + &checkout.join("workflow.fabro"), + "digraph Root { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", + ); + let first_commit = commit_all(&repository, "initial"); + let mut remote = repository + .remote( + "origin", + origin_path.to_str().expect("origin path should be UTF-8"), + ) + .expect("origin should be configured"); + remote + .push(&["refs/heads/main:refs/heads/main"], None) + .expect("initial commit should be pushed"); + drop(remote); + write_file(&checkout.join("README.md"), "ahead\n"); + let ahead_commit = commit_all(&repository, "ahead"); + assert_ne!(first_commit, ahead_commit); + + bundle_graph(&checkout, &checkout.join("workflow.fabro")).expect("workflow should bundle"); + + let origin_commit = origin + .find_reference("refs/heads/main") + .expect("origin main should exist") + .target() + .expect("origin main should point to a commit"); + assert_eq!(origin_commit, first_commit); + } +} From 7971c80fd4d63f78870693e86606309f48948298 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 6 Aug 2026 16:44:48 -0400 Subject: [PATCH 07/62] Retry code analysis From 996c7ade80a6ff51156744fdd4a7dcb1c9116e1e Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Fri, 7 Aug 2026 08:48:10 -0400 Subject: [PATCH 08/62] Retry code analysis From 10499e707caaf51a8da9dea5726ab6d20da3fb4b Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 10 Aug 2026 15:32:47 -0400 Subject: [PATCH 09/62] Share one blob store across run handles --- lib/components/fabro-store/src/keys.rs | 50 ++---------------- .../fabro-store/src/slate/blob_store.rs | 51 ++++++++++++++++++- lib/components/fabro-store/src/slate/mod.rs | 17 +++++++ .../fabro-store/src/slate/run_store.rs | 51 +++++++++++-------- 4 files changed, 100 insertions(+), 69 deletions(-) diff --git a/lib/components/fabro-store/src/keys.rs b/lib/components/fabro-store/src/keys.rs index 343cdc1d0..13f24a246 100644 --- a/lib/components/fabro-store/src/keys.rs +++ b/lib/components/fabro-store/src/keys.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Write}; use std::ops::Range; -use fabro_types::{RunBlobId, RunId, SessionId}; +use fabro_types::{RunId, SessionId}; pub(crate) const MAX_EVENT_SEQ: u32 = 999_999; @@ -91,10 +91,6 @@ pub(crate) fn run_events_range(run_id: &RunId, start_seq: u32) -> Range SlateKey { - SlateKey::new("blobs").with("sha256").into_prefix() -} - pub(crate) fn sessions_by_id_prefix() -> SlateKey { SlateKey::new("sessions").with("by-id").into_prefix() } @@ -115,21 +111,6 @@ pub(crate) fn parse_event_seq(key: &str) -> Option { segments.next()?.split_once('-')?.0.parse().ok() } -pub(crate) fn parse_blob_id(key: &str) -> Option { - let mut segments = SlateKey::segments(key); - if segments.next()? != "blobs" { - return None; - } - if segments.next()? != "sha256" { - return None; - } - let id = segments.next()?; - if segments.next().is_some() { - return None; - } - id.parse().ok() -} - #[cfg(test)] mod tests { use fabro_types::RunId; @@ -161,14 +142,6 @@ mod tests { ]); } - #[test] - fn blob_key_segments() { - let blob_id = RunBlobId::new(b"summary"); - let key = SlateKey::new("blobs").with("sha256").with(blob_id); - let segments: Vec<&str> = SlateKey::segments(key.as_str()).collect(); - assert_eq!(segments, ["blobs", "sha256", &blob_id.to_string()]); - } - #[test] fn sequence_keys_are_zero_padded() { let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); @@ -196,20 +169,16 @@ mod tests { } #[test] - fn parse_helpers_roundtrip() { + fn parse_event_seq_roundtrips() { let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); assert_eq!( parse_event_seq(run_event_key(&run_id, 7, 123).as_str()), Some(7) ); - - let blob_id = RunBlobId::new(b"summary"); - let key = SlateKey::new("blobs").with("sha256").with(blob_id); - assert_eq!(parse_blob_id(key.as_str()), Some(blob_id)); } #[test] - fn parse_helpers_reject_invalid_keys() { + fn parse_event_seq_rejects_invalid_keys() { assert_eq!( parse_event_seq( SlateKey::new("runs") @@ -220,18 +189,5 @@ mod tests { ), None ); - assert_eq!( - parse_blob_id(SlateKey::new("blobs").with("not-a-uuid").as_str()), - None - ); - assert_eq!( - parse_blob_id( - SlateKey::new("blobs") - .with("01JT56VE4Z5NZ814GZN2JZD65A") - .with("not-a-blob") - .as_str() - ), - None - ); } } diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs index 3c058b3c8..925f01789 100644 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ b/lib/components/fabro-store/src/slate/blob_store.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use bytes::Bytes; use fabro_types::RunBlobId; +use futures::StreamExt; -use crate::Result; use crate::record::{RawBytesCodec, Record, Repository}; +use crate::{Error, Result}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Blob(pub Bytes); @@ -63,6 +64,20 @@ impl BlobStore { pub async fn exists(&self, id: &RunBlobId) -> 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(err) => return Err(err), + } + } + ids.sort(); + Ok(ids) + } } #[cfg(test)] @@ -111,6 +126,40 @@ 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 = Arc::new( + slatedb::Db::open("blob-store-list-tests", Arc::new(InMemory::new())) + .await + .unwrap(), + ); + let store = BlobStore::new(Arc::clone(&raw_db)); + 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 = Arc::new( diff --git a/lib/components/fabro-store/src/slate/mod.rs b/lib/components/fabro-store/src/slate/mod.rs index 5d9ae3bd9..aed4c490e 100644 --- a/lib/components/fabro-store/src/slate/mod.rs +++ b/lib/components/fabro-store/src/slate/mod.rs @@ -146,11 +146,13 @@ impl Database { pub async fn create_run(&self, run_id: &RunId) -> Result { self.warm_projection_cache().await?; let db = self.open_db().await?; + let blob_store = self.blobs().await?; self.catalog_index().await?.add(run_id).await?; let run_store = RunDatabase::open_writer( *run_id, db, + blob_store, Arc::clone(&self.projection_cache), Arc::clone(&self.run_summary_store), ) @@ -163,6 +165,7 @@ impl Database { pub async fn open_run(&self, run_id: &RunId) -> Result { self.warm_projection_cache().await?; let db = self.open_db().await?; + let blob_store = self.blobs().await?; // Keep the active-writer miss and insert atomic. Otherwise concurrent // callers can create independent writers with the same recovered seq. let mut active_runs = self.active_runs.lock().await; @@ -181,6 +184,7 @@ impl Database { let run_store = RunDatabase::open_writer( *run_id, db, + blob_store, Arc::clone(&self.projection_cache), Arc::clone(&self.run_summary_store), ) @@ -202,9 +206,11 @@ impl Database { if !RunDatabase::has_any_events(&db, run_id).await? { return Err(Error::RunNotFound(run_id.to_string())); } + let blob_store = self.blobs().await?; RunDatabase::open_reader( *run_id, db, + blob_store, Arc::clone(&self.projection_cache), Arc::clone(&self.run_summary_store), ) @@ -857,8 +863,19 @@ mod tests { let (_object_store, store) = make_store(); 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 reader = store.open_run_reader(&test_run_id("run-1")).await.unwrap(); + assert_eq!( + 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)); + let err = reader .append_event(&event_payload( "run-1", diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index ced57d33d..d3999d964 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -37,7 +37,7 @@ impl std::fmt::Debug for RunDatabase { pub(crate) struct RunDatabaseInner { run_id: RunId, db: Db, - blob_store: BlobStore, + blob_store: Arc, // `None` for reader-built inners: readers never append, so they carry no // next-write sequence and any append through them fails as read-only. event_seq: Option, @@ -57,6 +57,7 @@ impl RunDatabase { pub(crate) async fn open_writer( run_id: RunId, db: Db, + blob_store: Arc, shared_projection_cache: Arc, run_summary_store: Arc>>, ) -> Result { @@ -64,6 +65,7 @@ impl RunDatabase { run_id, db, false, + blob_store, shared_projection_cache, run_summary_store, ) @@ -73,16 +75,26 @@ impl RunDatabase { pub(crate) async fn open_reader( run_id: RunId, db: Db, + blob_store: Arc, shared_projection_cache: Arc, run_summary_store: Arc>>, ) -> Result { - Self::build(run_id, db, true, shared_projection_cache, run_summary_store).await + Self::build( + run_id, + db, + true, + blob_store, + shared_projection_cache, + run_summary_store, + ) + .await } async fn build( run_id: RunId, db: Db, read_only: bool, + blob_store: Arc, shared_projection_cache: Arc, run_summary_store: Arc>>, ) -> Result { @@ -106,7 +118,6 @@ impl RunDatabase { Some(AtomicU32::new(next_seq)) }; let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16)); - let blob_store = BlobStore::new(Arc::new(db.clone())); Ok(Self { inner: Arc::new(RunDatabaseInner { run_id, @@ -591,7 +602,7 @@ impl RunDatabase { } pub async fn list_blobs(&self) -> Result> { - list_blobs(&self.inner.db).await + self.inner.blob_store.list().await } pub async fn state(&self) -> Result { @@ -904,23 +915,6 @@ where Ok(events) } -async fn list_blobs(db: &R) -> Result> -where - R: DbRead + Sync, -{ - let mut iter = db.scan_prefix(keys::blobs_prefix()).await?; - let mut blob_ids = Vec::new(); - while let Some(entry) = iter.next().await? { - let key = key_to_str(&entry.key)?; - let Some(blob_id) = keys::parse_blob_id(key) else { - continue; - }; - blob_ids.push(blob_id); - } - blob_ids.sort(); - Ok(blob_ids) -} - fn key_to_str(key: &Bytes) -> Result<&str> { std::str::from_utf8(key) .map_err(|err| Error::Other(format!("stored key is not valid UTF-8: {err}"))) @@ -938,6 +932,21 @@ mod tests { use crate::{Database, Error, EventPayload, keys}; + #[tokio::test] + async fn runs_share_database_blob_store() { + let object_store = Arc::new(InMemory::new()); + let store = Database::new(object_store, "", Duration::from_millis(1), None); + let shared = store.blobs().await.unwrap(); + let first_run_id = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); + let second_run_id = "01JT56VE4Z5NZ814GZN2JZD65B".parse().unwrap(); + + let first_run = store.create_run(&first_run_id).await.unwrap(); + let second_run = store.create_run(&second_run_id).await.unwrap(); + + assert!(Arc::ptr_eq(&shared, &first_run.inner.blob_store)); + assert!(Arc::ptr_eq(&shared, &second_run.inner.blob_store)); + } + #[tokio::test] async fn list_blobs_reads_global_cas_namespace() { let object_store = Arc::new(InMemory::new()); From f773a2475823d1d91010bbf0638deec5d4915093 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 11 Aug 2026 10:51:19 -0400 Subject: [PATCH 10/62] Simplify run handle construction and blob store test fixtures - Collapse RunDatabase::open_writer/open_reader wrappers into one pub(crate) build, with a Database::open_run_database helper that gathers the shared-store dependencies in one place - Stop fetching the blob store on open_run's active-cache hit path - Share a raw-db test fixture between the two BlobStore raw-key tests - Evict the cached writer in open_run_reader_is_read_only so the test exercises the real reader construction path Co-Authored-By: Claude Fable 5 --- .../fabro-store/src/slate/blob_store.rs | 24 +++++----- lib/components/fabro-store/src/slate/mod.rs | 47 ++++++++----------- .../fabro-store/src/slate/run_store.rs | 38 +-------------- 3 files changed, 32 insertions(+), 77 deletions(-) diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs index 925f01789..288755157 100644 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ b/lib/components/fabro-store/src/slate/blob_store.rs @@ -103,6 +103,16 @@ mod tests { 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; @@ -141,12 +151,7 @@ mod tests { #[tokio::test] async fn list_skips_malformed_blob_ids() { - let raw_db = Arc::new( - slatedb::Db::open("blob-store-list-tests", Arc::new(InMemory::new())) - .await - .unwrap(), - ); - let store = BlobStore::new(Arc::clone(&raw_db)); + let (raw_db, store) = raw_store("blob-store-list-tests").await; let id = store.write(b"valid").await.unwrap(); raw_db @@ -162,12 +167,7 @@ mod tests { #[tokio::test] async fn raw_db_reads_exact_blob_bytes() { - let raw_db = Arc::new( - slatedb::Db::open("blob-store-tests", Arc::new(InMemory::new())) - .await - .unwrap(), - ); - let store = BlobStore::new(Arc::clone(&raw_db)); + let (raw_db, store) = raw_store("blob-store-tests").await; let bytes = b"{\"ok\":true}"; let id = store.write(bytes).await.unwrap(); diff --git a/lib/components/fabro-store/src/slate/mod.rs b/lib/components/fabro-store/src/slate/mod.rs index aed4c490e..8f9c16ac7 100644 --- a/lib/components/fabro-store/src/slate/mod.rs +++ b/lib/components/fabro-store/src/slate/mod.rs @@ -143,20 +143,23 @@ impl Database { .map(RunDatabase::from_inner) } - pub async fn create_run(&self, run_id: &RunId) -> Result { - self.warm_projection_cache().await?; - let db = self.open_db().await?; - let blob_store = self.blobs().await?; - - self.catalog_index().await?.add(run_id).await?; - let run_store = RunDatabase::open_writer( + /// Builds a run handle wired to the Database-owned shared stores. + async fn open_run_database(&self, run_id: &RunId, read_only: bool) -> Result { + RunDatabase::build( *run_id, - db, - blob_store, + self.open_db().await?, + read_only, + self.blobs().await?, Arc::clone(&self.projection_cache), Arc::clone(&self.run_summary_store), ) - .await?; + .await + } + + pub async fn create_run(&self, run_id: &RunId) -> Result { + self.warm_projection_cache().await?; + self.catalog_index().await?.add(run_id).await?; + let run_store = self.open_run_database(run_id, false).await?; let mut active_runs = self.active_runs.lock().await; Self::cache_active_run(&mut active_runs, &run_store); Ok(run_store) @@ -165,7 +168,6 @@ impl Database { pub async fn open_run(&self, run_id: &RunId) -> Result { self.warm_projection_cache().await?; let db = self.open_db().await?; - let blob_store = self.blobs().await?; // Keep the active-writer miss and insert atomic. Otherwise concurrent // callers can create independent writers with the same recovered seq. let mut active_runs = self.active_runs.lock().await; @@ -181,14 +183,7 @@ impl Database { if !RunDatabase::has_any_events(&db, run_id).await? { return Err(Error::RunNotFound(run_id.to_string())); } - let run_store = RunDatabase::open_writer( - *run_id, - db, - blob_store, - Arc::clone(&self.projection_cache), - Arc::clone(&self.run_summary_store), - ) - .await?; + let run_store = self.open_run_database(run_id, false).await?; Self::cache_active_run(&mut active_runs, &run_store); Ok(run_store) } @@ -206,15 +201,7 @@ impl Database { if !RunDatabase::has_any_events(&db, run_id).await? { return Err(Error::RunNotFound(run_id.to_string())); } - let blob_store = self.blobs().await?; - RunDatabase::open_reader( - *run_id, - db, - blob_store, - Arc::clone(&self.projection_cache), - Arc::clone(&self.run_summary_store), - ) - .await + self.open_run_database(run_id, true).await } pub async fn list_runs(&self, query: &ListRunsQuery, now: DateTime) -> Result> { @@ -866,6 +853,10 @@ mod tests { let blob = br#"{"summary":"readable"}"#; let blob_id = 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. + let _ = store.remove_active_run(&test_run_id("run-1")).await; + let reader = store.open_run_reader(&test_run_id("run-1")).await.unwrap(); assert_eq!( reader.read_blob(&blob_id).await.unwrap().as_deref(), diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index d3999d964..5365ec30b 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -54,43 +54,7 @@ pub(crate) struct RunDatabaseInner { } impl RunDatabase { - pub(crate) async fn open_writer( - run_id: RunId, - db: Db, - blob_store: Arc, - shared_projection_cache: Arc, - run_summary_store: Arc>>, - ) -> Result { - Self::build( - run_id, - db, - false, - blob_store, - shared_projection_cache, - run_summary_store, - ) - .await - } - - pub(crate) async fn open_reader( - run_id: RunId, - db: Db, - blob_store: Arc, - shared_projection_cache: Arc, - run_summary_store: Arc>>, - ) -> Result { - Self::build( - run_id, - db, - true, - blob_store, - shared_projection_cache, - run_summary_store, - ) - .await - } - - async fn build( + pub(crate) async fn build( run_id: RunId, db: Db, read_only: bool, From 2ee61090060c1227323a57c61d5124919f386c31 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Wed, 12 Aug 2026 11:26:35 -0400 Subject: [PATCH 11/62] Warn on malformed blob keys and drop structural sharing test Skipping a malformed key under blobs/sha256 during listing now emits a warn! so operators get a signal when the CAS namespace contains garbage, matching the projection-cache warmup skip path. Also removes the runs_share_database_blob_store test, which asserted Arc pointer identity of internal wiring rather than any observable behavior. Co-Authored-By: Claude Fable 5 --- .../fabro-store/src/slate/blob_store.rs | 5 ++++- lib/components/fabro-store/src/slate/run_store.rs | 15 --------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs index 288755157..9eabface9 100644 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ b/lib/components/fabro-store/src/slate/blob_store.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use bytes::Bytes; use fabro_types::RunBlobId; use futures::StreamExt; +use tracing::warn; use crate::record::{RawBytesCodec, Record, Repository}; use crate::{Error, Result}; @@ -71,7 +72,9 @@ impl BlobStore { while let Some(result) = stream.next().await { match result { Ok(id) => ids.push(id), - Err(Error::KeyParse(_)) => {} + Err(Error::KeyParse(err)) => { + warn!(error = %err, "Skipping malformed blob key during listing"); + } Err(err) => return Err(err), } } diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index 5365ec30b..7d5b5ee2c 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -896,21 +896,6 @@ mod tests { use crate::{Database, Error, EventPayload, keys}; - #[tokio::test] - async fn runs_share_database_blob_store() { - let object_store = Arc::new(InMemory::new()); - let store = Database::new(object_store, "", Duration::from_millis(1), None); - let shared = store.blobs().await.unwrap(); - let first_run_id = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); - let second_run_id = "01JT56VE4Z5NZ814GZN2JZD65B".parse().unwrap(); - - let first_run = store.create_run(&first_run_id).await.unwrap(); - let second_run = store.create_run(&second_run_id).await.unwrap(); - - assert!(Arc::ptr_eq(&shared, &first_run.inner.blob_store)); - assert!(Arc::ptr_eq(&shared, &second_run.inner.blob_store)); - } - #[tokio::test] async fn list_blobs_reads_global_cas_namespace() { let object_store = Arc::new(InMemory::new()); From 62ed7cb8a2b542072bf1ed9186fe2ca4d35c966f Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Tue, 11 Aug 2026 13:37:03 -0400 Subject: [PATCH 12/62] Rename RunBlobId to BlobHash --- lib/apps/fabro-cli/src/commands/run/output.rs | 4 +- lib/apps/fabro-cli/src/commands/run/runner.rs | 6 +-- .../fabro-server/src/principal_middleware.rs | 4 +- lib/apps/fabro-server/src/server.rs | 12 +++--- lib/apps/fabro-server/src/server/tests.rs | 20 ++++----- lib/components/fabro-dump/src/lib.rs | 12 +++--- lib/components/fabro-store/src/lib.rs | 2 +- .../fabro-store/src/record/record_id.rs | 8 ++-- lib/components/fabro-store/src/run_state.rs | 14 +++---- .../fabro-store/src/slate/blob_store.rs | 18 ++++---- .../fabro-store/src/slate/run_store.rs | 8 ++-- lib/components/fabro-workflow/src/artifact.rs | 16 +++---- .../fabro-workflow/src/event/events.rs | 12 +++--- .../fabro-workflow/src/handler/command.rs | 8 ++-- .../fabro-workflow/src/handler/parallel.rs | 2 +- .../fabro-workflow/src/lifecycle/git.rs | 8 ++-- .../fabro-workflow/src/operations/retry.rs | 8 ++-- .../fabro-workflow/src/operations/start.rs | 2 +- .../fabro-workflow/src/pipeline/finalize.rs | 8 ++-- .../fabro-workflow/src/runtime_store.rs | 14 +++---- .../tests/it/daytona_integration.rs | 2 +- .../fabro-workflow/tests/it/integration.rs | 4 +- lib/foundation/fabro-client/src/client.rs | 12 ++---- .../src/{run_blob_id.rs => blob_hash.rs} | 42 +++++++++---------- lib/foundation/fabro-types/src/blob_ref.rs | 40 +++++++++--------- lib/foundation/fabro-types/src/lib.rs | 4 +- lib/foundation/fabro-types/src/run.rs | 6 +-- .../fabro-types/src/run_event/mod.rs | 6 +-- .../fabro-types/src/run_event/run.rs | 6 +-- 29 files changed, 152 insertions(+), 156 deletions(-) rename lib/foundation/fabro-types/src/{run_blob_id.rs => blob_hash.rs} (60%) diff --git a/lib/apps/fabro-cli/src/commands/run/output.rs b/lib/apps/fabro-cli/src/commands/run/output.rs index 136d2b506..0e5ba2d42 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::{PullRequestLink, RunBlobId, RunId, StageId, parse_blob_ref}; +use fabro_types::{BlobHash, 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; @@ -341,7 +341,7 @@ async fn resolve_response_string( })) } -fn blob_id_from_response(response: &str) -> Option { +fn blob_id_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 1d825d3c8..446888e7e 100644 --- a/lib/apps/fabro-cli/src/commands/run/runner.rs +++ b/lib/apps/fabro-cli/src/commands/run/runner.rs @@ -21,7 +21,7 @@ use fabro_store::{EventEnvelope, RunProjection, RunProjectionReducer}; use fabro_tool::fabro_client::ClientBackend; use fabro_types::settings::run::{RunMode, RunNamespace}; use fabro_types::{ - ArtifactUpload, EventBody, FailureReason, Principal, RunBlobId, RunEvent, RunId, + ArtifactUpload, BlobHash, EventBody, FailureReason, Principal, RunEvent, RunId, WorkflowSettings, }; use fabro_vault::{SecretStore, Vault}; @@ -1008,7 +1008,7 @@ impl RunStoreBackend for HttpRunStore { self.apply_acknowledged_event(seq, event).await } - async fn write_blob(&self, data: &[u8]) -> Result { + async fn write_blob(&self, data: &[u8]) -> Result { self.with_retries("write run blob", || { let client = self.client.clone_for_reuse(); let run_id = self.run_id; @@ -1018,7 +1018,7 @@ impl RunStoreBackend for HttpRunStore { .await } - async fn read_blob(&self, id: &RunBlobId) -> Result> { + async fn read_blob(&self, id: &BlobHash) -> Result> { self.with_retries("read run blob", || { let client = self.client.clone_for_reuse(); let run_id = self.run_id; diff --git a/lib/apps/fabro-server/src/principal_middleware.rs b/lib/apps/fabro-server/src/principal_middleware.rs index 2559b07bf..2db165547 100644 --- a/lib/apps/fabro-server/src/principal_middleware.rs +++ b/lib/apps/fabro-server/src/principal_middleware.rs @@ -7,7 +7,7 @@ use axum::http::StatusCode; use axum::http::request::Parts; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; -use fabro_types::{AuthMethod, IdpIdentity, Principal, RunBlobId, RunId, StageId, UserPrincipal}; +use fabro_types::{AuthMethod, BlobHash, IdpIdentity, Principal, RunId, StageId, UserPrincipal}; use jsonwebtoken::decode_header; use strum::IntoStaticStr; @@ -61,7 +61,7 @@ pub(crate) struct RequiredRunToolActor(pub(crate) Principal); pub(crate) struct RequireRunScoped(pub(crate) RunId); pub(crate) struct RequireWorkerRunScoped(pub(crate) RunId); pub(crate) struct RequireRunManagementTarget(pub(crate) RunId, pub(crate) Principal); -pub(crate) struct RequireRunBlob(pub(crate) RunId, pub(crate) RunBlobId); +pub(crate) struct RequireRunBlob(pub(crate) RunId, pub(crate) BlobHash); pub(crate) struct RequireRunStageScoped(pub(crate) RunId, pub(crate) String); pub(crate) struct RequireStageArtifact(pub(crate) RunId, pub(crate) StageId); pub(crate) struct RequireCommandLog(pub(crate) RunId, pub(crate) StageId); diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index 5445ad619..fa8af18c8 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -96,10 +96,10 @@ use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, LogDestination, }; use fabro_types::{ - AgentBackend, AskFabro, AskFabroUnavailableReason, EventBody, InterviewQuestionRecord, PairId, - PairMessageId, PairTarget, PendingReason, Principal, PullRequestLink, QuestionType, RunBlobId, - RunControlAction, RunEvent, RunId, RunRunnableSource, SandboxProviderKind, ServerSettings, - SessionCapability, + AgentBackend, AskFabro, AskFabroUnavailableReason, BlobHash, EventBody, + InterviewQuestionRecord, PairId, PairMessageId, PairTarget, PendingReason, Principal, + PullRequestLink, QuestionType, RunControlAction, RunEvent, RunId, RunRunnableSource, + SandboxProviderKind, ServerSettings, SessionCapability, }; use fabro_util::error::{ SharedError, collect_causes, render_compact_with_causes, render_with_causes, @@ -2891,8 +2891,8 @@ pub(crate) fn parse_stage_id_path(stage_id: &str) -> Result { clippy::result_large_err, reason = "Blob ID parsing returns HTTP 400 responses directly." )] -pub(crate) fn parse_blob_id_path(blob_id: &str) -> Result { - RunBlobId::from_str(blob_id) +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()) } diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index a05443a00..2ffec61de 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -26,12 +26,12 @@ use fabro_model::{Catalog, ModelRef, ProviderId, ReasoningEffort, Speed}; use fabro_types::settings::ServerAuthMethod; use fabro_types::settings::run::EnvironmentProvider; use fabro_types::{ - AgentBackend, AttrValue, AuthMethod, CommandTermination, FailureCategory, FailureDetail, Graph, - InterviewQuestionRecord, Node, Outcome, ParallelBranchId, QuestionType, RunBlobId, RunId, - RunSpec, SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory, - StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, - StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind, - WorkflowSettings, fixtures, test_support, + AgentBackend, AttrValue, AuthMethod, BlobHash, CommandTermination, FailureCategory, + FailureDetail, Graph, InterviewQuestionRecord, Node, Outcome, ParallelBranchId, QuestionType, + RunId, RunSpec, SandboxProviderKind, StageContextWindowBreakdownItem, + StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection, + StageContextWindowStaleness, StageContextWindowWarning, StageModelUsage, StageTiming, + SuccessReason, SystemActorKind, WorkflowSettings, fixtures, test_support, }; use fabro_util::check_report::CheckStatus; use fabro_workflow::records::CheckpointExt; @@ -3890,7 +3890,7 @@ layer = "project" let manifest_blob = created["properties"]["manifest_blob"] .as_str() .expect("run.created should carry the submitted source blob") - .parse::() + .parse::() .unwrap(); let persisted_manifest = run_store .read_blob(&manifest_blob) @@ -10786,12 +10786,12 @@ async fn create_run_persists_manifest_and_definition_blobs_without_bundle_file() let manifest_blob = created["properties"]["manifest_blob"] .as_str() .expect("run.created should carry manifest_blob") - .parse::() + .parse::() .unwrap(); let definition_blob = submitted["properties"]["definition_blob"] .as_str() .expect("run.submitted should carry definition_blob") - .parse::() + .parse::() .unwrap(); let submitted_manifest_bytes = run_store @@ -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 = RunBlobId::new(b"blob"); + let blob_id = BlobHash::new(b"blob"); let user_only_routes = vec![ (Method::GET, "/runs".to_string()), (Method::POST, "/runs".to_string()), diff --git a/lib/components/fabro-dump/src/lib.rs b/lib/components/fabro-dump/src/lib.rs index 1b3a10a4c..1028408e0 100644 --- a/lib/components/fabro-dump/src/lib.rs +++ b/lib/components/fabro-dump/src/lib.rs @@ -16,10 +16,10 @@ use bytes::Bytes; use fabro_store::{ EventEnvelope, RunProjection, SerializableProjection, StageId, retry_storage_segment, }; -use fabro_types::{RunBlobId, parse_blob_ref}; +use fabro_types::{BlobHash, parse_blob_ref}; use futures::future::BoxFuture; -pub type BlobReader = Box BoxFuture<'static, Result>> + Send>; +pub type BlobReader = Box BoxFuture<'static, Result>> + Send>; const STAGE_RANK_WIDTH: usize = 3; const MAX_STAGES_IN_DUMP: usize = { @@ -208,7 +208,7 @@ impl RunDump { mut read_blob: F, ) -> Result<()> where - F: FnMut(RunBlobId) -> BoxFuture<'a, Result>>, + F: FnMut(BlobHash) -> BoxFuture<'a, Result>>, { let mut cache = HashMap::new(); for entry in &mut self.entries { @@ -386,7 +386,7 @@ 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_ids: &mut Vec) { match value { serde_json::Value::String(current) => { if let Some(blob_id) = parse_blob_ref(current) { @@ -409,7 +409,7 @@ fn collect_blob_refs_in_value(value: &serde_json::Value, blob_ids: &mut Vec, + cache: &HashMap, ) -> Result<()> { match value { serde_json::Value::String(current) => { @@ -724,7 +724,7 @@ 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::RunBlobId::new(&blob); + let blob_id = fabro_types::BlobHash::new(&blob); let legacy_ref = format!("file:///sandbox/.fabro/artifacts/{blob_id}.json"); let mut dump = RunDump { entries: vec![RunDumpEntry::json( diff --git a/lib/components/fabro-store/src/lib.rs b/lib/components/fabro-store/src/lib.rs index e99eadd0f..1b514a8e0 100644 --- a/lib/components/fabro-store/src/lib.rs +++ b/lib/components/fabro-store/src/lib.rs @@ -20,7 +20,7 @@ pub use artifact_store::{ }; pub use error::{Error, Result}; pub use fabro_types::{ - EventEnvelope, PendingInterviewRecord, Run, RunBlobId, RunProjection, StageId, StageProjection, + BlobHash, EventEnvelope, PendingInterviewRecord, Run, RunProjection, StageId, StageProjection, }; pub use keyed_mutex::{KeyedMutex, KeyedMutexGuard}; pub use run_sessions::{ diff --git a/lib/components/fabro-store/src/record/record_id.rs b/lib/components/fabro-store/src/record/record_id.rs index 1ce67f069..b926d9c12 100644 --- a/lib/components/fabro-store/src/record/record_id.rs +++ b/lib/components/fabro-store/src/record/record_id.rs @@ -1,4 +1,4 @@ -use fabro_types::{RunBlobId, RunId}; +use fabro_types::{BlobHash, RunId}; use super::RecordId; use crate::{Error, Result}; @@ -38,7 +38,7 @@ impl RecordId for String { } } -impl RecordId for RunBlobId { +impl RecordId for BlobHash { fn key_segments(&self) -> Vec { vec![self.to_string()] } @@ -46,13 +46,13 @@ impl RecordId for RunBlobId { fn from_key_segments(segs: &[&str]) -> Result { let [segment] = segs else { return Err(Error::KeyParse(format!( - "expected 1 segment for RunBlobId, got {}", + "expected 1 segment for BlobHash, got {}", segs.len() ))); }; segment .parse() - .map_err(|err| Error::KeyParse(format!("invalid RunBlobId segment {segment:?}: {err}"))) + .map_err(|err| Error::KeyParse(format!("invalid BlobHash segment {segment:?}: {err}"))) } } diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index 5e4ab5e37..c89587e59 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -1690,11 +1690,11 @@ mod tests { use fabro_types::settings::run::{DockerfileSource, EnvironmentProvider}; use fabro_types::{ AgentBackend, AgentControlState, AttrValue, AutomationRef, BilledModelUsage, - 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, + BilledTokenCounts, BlobHash, BlockedReason, Checkpoint, CheckpointRecord, + CommandTermination, EventBody, FailureCategory, FailureDetail, FailureReason, Graph, + McpServerStatus, Node, Outcome, ParallelBranchId, PendingReason, PermissionLevel, + PullRequestCreationStatus, PullRequestLink, QuestionType, ReasoningEffort, + RunApprovalState, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, Speed, StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning, StageHandler, StageModelUsage, StageOutcome, StageState, StageTiming, SubAgentStatus, @@ -4238,9 +4238,9 @@ mod tests { #[test] fn projection_serialization_includes_manifest_and_definition_blob_refs() { - let manifest_blob = RunBlobId::new(br#"{"version":1}"#).to_string(); + let manifest_blob = BlobHash::new(br#"{"version":1}"#).to_string(); let definition_blob = - RunBlobId::new(br#"{"version":1,"workflow_path":"workflow.fabro"}"#).to_string(); + BlobHash::new(br#"{"version":1,"workflow_path":"workflow.fabro"}"#).to_string(); let events = vec![ EventEnvelope { seq: 1, diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs index 9eabface9..68c6a9a54 100644 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ b/lib/components/fabro-store/src/slate/blob_store.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use bytes::Bytes; -use fabro_types::RunBlobId; +use fabro_types::BlobHash; use futures::StreamExt; use tracing::warn; @@ -24,13 +24,13 @@ impl From for Blob { } impl Record for Blob { - type Id = RunBlobId; + type Id = BlobHash; type Codec = RawBytesCodec; const PREFIX: &'static str = "blobs/sha256"; fn id(&self) -> Self::Id { - RunBlobId::new(&self.0) + BlobHash::new(&self.0) } } @@ -51,22 +51,22 @@ impl BlobStore { } } - pub async fn write(&self, bytes: &[u8]) -> Result { + 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, id: &RunBlobId) -> Result> { + pub async fn read(&self, id: &BlobHash) -> Result> { Ok(self.repo.get(id).await?.map(|blob| blob.0)) } - pub async fn exists(&self, id: &RunBlobId) -> Result { + pub async fn exists(&self, id: &BlobHash) -> Result { self.repo.exists(id).await } - pub(crate) async fn list(&self) -> Result> { + 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 { @@ -89,7 +89,7 @@ mod tests { use std::time::Duration; use bytes::Bytes; - use fabro_types::RunBlobId; + use fabro_types::BlobHash; use object_store::memory::InMemory; use super::BlobStore; @@ -128,7 +128,7 @@ mod tests { ); assert_eq!(store.write(bytes).await.unwrap(), id); assert!(store.exists(&id).await.unwrap()); - assert!(!store.exists(&RunBlobId::new(b"missing")).await.unwrap()); + assert!(!store.exists(&BlobHash::new(b"missing")).await.unwrap()); } #[tokio::test] diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index 7d5b5ee2c..646b636ab 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, OnceLock}; use bytes::Bytes; use chrono::Utc; -use fabro_types::{RunBlobId, RunEvent, RunId, SessionId}; +use fabro_types::{BlobHash, RunEvent, RunId, SessionId}; use futures::Stream; use slatedb::{Db, DbIterator, DbRead}; use tokio::sync::{Mutex, broadcast, mpsc}; @@ -554,18 +554,18 @@ impl RunDatabase { Ok(Box::pin(UnboundedReceiverStream::new(receiver))) } - pub async fn write_blob(&self, data: &[u8]) -> Result { + pub async fn write_blob(&self, data: &[u8]) -> Result { if self.read_only { return Err(Error::ReadOnly); } self.inner.blob_store.write(data).await } - pub async fn read_blob(&self, id: &RunBlobId) -> Result> { + pub async fn read_blob(&self, id: &BlobHash) -> Result> { self.inner.blob_store.read(id).await } - pub async fn list_blobs(&self) -> Result> { + pub async fn list_blobs(&self) -> Result> { self.inner.blob_store.list().await } diff --git a/lib/components/fabro-workflow/src/artifact.rs b/lib/components/fabro-workflow/src/artifact.rs index 517f5a435..ef892b442 100644 --- a/lib/components/fabro-workflow/src/artifact.rs +++ b/lib/components/fabro-workflow/src/artifact.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use fabro_agent::Sandbox; use fabro_config::RunScratch; use fabro_types::{ - ParallelBranchResult, RunBlobId, format_blob_ref, parse_blob_ref, parse_managed_blob_file_ref, + BlobHash, ParallelBranchResult, format_blob_ref, parse_blob_ref, parse_managed_blob_file_ref, }; use futures::future::BoxFuture; use serde_json::Value; @@ -413,7 +413,7 @@ fn resolve_execution_value<'a>( } async fn materialize_blob_ref( - blob_id: &RunBlobId, + blob_id: &BlobHash, run_store: &RunStoreHandle, env: &dyn Sandbox, run_dir: &Path, @@ -457,7 +457,7 @@ async fn materialize_blob_ref( } async fn read_required_blob( - blob_id: &RunBlobId, + blob_id: &BlobHash, run_store: &RunStoreHandle, ) -> Result { run_store @@ -508,7 +508,7 @@ 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: &RunBlobId) -> PathBuf { +fn local_materialized_blob_path(run_dir: &Path, blob_id: &BlobHash) -> PathBuf { RunScratch::new(run_dir) .runtime_dir() .join("blobs") @@ -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::RunBlobId::new(&serialized); + let expected_blob_id = fabro_types::BlobHash::new(&serialized); let mut updates = HashMap::new(); updates.insert("response.plan".to_string(), serde_json::json!(large_string)); @@ -636,7 +636,7 @@ mod tests { Value::String("small".to_string()); BLOB_OFFLOAD_THRESHOLD / 4 ]); - let expected_report_blob = RunBlobId::new(&serde_json::to_vec(&large_report).unwrap()); + let expected_report_blob = BlobHash::new(&serde_json::to_vec(&large_report).unwrap()); let mut typed_results = vec![ParallelBranchResult { id: "branch_a".to_string(), index: Some(0), @@ -789,7 +789,7 @@ mod tests { #[test] fn normalize_durable_updates_rewrites_managed_blob_file_refs_recursively() { - let blob_id = fabro_types::RunBlobId::new(b"hello"); + let blob_id = fabro_types::BlobHash::new(b"hello"); let mut updates = HashMap::from([( "nested".to_string(), serde_json::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::RunBlobId::new(b"managed"); + let blob_id = fabro_types::BlobHash::new(b"managed"); let mut checkpoint = crate::records::Checkpoint { timestamp: chrono::Utc::now(), current_node: "work".to_string(), diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index f36d01a16..a38cefbd0 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -1,12 +1,12 @@ use std::collections::BTreeMap; use ::fabro_types::{ - AutomationRef, BilledTokenCounts, BlockedReason, CommandTermination, DiffSummary, + AutomationRef, BilledTokenCounts, BlobHash, BlockedReason, CommandTermination, DiffSummary, FailureReason, ForkSourceRef, GitContext, PairId, PairMessageId, PairSystemMessageKind, PairTarget, ParallelBranchId, ParallelBranchResult, PendingReason, PermissionLevel, Principal, - PullRequestCreationId, PullRequestLink, ReviewTarget, RunBlobId, RunFailure, RunId, - RunNoticeLevel, RunPairEndedReason, RunPairFailedReason, RunProvenance, RunRunnableSource, - RunTiming, SandboxProviderKind, StageId, StageOutcome, StageTiming, SuccessReason, + PullRequestCreationId, PullRequestLink, ReviewTarget, RunFailure, RunId, RunNoticeLevel, + RunPairEndedReason, RunPairFailedReason, RunProvenance, RunRunnableSource, RunTiming, + SandboxProviderKind, StageId, StageOutcome, StageTiming, SuccessReason, run_event as fabro_types, }; use fabro_agent::{AgentEvent, SandboxEvent}; @@ -39,7 +39,7 @@ pub enum Event { automation: Option, provenance: RunProvenance, #[serde(default, skip_serializing_if = "Option::is_none")] - manifest_blob: Option, + manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -67,7 +67,7 @@ pub enum Event { }, RunSubmitted { #[serde(default, skip_serializing_if = "Option::is_none")] - definition_blob: Option, + definition_blob: Option, }, RunStartRequested { resume: bool, diff --git a/lib/components/fabro-workflow/src/handler/command.rs b/lib/components/fabro-workflow/src/handler/command.rs index 5dfc41e46..f4fef254c 100644 --- a/lib/components/fabro-workflow/src/handler/command.rs +++ b/lib/components/fabro-workflow/src/handler/command.rs @@ -354,7 +354,7 @@ mod tests { #[derive(Default)] struct MemoryRunStoreBackend { - blobs: Mutex>, + blobs: Mutex>, } #[async_trait::async_trait] @@ -389,8 +389,8 @@ mod tests { Ok(()) } - async fn write_blob(&self, data: &[u8]) -> anyhow::Result { - let blob_id = fabro_types::RunBlobId::new(data); + async fn write_blob(&self, data: &[u8]) -> anyhow::Result { + let blob_id = fabro_types::BlobHash::new(data); self.blobs .lock() .await @@ -398,7 +398,7 @@ mod tests { Ok(blob_id) } - async fn read_blob(&self, id: &fabro_types::RunBlobId) -> anyhow::Result> { + async fn read_blob(&self, id: &fabro_types::BlobHash) -> anyhow::Result> { Ok(self.blobs.lock().await.get(id).cloned()) } diff --git a/lib/components/fabro-workflow/src/handler/parallel.rs b/lib/components/fabro-workflow/src/handler/parallel.rs index 62fc59630..f20d5d5f5 100644 --- a/lib/components/fabro-workflow/src/handler/parallel.rs +++ b/lib/components/fabro-workflow/src/handler/parallel.rs @@ -1847,7 +1847,7 @@ mod tests { Some(serde_json::json!({"not": "an array"})), Some(serde_json::json!("ordinary string")), Some(serde_json::json!(format_blob_ref( - &fabro_types::RunBlobId::new(b"missing") + &fabro_types::BlobHash::new(b"missing") ))), ] { let (handler, calls) = ScriptedHandler::new(Scripted::Succeed); diff --git a/lib/components/fabro-workflow/src/lifecycle/git.rs b/lib/components/fabro-workflow/src/lifecycle/git.rs index fce72da6e..73100e6c1 100644 --- a/lib/components/fabro-workflow/src/lifecycle/git.rs +++ b/lib/components/fabro-workflow/src/lifecycle/git.rs @@ -613,7 +613,7 @@ mod tests { use fabro_model::Catalog; use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection}; use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; - use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures, test_support}; + use fabro_types::{BlobHash, EventBody, RunEvent, WorkflowSettings, fixtures, test_support}; use object_store::memory::InMemory; use super::*; @@ -1324,11 +1324,11 @@ mod tests { Ok(()) } - async fn write_blob(&self, data: &[u8]) -> Result { - Ok(RunBlobId::new(data)) + async fn write_blob(&self, data: &[u8]) -> Result { + Ok(BlobHash::new(data)) } - async fn read_blob(&self, _id: &RunBlobId) -> Result> { + async fn read_blob(&self, _id: &BlobHash) -> Result> { Ok(None) } diff --git a/lib/components/fabro-workflow/src/operations/retry.rs b/lib/components/fabro-workflow/src/operations/retry.rs index 27a9df68a..8163efa55 100644 --- a/lib/components/fabro-workflow/src/operations/retry.rs +++ b/lib/components/fabro-workflow/src/operations/retry.rs @@ -117,8 +117,8 @@ mod tests { use fabro_store::{Database, RunProjectionReducer}; use fabro_types::{ - AuthMethod, DirtyStatus, FailureReason, ForkSourceRef, GitContext, Graph, IdpIdentity, - Principal, PullRequestLink, RunBlobId, RunRunnableSource, RunServerProvenance, RunTiming, + AuthMethod, BlobHash, DirtyStatus, FailureReason, ForkSourceRef, GitContext, Graph, + IdpIdentity, Principal, PullRequestLink, RunRunnableSource, RunServerProvenance, RunTiming, WorkflowSettings, fixtures, }; use object_store::memory::InMemory; @@ -164,7 +164,7 @@ mod tests { async fn append_created( store: &fabro_store::RunDatabase, run_id: RunId, - manifest_blob: Option, + manifest_blob: Option, fork_source_ref: Option, ) { let mut settings = WorkflowSettings::default(); @@ -248,7 +248,7 @@ mod tests { async fn seed_retryable_failed_source( store: &Database, source_run_id: RunId, - ) -> (Option, Option, ForkSourceRef) { + ) -> (Option, Option, ForkSourceRef) { let source_store = store.create_run(&source_run_id).await.unwrap(); let manifest_blob = Some( source_store diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index 12b7455a9..adbaab3f9 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -570,7 +570,7 @@ fn vault_token_lookup(vault: &Vault, name: &str) -> Option { async fn load_accepted_run_definition( run_store: &RunStoreHandle, - blob_id: fabro_types::RunBlobId, + blob_id: fabro_types::BlobHash, ) -> Result { let bytes = run_store .read_blob(&blob_id) diff --git a/lib/components/fabro-workflow/src/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index 8c497175d..9f43e86ff 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -684,7 +684,7 @@ mod tests { use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection}; use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; use fabro_types::{ - BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion, + BilledTokenCounts, BlobHash, EventBody, RunEvent, RunId, RunSpec, StageCompletion, WorkflowSettings, first_event_seq, fixtures, test_support, }; use object_store::memory::InMemory; @@ -1819,11 +1819,11 @@ mod tests { Ok(()) } - async fn write_blob(&self, data: &[u8]) -> Result { - Ok(RunBlobId::new(data)) + async fn write_blob(&self, data: &[u8]) -> Result { + Ok(BlobHash::new(data)) } - async fn read_blob(&self, _id: &RunBlobId) -> Result> { + async fn read_blob(&self, _id: &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 c376c47e7..45252d5d3 100644 --- a/lib/components/fabro-workflow/src/runtime_store.rs +++ b/lib/components/fabro-workflow/src/runtime_store.rs @@ -4,7 +4,7 @@ use anyhow::Result; use async_trait::async_trait; use bytes::Bytes; use fabro_store::{EventEnvelope, RunDatabase, RunProjection}; -use fabro_types::{RunBlobId, RunEvent}; +use fabro_types::{BlobHash, RunEvent}; use crate::event::build_redacted_event_payload; @@ -13,8 +13,8 @@ pub trait RunStoreBackend: Send + Sync { async fn load_state(&self) -> Result; 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: &RunBlobId) -> Result>; + async fn write_blob(&self, data: &[u8]) -> Result; + async fn read_blob(&self, id: &BlobHash) -> Result>; async fn read_run_log(&self) -> Result>>; } @@ -46,11 +46,11 @@ impl RunStoreHandle { self.backend.append_run_event(event).await } - pub async fn write_blob(&self, data: &[u8]) -> Result { + pub async fn write_blob(&self, data: &[u8]) -> Result { self.backend.write_blob(data).await } - pub async fn read_blob(&self, id: &RunBlobId) -> Result> { + pub async fn read_blob(&self, id: &BlobHash) -> Result> { self.backend.read_blob(id).await } @@ -91,14 +91,14 @@ impl RunStoreBackend for LocalRunStoreBackend { .map_err(anyhow::Error::from) } - async fn write_blob(&self, data: &[u8]) -> Result { + async fn write_blob(&self, data: &[u8]) -> Result { self.run_store .write_blob(data) .await .map_err(anyhow::Error::from) } - async fn read_blob(&self, id: &RunBlobId) -> Result> { + async fn read_blob(&self, id: &BlobHash) -> Result> { self.run_store .read_blob(id) .await diff --git a/lib/components/fabro-workflow/tests/it/daytona_integration.rs b/lib/components/fabro-workflow/tests/it/daytona_integration.rs index 5482f642d..021eecf11 100644 --- a/lib/components/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/components/fabro-workflow/tests/it/daytona_integration.rs @@ -544,7 +544,7 @@ 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::RunBlobId::new( + let expected_blob_id = fabro_types::BlobHash::new( &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) .expect("large value should serialize"), ); diff --git a/lib/components/fabro-workflow/tests/it/integration.rs b/lib/components/fabro-workflow/tests/it/integration.rs index 7ddb72647..0206d2c38 100644 --- a/lib/components/fabro-workflow/tests/it/integration.rs +++ b/lib/components/fabro-workflow/tests/it/integration.rs @@ -10059,7 +10059,7 @@ 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::RunBlobId::new( + let expected_blob_id = fabro_types::BlobHash::new( &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) .expect("large value should serialize"), ); @@ -10258,7 +10258,7 @@ 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::RunBlobId::new( + let expected_blob_id = fabro_types::BlobHash::new( &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) .expect("large value should serialize"), ); diff --git a/lib/foundation/fabro-client/src/client.rs b/lib/foundation/fabro-client/src/client.rs index a22a2ebf3..4ece0a9de 100644 --- a/lib/foundation/fabro-client/src/client.rs +++ b/lib/foundation/fabro-client/src/client.rs @@ -13,8 +13,8 @@ use fabro_http::multipart::{Form, Part}; use fabro_model::{Model, ModelTestMode, ProviderId}; use fabro_types::settings::run::MergeStrategy; use fabro_types::{ - ArtifactUpload, EventEnvelope, PairId, PairMessageRecord, PairMessageRequest, PairRecord, - PairStartRequest, PairTranscriptResponse, Run, RunBlobId, RunEvent, RunEventDetailResponse, + ArtifactUpload, BlobHash, EventEnvelope, PairId, PairMessageRecord, PairMessageRequest, + PairRecord, PairStartRequest, PairTranscriptResponse, Run, RunEvent, RunEventDetailResponse, RunId, RunPairStatusResponse, RunProjection, SessionId, SessionRecord, StageId, }; use fabro_util::exit::{ErrorExt, ExitClass}; @@ -1828,7 +1828,7 @@ impl Client { u32::try_from(response.into_inner().seq).context("append_run_event returned invalid seq") } - pub async fn write_run_blob(&self, run_id: &RunId, data: &[u8]) -> Result { + pub async fn write_run_blob(&self, run_id: &RunId, data: &[u8]) -> Result { let response = self .send_api(|client| async move { client @@ -1846,11 +1846,7 @@ impl Client { .context("write_run_blob returned invalid blob id") } - pub async fn read_run_blob( - &self, - run_id: &RunId, - blob_id: &RunBlobId, - ) -> Result> { + pub async fn read_run_blob(&self, run_id: &RunId, blob_id: &BlobHash) -> Result> { let response = self .current_state() .client diff --git a/lib/foundation/fabro-types/src/run_blob_id.rs b/lib/foundation/fabro-types/src/blob_hash.rs similarity index 60% rename from lib/foundation/fabro-types/src/run_blob_id.rs rename to lib/foundation/fabro-types/src/blob_hash.rs index b7c43d708..a99dd007a 100644 --- a/lib/foundation/fabro-types/src/run_blob_id.rs +++ b/lib/foundation/fabro-types/src/blob_hash.rs @@ -7,9 +7,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::{Digest, Sha256}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct RunBlobId([u8; 32]); +pub struct BlobHash([u8; 32]); -impl RunBlobId { +impl BlobHash { pub fn new(content: &[u8]) -> Self { let hash = Sha256::digest(content); let mut bytes = [0_u8; 32]; @@ -18,13 +18,13 @@ impl RunBlobId { } } -impl fmt::Display for RunBlobId { +impl fmt::Display for BlobHash { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&hex::encode(self.0)) } } -impl FromStr for RunBlobId { +impl FromStr for BlobHash { type Err = FromHexError; fn from_str(s: &str) -> Result { @@ -34,7 +34,7 @@ impl FromStr for RunBlobId { } } -impl Serialize for RunBlobId { +impl Serialize for BlobHash { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -43,7 +43,7 @@ impl Serialize for RunBlobId { } } -impl<'de> Deserialize<'de> for RunBlobId { +impl<'de> Deserialize<'de> for BlobHash { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -55,44 +55,44 @@ impl<'de> Deserialize<'de> for RunBlobId { #[cfg(test)] mod tests { - use crate::RunBlobId; + use crate::BlobHash; #[test] - fn same_content_produces_same_blob_id() { - assert_eq!(RunBlobId::new(b"hello"), RunBlobId::new(b"hello")); + fn same_content_produces_same_blob_hash() { + assert_eq!(BlobHash::new(b"hello"), BlobHash::new(b"hello")); } #[test] fn display_is_lowercase_sha256_hex() { assert_eq!( - RunBlobId::new(b"hello").to_string(), + BlobHash::new(b"hello").to_string(), "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" ); } #[test] - fn different_content_produces_different_blob_ids() { - assert_ne!(RunBlobId::new(b"hello"), RunBlobId::new(b"world")); + fn different_content_produces_different_blob_hashes() { + assert_ne!(BlobHash::new(b"hello"), BlobHash::new(b"world")); } #[test] fn display_and_parse_round_trip() { - let blob_id = RunBlobId::new(b"hello"); - let parsed: RunBlobId = blob_id.to_string().parse().unwrap(); - assert_eq!(parsed, blob_id); + let blob_hash = BlobHash::new(b"hello"); + let parsed: BlobHash = blob_hash.to_string().parse().unwrap(); + assert_eq!(parsed, blob_hash); } #[test] fn serde_round_trip() { - let blob_id = RunBlobId::new(b"hello"); - let value = serde_json::to_value(blob_id).unwrap(); - let parsed: RunBlobId = serde_json::from_value(value).unwrap(); - assert_eq!(parsed, blob_id); + let blob_hash = BlobHash::new(b"hello"); + let value = serde_json::to_value(blob_hash).unwrap(); + let parsed: BlobHash = serde_json::from_value(value).unwrap(); + assert_eq!(parsed, blob_hash); } #[test] - fn parse_rejects_non_hex_blob_ids() { - let parsed = "not-a-blob-id".parse::(); + fn parse_rejects_non_hex_blob_hashes() { + let parsed = "not-a-blob-hash".parse::(); assert!(parsed.is_err()); } } diff --git a/lib/foundation/fabro-types/src/blob_ref.rs b/lib/foundation/fabro-types/src/blob_ref.rs index 3db7c719a..f413cd6ff 100644 --- a/lib/foundation/fabro-types/src/blob_ref.rs +++ b/lib/foundation/fabro-types/src/blob_ref.rs @@ -1,35 +1,35 @@ use std::path::Path; -use crate::RunBlobId; +use crate::BlobHash; const BLOB_REF_PREFIX: &str = "blob://sha256/"; #[must_use] -pub fn format_blob_ref(blob_id: &RunBlobId) -> String { - format!("{BLOB_REF_PREFIX}{blob_id}") +pub fn format_blob_ref(blob_hash: &BlobHash) -> String { + format!("{BLOB_REF_PREFIX}{blob_hash}") } #[must_use] -pub fn parse_blob_ref(value: &str) -> Option { +pub fn parse_blob_ref(value: &str) -> Option { value.strip_prefix(BLOB_REF_PREFIX)?.parse().ok() } #[must_use] -pub fn parse_managed_blob_file_ref(value: &str) -> Option { +pub fn parse_managed_blob_file_ref(value: &str) -> Option { let path = value.strip_prefix("file://")?; - let blob_id = parse_blob_file_name(path)?; + let blob_hash = parse_blob_file_name(path)?; if has_path_suffix(path, &["runtime", "blobs"]) || has_path_suffix(path, &[".fabro", "blobs"]) { - Some(blob_id) + Some(blob_hash) } else { None } } -fn parse_blob_file_name(path: &str) -> Option { +fn parse_blob_file_name(path: &str) -> Option { let file_name = Path::new(path).file_name()?.to_str()?; - let blob_id = file_name.strip_suffix(".json")?; - blob_id.parse().ok() + let blob_hash = file_name.strip_suffix(".json")?; + blob_hash.parse().ok() } fn has_path_suffix(path: &str, suffix: &[&str]) -> bool { @@ -46,30 +46,30 @@ fn has_path_suffix(path: &str, suffix: &[&str]) -> bool { #[cfg(test)] mod tests { use super::{format_blob_ref, parse_blob_ref, parse_managed_blob_file_ref}; - use crate::RunBlobId; + use crate::BlobHash; #[test] fn blob_ref_round_trips() { - let blob_id = RunBlobId::new(br#"{"kind":"summary"}"#); - let formatted = format_blob_ref(&blob_id); + let blob_hash = BlobHash::new(br#"{"kind":"summary"}"#); + let formatted = format_blob_ref(&blob_hash); - assert_eq!(parse_blob_ref(&formatted), Some(blob_id)); + assert_eq!(parse_blob_ref(&formatted), Some(blob_hash)); } #[test] fn managed_local_blob_file_ref_is_recognized() { - let blob_id = RunBlobId::new(b"hello"); - let value = format!("file:///tmp/run/runtime/blobs/{blob_id}.json"); + let blob_hash = BlobHash::new(b"hello"); + let value = format!("file:///tmp/run/runtime/blobs/{blob_hash}.json"); - assert_eq!(parse_managed_blob_file_ref(&value), Some(blob_id)); + assert_eq!(parse_managed_blob_file_ref(&value), Some(blob_hash)); } #[test] fn managed_remote_blob_file_ref_is_recognized() { - let blob_id = RunBlobId::new(b"hello"); - let value = format!("file:///sandbox/.fabro/blobs/{blob_id}.json"); + let blob_hash = BlobHash::new(b"hello"); + let value = format!("file:///sandbox/.fabro/blobs/{blob_hash}.json"); - assert_eq!(parse_managed_blob_file_ref(&value), Some(blob_id)); + assert_eq!(parse_managed_blob_file_ref(&value), Some(blob_hash)); } #[test] diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index 9dba7fae6..e2b05384c 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -3,6 +3,7 @@ extern crate self as fabro_types; pub mod artifact; pub mod auth; pub mod billing; +pub mod blob_hash; pub mod blob_ref; pub mod checkpoint; pub mod command_output; @@ -26,7 +27,6 @@ pub mod pull_request; pub mod reasoning; pub mod repository; pub mod run; -pub mod run_blob_id; pub mod run_event; pub mod run_failure; pub mod run_id; @@ -63,6 +63,7 @@ pub use billing::{ ModelBillingFacts, ModelBillingInput, ModelPricing, ModelPricingPolicy, ModelRef, ModelUsage, OpenAiBillingFacts, OpenAiModelPricing, PricePerMTok, Speed, TokenCounts, UsdMicros, }; +pub use blob_hash::BlobHash; pub use blob_ref::{format_blob_ref, parse_blob_ref, parse_managed_blob_file_ref}; pub use checkpoint::Checkpoint; pub use command_output::{CommandOutputStream, CommandTermination}; @@ -113,7 +114,6 @@ pub use run::{ DirtyStatus, ForkSourceRef, GitContext, RunClientProvenance, RunProvenance, RunServerProvenance, RunSpec, }; -pub use run_blob_id::RunBlobId; pub use run_event::{ AgentMcpToolSummary, AgentMemoryFileProps, AgentSkillActivationSource, AgentSkillSummary, AgentToolCategory, AgentToolSource, AgentToolSummary, AgentToolsAvailableProps, EventBody, diff --git a/lib/foundation/fabro-types/src/run.rs b/lib/foundation/fabro-types/src/run.rs index cf0fbe1ff..287f5b85f 100644 --- a/lib/foundation/fabro-types/src/run.rs +++ b/lib/foundation/fabro-types/src/run.rs @@ -3,9 +3,9 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use crate::WorkflowSettings; +use crate::blob_hash::BlobHash; use crate::graph::Graph; use crate::principal::Principal; -use crate::run_blob_id::RunBlobId; use crate::run_id::RunId; use crate::run_summary::AutomationRef; @@ -73,9 +73,9 @@ pub struct RunSpec { pub labels: HashMap, pub provenance: RunProvenance, #[serde(default, skip_serializing_if = "Option::is_none")] - pub manifest_blob: Option, + pub manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub definition_blob: Option, + pub definition_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/mod.rs b/lib/foundation/fabro-types/src/run_event/mod.rs index d3fa5c330..99c9c4aa5 100644 --- a/lib/foundation/fabro-types/src/run_event/mod.rs +++ b/lib/foundation/fabro-types/src/run_event/mod.rs @@ -937,7 +937,7 @@ mod tests { use super::*; use crate::{ - AuthMethod, CommandTermination, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, + AuthMethod, BlobHash, CommandTermination, Edge, Graph, IdpIdentity, Node, PendingReason, WorkflowSettings, fixtures, test_support, }; @@ -1059,7 +1059,7 @@ mod tests { "labels": {}, "source_directory": "/tmp/run", "provenance": test_support::test_run_provenance(), - "manifest_blob": RunBlobId::new(br#"{"version":1}"#).to_string() + "manifest_blob": BlobHash::new(br#"{"version":1}"#).to_string() } }); @@ -1337,7 +1337,7 @@ mod tests { "run_id": fixtures::RUN_1, "event": "run.submitted", "properties": { - "definition_blob": RunBlobId::new(br#"{"workflow_path":"workflow.fabro"}"#).to_string() + "definition_blob": BlobHash::new(br#"{"workflow_path":"workflow.fabro"}"#).to_string() } }); diff --git a/lib/foundation/fabro-types/src/run_event/run.rs b/lib/foundation/fabro-types/src/run_event/run.rs index d070d8aef..68a994dc9 100644 --- a/lib/foundation/fabro-types/src/run_event/run.rs +++ b/lib/foundation/fabro-types/src/run_event/run.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use super::{BilledTokenCounts, ExecOutputTail, RunNoticeLevel}; use crate::status::{BlockedReason, PendingReason, SuccessReason}; use crate::{ - AutomationRef, DiffSummary, ForkSourceRef, GitContext, Graph, PairId, PairTarget, RunBlobId, + AutomationRef, BlobHash, DiffSummary, ForkSourceRef, GitContext, Graph, PairId, PairTarget, RunControlAction, RunFailure, RunId, RunProvenance, RunTiming, WorkflowSettings, }; @@ -27,7 +27,7 @@ pub struct RunCreatedProps { pub automation: Option, pub provenance: RunProvenance, #[serde(default, skip_serializing_if = "Option::is_none")] - pub manifest_blob: Option, + pub manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -132,7 +132,7 @@ pub struct RunPairFailedProps { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunSubmittedProps { #[serde(default, skip_serializing_if = "Option::is_none")] - pub definition_blob: Option, + pub definition_blob: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] From 04f45b7c6b1ac22c54740e1e8470c53a3fcf7e4b Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Wed, 12 Aug 2026 13:44:18 -0400 Subject: [PATCH 13/62] Restore lexical root path normalization and simplify bundler internals Route the root workflow through collect_workflow_entry so relative root arguments are lexically normalized before reading, matching the pre-refactor behavior: `..` segments no longer resolve through symlinks to a file other than the one the manifest key names, and `~`-prefixed references are rejected again. Adds a symlink regression test for the root argument. Also: - collect_workflow_entry/collect_workflow_location return the manifest key, so bundle() no longer recomputes the root key - hold one FilesystemTemplateStore on the bundler instead of rebuilding it per template reference - drop the unused Clone derive on WorkflowScanInput - replace the hand-rolled JSON literal in the characterization test with an insta snapshot per the testing strategy - share one write_file fixture helper between the lib and bundler test modules - remove the bundler git-push test; the bundler has no git code path, so the test could not fail Co-Authored-By: Claude Fable 5 --- Cargo.lock | 2 + lib/components/fabro-manifest/Cargo.toml | 2 + lib/components/fabro-manifest/src/lib.rs | 151 +++++------------ ...erizes_the_complete_legacy_projection.snap | 126 +++++++++++++++ .../fabro-manifest/src/workflow_bundler.rs | 153 +++++++----------- 5 files changed, 227 insertions(+), 207 deletions(-) create mode 100644 lib/components/fabro-manifest/src/snapshots/fabro_manifest__tests__build_manifest_characterizes_the_complete_legacy_projection.snap diff --git a/Cargo.lock b/Cargo.lock index 9fba2f78c..30ab18be8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2816,9 +2816,11 @@ dependencies = [ "fabro-github", "fabro-graphviz", "fabro-template", + "fabro-test", "fabro-types", "fabro-workflow", "git2", + "insta", "serde_json", "temp-env", "tempfile", diff --git a/lib/components/fabro-manifest/Cargo.toml b/lib/components/fabro-manifest/Cargo.toml index 29b32aadb..36b9d44c1 100644 --- a/lib/components/fabro-manifest/Cargo.toml +++ b/lib/components/fabro-manifest/Cargo.toml @@ -25,6 +25,8 @@ git2.workspace = true toml.workspace = true [dev-dependencies] +fabro-test.workspace = true +insta.workspace = true serde_json.workspace = true tempfile = "3" temp-env = "0.3" diff --git a/lib/components/fabro-manifest/src/lib.rs b/lib/components/fabro-manifest/src/lib.rs index eff4ff2fc..3a3e89890 100644 --- a/lib/components/fabro-manifest/src/lib.rs +++ b/lib/components/fabro-manifest/src/lib.rs @@ -175,7 +175,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result { .as_ref() .map(|(_, path, source)| (path, source.as_str())); let workflows = WorkflowBundler::new(&input.cwd, &workflow_settings.run.inputs) - .bundle(&root_location, project_config_input)?; + .bundle(&input.workflow, project_config_input)?; let root_source = workflows .get(&target_key) .map(|workflow| workflow.source.clone()) @@ -434,6 +434,18 @@ pub fn manifest_args_is_empty(args: &types::ManifestArgs) -> bool { && args.verbose.is_none() } +#[cfg(test)] +pub(crate) mod test_fixtures { + use std::path::Path; + + pub(crate) fn write_file(path: &Path, source: &str) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("fixture directory should be created"); + } + std::fs::write(path, source).expect("fixture file should be written"); + } +} + #[cfg(test)] mod tests { use super::*; @@ -537,12 +549,7 @@ graph = "workflow.fabro" let plan_prompt = "{% include \"partial.md\" %}\n{% from \"helpers.md\" import render %}"; let helpers = "{% macro render() %}{% include \"deep.md\" %}{% endmacro %}"; let output_schema = r#"{"type":"object"}"#; - let write = |path: &Path, source: &str| { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).unwrap(); - } - std::fs::write(path, source).unwrap(); - }; + let write = test_fixtures::write_file; write(&project.join(".fabro/project.toml"), project_config); write(&project.join(".fabro/Project.Dockerfile"), "FROM project\n"); write(&user_config_path, user_config); @@ -579,115 +586,29 @@ graph = "workflow.fabro" actual["cwd"] = serde_json::json!(""); actual["configs"][0]["path"] = serde_json::json!(""); actual["configs"][1]["path"] = serde_json::json!(""); - let file = |content: &str, from: &str, original: &str, type_: &str| { - serde_json::json!({ - "content": content, - "ref": { - "from": from, - "original": original, - "type": type_, - } - }) - }; + fabro_test::fabro_json_snapshot!(sorted_json(actual)); + } - assert_eq!( - actual, - serde_json::json!({ - "args": { - "dry_run": true, - "input": ["feature=true"], - "label": ["suite=characterization"], - }, - "configs": [ - { - "path": "", - "source": project_config, - "type": "project", - }, - { - "path": "", - "source": user_config, - "type": "user", - }, - ], - "cwd": "", - "goal": { "type": "graph", "text": "ship it\n" }, - "target": { "path": ".fabro/workflows/root/workflow.fabro" }, - "version": 1, - "workflows": { - ".fabro/workflows/child/workflow.fabro": { - "config": { - "path": ".fabro/workflows/child/workflow.toml", - "source": child_config, - }, - "source": child_graph, - }, - ".fabro/workflows/root/workflow.fabro": { - "config": { - "path": ".fabro/workflows/root/workflow.toml", - "source": root_config, - }, - "files": { - ".fabro/Project.Dockerfile": file( - "FROM project\n", - ".fabro/project.toml", - "Project.Dockerfile", - "dockerfile", - ), - ".fabro/workflows/root/goals/goal.md": file( - "ship it\n", - ".fabro/workflows/root/workflow.fabro", - "goals/goal.md", - "file_inline", - ), - ".fabro/workflows/root/imports/shared.fabro": file( - imported_graph, - ".fabro/workflows/root/workflow.fabro", - "imports/shared.fabro", - "import", - ), - ".fabro/workflows/root/prompts/deep.md": file( - "deep\n", - ".fabro/workflows/root/prompts/plan.md", - ".fabro/workflows/root/prompts/deep.md", - "file_inline", - ), - ".fabro/workflows/root/prompts/helpers.md": file( - helpers, - ".fabro/workflows/root/prompts/plan.md", - ".fabro/workflows/root/prompts/helpers.md", - "file_inline", - ), - ".fabro/workflows/root/prompts/partial.md": file( - "partial\n", - ".fabro/workflows/root/prompts/plan.md", - ".fabro/workflows/root/prompts/partial.md", - "file_inline", - ), - ".fabro/workflows/root/prompts/plan.md": file( - plan_prompt, - ".fabro/workflows/root/workflow.fabro", - "prompts/plan.md", - "file_inline", - ), - ".fabro/workflows/root/prompts/shared.md": file( - "shared\n", - ".fabro/workflows/root/imports/shared.fabro", - "../prompts/shared.md", - "file_inline", - ), - ".fabro/workflows/root/schemas/output.json": file( - output_schema, - ".fabro/workflows/root/workflow.fabro", - "schemas/output.json", - "file_inline", - ), - }, - "source": root_graph, - }, - }, - }) - ); + /// `serde_json` is built with `preserve_order`, so `HashMap`-backed + /// manifest maps serialize in nondeterministic order; sort recursively + /// for a stable snapshot. + fn sorted_json(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Object(map) => { + let mut entries: Vec<_> = map.into_iter().collect(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + serde_json::Value::Object( + entries + .into_iter() + .map(|(key, value)| (key, sorted_json(value))) + .collect(), + ) + } + serde_json::Value::Array(items) => { + serde_json::Value::Array(items.into_iter().map(sorted_json).collect()) + } + other => other, + } } #[test] diff --git a/lib/components/fabro-manifest/src/snapshots/fabro_manifest__tests__build_manifest_characterizes_the_complete_legacy_projection.snap b/lib/components/fabro-manifest/src/snapshots/fabro_manifest__tests__build_manifest_characterizes_the_complete_legacy_projection.snap new file mode 100644 index 000000000..4c655361e --- /dev/null +++ b/lib/components/fabro-manifest/src/snapshots/fabro_manifest__tests__build_manifest_characterizes_the_complete_legacy_projection.snap @@ -0,0 +1,126 @@ +--- +source: lib/components/fabro-manifest/src/lib.rs +expression: rendered +--- +{ + "args": { + "dry_run": true, + "input": [ + "feature=true" + ], + "label": [ + "suite=characterization" + ] + }, + "configs": [ + { + "path": "", + "source": "_version = 1/n/n[environments.project]/nprovider = \"docker\"/n/n[environments.project.image]/ndockerfile = { path = \"Project.Dockerfile\" }/n", + "type": "project" + }, + { + "path": "", + "source": "_version = 1/n", + "type": "user" + } + ], + "cwd": "", + "goal": { + "text": "ship it/n", + "type": "graph" + }, + "target": { + "path": ".fabro/workflows/root/workflow.fabro" + }, + "version": 1, + "workflows": { + ".fabro/workflows/child/workflow.fabro": { + "config": { + "path": ".fabro/workflows/child/workflow.toml", + "source": "_version = 1/n/n[workflow]/ngraph = \"workflow.fabro\"/n" + }, + "source": "digraph Child { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" + }, + ".fabro/workflows/root/workflow.fabro": { + "config": { + "path": ".fabro/workflows/root/workflow.toml", + "source": "_version = 1/n/n[workflow]/ngraph = \"workflow.fabro\"/n" + }, + "files": { + ".fabro/Project.Dockerfile": { + "content": "FROM project/n", + "ref": { + "from": ".fabro/project.toml", + "original": "Project.Dockerfile", + "type": "dockerfile" + } + }, + ".fabro/workflows/root/goals/goal.md": { + "content": "ship it/n", + "ref": { + "from": ".fabro/workflows/root/workflow.fabro", + "original": "goals/goal.md", + "type": "file_inline" + } + }, + ".fabro/workflows/root/imports/shared.fabro": { + "content": "digraph Shared {/n start [shape=Mdiamond]/n shared [prompt=\"@../prompts/shared.md\"]/n exit [shape=Msquare]/n start -> shared -> exit/n }", + "ref": { + "from": ".fabro/workflows/root/workflow.fabro", + "original": "imports/shared.fabro", + "type": "import" + } + }, + ".fabro/workflows/root/prompts/deep.md": { + "content": "deep/n", + "ref": { + "from": ".fabro/workflows/root/prompts/plan.md", + "original": ".fabro/workflows/root/prompts/deep.md", + "type": "file_inline" + } + }, + ".fabro/workflows/root/prompts/helpers.md": { + "content": "{% macro render() %}{% include \"deep.md\" %}{% endmacro %}", + "ref": { + "from": ".fabro/workflows/root/prompts/plan.md", + "original": ".fabro/workflows/root/prompts/helpers.md", + "type": "file_inline" + } + }, + ".fabro/workflows/root/prompts/partial.md": { + "content": "partial/n", + "ref": { + "from": ".fabro/workflows/root/prompts/plan.md", + "original": ".fabro/workflows/root/prompts/partial.md", + "type": "file_inline" + } + }, + ".fabro/workflows/root/prompts/plan.md": { + "content": "{% include \"partial.md\" %}/n{% from \"helpers.md\" import render %}", + "ref": { + "from": ".fabro/workflows/root/workflow.fabro", + "original": "prompts/plan.md", + "type": "file_inline" + } + }, + ".fabro/workflows/root/prompts/shared.md": { + "content": "shared/n", + "ref": { + "from": ".fabro/workflows/root/imports/shared.fabro", + "original": "../prompts/shared.md", + "type": "file_inline" + } + }, + ".fabro/workflows/root/schemas/output.json": { + "content": "{\"type\":\"object\"}", + "ref": { + "from": ".fabro/workflows/root/workflow.fabro", + "original": "schemas/output.json", + "type": "file_inline" + } + } + }, + "source": "digraph Root {/n graph [goal=\"@goals/goal.md\"]/n start [shape=Mdiamond]/n prompt [prompt=\"@prompts/plan.md\"]/n schema [type=\"agent\", prompt=\"schema\", output_schema=\"@schemas/output.json\"]/n imported [import=\"imports/shared.fabro\"]/n child [shape=house, stack.child_workflow=\"../child/workflow.fabro\"]/n exit [shape=Msquare]/n start -> prompt -> schema -> imported -> child -> exit/n }" + } + } +} diff --git a/lib/components/fabro-manifest/src/workflow_bundler.rs b/lib/components/fabro-manifest/src/workflow_bundler.rs index 15d0a3d20..61633c229 100644 --- a/lib/components/fabro-manifest/src/workflow_bundler.rs +++ b/lib/components/fabro-manifest/src/workflow_bundler.rs @@ -20,6 +20,7 @@ use crate::{manifest_path_from_absolute, normalize_absolute_path}; pub(super) struct WorkflowBundler<'a> { cwd: &'a Path, inputs: &'a HashMap, + template_store: FilesystemTemplateStore, workflows: HashMap, visited_workflows: HashSet, } @@ -29,6 +30,7 @@ impl<'a> WorkflowBundler<'a> { Self { cwd, inputs, + template_store: FilesystemTemplateStore::new(cwd), workflows: HashMap::new(), visited_workflows: HashSet::new(), } @@ -36,13 +38,12 @@ impl<'a> WorkflowBundler<'a> { pub(super) fn bundle( mut self, - root_location: &WorkflowLocation, + workflow: &Path, project_config: Option<(&ManifestPath, &str)>, ) -> Result> { - self.collect_workflow_location(root_location)?; + let root_key = self.collect_workflow_entry(workflow, self.cwd)?; if let Some((config_path, source)) = project_config { - let root_key = manifest_path_from_absolute(&root_location.graph, self.cwd)?.to_string(); let mut root = self .workflows .remove(&root_key) @@ -54,11 +55,12 @@ impl<'a> WorkflowBundler<'a> { Ok(self.workflows) } - fn collect_workflow_location(&mut self, location: &WorkflowLocation) -> Result<()> { + /// Collects the workflow at `location` and returns its manifest key. + fn collect_workflow_location(&mut self, location: &WorkflowLocation) -> Result { let dot_path = manifest_path_from_absolute(&location.graph, self.cwd)?; let dot_key = dot_path.to_string(); if !self.visited_workflows.insert(dot_key.clone()) { - return Ok(()); + return Ok(dot_key); } let source = std::fs::read_to_string(&location.graph) @@ -87,16 +89,21 @@ impl<'a> WorkflowBundler<'a> { } self.collect_workflow_files(&scan, &mut files, &mut visited_imports)?; - self.workflows.insert(dot_key, types::ManifestWorkflow { - config, - files, - source, - }); + self.workflows + .insert(dot_key.clone(), types::ManifestWorkflow { + config, + files, + source, + }); - Ok(()) + Ok(dot_key) } - fn collect_workflow_entry(&mut self, workflow: &Path, resolve_from: &Path) -> Result<()> { + /// Relative workflow references with an extension are lexically + /// normalized (`..` segments resolved without consulting the filesystem, + /// `~` rejected) before resolution, so the file read matches the manifest + /// key. Returns the collected workflow's manifest key. + fn collect_workflow_entry(&mut self, workflow: &Path, resolve_from: &Path) -> Result { let normalized_workflow = if workflow.extension().is_some() && workflow.is_relative() { normalize_absolute_path(resolve_from, &workflow.to_string_lossy()).ok_or_else(|| { anyhow!( @@ -264,9 +271,9 @@ impl<'a> WorkflowBundler<'a> { from: Option<&ManifestPath>, ) -> Result<()> { let source_path = source.path.clone(); - let store = FilesystemTemplateStore::new(self.cwd.to_path_buf()); - let closure = fabro_template::discover_static_dependency_closure([source], &store) - .context("failed to discover template dependencies")?; + let closure = + fabro_template::discover_static_dependency_closure([source], &self.template_store) + .context("failed to discover template dependencies")?; self.verify_recorded_template_dependencies(&source_path, &closure, files, from)?; for (path, source) in closure.sources { @@ -418,7 +425,6 @@ impl<'a> WorkflowBundler<'a> { } } -#[derive(Clone)] struct WorkflowScanInput { absolute_dot_path: PathBuf, dot_path: ManifestPath, @@ -469,18 +475,11 @@ fn manifest_attr_reference_kind( #[cfg(test)] mod tests { use super::*; - - fn write_file(path: &Path, source: &str) { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).expect("fixture directory should be created"); - } - std::fs::write(path, source).expect("fixture file should be written"); - } + use crate::test_fixtures::write_file; fn bundle_graph(cwd: &Path, graph: &Path) -> Result> { let inputs = HashMap::new(); - let root_location = WorkflowLocation::resolve(graph, cwd)?; - WorkflowBundler::new(cwd, &inputs).bundle(&root_location, None) + WorkflowBundler::new(cwd, &inputs).bundle(graph, None) } #[test] @@ -544,75 +543,45 @@ mod tests { ); } + #[cfg(unix)] #[test] - fn bundler_does_not_push_an_ahead_branch() { - fn commit_all(repository: &git2::Repository, message: &str) -> git2::Oid { - let mut index = repository.index().expect("index should open"); - index - .add_all(["*"], git2::IndexAddOption::DEFAULT, None) - .expect("fixture files should be staged"); - index.write().expect("index should be written"); - let tree_id = index.write_tree().expect("tree should be written"); - let tree = repository.find_tree(tree_id).expect("tree should exist"); - let signature = git2::Signature::now("Fabro Test", "fabro@example.com") - .expect("signature should be valid"); - let parents = repository - .head() - .ok() - .and_then(|head| head.target()) - .map(|oid| { - repository - .find_commit(oid) - .expect("parent commit should exist") - }); - let parent_refs = parents.iter().collect::>(); - repository - .commit( - Some("refs/heads/main"), - &signature, - &signature, - message, - &tree, - &parent_refs, - ) - .expect("commit should be created") - } - + fn root_workflow_normalizes_parent_components_lexically_before_reading() { let temp = tempfile::tempdir().expect("temp directory should be created"); - let origin_path = temp.path().join("origin.git"); - let checkout = temp.path().join("checkout"); - let origin = - git2::Repository::init_bare(&origin_path).expect("bare origin should be initialized"); - let repository = git2::Repository::init(&checkout).expect("checkout should be initialized"); - repository - .set_head("refs/heads/main") - .expect("main should be selected"); - write_file( - &checkout.join("workflow.fabro"), - "digraph Root { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", + let cwd = temp.path(); + let lexical_graph = + "digraph Lexical { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }"; + let symlinked_graph = + "digraph Symlinked { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }"; + write_file(&cwd.join("wf/workflow.fabro"), lexical_graph); + write_file(&cwd.join("nested/wf/workflow.fabro"), symlinked_graph); + std::fs::create_dir_all(cwd.join("nested/elsewhere")) + .expect("symlink target should be created"); + // `link` points into `nested/`, so OS resolution of `link/..` lands in + // `nested/` while lexical resolution lands in the invocation directory. + std::os::unix::fs::symlink(cwd.join("nested/elsewhere"), cwd.join("link")) + .expect("symlink should be created"); + + let workflows = bundle_graph(cwd, Path::new("link/../wf/workflow.fabro")) + .expect("workflow should bundle"); + + // `link/..` must resolve lexically to `wf/workflow.fabro`, not through + // the symlink to `nested/wf/workflow.fabro`, so the bundled source + // matches the file the manifest key names. + assert_eq!(workflows["wf/workflow.fabro"].source, lexical_graph); + } + + #[test] + fn root_workflow_rejects_tilde_relative_references() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + + let error = bundle_graph(temp.path(), Path::new("~/workflow.fabro")) + .expect_err("tilde reference should be rejected"); + + assert!( + error + .to_string() + .contains("unsupported manifest workflow reference"), + "unexpected error: {error:#}" ); - let first_commit = commit_all(&repository, "initial"); - let mut remote = repository - .remote( - "origin", - origin_path.to_str().expect("origin path should be UTF-8"), - ) - .expect("origin should be configured"); - remote - .push(&["refs/heads/main:refs/heads/main"], None) - .expect("initial commit should be pushed"); - drop(remote); - write_file(&checkout.join("README.md"), "ahead\n"); - let ahead_commit = commit_all(&repository, "ahead"); - assert_ne!(first_commit, ahead_commit); - - bundle_graph(&checkout, &checkout.join("workflow.fabro")).expect("workflow should bundle"); - - let origin_commit = origin - .find_reference("refs/heads/main") - .expect("origin main should exist") - .target() - .expect("origin main should point to a commit"); - assert_eq!(origin_commit, first_commit); } } From d5b3da87fc5ec6fe3203f3f28024b00559924726 Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Thu, 13 Aug 2026 09:41:35 +0000 Subject: [PATCH 14/62] Bump version to 0.324.0-nightly.0 --- Cargo.lock | 102 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30ab18be8..661b4ab8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2673,7 +2673,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2688,7 +2688,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -2711,7 +2711,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2721,7 +2721,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2740,7 +2740,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2755,7 +2755,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2797,7 +2797,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2808,7 +2808,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "fabro-api", @@ -2829,7 +2829,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2895,7 +2895,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2933,7 +2933,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2941,7 +2941,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "cc", "libc", @@ -2950,7 +2950,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2966,7 +2966,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3010,7 +3010,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3126,18 +3126,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" [[package]] name = "fabro-store" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3167,7 +3167,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3193,7 +3193,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3207,7 +3207,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3232,7 +3232,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3253,7 +3253,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3267,7 +3267,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3290,7 +3290,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3313,7 +3313,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3326,7 +3326,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3343,7 +3343,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3362,7 +3362,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -8527,7 +8527,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "axum", "base64", @@ -8546,7 +8546,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index fd260535c..80bb89390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.316.0-nightly.0" +version = "0.324.0-nightly.0" license = "MIT" [workspace.dependencies] From 09c6bd836b9eed98667617704946d4c3b99fc553 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 14:34:23 -0400 Subject: [PATCH 15/62] Rename SettingsLayer::image_layers to environment_images Also add environment_images_mut and adopt it in the run compiler's Dockerfile resolution, replacing the hand-rolled iteration over named environments plus the run environment. Co-Authored-By: Claude Fable 5 --- lib/apps/fabro-server/src/run_compiler.rs | 12 +------ .../fabro-config/src/layers/settings.rs | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/apps/fabro-server/src/run_compiler.rs b/lib/apps/fabro-server/src/run_compiler.rs index 437df572b..a018f46d0 100644 --- a/lib/apps/fabro-server/src/run_compiler.rs +++ b/lib/apps/fabro-server/src/run_compiler.rs @@ -586,17 +586,7 @@ fn resolve_dockerfiles( config_path: &ManifestPath, files: &HashMap, ) -> Result<()> { - for environment in layer.environments.values_mut() { - if let Some(image) = environment.image.as_mut() { - resolve_dockerfile(image, config_path, files)?; - } - } - if let Some(image) = layer - .run - .as_mut() - .and_then(|run| run.environment.as_mut()) - .and_then(|environment| environment.image.as_mut()) - { + for image in layer.environment_images_mut() { resolve_dockerfile(image, config_path, files)?; } Ok(()) diff --git a/lib/foundation/fabro-config/src/layers/settings.rs b/lib/foundation/fabro-config/src/layers/settings.rs index 8c5c362fb..4863c8833 100644 --- a/lib/foundation/fabro-config/src/layers/settings.rs +++ b/lib/foundation/fabro-config/src/layers/settings.rs @@ -10,7 +10,7 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use super::cli::CliLayer; -use super::environment::EnvironmentLayer; +use super::environment::{EnvironmentImageLayer, EnvironmentLayer}; use super::llm::LlmLayer; use super::maps::MergeMap; use super::project::ProjectLayer; @@ -102,6 +102,40 @@ impl From for SettingsLayer { } } +impl SettingsLayer { + /// Every environment image a settings layer can carry: the image of each + /// named `[environments.*]` entry plus the `[run.environment]` image. + /// + /// This is the single definition of "where images live in a settings + /// layer". The dockerfile walkers (run compilation, manifest bundling, + /// workflow-version validation) all iterate through here so a new + /// image-bearing location only needs to be added once. + pub fn environment_images(&self) -> impl Iterator { + self.environments + .values() + .filter_map(|environment| environment.image.as_ref()) + .chain( + self.run + .as_ref() + .and_then(|run| run.environment.as_ref()) + .and_then(|environment| environment.image.as_ref()), + ) + } + + /// Mutable variant of [`Self::environment_images`]. + pub fn environment_images_mut(&mut self) -> impl Iterator { + self.environments + .values_mut() + .filter_map(|environment| environment.image.as_mut()) + .chain( + self.run + .as_mut() + .and_then(|run| run.environment.as_mut()) + .and_then(|environment| environment.image.as_mut()), + ) + } +} + #[cfg(test)] impl SettingsLayer { /// A default layer that resolves cleanly: populates `server.auth.methods` From 13755d7c2b3170071cf7ec904d854bfb9693d7e8 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 14:34:23 -0400 Subject: [PATCH 16/62] Unify the workflow graph reference walkers Move the static-reference vocabulary out of fabro-workflow so every consumer shares one definition: ReferenceKind, AttributeScope, and reference_kind_for_attribute land in fabro-types::graph, and validate_static_reference plus a new visit_graph_references walker land in fabro-template. The manifest bundler drops its ad-hoc graph scan and walks references through the shared walker. Unifying the walkers forces three semantic alignments, each matching what the engine actually executes rather than what the old scanners happened to match: - stack.child_dotfile is no longer classified as a child-workflow reference; the engine never resolved it as one. - import and stack.child_workflow only count at node scope; graph- and edge-level occurrences were scanned but never executed. - @@-escaped goals flow through the shared walker's escape handling instead of the bundler's own prefix stripping. Co-Authored-By: Claude Fable 5 --- lib/components/fabro-manifest/src/lib.rs | 6 +- .../fabro-manifest/src/workflow_bundler.rs | 223 ++++++---------- .../src/handler/manager_loop.rs | 3 +- lib/components/fabro-workflow/src/lib.rs | 1 - .../fabro-workflow/src/static_reference.rs | 143 ---------- .../fabro-workflow/src/transforms/import.rs | 4 +- .../src/transforms/importable_field.rs | 4 +- .../src/transforms/variable_expansion.rs | 6 +- lib/foundation/fabro-template/src/lib.rs | 5 + .../fabro-template/src/static_reference.rs | 248 ++++++++++++++++++ lib/foundation/fabro-types/src/graph.rs | 68 +++++ 11 files changed, 419 insertions(+), 292 deletions(-) delete mode 100644 lib/components/fabro-workflow/src/static_reference.rs create mode 100644 lib/foundation/fabro-template/src/static_reference.rs diff --git a/lib/components/fabro-manifest/src/lib.rs b/lib/components/fabro-manifest/src/lib.rs index 3a3e89890..35891f419 100644 --- a/lib/components/fabro-manifest/src/lib.rs +++ b/lib/components/fabro-manifest/src/lib.rs @@ -19,13 +19,14 @@ use fabro_config::{ }; use fabro_graphviz::graph::AttrValue; use fabro_graphviz::parser; +use fabro_template::validate_static_reference; +use fabro_types::graph::ReferenceKind; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{ApprovalMode, ResolvedGoalSource, ResolvedRunGoal, RunMode}; use fabro_types::{DirtyStatus, GitContext, ManifestPath, WorkflowSettings}; use fabro_workflow::git::{ GitSyncStatus, branch_needs_push, head_sha, push_branch_noninteractive, sync_status, }; -use fabro_workflow::static_reference::ReferenceKind; use crate::workflow_bundler::WorkflowBundler; @@ -265,8 +266,7 @@ fn resolve_manifest_goal( return Ok(None); }; if let Some(reference) = goal.strip_prefix('@') { - ReferenceKind::GraphGoalFile - .validate(reference) + validate_static_reference(reference, ReferenceKind::GraphGoalFile) .map_err(anyhow::Error::new)?; let goal_path = normalize_absolute_path( root_dot_path.parent().unwrap_or_else(|| Path::new(".")), diff --git a/lib/components/fabro-manifest/src/workflow_bundler.rs b/lib/components/fabro-manifest/src/workflow_bundler.rs index 61633c229..6f6b8dd9e 100644 --- a/lib/components/fabro-manifest/src/workflow_bundler.rs +++ b/lib/components/fabro-manifest/src/workflow_bundler.rs @@ -6,14 +6,14 @@ use anyhow::{Context as _, Result, anyhow}; use fabro_api::types; use fabro_config::project::WorkflowLocation; use fabro_config::{EnvironmentDockerfileLayer, EnvironmentImageLayer, SettingsLayer}; -use fabro_graphviz::graph::AttrValue; use fabro_graphviz::parser; use fabro_template::{ - BundleTemplateStore, FilesystemTemplateStore, RecordingTemplateStore, TemplateContext, - TemplateDependencyClosure, TemplateRenderMode, TemplateSource, + BundleTemplateStore, FilesystemTemplateStore, GraphReference, GraphReferenceError, + RecordingTemplateStore, TemplateContext, TemplateDependencyClosure, TemplateRenderMode, + TemplateSource, validate_static_reference, visit_graph_references, }; use fabro_types::ManifestPath; -use fabro_workflow::static_reference::{self, AttributeScope, ReferenceKind}; +use fabro_types::graph::ReferenceKind; use crate::{manifest_path_from_absolute, normalize_absolute_path}; @@ -132,117 +132,91 @@ impl<'a> WorkflowBundler<'a> { .unwrap_or_else(|| Path::new(".")); let workflow_template_root = manifest_parent_or_dot(&workflow.dot_path)?; - if let Some(goal_ref) = graph.attrs.get("goal").and_then(AttrValue::as_str) { - if goal_ref.starts_with('@') { - let bundled = self.collect_bundled_file( - files, - workflow_base_dir, - goal_ref.trim_start_matches('@'), - types::ManifestFileRefType::FileInline, - manifest_attr_reference_kind(AttributeScope::Graph, "goal", goal_ref)?, - Some(workflow.dot_path.clone()), - )?; - self.collect_bundled_template_includes(files, &bundled, &workflow_template_root)?; - } else { - self.collect_template_include_files( + // Imports and child workflows require a mutable borrow of self, so + // collect them during the walk and recurse after the visitor returns. + let mut imports = Vec::new(); + let mut children = Vec::new(); + + visit_graph_references(&graph, |reference| -> Result<()> { + match reference { + GraphReference::GoalFile { reference } => { + let bundled = self.collect_bundled_file( + files, + workflow_base_dir, + reference, + types::ManifestFileRefType::FileInline, + ReferenceKind::GraphGoalFile, + Some(workflow.dot_path.clone()), + )?; + self.collect_bundled_template_includes(files, &bundled, &workflow_template_root) + } + GraphReference::GoalInline { content } + | GraphReference::InlinePrompt { content } => self.collect_template_include_files( files, TemplateSource::new( workflow.dot_path.clone(), workflow_template_root.clone(), - goal_ref.to_owned(), + content.to_owned(), ), Some(&workflow.dot_path), - )?; + ), + GraphReference::FileInline { key, reference } => { + let bundled = self.collect_bundled_file( + files, + workflow_base_dir, + reference, + types::ManifestFileRefType::FileInline, + ReferenceKind::FileInline, + Some(workflow.dot_path.clone()), + )?; + if key == "prompt" { + self.collect_bundled_template_includes( + files, + &bundled, + &workflow_template_root, + )?; + } + Ok(()) + } + GraphReference::Import { reference } => { + let imported = self.collect_bundled_file( + files, + workflow_base_dir, + reference, + types::ManifestFileRefType::Import, + ReferenceKind::Import, + Some(workflow.dot_path.clone()), + )?; + imports.push(imported); + Ok(()) + } + GraphReference::ChildWorkflow { reference } => { + children.push(reference); + Ok(()) + } + } + }) + .map_err(|error| match error { + GraphReferenceError::StaticReference(source) => anyhow::Error::new(source), + GraphReferenceError::Visit(error) => error, + })?; + + for imported in imports { + if visited_imports.insert(imported.path.to_string()) { + let imported_source = std::fs::read_to_string(&imported.absolute_path) + .with_context(|| { + format!("Failed to read {}", imported.absolute_path.display()) + })?; + let imported_scan = WorkflowScanInput { + absolute_dot_path: imported.absolute_path, + dot_path: imported.path, + source: imported_source, + }; + self.collect_workflow_files(&imported_scan, files, visited_imports)?; } } - - for node in graph.nodes.values() { - if let Some(prompt_ref) = node.attrs.get("prompt").and_then(AttrValue::as_str) { - if !prompt_ref.starts_with('@') { - self.collect_template_include_files( - files, - TemplateSource::new( - workflow.dot_path.clone(), - workflow_template_root.clone(), - prompt_ref.to_owned(), - ), - Some(&workflow.dot_path), - )?; - } - } - - for (name, value) in &node.attrs { - let Some(value) = value.as_str() else { - continue; - }; - let Some(ReferenceKind::FileInline) = - static_reference::reference_kind_for_attribute( - AttributeScope::Node, - name, - value, - ) - else { - continue; - }; - let reference = value.strip_prefix('@').ok_or_else(|| { - anyhow!("file inline reference must start with '@': {name}={value}") - })?; - let bundled = self.collect_bundled_file( - files, - workflow_base_dir, - reference, - types::ManifestFileRefType::FileInline, - ReferenceKind::FileInline, - Some(workflow.dot_path.clone()), - )?; - - if name == "prompt" { - self.collect_bundled_template_includes( - files, - &bundled, - &workflow_template_root, - )?; - } - } - - if let Some(import_ref) = node.attrs.get("import").and_then(AttrValue::as_str) { - let imported = self.collect_bundled_file( - files, - workflow_base_dir, - import_ref, - types::ManifestFileRefType::Import, - manifest_attr_reference_kind(AttributeScope::Node, "import", import_ref)?, - Some(workflow.dot_path.clone()), - )?; - let import_key = imported.path.to_string(); - if visited_imports.insert(import_key) { - let imported_source = std::fs::read_to_string(&imported.absolute_path) - .with_context(|| { - format!("Failed to read {}", imported.absolute_path.display()) - })?; - let imported_scan = WorkflowScanInput { - absolute_dot_path: imported.absolute_path, - dot_path: imported.path, - source: imported_source, - }; - self.collect_workflow_files(&imported_scan, files, visited_imports)?; - } - } - - if let Some(child_ref) = node - .attrs - .get("stack.child_workflow") - .and_then(AttrValue::as_str) - { - manifest_attr_reference_kind( - AttributeScope::Node, - "stack.child_workflow", - child_ref, - )? - .validate(child_ref) - .map_err(anyhow::Error::new)?; - self.collect_workflow_entry(Path::new(child_ref), workflow_base_dir)?; - } + for child in children { + self.collect_workflow_entry(Path::new(child), workflow_base_dir)?; } Ok(()) @@ -347,21 +321,8 @@ impl<'a> WorkflowBundler<'a> { .parent() .unwrap_or_else(|| Path::new(".")); - for environment in layer.environments.values() { - self.collect_environment_dockerfile( - files, - base_dir, - config_path, - environment.image.as_ref(), - )?; - } - if let Some(run_environment) = layer.run.as_ref().and_then(|run| run.environment.as_ref()) { - self.collect_environment_dockerfile( - files, - base_dir, - config_path, - run_environment.image.as_ref(), - )?; + for image in layer.environment_images() { + self.collect_environment_dockerfile(files, base_dir, config_path, image)?; } Ok(()) } @@ -371,10 +332,9 @@ impl<'a> WorkflowBundler<'a> { files: &mut HashMap, base_dir: &Path, config_path: &ManifestPath, - image: Option<&EnvironmentImageLayer>, + image: &EnvironmentImageLayer, ) -> Result<()> { - let dockerfile = image.and_then(|image| image.dockerfile.as_ref()); - let Some(EnvironmentDockerfileLayer::Path { path }) = dockerfile else { + let Some(EnvironmentDockerfileLayer::Path { path }) = image.dockerfile.as_ref() else { return Ok(()); }; self.collect_bundled_file( @@ -397,9 +357,7 @@ impl<'a> WorkflowBundler<'a> { reference_kind: ReferenceKind, from: Option, ) -> Result { - reference_kind - .validate(reference) - .map_err(anyhow::Error::new)?; + validate_static_reference(reference, reference_kind).map_err(anyhow::Error::new)?; let absolute_path = normalize_absolute_path(base_dir, reference) .ok_or_else(|| anyhow!("unsupported manifest reference: {reference}"))?; @@ -463,15 +421,6 @@ fn manifest_path_is_within_root(path: &ManifestPath, root: &ManifestPath) -> boo path.starts_with(root) } -fn manifest_attr_reference_kind( - scope: AttributeScope, - key: &str, - value: &str, -) -> Result { - static_reference::reference_kind_for_attribute(scope, key, value) - .ok_or_else(|| anyhow!("unsupported manifest reference attribute: {key}={value}")) -} - #[cfg(test)] mod tests { use super::*; diff --git a/lib/components/fabro-workflow/src/handler/manager_loop.rs b/lib/components/fabro-workflow/src/handler/manager_loop.rs index 5668954c6..8cc82f634 100644 --- a/lib/components/fabro-workflow/src/handler/manager_loop.rs +++ b/lib/components/fabro-workflow/src/handler/manager_loop.rs @@ -6,7 +6,9 @@ use std::time::Duration; use async_trait::async_trait; use fabro_graphviz::graph::{AttrValue, Graph, Node}; use fabro_store::{ArtifactStore, Database}; +use fabro_template::validate_static_reference; use fabro_types::WorkflowSettings; +use fabro_types::graph::ReferenceKind; use object_store::memory::InMemory; use tokio::fs; use tokio::time::{sleep, timeout}; @@ -20,7 +22,6 @@ use crate::operations::{ValidateInput, WorkflowInput, validate_with_catalog}; use crate::outcome::{Outcome, OutcomeExt, StageOutcome}; use crate::pipeline::types::Initialized; use crate::run_options::RunOptions; -use crate::static_reference::{ReferenceKind, validate_static_reference}; use crate::{ManifestPath, pipeline, stage_scope}; /// Orchestrates a child workflow engine, polling for completion or stop diff --git a/lib/components/fabro-workflow/src/lib.rs b/lib/components/fabro-workflow/src/lib.rs index bd7f3e665..c34bec62c 100644 --- a/lib/components/fabro-workflow/src/lib.rs +++ b/lib/components/fabro-workflow/src/lib.rs @@ -332,7 +332,6 @@ pub(crate) mod sandbox_git_runtime; pub mod services; pub(crate) mod stage_execution; mod stage_scope; -pub mod static_reference; pub mod steering_hub; #[cfg(any(test, feature = "test-support"))] pub mod test_support; diff --git a/lib/components/fabro-workflow/src/static_reference.rs b/lib/components/fabro-workflow/src/static_reference.rs deleted file mode 100644 index af83539eb..000000000 --- a/lib/components/fabro-workflow/src/static_reference.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::fmt; - -use fabro_template::contains_template_syntax; -use thiserror::Error; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ReferenceKind { - FileInline, - Import, - ChildWorkflow, - Dockerfile, - GraphGoalFile, -} - -impl fmt::Display for ReferenceKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let label = match self { - Self::FileInline => "file inline reference", - Self::Import => "import reference", - Self::ChildWorkflow => "child workflow reference", - Self::Dockerfile => "Dockerfile reference", - Self::GraphGoalFile => "graph goal file reference", - }; - f.write_str(label) - } -} - -impl ReferenceKind { - pub fn validate(self, value: &str) -> Result<(), StaticReferenceError> { - validate_static_reference(value, self) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum AttributeScope { - Graph, - Node, - Edge, -} - -#[derive(Debug, Error)] -#[error("templates are not supported in {kind}s: {value}")] -pub struct StaticReferenceError { - kind: ReferenceKind, - value: String, -} - -impl StaticReferenceError { - #[must_use] - pub fn new(kind: ReferenceKind, value: impl Into) -> Self { - Self { - kind, - value: value.into(), - } - } - - #[must_use] - pub fn kind(&self) -> ReferenceKind { - self.kind - } - - #[must_use] - pub fn value(&self) -> &str { - &self.value - } -} - -pub fn validate_static_reference( - value: &str, - kind: ReferenceKind, -) -> Result<(), StaticReferenceError> { - if contains_template_syntax(value) { - return Err(StaticReferenceError::new(kind, value)); - } - Ok(()) -} - -#[must_use] -pub fn reference_kind_for_attribute( - scope: AttributeScope, - key: &str, - value: &str, -) -> Option { - match key { - "import" => Some(ReferenceKind::Import), - "stack.child_workflow" | "stack.child_dotfile" => Some(ReferenceKind::ChildWorkflow), - "goal" if matches!(scope, AttributeScope::Graph) && value.starts_with('@') => { - Some(ReferenceKind::GraphGoalFile) - } - "prompt" | "output_schema" - if matches!(scope, AttributeScope::Node) && value.starts_with('@') => - { - Some(ReferenceKind::FileInline) - } - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn output_schema_at_value_is_file_inline_reference() { - assert_eq!( - reference_kind_for_attribute( - AttributeScope::Node, - "output_schema", - "@schemas/result.schema.json", - ), - Some(ReferenceKind::FileInline), - ); - } - - #[test] - fn output_schema_builtin_keyword_is_not_file_inline_reference() { - assert_eq!( - reference_kind_for_attribute(AttributeScope::Node, "output_schema", "routing"), - None, - ); - } - - #[test] - fn output_schema_reference_rejects_template_syntax() { - let error = reference_kind_for_attribute( - AttributeScope::Node, - "output_schema", - "@schemas/{{ inputs.schema }}.json", - ) - .expect("output_schema @ references should be static references") - .validate("@schemas/{{ inputs.schema }}.json") - .unwrap_err(); - - assert_eq!(error.kind(), ReferenceKind::FileInline); - assert_eq!(error.value(), "@schemas/{{ inputs.schema }}.json"); - assert!( - error - .to_string() - .contains("templates are not supported in file inline references"), - "unexpected error: {error}", - ); - } -} diff --git a/lib/components/fabro-workflow/src/transforms/import.rs b/lib/components/fabro-workflow/src/transforms/import.rs index e0082ba9d..bab7ee1b1 100644 --- a/lib/components/fabro-workflow/src/transforms/import.rs +++ b/lib/components/fabro-workflow/src/transforms/import.rs @@ -4,14 +4,14 @@ use std::sync::Arc; use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node}; use fabro_graphviz::parser; -use fabro_template::TemplateContext; +use fabro_template::{TemplateContext, validate_static_reference}; +use fabro_types::graph::ReferenceKind; use fabro_validate::Diagnostic; use super::file_inlining::template_render_store; use super::{FileInliningTransform, Transform}; use crate::error::Error; use crate::file_resolver::{FileResolver, ResolvedFile}; -use crate::static_reference::{ReferenceKind, validate_static_reference}; use crate::transforms::variable_expansion::{ RenderMode, TemplateRenderTarget, TemplateTransform, render_template_for_target, }; diff --git a/lib/components/fabro-workflow/src/transforms/importable_field.rs b/lib/components/fabro-workflow/src/transforms/importable_field.rs index acfcc52e8..cf4b5c2d1 100644 --- a/lib/components/fabro-workflow/src/transforms/importable_field.rs +++ b/lib/components/fabro-workflow/src/transforms/importable_field.rs @@ -13,8 +13,10 @@ //! [`super::file_inlining`], where the `FileResolver` and current-dir context //! live. +use fabro_template::validate_static_reference; +use fabro_types::graph::ReferenceKind; + use crate::error::Error; -use crate::static_reference::{ReferenceKind, validate_static_reference}; /// A field value that is either inline content or an `@path` file import. /// diff --git a/lib/components/fabro-workflow/src/transforms/variable_expansion.rs b/lib/components/fabro-workflow/src/transforms/variable_expansion.rs index e2c11d169..da7aba76d 100644 --- a/lib/components/fabro-workflow/src/transforms/variable_expansion.rs +++ b/lib/components/fabro-workflow/src/transforms/variable_expansion.rs @@ -6,8 +6,9 @@ use std::sync::Arc; use fabro_graphviz::graph::{AttrValue, Graph, Node}; use fabro_template::{ TemplateContext, TemplateError, TemplateRenderMode, TemplateSource, TemplateSourceOrigin, - TemplateStore, + TemplateStore, validate_static_reference, }; +use fabro_types::graph::{AttributeScope, ReferenceKind, reference_kind_for_attribute}; use fabro_types::settings::interp::Namespace; use fabro_types::settings::{InterpString, ResolveCtx, ResolveError, ResolveErrorKind}; use fabro_util::error::collect_chain; @@ -17,9 +18,6 @@ use fabro_validate::{Diagnostic, Severity}; use super::Transform; use crate::error::Error; use crate::pipeline::types::{GOAL_SELF_REFERENCE_RULE, TEMPLATE_UNDEFINED_VARIABLE_RULE}; -use crate::static_reference::{ - AttributeScope, ReferenceKind, reference_kind_for_attribute, validate_static_reference, -}; /// How the template-expansion pass should treat undefined input variables. /// diff --git a/lib/foundation/fabro-template/src/lib.rs b/lib/foundation/fabro-template/src/lib.rs index f312e9416..8381f5fec 100644 --- a/lib/foundation/fabro-template/src/lib.rs +++ b/lib/foundation/fabro-template/src/lib.rs @@ -8,6 +8,7 @@ use minijinja::value::{Object, Value}; use minijinja::{AutoEscape, Environment, ErrorKind, UndefinedBehavior}; mod dependency; +mod static_reference; mod store; pub use dependency::{ @@ -15,6 +16,10 @@ pub use dependency::{ TemplateDependencyKind, TemplateDiscoveryError, discover_static_dependency_closure, extract_template_dependencies, }; +pub use static_reference::{ + GraphReference, GraphReferenceError, StaticReferenceError, validate_static_reference, + visit_graph_references, +}; pub use store::{ BundleTemplateStore, CachedTemplateStore, FilesystemTemplateStore, RecordingTemplateStore, TemplateIncludeResolver, TemplateLoadError, TemplateSource, TemplateSourceOrigin, diff --git a/lib/foundation/fabro-template/src/static_reference.rs b/lib/foundation/fabro-template/src/static_reference.rs new file mode 100644 index 000000000..b832f2956 --- /dev/null +++ b/lib/foundation/fabro-template/src/static_reference.rs @@ -0,0 +1,248 @@ +//! Static file references in workflow graphs. +//! +//! Workflow graphs name other files through a fixed attribute vocabulary +//! (`import`, `stack.child_workflow`, `@`-prefixed `prompt`/`output_schema` +//! values, and the graph `goal`). These references are *static*: they may not +//! contain template syntax, because they are resolved before any template +//! rendering happens. +//! +//! [`visit_graph_references`] is the one walker over that vocabulary. The +//! manifest bundler and workflow-version validation both consume it, so a new +//! 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 crate::contains_template_syntax; + +/// A static file reference that unexpectedly contains template syntax. +#[derive(Debug, thiserror::Error)] +#[error("templates are not supported in {kind}s: {value}")] +pub struct StaticReferenceError { + kind: ReferenceKind, + value: String, +} + +impl StaticReferenceError { + #[must_use] + pub fn new(kind: ReferenceKind, value: impl Into) -> Self { + Self { + kind, + value: value.into(), + } + } + + #[must_use] + pub fn kind(&self) -> ReferenceKind { + self.kind + } + + #[must_use] + pub fn value(&self) -> &str { + &self.value + } +} + +/// Reject static file references (imports, child workflows, `@` file values) +/// that contain template syntax. +pub fn validate_static_reference( + value: &str, + kind: ReferenceKind, +) -> Result<(), StaticReferenceError> { + if contains_template_syntax(value) { + return Err(StaticReferenceError::new(kind, value)); + } + Ok(()) +} + +/// One file reference or inline template discovered in a workflow graph. +/// +/// `@` prefixes are already stripped from file references; inline variants +/// carry template content that the consumer should feed to template-dependency +/// discovery. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum GraphReference<'graph> { + /// `graph [goal="@"]`. + GoalFile { reference: &'graph str }, + /// A non-`@` graph `goal`: inline template content. + GoalInline { content: &'graph str }, + /// `node [import=""]` — another graph file to walk. + Import { reference: &'graph str }, + /// `node [stack.child_workflow=""]`. + ChildWorkflow { reference: &'graph str }, + /// `node [="@"]` for file-inlined attributes + /// (`prompt`, `output_schema`). + FileInline { + key: &'graph str, + reference: &'graph str, + }, + /// A non-`@` node prompt: inline template content. + InlinePrompt { content: &'graph str }, +} + +/// Error from [`visit_graph_references`]. +#[derive(Debug, thiserror::Error)] +pub enum GraphReferenceError { + #[error(transparent)] + StaticReference(StaticReferenceError), + #[error(transparent)] + Visit(E), +} + +/// Walk every static file reference and inline template in one parsed graph, +/// validating that file references are template-free before emitting them. +/// +/// The walker covers a single graph; recursion into `Import` targets and +/// resolution of references against a file source are the consumer's job. +pub fn visit_graph_references<'graph, E>( + graph: &'graph Graph, + mut visit: impl FnMut(GraphReference<'graph>) -> Result<(), E>, +) -> Result<(), GraphReferenceError> { + let goal = graph.goal(); + if !goal.is_empty() { + if let Some(reference) = goal.strip_prefix('@') { + validate_static_reference(reference, ReferenceKind::GraphGoalFile) + .map_err(GraphReferenceError::StaticReference)?; + visit(GraphReference::GoalFile { reference }).map_err(GraphReferenceError::Visit)?; + } else { + visit(GraphReference::GoalInline { content: goal }) + .map_err(GraphReferenceError::Visit)?; + } + } + + for node in graph.nodes.values() { + for (key, value) in &node.attrs { + let Some(value) = value.as_str() else { + continue; + }; + let Some(kind) = reference_kind_for_attribute(AttributeScope::Node, key, value) else { + continue; + }; + let reference = match kind { + ReferenceKind::Import | ReferenceKind::ChildWorkflow => value, + // Classification only yields FileInline for `@` values. + ReferenceKind::FileInline => value + .strip_prefix('@') + .expect("file inline classification requires a leading '@'"), + ReferenceKind::Dockerfile | ReferenceKind::GraphGoalFile => continue, + }; + validate_static_reference(reference, kind) + .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!(), + }; + visit(event).map_err(GraphReferenceError::Visit)?; + } + + if let Some(prompt) = node.prompt().filter(|prompt| !prompt.starts_with('@')) { + visit(GraphReference::InlinePrompt { content: prompt }) + .map_err(GraphReferenceError::Visit)?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use fabro_types::graph::{AttrValue, Graph, Node, ReferenceKind}; + + use super::{GraphReference, GraphReferenceError, validate_static_reference}; + + #[test] + fn static_reference_rejects_template_syntax() { + let error = validate_static_reference( + "@schemas/{{ inputs.schema }}.json", + ReferenceKind::FileInline, + ) + .unwrap_err(); + + assert_eq!(error.kind(), ReferenceKind::FileInline); + assert_eq!(error.value(), "@schemas/{{ inputs.schema }}.json"); + assert!( + error + .to_string() + .contains("templates are not supported in file inline references"), + "unexpected error: {error}", + ); + assert!( + validate_static_reference("@schemas/result.json", ReferenceKind::FileInline).is_ok() + ); + } + + fn node_with(id: &str, attrs: &[(&str, &str)]) -> Node { + let mut node = Node::new(id); + for (key, value) in attrs { + node.attrs + .insert((*key).to_string(), AttrValue::String((*value).to_string())); + } + node + } + + #[test] + fn visits_every_reference_kind_once() { + let mut graph = Graph::new("test"); + graph.attrs.insert( + "goal".to_string(), + AttrValue::String("@goal.md".to_string()), + ); + for node in [ + node_with("imported", &[("import", "graphs/child.fabro")]), + node_with("child", &[("stack.child_workflow", "children/check.fabro")]), + node_with("file_prompt", &[("prompt", "@prompts/task.md")]), + node_with("inline", &[("prompt", "Do the {{ thing }}")]), + ] { + graph.nodes.insert(node.id.clone(), node); + } + + let mut seen = BTreeSet::new(); + super::visit_graph_references( + &graph, + |reference| -> Result<(), std::convert::Infallible> { + seen.insert(match reference { + GraphReference::GoalFile { reference } => format!("goal-file:{reference}"), + GraphReference::GoalInline { content } => format!("goal-inline:{content}"), + GraphReference::Import { reference } => format!("import:{reference}"), + GraphReference::ChildWorkflow { reference } => format!("child:{reference}"), + GraphReference::FileInline { key, reference } => { + format!("file:{key}:{reference}") + } + GraphReference::InlinePrompt { content } => format!("inline:{content}"), + }); + Ok(()) + }, + ) + .unwrap(); + + assert_eq!( + seen, + BTreeSet::from([ + "goal-file:goal.md".to_string(), + "import:graphs/child.fabro".to_string(), + "child:children/check.fabro".to_string(), + "file:prompt:prompts/task.md".to_string(), + "inline:Do the {{ thing }}".to_string(), + ]) + ); + } + + #[test] + fn rejects_template_syntax_in_references_before_visiting() { + let mut graph = Graph::new("test"); + graph.nodes.insert( + "imported".to_string(), + node_with("imported", &[("import", "graphs/{{ name }}.fabro")]), + ); + + let error = + super::visit_graph_references(&graph, |_| -> Result<(), std::convert::Infallible> { + panic!("references with template syntax must not be visited") + }) + .unwrap_err(); + assert!(matches!(error, GraphReferenceError::StaticReference(_))); + } +} diff --git a/lib/foundation/fabro-types/src/graph.rs b/lib/foundation/fabro-types/src/graph.rs index e2ed3fab4..b9f74b79f 100644 --- a/lib/foundation/fabro-types/src/graph.rs +++ b/lib/foundation/fabro-types/src/graph.rs @@ -590,6 +590,54 @@ impl Graph { } } +/// Where an attribute appears in a workflow graph. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AttributeScope { + Graph, + Node, + Edge, +} + +/// Kinds of static (non-templated) file references a graph attribute can +/// carry. +#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display)] +pub enum ReferenceKind { + #[strum(to_string = "file inline reference")] + FileInline, + #[strum(to_string = "import reference")] + Import, + #[strum(to_string = "child workflow reference")] + ChildWorkflow, + #[strum(to_string = "Dockerfile reference")] + Dockerfile, + #[strum(to_string = "graph goal file reference")] + 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 { + match key { + "import" if matches!(scope, AttributeScope::Node) => Some(ReferenceKind::Import), + "stack.child_workflow" if matches!(scope, AttributeScope::Node) => { + Some(ReferenceKind::ChildWorkflow) + } + "goal" if matches!(scope, AttributeScope::Graph) && value.starts_with('@') => { + Some(ReferenceKind::GraphGoalFile) + } + "prompt" | "output_schema" + if matches!(scope, AttributeScope::Node) && value.starts_with('@') => + { + Some(ReferenceKind::FileInline) + } + _ => None, + } +} + #[cfg(test)] mod tests { use super::*; @@ -1111,4 +1159,24 @@ mod tests { ); assert_eq!(g.loop_restart_signature_limit(), 3); } + + #[test] + fn output_schema_at_value_is_file_inline_reference() { + assert_eq!( + reference_kind_for_attribute( + AttributeScope::Node, + "output_schema", + "@schemas/result.schema.json", + ), + Some(ReferenceKind::FileInline), + ); + } + + #[test] + fn output_schema_builtin_keyword_is_not_file_inline_reference() { + assert_eq!( + reference_kind_for_attribute(AttributeScope::Node, "output_schema", "routing"), + None, + ); + } } From a76e2d7ddc7faa7e5208ab678c11e3beb1c16edf Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 15:52:54 -0400 Subject: [PATCH 17/62] Test graph goal filenames with at prefix --- .../fabro-manifest/src/workflow_bundler.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/components/fabro-manifest/src/workflow_bundler.rs b/lib/components/fabro-manifest/src/workflow_bundler.rs index 6f6b8dd9e..bd031df0b 100644 --- a/lib/components/fabro-manifest/src/workflow_bundler.rs +++ b/lib/components/fabro-manifest/src/workflow_bundler.rs @@ -452,6 +452,28 @@ mod tests { assert_eq!(workflows["workflow.fabro"].files.len(), 1); } + #[test] + fn graph_goal_bundles_filename_with_at_prefix() { + let temp = tempfile::tempdir().expect("temp directory should be created"); + let graph = temp.path().join("workflow.fabro"); + write_file( + &graph, + r#"digraph Root { + graph [goal="@@goal.md"] + start [shape=Mdiamond] + exit [shape=Msquare] + start -> exit + }"#, + ); + write_file(&temp.path().join("@goal.md"), "goal\n"); + + let workflows = bundle_graph(temp.path(), &graph).expect("workflow should bundle"); + + let goal = &workflows["workflow.fabro"].files["@goal.md"]; + assert_eq!(goal.content, "goal\n"); + assert_eq!(goal.ref_.original, "@goal.md"); + } + #[test] fn parse_errors_keep_the_graphviz_error_in_the_source_chain() { let temp = tempfile::tempdir().expect("temp directory should be created"); From 178320e7a56d270fefecc01d25eea414832cf26e Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 14:53:32 -0400 Subject: [PATCH 18/62] Add immutable workflow version resource Add the WorkflowVersion domain resource with exactly entrypoint, files, and workflow_dependencies, plus strict WorkflowPath validation and deterministic canonical raw JSON. Semantic validation of graph imports, templates, file references, workflow.toml rules, Dockerfile paths, and exact child-workflow dependency bindings lives in the new fabro-workflow-version crate, which validates the complete stored dependency closure through the shared blob store before writing a root. The authenticated create-only POST /api/v1/workflow-versions endpoint ships with its OpenAPI contract, Rust type replacements, and generated TypeScript client. Squashed from the resource commits of the original combined branch; the walker unification this builds on landed separately. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 17 + docs/public/api-reference/fabro-api.yaml | 130 +++++ lib/apps/fabro-server/Cargo.toml | 1 + .../fabro-server/src/server/handler/mod.rs | 2 + .../src/server/handler/workflow_versions.rs | 357 ++++++++++++ .../fabro-workflow-version/Cargo.toml | 27 + .../fabro-workflow-version/src/lib.rs | 507 ++++++++++++++++++ .../fabro-workflow-version/src/store.rs | 306 +++++++++++ lib/foundation/fabro-api/build.rs | 3 + lib/foundation/fabro-api/src/lib.rs | 3 +- .../tests/workflow_version_round_trip.rs | 54 ++ lib/foundation/fabro-types/src/lib.rs | 11 + .../fabro-types/src/workflow_path.rs | 301 +++++++++++ .../fabro-types/src/workflow_version.rs | 419 +++++++++++++++ .../fabro-types/src/workflow_version_id.rs | 96 ++++ .../src/.openapi-generator/FILES | 3 + lib/packages/fabro-api-client/src/api.ts | 1 + .../src/api/workflow-versions-api.ts | 134 +++++ .../create-workflow-version-response.ts | 25 + .../fabro-api-client/src/models/index.ts | 2 + .../src/models/workflow-version.ts | 33 ++ 21 files changed, 2431 insertions(+), 1 deletion(-) create mode 100644 lib/apps/fabro-server/src/server/handler/workflow_versions.rs create mode 100644 lib/components/fabro-workflow-version/Cargo.toml create mode 100644 lib/components/fabro-workflow-version/src/lib.rs create mode 100644 lib/components/fabro-workflow-version/src/store.rs create mode 100644 lib/foundation/fabro-api/tests/workflow_version_round_trip.rs create mode 100644 lib/foundation/fabro-types/src/workflow_path.rs create mode 100644 lib/foundation/fabro-types/src/workflow_version.rs create mode 100644 lib/foundation/fabro-types/src/workflow_version_id.rs create mode 100644 lib/packages/fabro-api-client/src/api/workflow-versions-api.ts create mode 100644 lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts create mode 100644 lib/packages/fabro-api-client/src/models/workflow-version.ts diff --git a/Cargo.lock b/Cargo.lock index 661b4ab8f..93c745736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3060,6 +3060,7 @@ dependencies = [ "fabro-variable", "fabro-vault", "fabro-workflow", + "fabro-workflow-version", "futures-util", "globset", "hex", @@ -3430,6 +3431,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "fabro-workflow-version" +version = "0.324.0-nightly.0" +dependencies = [ + "fabro-config", + "fabro-graphviz", + "fabro-store", + "fabro-template", + "fabro-types", + "object_store", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "fail-parallel" version = "0.5.1" diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 261badbbb..274282e72 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -33,6 +33,8 @@ tags: description: Internal run details (stages, turns, context, configuration) - name: Workflows description: Workflow definitions and execution + - name: Workflow Versions + description: Immutable, content-addressed workflow packages - name: Billing description: Token counts and billed totals - name: Insights @@ -1065,6 +1067,69 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" + # ── Workflow Versions ───────────────────────────────────────────────── + + /api/v1/workflow-versions: + post: + operationId: createWorkflowVersion + tags: [Workflow Versions] + summary: Create Workflow Version + description: >- + Validates and stores an immutable workflow package in content-addressed + storage. Repeating the same canonical content returns the same identifier. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/WorkflowVersion" + responses: + "201": + description: Workflow version stored or already present + content: + application/json: + schema: + $ref: "#/components/schemas/CreateWorkflowVersionResponse" + "400": + description: Malformed JSON (`invalid_json`) + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "413": + description: Request body exceeds 2 MiB (`workflow_version_too_large`) + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "422": + description: >- + Invalid workflow content (`workflow_version_invalid`) or an absent, + invalid, or non-canonical dependency + (`workflow_version_dependency_not_found`) + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: Workflow version storage failed + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + # ── Runs ────────────────────────────────────────────────────────────── /api/v1/runs: @@ -9074,6 +9139,71 @@ components: detail: $ref: "#/components/schemas/FailureDetail" + WorkflowPath: + description: >- + Canonical portable path inside one workflow version. Paths are UTF-8, + relative, at most 240 bytes and 16 components, and cannot contain empty, + dot, parent, backslash, control, tilde-root, or drive-letter segments. + Map keys receive stricter byte and structural validation in the domain + model than OpenAPI can express. + type: string + minLength: 1 + maxLength: 240 + example: graphs/main.fabro + + WorkflowVersionId: + description: SHA-256 identity of validated canonical workflow-version bytes. + type: string + pattern: "^[0-9a-f]{64}$" + example: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + WorkflowVersion: + description: >- + Complete immutable package for one rooted workflow. It contains at most + 512 files and 512 workflow dependencies, each file is at most 512 KiB + of UTF-8 content, and its compact canonical JSON representation is at + most 2 MiB. + type: object + additionalProperties: false + required: + - entrypoint + - files + - workflow_dependencies + properties: + entrypoint: + $ref: "#/components/schemas/WorkflowPath" + files: + type: object + description: >- + Workflow-local text files keyed by canonical path. Keys receive + stricter domain validation than OpenAPI can express; each value is + limited to 512 KiB of UTF-8 bytes. + maxProperties: 512 + propertyNames: + $ref: "#/components/schemas/WorkflowPath" + additionalProperties: + type: string + workflow_dependencies: + type: object + description: >- + Exact stored workflow-version IDs keyed by resolved child-workflow + path. Keys receive stricter domain validation than OpenAPI can express. + maxProperties: 512 + propertyNames: + $ref: "#/components/schemas/WorkflowPath" + additionalProperties: + $ref: "#/components/schemas/WorkflowVersionId" + + CreateWorkflowVersionResponse: + description: Identity of the stored immutable workflow version. + type: object + additionalProperties: false + required: + - workflow_version_id + properties: + workflow_version_id: + $ref: "#/components/schemas/WorkflowVersionId" + RunManifest: description: Self-contained workflow run manifest. type: object diff --git a/lib/apps/fabro-server/Cargo.toml b/lib/apps/fabro-server/Cargo.toml index 8aea2bade..5ad20cfbb 100644 --- a/lib/apps/fabro-server/Cargo.toml +++ b/lib/apps/fabro-server/Cargo.toml @@ -32,6 +32,7 @@ fabro-hooks = { path = "../../components/fabro-hooks" } fabro-interview = { path = "../../components/fabro-interview" } fabro-slack = { path = "../../components/fabro-slack" } fabro-workflow = { path = "../../components/fabro-workflow" } +fabro-workflow-version = { path = "../../components/fabro-workflow-version" } fabro-validate = { path = "../../components/fabro-validate" } fabro-sandbox = { path = "../../components/fabro-sandbox", features = ["daytona", "docker"] } fabro-github = { path = "../../components/fabro-github" } diff --git a/lib/apps/fabro-server/src/server/handler/mod.rs b/lib/apps/fabro-server/src/server/handler/mod.rs index c186464d7..bcb7f9ef1 100644 --- a/lib/apps/fabro-server/src/server/handler/mod.rs +++ b/lib/apps/fabro-server/src/server/handler/mod.rs @@ -30,6 +30,7 @@ mod steer; pub(in crate::server) mod system; mod variables; mod worker_control; +mod workflow_versions; pub(super) use system::{health, openapi_spec}; @@ -226,6 +227,7 @@ pub(super) fn real_routes() -> Router> { .merge(secrets::routes()) .merge(variables::routes()) .merge(worker_control::routes()) + .merge(workflow_versions::routes()) .merge(sessions::routes()) .merge(system::routes()) .merge(completions::routes()) diff --git a/lib/apps/fabro-server/src/server/handler/workflow_versions.rs b/lib/apps/fabro-server/src/server/handler/workflow_versions.rs new file mode 100644 index 000000000..ebff58d82 --- /dev/null +++ b/lib/apps/fabro-server/src/server/handler/workflow_versions.rs @@ -0,0 +1,357 @@ +use std::sync::Arc; + +use axum::extract::DefaultBodyLimit; +use axum::extract::rejection::JsonRejection; +use fabro_api::types::{CreateWorkflowVersionResponse, WorkflowVersion}; +use fabro_types::MAX_WORKFLOW_VERSION_BYTES; +use fabro_util::error; +use fabro_workflow_version::{ + ValidatedWorkflowVersion, WorkflowVersionStore, WorkflowVersionStoreError, +}; + +use super::super::{ + ApiError, AppState, IntoResponse, Json, RequiredUser, Response, Router, State, StatusCode, post, +}; + +const INVALID_JSON_CODE: &str = "invalid_json"; +const INVALID_VERSION_CODE: &str = "workflow_version_invalid"; +const DEPENDENCY_NOT_FOUND_CODE: &str = "workflow_version_dependency_not_found"; +const VERSION_TOO_LARGE_CODE: &str = "workflow_version_too_large"; + +pub(super) fn routes() -> Router> { + Router::new().route( + "/workflow-versions", + post(create_workflow_version).layer(DefaultBodyLimit::max(MAX_WORKFLOW_VERSION_BYTES)), + ) +} + +async fn create_workflow_version( + _auth: RequiredUser, + State(state): State>, + payload: Result, JsonRejection>, +) -> Result { + let Json(version) = payload.map_err(json_rejection)?; + let version = ValidatedWorkflowVersion::new(version).map_err(|err| { + ApiError::with_code( + StatusCode::UNPROCESSABLE_ENTITY, + err.to_string(), + INVALID_VERSION_CODE, + ) + })?; + let blobs = state.store_ref().blobs().await.map_err(|err| { + tracing::error!( + error = %err, + error_chain = ?error::collect_chain(&err), + "Failed to open workflow version storage" + ); + internal_store_error() + })?; + let store = WorkflowVersionStore::new(blobs); + let workflow_version_id = store.put(&version).await.map_err(store_error)?; + + Ok(( + StatusCode::CREATED, + Json(CreateWorkflowVersionResponse { + workflow_version_id, + }), + ) + .into_response()) +} + +fn json_rejection(rejection: JsonRejection) -> ApiError { + if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE { + return ApiError::with_code( + StatusCode::PAYLOAD_TOO_LARGE, + "workflow version request exceeds 2 MiB", + VERSION_TOO_LARGE_CODE, + ); + } + + match rejection { + JsonRejection::JsonDataError(err) => ApiError::with_code( + StatusCode::UNPROCESSABLE_ENTITY, + err.body_text(), + INVALID_VERSION_CODE, + ), + other => ApiError::with_code( + StatusCode::BAD_REQUEST, + other.body_text(), + INVALID_JSON_CODE, + ), + } +} + +fn store_error(err: WorkflowVersionStoreError) -> ApiError { + match err { + err @ WorkflowVersionStoreError::DependencyNotFound { .. } => ApiError::with_code( + StatusCode::UNPROCESSABLE_ENTITY, + err.to_string(), + DEPENDENCY_NOT_FOUND_CODE, + ), + WorkflowVersionStoreError::InvalidVersion(source) => ApiError::with_code( + StatusCode::UNPROCESSABLE_ENTITY, + source.to_string(), + INVALID_VERSION_CODE, + ), + WorkflowVersionStoreError::InvalidShape(source) => ApiError::with_code( + StatusCode::UNPROCESSABLE_ENTITY, + source.to_string(), + INVALID_VERSION_CODE, + ), + err => { + tracing::error!( + error = %err, + error_chain = ?error::collect_chain(&err), + "Workflow version store operation failed" + ); + internal_store_error() + } + } +} + +fn internal_store_error() -> ApiError { + ApiError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "workflow version store operation failed", + ) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use axum::body::{Body, to_bytes}; + use axum::http::{Method, Request, StatusCode, header}; + use axum::response::IntoResponse; + use fabro_types::WorkflowVersionId; + use serde_json::{Value, json}; + use tower::ServiceExt; + + use super::{ + DEPENDENCY_NOT_FOUND_CODE, INVALID_JSON_CODE, INVALID_VERSION_CODE, + MAX_WORKFLOW_VERSION_BYTES, VERSION_TOO_LARGE_CODE, store_error, + }; + use crate::server; + use crate::test_support::{self, TestAppStateBuilder}; + + const GRAPH: &str = "digraph W { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }"; + + fn request(body: impl Into) -> Request { + Request::builder() + .method(Method::POST) + .uri("/api/v1/workflow-versions") + .header(header::CONTENT_TYPE, "application/json") + .body(body.into()) + .unwrap() + } + + fn version(graph: &str) -> Value { + json!({ + "entrypoint": "workflow.fabro", + "files": { "workflow.fabro": graph }, + "workflow_dependencies": {} + }) + } + + async fn response_json(response: axum::response::Response) -> Value { + let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + serde_json::from_slice(&bytes).unwrap() + } + + fn error_code(body: &Value) -> &str { + body["errors"][0]["code"].as_str().unwrap() + } + + #[tokio::test] + async fn create_requires_authenticated_user() { + let state = TestAppStateBuilder::new().build(); + let app = server::build_router(state, test_support::test_auth_mode()); + let response = app + .oneshot(request(serde_json::to_vec(&version(GRAPH)).unwrap())) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn valid_and_equivalent_requests_return_the_same_id() { + let state = TestAppStateBuilder::new().build(); + let app = test_support::build_test_router(Arc::clone(&state)); + let first = app + .clone() + .oneshot(request(serde_json::to_vec(&version(GRAPH)).unwrap())) + .await + .unwrap(); + assert_eq!(first.status(), StatusCode::CREATED); + let first = response_json(first).await; + assert_eq!(first.as_object().unwrap().len(), 1); + + let reordered = format!( + r#"{{"workflow_dependencies":{{}},"files":{{"workflow.fabro":{}}},"entrypoint":"workflow.fabro"}}"#, + serde_json::to_string(GRAPH).unwrap() + ); + let second = app.oneshot(request(reordered)).await.unwrap(); + assert_eq!(second.status(), StatusCode::CREATED); + assert_eq!(response_json(second).await, first); + + let id = first["workflow_version_id"] + .as_str() + .unwrap() + .parse::() + .unwrap(); + assert!( + state + .store_ref() + .blobs() + .await + .unwrap() + .read(&id.into()) + .await + .unwrap() + .is_some() + ); + } + + #[tokio::test] + async fn invalid_json_and_domain_content_have_distinct_codes() { + let app = test_support::build_test_router(TestAppStateBuilder::new().build()); + let malformed = app.clone().oneshot(request("{")).await.unwrap(); + assert_eq!(malformed.status(), StatusCode::BAD_REQUEST); + assert_eq!( + error_code(&response_json(malformed).await), + INVALID_JSON_CODE + ); + + let unknown = json!({ + "entrypoint": "workflow.fabro", + "files": { "workflow.fabro": GRAPH }, + "workflow_dependencies": {}, + "metadata": {} + }); + let invalid = app + .oneshot(request(serde_json::to_vec(&unknown).unwrap())) + .await + .unwrap(); + assert_eq!(invalid.status(), StatusCode::UNPROCESSABLE_ENTITY); + assert_eq!( + error_code(&response_json(invalid).await), + INVALID_VERSION_CODE + ); + } + + #[tokio::test] + async fn unavailable_dependency_has_specific_code() { + let state = TestAppStateBuilder::new().build(); + let app = test_support::build_test_router(Arc::clone(&state)); + let missing_id = WorkflowVersionId::from(fabro_types::BlobHash::new(b"missing")); + let root = json!({ + "entrypoint": "workflow.fabro", + "files": { + "workflow.fabro": "digraph W { child [stack.child_workflow=\"child.fabro\"] }" + }, + "workflow_dependencies": { "child.fabro": missing_id } + }); + let response = app + .oneshot(request(serde_json::to_vec(&root).unwrap())) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); + assert_eq!( + error_code(&response_json(response).await), + DEPENDENCY_NOT_FOUND_CODE + ); + } + + #[tokio::test] + async fn corrupt_stored_dependency_returns_curated_internal_error() { + let state = TestAppStateBuilder::new().build(); + let app = test_support::build_test_router(Arc::clone(&state)); + let dependency_id = WorkflowVersionId::from( + state + .store_ref() + .blobs() + .await + .unwrap() + .write(b"not a workflow version") + .await + .unwrap(), + ); + let root = json!({ + "entrypoint": "workflow.fabro", + "files": { + "workflow.fabro": "digraph W { child [stack.child_workflow=\"child.fabro\"] }" + }, + "workflow_dependencies": { "child.fabro": dependency_id } + }); + + let response = app + .oneshot(request(serde_json::to_vec(&root).unwrap())) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + let body = response_json(response).await; + assert_eq!( + body["errors"][0]["detail"], + "workflow version store operation failed" + ); + assert!(!body.to_string().contains("cannot be decoded")); + } + + #[tokio::test] + async fn stored_child_can_be_pinned_as_a_dependency() { + let app = test_support::build_test_router(TestAppStateBuilder::new().build()); + let child = app + .clone() + .oneshot(request(serde_json::to_vec(&version(GRAPH)).unwrap())) + .await + .unwrap(); + assert_eq!(child.status(), StatusCode::CREATED); + let child_id = response_json(child).await["workflow_version_id"].clone(); + let root = json!({ + "entrypoint": "workflow.fabro", + "files": { + "workflow.fabro": "digraph W { child [stack.child_workflow=\"child.fabro\"] }" + }, + "workflow_dependencies": { "child.fabro": child_id } + }); + + let response = app + .oneshot(request(serde_json::to_vec(&root).unwrap())) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response_json(response).await.as_object().unwrap().len(), 1); + } + + #[tokio::test] + async fn body_limit_has_specific_code() { + let app = test_support::build_test_router(TestAppStateBuilder::new().build()); + let response = app + .oneshot(request(vec![b' '; MAX_WORKFLOW_VERSION_BYTES + 1])) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + error_code(&response_json(response).await), + VERSION_TOO_LARGE_CODE + ); + } + + #[tokio::test] + async fn storage_fault_response_is_curated() { + let response = store_error(fabro_workflow_version::WorkflowVersionStoreError::Storage { + source: fabro_store::Error::Other("private persistence detail".to_string()), + }) + .into_response(); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + + let body = response_json(response).await; + assert_eq!( + body["errors"][0]["detail"], + "workflow version store operation failed" + ); + assert!(!body.to_string().contains("private persistence detail")); + } +} diff --git a/lib/components/fabro-workflow-version/Cargo.toml b/lib/components/fabro-workflow-version/Cargo.toml new file mode 100644 index 000000000..bc91e3f89 --- /dev/null +++ b/lib/components/fabro-workflow-version/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "fabro-workflow-version" +edition.workspace = true +version.workspace = true +publish = false +license.workspace = true +description = "Semantic validation and storage for immutable workflow versions" + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +fabro-config = { path = "../../foundation/fabro-config" } +fabro-graphviz = { path = "../fabro-graphviz" } +fabro-store = { path = "../fabro-store" } +fabro-template = { path = "../../foundation/fabro-template" } +fabro-types = { path = "../../foundation/fabro-types" } +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true + +[dev-dependencies] +object_store.workspace = true +tokio = { workspace = true, features = ["full"] } diff --git a/lib/components/fabro-workflow-version/src/lib.rs b/lib/components/fabro-workflow-version/src/lib.rs new file mode 100644 index 000000000..320d09139 --- /dev/null +++ b/lib/components/fabro-workflow-version/src/lib.rs @@ -0,0 +1,507 @@ +//! Semantic validation for immutable workflow versions. +//! +//! The wire type ([`fabro_types::WorkflowVersion`]) enforces structural +//! invariants at construction. This crate owns the expensive semantic +//! validation — graph closure, config, and template checks — behind the +//! [`ValidatedWorkflowVersion`] newtype, and the content-addressed +//! [`WorkflowVersionStore`] that only accepts and returns validated versions. + +use std::collections::{BTreeSet, HashMap, VecDeque}; + +use fabro_config::parse::{SettingsSource, validate_settings_source}; +use fabro_config::{EnvironmentDockerfileLayer, EnvironmentImageLayer, SettingsLayer}; +use fabro_graphviz::parser; +use fabro_template::{ + BundleTemplateStore, GraphReference, GraphReferenceError, StaticReferenceError, + TemplateDiscoveryError, TemplateSource, discover_static_dependency_closure, + validate_static_reference, visit_graph_references, +}; +use fabro_types::graph::ReferenceKind; +use fabro_types::{ManifestPath, WorkflowPath, WorkflowPathParseError, WorkflowVersion}; +use thiserror::Error; + +mod store; + +pub use store::{WorkflowVersionStore, WorkflowVersionStoreError}; + +#[derive(Debug, Error)] +pub enum WorkflowVersionError { + #[error("workflow graph `{path}` is invalid")] + GraphParse { + path: WorkflowPath, + #[source] + source: fabro_graphviz::Error, + }, + #[error("invalid {kind} in `{path}`: `{reference}`")] + InvalidReference { + path: WorkflowPath, + kind: ReferenceKind, + reference: String, + #[source] + source: WorkflowPathParseError, + }, + #[error("invalid static reference in `{path}`")] + StaticReference { + path: WorkflowPath, + #[source] + source: StaticReferenceError, + }, + #[error("{kind} in `{path}` references missing file `{target}`")] + MissingFile { + path: WorkflowPath, + kind: ReferenceKind, + target: WorkflowPath, + }, + #[error("template dependencies for `{path}` are invalid")] + Template { + path: WorkflowPath, + #[source] + source: Box, + }, + #[error("workflow.toml is invalid")] + Config { + #[source] + source: fabro_config::ParseError, + }, + #[error( + "workflow.toml selects graph `{configured}`, but the version entrypoint is `{entrypoint}`" + )] + ConfigEntrypointMismatch { + configured: WorkflowPath, + entrypoint: WorkflowPath, + }, + #[error("workflow dependencies do not match child workflow references")] + DependencyMismatch { + missing: Vec, + unused: Vec, + }, +} + +/// A workflow version whose graph, config, and template content passed +/// semantic validation. +/// +/// This is the only door: functions that require a semantically valid +/// version take this type, and the only way to obtain one is [`Self::new`] +/// (or loading through [`WorkflowVersionStore`], which validates on read). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ValidatedWorkflowVersion(WorkflowVersion); + +impl ValidatedWorkflowVersion { + pub fn new(version: WorkflowVersion) -> Result { + validate_config(&version)?; + validate_graph_closure(&version)?; + Ok(Self(version)) + } + + #[must_use] + pub fn version(&self) -> &WorkflowVersion { + &self.0 + } + + #[must_use] + pub fn into_version(self) -> WorkflowVersion { + self.0 + } +} + +fn validate_config(version: &WorkflowVersion) -> 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 { + return Ok(()); + }; + let layer = source + .parse::() + .map_err(|source| WorkflowVersionError::Config { source })?; + validate_settings_source(&layer, SettingsSource::Workflow) + .map_err(|source| WorkflowVersionError::Config { source })?; + + if let Some(configured) = layer + .workflow + .as_ref() + .and_then(|workflow| workflow.graph.as_deref()) + { + let configured = resolve_reference(&config_path, ReferenceKind::FileInline, configured)?; + if configured != *version.entrypoint() { + return Err(WorkflowVersionError::ConfigEntrypointMismatch { + configured, + entrypoint: version.entrypoint().clone(), + }); + } + } + + for image in layer.environment_images() { + validate_dockerfile(version, &config_path, image)?; + } + Ok(()) +} + +fn validate_dockerfile( + version: &WorkflowVersion, + config_path: &WorkflowPath, + image: &EnvironmentImageLayer, +) -> Result<(), WorkflowVersionError> { + let Some(EnvironmentDockerfileLayer::Path { path }) = image.dockerfile.as_ref() else { + return Ok(()); + }; + validate_static_reference(path, ReferenceKind::Dockerfile).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(|_| ()) +} + +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"); + let mut queue = VecDeque::from([version.entrypoint().clone()]); + let mut visited = BTreeSet::new(); + let mut child_workflows = BTreeSet::new(); + + while let Some(path) = queue.pop_front() { + if !visited.insert(path.clone()) { + continue; + } + let source = + version + .files() + .get(&path) + .ok_or_else(|| WorkflowVersionError::MissingFile { + path: path.clone(), + kind: ReferenceKind::Import, + target: path.clone(), + })?; + let graph = parser::parse(source).map_err(|source| WorkflowVersionError::GraphParse { + path: path.clone(), + source, + })?; + + visit_graph_references(&graph, |reference| match reference { + GraphReference::GoalFile { reference } => { + 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) + } + GraphReference::GoalInline { content } | GraphReference::InlinePrompt { content } => { + validate_template(&path, content, &template_store, &template_root) + } + GraphReference::Import { reference } => { + let target = resolve_reference(&path, ReferenceKind::Import, reference)?; + require_file(version, &path, ReferenceKind::Import, target.clone())?; + queue.push_back(target); + Ok(()) + } + GraphReference::ChildWorkflow { reference } => { + let target = resolve_reference(&path, ReferenceKind::ChildWorkflow, reference)?; + child_workflows.insert(target); + Ok(()) + } + GraphReference::FileInline { key, reference } => { + let target = resolve_reference(&path, ReferenceKind::FileInline, reference)?; + let content = + require_file(version, &path, ReferenceKind::FileInline, target.clone())?; + if key == "prompt" { + validate_template(&target, content, &template_store, &template_root)?; + } + Ok(()) + } + }) + .map_err(|error| match error { + GraphReferenceError::StaticReference(source) => WorkflowVersionError::StaticReference { + path: path.clone(), + source, + }, + GraphReferenceError::Visit(error) => error, + })?; + } + + let configured = version + .workflow_dependencies() + .keys() + .cloned() + .collect::>(); + if child_workflows != configured { + return Err(WorkflowVersionError::DependencyMismatch { + missing: child_workflows.difference(&configured).cloned().collect(), + unused: configured.difference(&child_workflows).cloned().collect(), + }); + } + Ok(()) +} + +fn validate_template( + path: &WorkflowPath, + content: &str, + store: &BundleTemplateStore, + root: &ManifestPath, +) -> 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), + })?; + Ok(()) +} + +fn template_store(version: &WorkflowVersion) -> BundleTemplateStore { + BundleTemplateStore::new( + version + .files() + .iter() + .map(|(path, content)| (manifest_path(path), content.clone())) + .collect::>(), + ) +} + +fn resolve_reference( + path: &WorkflowPath, + kind: ReferenceKind, + reference: &str, +) -> Result { + path.resolve_reference(reference) + .map_err(|source| WorkflowVersionError::InvalidReference { + path: path.clone(), + kind, + reference: reference.to_owned(), + source, + }) +} + +fn require_file<'version>( + version: &'version WorkflowVersion, + path: &WorkflowPath, + kind: ReferenceKind, + target: WorkflowPath, +) -> Result<&'version str, WorkflowVersionError> { + version + .files() + .get(&target) + .map(String::as_str) + .ok_or_else(|| WorkflowVersionError::MissingFile { + path: path.clone(), + kind, + target, + }) +} + +fn manifest_path(path: &WorkflowPath) -> ManifestPath { + ManifestPath::from_wire(path.as_str()) + .expect("validated workflow paths must also be valid manifest paths") +} + +#[cfg(test)] +mod tests { + use fabro_types::{BlobHash, WorkflowPath, WorkflowVersion, WorkflowVersionId}; + + use super::{ValidatedWorkflowVersion, WorkflowVersionError}; + + fn path(value: &str) -> WorkflowPath { + value.parse().unwrap() + } + + fn dependency_id(value: &[u8]) -> WorkflowVersionId { + BlobHash::new(value).into() + } + + fn version_with( + files: impl IntoIterator, + dependencies: impl IntoIterator, + ) -> Result { + ValidatedWorkflowVersion::new( + WorkflowVersion::new( + path("workflow.fabro"), + files + .into_iter() + .map(|(path_value, content)| (path(path_value), content.to_owned())) + .collect(), + dependencies + .into_iter() + .map(|(path_value, id)| (path(path_value), id)) + .collect(), + ) + .expect("test fixtures must be structurally valid"), + ) + } + + #[test] + fn validates_imports_templates_file_refs_and_dependencies() { + let version = version_with( + [ + ( + "workflow.fabro", + r#"digraph W { + graph [goal="@prompts/goal.md"] + start [shape=Mdiamond] + imported [import="graphs/imported.fabro"] + child [stack.child_workflow="children/check.fabro"] + exit [shape=Msquare] + start -> imported -> child -> exit + }"#, + ), + ( + "graphs/imported.fabro", + r#"digraph I { step [prompt="{% include \"../prompts/partial.md\" %}"] }"#, + ), + ("prompts/goal.md", "{% include \"partial.md\" %}"), + ("prompts/partial.md", "Do the work"), + ], + [("children/check.fabro", dependency_id(b"child"))], + ) + .unwrap(); + + assert_eq!(version.version().workflow_dependencies().len(), 1); + } + + #[test] + fn rejects_missing_and_unused_dependencies() { + let error = version_with( + [( + "workflow.fabro", + r#"digraph W { child [stack.child_workflow="child.fabro"] }"#, + )], + [("unused.fabro", dependency_id(b"unused"))], + ) + .unwrap_err(); + + let WorkflowVersionError::DependencyMismatch { missing, unused } = error else { + panic!("expected dependency mismatch"); + }; + assert_eq!(missing, vec![path("child.fabro")]); + assert_eq!(unused, vec![path("unused.fabro")]); + } + + #[test] + fn rejects_config_entrypoint_and_missing_dockerfile() { + let error = version_with( + [ + ( + "workflow.fabro", + "digraph W { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }", + ), + ( + "workflow.toml", + "_version = 1\n[workflow]\ngraph = \"other.fabro\"\n", + ), + ], + [], + ) + .unwrap_err(); + assert!(matches!( + error, + WorkflowVersionError::ConfigEntrypointMismatch { .. } + )); + + let missing_dockerfile = version_with( + [ + ("workflow.fabro", "digraph W {}"), + ( + "workflow.toml", + "_version = 1\n[run.environment.image]\ndockerfile = { path = \"docker/Dockerfile\" }\n", + ), + ], + [], + ) + .unwrap_err(); + assert!(matches!( + missing_dockerfile, + WorkflowVersionError::MissingFile { .. } + )); + + let invalid_config = version_with( + [ + ("workflow.fabro", "digraph W {}"), + ("workflow.toml", "not valid toml = ["), + ], + [], + ) + .unwrap_err(); + assert!(matches!( + invalid_config, + WorkflowVersionError::Config { .. } + )); + } + + #[test] + fn accepts_root_config_and_all_dockerfile_path_sources() { + let version = version_with( + [ + ("workflow.fabro", "digraph W {}"), + ( + "workflow.toml", + r#"_version = 1 +[workflow] +graph = "workflow.fabro" + +[environments.cloud] +provider = "daytona" + +[environments.cloud.image] +dockerfile = { path = "docker/named.Dockerfile" } + +[run.environment.image] +dockerfile = { path = "docker/run.Dockerfile" } +"#, + ), + ("docker/named.Dockerfile", "FROM alpine\n"), + ("docker/run.Dockerfile", "FROM ubuntu\n"), + ], + [], + ) + .unwrap(); + + assert_eq!(version.version().entrypoint(), &path("workflow.fabro")); + } + + #[test] + fn rejects_server_managed_environment_cwd_in_workflow_config() { + let error = version_with( + [ + ("workflow.fabro", "digraph W {}"), + ( + "workflow.toml", + "_version = 1\n[environments.local]\nprovider = \"local\"\ncwd = \"/tmp\"\n", + ), + ], + [], + ) + .unwrap_err(); + + assert!(matches!(error, WorkflowVersionError::Config { .. })); + assert!(error.to_string().contains("workflow.toml is invalid")); + } + + #[test] + fn rejects_escaping_and_dynamic_template_references() { + let escaping = version_with( + [( + "workflow.fabro", + r#"digraph W { imported [import="../outside.fabro"] }"#, + )], + [], + ) + .unwrap_err(); + assert!(matches!( + escaping, + WorkflowVersionError::InvalidReference { .. } + )); + + let dynamic = version_with( + [( + "workflow.fabro", + r#"digraph W { step [prompt="{% include template_name %}"] }"#, + )], + [], + ) + .unwrap_err(); + assert!(matches!(dynamic, WorkflowVersionError::Template { .. })); + } +} diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs new file mode 100644 index 000000000..80dcebd53 --- /dev/null +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -0,0 +1,306 @@ +use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::sync::Arc; + +use fabro_store::BlobStore; +use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId, WorkflowVersionShapeError}; +use thiserror::Error; + +use crate::{ValidatedWorkflowVersion, WorkflowVersionError}; + +#[derive(Debug, Error)] +pub enum WorkflowVersionStoreError { + #[error(transparent)] + InvalidVersion(#[from] WorkflowVersionError), + #[error(transparent)] + InvalidShape(#[from] WorkflowVersionShapeError), + #[error("workflow-version dependency `{id}` at `{path}` is not stored")] + DependencyNotFound { + path: WorkflowPath, + id: WorkflowVersionId, + }, + #[error("workflow-version dependency `{id}` at `{path}` is invalid")] + DependencyInvalid { + path: WorkflowPath, + id: WorkflowVersionId, + #[source] + source: Box, + }, + #[error("workflow-version blob `{id}` cannot be decoded as a valid workflow version")] + Decode { + id: WorkflowVersionId, + #[source] + source: serde_json::Error, + }, + #[error("workflow-version blob `{id}` is not canonical")] + NonCanonical { id: WorkflowVersionId }, + #[error("workflow-version storage operation failed")] + Storage { + #[source] + source: fabro_store::Error, + }, +} + +/// Content-addressed storage for validated workflow versions. +/// +/// `put` only accepts semantically validated versions; `get` re-validates +/// blobs on read because the blob namespace is shared and storage is not +/// trusted to contain only canonical versions. +#[derive(Clone, Debug)] +pub struct WorkflowVersionStore { + blobs: Arc, +} + +impl WorkflowVersionStore { + #[must_use] + pub fn new(blobs: Arc) -> Self { + Self { blobs } + } + + pub async fn put( + &self, + version: &ValidatedWorkflowVersion, + ) -> Result { + let canonical = version.version().canonical_bytes()?; + self.validate_dependency_closure(version.version().workflow_dependencies()) + .await?; + self.blobs + .write(&canonical) + .await + .map(WorkflowVersionId::from) + .map_err(|source| WorkflowVersionStoreError::Storage { source }) + } + + pub async fn get( + &self, + id: &WorkflowVersionId, + ) -> Result, WorkflowVersionStoreError> { + let Some(version) = self.load_one(id).await? else { + return Ok(None); + }; + self.validate_dependency_closure(version.version().workflow_dependencies()) + .await?; + Ok(Some(version)) + } + + async fn load_one( + &self, + id: &WorkflowVersionId, + ) -> Result, WorkflowVersionStoreError> { + let blob_id = (*id).into(); + let Some(bytes) = self + .blobs + .read(&blob_id) + .await + .map_err(|source| WorkflowVersionStoreError::Storage { source })? + else { + return Ok(None); + }; + let version = serde_json::from_slice::(&bytes) + .map_err(|source| WorkflowVersionStoreError::Decode { id: *id, source })?; + let validated = ValidatedWorkflowVersion::new(version)?; + let canonical = validated.version().canonical_bytes()?; + if canonical.as_slice() != bytes.as_ref() { + return Err(WorkflowVersionStoreError::NonCanonical { id: *id }); + } + Ok(Some(validated)) + } + + async fn validate_dependency_closure( + &self, + dependencies: &BTreeMap, + ) -> Result<(), WorkflowVersionStoreError> { + let mut pending = dependencies + .iter() + .map(|(path, id)| (path.clone(), *id)) + .collect::>(); + let mut visited = HashSet::new(); + + while let Some((path, id)) = pending.pop_front() { + if !visited.insert(id) { + continue; + } + match self.load_one(&id).await { + Ok(Some(dependency)) => { + pending.extend( + dependency + .version() + .workflow_dependencies() + .iter() + .map(|(path, id)| (path.clone(), *id)), + ); + } + Ok(None) => { + return Err(WorkflowVersionStoreError::DependencyNotFound { path, id }); + } + // Persistence failures are server faults, not evidence that + // the caller supplied an invalid dependency. + Err(source @ WorkflowVersionStoreError::Storage { .. }) => return Err(source), + Err(source) => { + return Err(WorkflowVersionStoreError::DependencyInvalid { + path, + id, + source: Box::new(source), + }); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::sync::Arc; + use std::time::Duration; + + use fabro_store::{BlobStore, Database}; + use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId}; + use object_store::memory::InMemory; + + use super::{WorkflowVersionStore, WorkflowVersionStoreError}; + use crate::ValidatedWorkflowVersion; + + fn path(value: &str) -> WorkflowPath { + value.parse().unwrap() + } + + fn version( + graph: &str, + dependencies: BTreeMap, + ) -> ValidatedWorkflowVersion { + ValidatedWorkflowVersion::new( + WorkflowVersion::new( + path("workflow.fabro"), + BTreeMap::from([(path("workflow.fabro"), graph.to_owned())]), + dependencies, + ) + .unwrap(), + ) + .unwrap() + } + + async fn stores() -> (Arc, WorkflowVersionStore) { + let database = Database::new( + Arc::new(InMemory::new()), + "", + Duration::from_millis(1), + None, + ); + let blobs = database.blobs().await.unwrap(); + let versions = WorkflowVersionStore::new(Arc::clone(&blobs)); + (blobs, versions) + } + + #[tokio::test] + async fn put_get_reuses_exact_blob_digest() { + 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 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); + assert_eq!(store.get(&id).await.unwrap(), Some(version)); + } + + #[tokio::test] + async fn identical_content_is_idempotent() { + let (_, store) = stores().await; + let original = version("digraph W {}", BTreeMap::new()); + + assert_eq!( + store.put(&original).await.unwrap(), + store.put(&original).await.unwrap() + ); + + let changed = version("digraph W { changed [label=\"yes\"] }", BTreeMap::new()); + assert_ne!( + store.put(&original).await.unwrap(), + store.put(&changed).await.unwrap() + ); + } + + #[tokio::test] + 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 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 error = store.put(&root).await.unwrap_err(); + assert!(matches!( + error, + WorkflowVersionStoreError::DependencyNotFound { .. } + )); + assert!(!blobs.exists(&root_id.into()).await.unwrap()); + assert_eq!(store.put(&child).await.unwrap(), child_id); + assert!(store.put(&root).await.is_ok()); + } + + #[tokio::test] + async fn dependency_closure_must_be_complete_before_root_write() { + let (blobs, store) = stores().await; + let missing_grandchild_id = WorkflowVersionId::from(fabro_types::BlobHash::new(b"missing")); + let child = version( + r#"digraph Child { grandchild [stack.child_workflow="grandchild.fabro"] }"#, + BTreeMap::from([(path("grandchild.fabro"), missing_grandchild_id)]), + ); + let child_bytes = child.version().canonical_bytes().unwrap(); + let child_id = WorkflowVersionId::from(blobs.write(&child_bytes).await.unwrap()); + 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(), + )); + + assert!(matches!( + store.put(&root).await.unwrap_err(), + WorkflowVersionStoreError::DependencyNotFound { id, .. } + if id == missing_grandchild_id + )); + assert!(!blobs.exists(&root_id.into()).await.unwrap()); + assert!(matches!( + store.get(&child_id).await.unwrap_err(), + WorkflowVersionStoreError::DependencyNotFound { id, .. } + if id == missing_grandchild_id + )); + } + + #[tokio::test] + async fn get_rejects_arbitrary_and_noncanonical_blobs() { + let (blobs, store) = stores().await; + let arbitrary = WorkflowVersionId::from(blobs.write(b"not json").await.unwrap()); + assert!(matches!( + store.get(&arbitrary).await.unwrap_err(), + WorkflowVersionStoreError::Decode { .. } + )); + + let invalid_bytes = br#"{"entrypoint":"missing.fabro","files":{"workflow.fabro":"digraph W {}"},"workflow_dependencies":{}}"#; + let invalid = WorkflowVersionId::from(blobs.write(invalid_bytes).await.unwrap()); + assert!(matches!( + store.get(&invalid).await.unwrap_err(), + WorkflowVersionStoreError::Decode { .. } + )); + + let version = version("digraph W {}", BTreeMap::new()); + let pretty = serde_json::to_vec_pretty(version.version()).unwrap(); + let noncanonical = WorkflowVersionId::from(blobs.write(&pretty).await.unwrap()); + assert!(matches!( + store.get(&noncanonical).await.unwrap_err(), + WorkflowVersionStoreError::NonCanonical { .. } + )); + } +} diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 38811dd01..ea8ece1eb 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -722,6 +722,9 @@ fn main() { ("CompletionMessage", "fabro_types::Message", &[]), ("CompletionMessageRole", "fabro_types::Role", &[]), ("CompletionContentPart", "fabro_types::ContentPart", &[]), + ("WorkflowVersion", "fabro_types::WorkflowVersion", &[]), + ("WorkflowPath", "fabro_types::WorkflowPath", &[]), + ("WorkflowVersionId", "fabro_types::WorkflowVersionId", &[]), ("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 d9d6aa025..b40087831 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -73,7 +73,8 @@ pub mod types { StageModelUsage, StageOutcome, StageProjection, StageState, StageToolBatchProjection, SubAgentProjection, SubAgentStatus, SystemActorKind, SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, TurnId, UpdateVariableRequest, - UserPrincipal, Variable, VariableListResponse, WorkflowSettings, + UserPrincipal, Variable, VariableListResponse, WorkflowPath, WorkflowSettings, + WorkflowVersion, WorkflowVersionId, }; pub use crate::generated::types::*; diff --git a/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs b/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs new file mode 100644 index 000000000..894c1c910 --- /dev/null +++ b/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs @@ -0,0 +1,54 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::{ + WorkflowPath as ApiWorkflowPath, WorkflowVersion as ApiWorkflowVersion, + WorkflowVersionId as ApiWorkflowVersionId, +}; +use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId}; +use serde_json::json; + +#[test] +fn workflow_version_schemas_reuse_domain_types() { + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); +} + +#[test] +fn workflow_version_round_trips_exact_wire_shape() { + let value = json!({ + "entrypoint": "workflow.fabro", + "files": { + "prompts/goal.md": "Ship it", + "workflow.fabro": "digraph W { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" + }, + "workflow_dependencies": {} + }); + + let version: ApiWorkflowVersion = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(version).unwrap(), value); +} + +#[test] +fn workflow_version_replacement_rejects_unknown_fields() { + let value = json!({ + "entrypoint": "workflow.fabro", + "files": { + "workflow.fabro": "digraph W {}" + }, + "workflow_dependencies": {}, + "metadata": {} + }); + + assert!(serde_json::from_value::(value).is_err()); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} and {} should be the same type", + type_name::(), + type_name::() + ); +} diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index e2b05384c..bb3c86620 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -54,6 +54,9 @@ pub mod timing; pub mod todo; pub mod transcript; pub mod variable; +pub mod workflow_path; +pub mod workflow_version; +pub mod workflow_version_id; pub use artifact::ArtifactUpload; pub use auth::{IdpIdentity, IdpIdentityError}; @@ -183,3 +186,11 @@ pub use transcript::{ pub use variable::{ CreateVariableRequest, UpdateVariableRequest, Variable, VariableListResponse, is_env_style_name, }; +pub use workflow_path::{ + MAX_WORKFLOW_PATH_BYTES, MAX_WORKFLOW_PATH_COMPONENTS, WorkflowPath, WorkflowPathParseError, +}; +pub use workflow_version::{ + MAX_WORKFLOW_VERSION_BYTES, MAX_WORKFLOW_VERSION_DEPENDENCIES, MAX_WORKFLOW_VERSION_FILE_BYTES, + MAX_WORKFLOW_VERSION_FILES, WorkflowVersion, WorkflowVersionShapeError, +}; +pub use workflow_version_id::{WorkflowVersionId, WorkflowVersionIdParseError}; diff --git a/lib/foundation/fabro-types/src/workflow_path.rs b/lib/foundation/fabro-types/src/workflow_path.rs new file mode 100644 index 000000000..32297d93e --- /dev/null +++ b/lib/foundation/fabro-types/src/workflow_path.rs @@ -0,0 +1,301 @@ +use std::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +pub const MAX_WORKFLOW_PATH_BYTES: usize = 240; +pub const MAX_WORKFLOW_PATH_COMPONENTS: usize = 16; + +#[derive(Clone, Debug, PartialEq, Eq, Error)] +#[error("invalid workflow path `{value}`: {reason}")] +pub struct WorkflowPathParseError { + value: String, + reason: &'static str, +} + +impl WorkflowPathParseError { + fn new(value: &str, reason: &'static str) -> Self { + Self { + value: value.to_owned(), + reason, + } + } + + #[must_use] + pub fn value(&self) -> &str { + &self.value + } + + #[must_use] + pub fn reason(&self) -> &'static str { + self.reason + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(into = "String", try_from = "String")] +pub struct WorkflowPath(String); + +impl WorkflowPath { + pub fn new(value: impl Into) -> Result { + let value = value.into(); + validate(&value)?; + Ok(Self(value)) + } + + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } + + #[must_use] + pub fn parent(&self) -> Option { + self.0 + .rsplit_once('/') + .map(|(parent, _)| Self(parent.to_owned())) + } + + #[must_use] + pub fn is_ancestor_of(&self, other: &Self) -> bool { + other.0.len() > self.0.len() + && other.0.starts_with(self.0.as_str()) + && other.0.as_bytes()[self.0.len()] == b'/' + } + + pub fn resolve_reference(&self, reference: &str) -> Result { + validate_reference_shape(reference)?; + let mut components = self + .0 + .rsplit_once('/') + .map_or_else(Vec::new, |(parent, _)| { + parent.split('/').collect::>() + }); + + for component in reference.split('/') { + match component { + "" | "." => {} + ".." => { + if components.pop().is_none() { + return Err(WorkflowPathParseError::new( + reference, + "reference escapes the workflow root", + )); + } + } + value => components.push(value), + } + } + + Self::new(components.join("/")) + } +} + +impl FromStr for WorkflowPath { + type Err = WorkflowPathParseError; + + fn from_str(value: &str) -> Result { + Self::new(value) + } +} + +impl TryFrom for WorkflowPath { + type Error = WorkflowPathParseError; + + fn try_from(value: String) -> Result { + Self::new(value) + } +} + +impl From for String { + fn from(value: WorkflowPath) -> Self { + value.0 + } +} + +impl fmt::Display for WorkflowPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +fn validate(value: &str) -> Result<(), WorkflowPathParseError> { + validate_reference_shape(value)?; + if value + .split('/') + .any(|component| matches!(component, "." | "..")) + { + return Err(WorkflowPathParseError::new( + value, + "dot segments are not allowed in stored paths", + )); + } + if value.split('/').count() > MAX_WORKFLOW_PATH_COMPONENTS { + return Err(WorkflowPathParseError::new( + value, + "path has too many components", + )); + } + if value.len() > MAX_WORKFLOW_PATH_BYTES { + return Err(WorkflowPathParseError::new(value, "path is too long")); + } + Ok(()) +} + +fn validate_reference_shape(value: &str) -> Result<(), WorkflowPathParseError> { + if value.is_empty() { + return Err(WorkflowPathParseError::new(value, "path is empty")); + } + if value.starts_with('/') { + return Err(WorkflowPathParseError::new( + value, + "absolute paths are not allowed", + )); + } + if value.starts_with('~') { + return Err(WorkflowPathParseError::new( + value, + "tilde-prefixed paths are not allowed", + )); + } + if value.contains('\\') { + return Err(WorkflowPathParseError::new( + value, + "backslashes are not allowed", + )); + } + if value.ends_with('/') { + return Err(WorkflowPathParseError::new( + value, + "trailing slashes are not allowed", + )); + } + if value.contains("//") { + return Err(WorkflowPathParseError::new( + value, + "repeated slashes are not allowed", + )); + } + let bytes = value.as_bytes(); + if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { + return Err(WorkflowPathParseError::new( + value, + "Windows drive paths are not allowed", + )); + } + if value.bytes().any(|byte| byte.is_ascii_control()) { + return Err(WorkflowPathParseError::new( + value, + "control characters are not allowed", + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use serde_json::json; + + use super::{MAX_WORKFLOW_PATH_BYTES, MAX_WORKFLOW_PATH_COMPONENTS, WorkflowPath}; + + #[test] + fn accepts_canonical_portable_paths() { + for value in ["workflow.fabro", "graphs/main.fabro", "prompts/日本語.md"] { + let path: WorkflowPath = value.parse().expect("path should parse"); + assert_eq!(path.as_str(), value); + } + } + + #[test] + fn rejects_non_canonical_or_unsafe_paths() { + for value in [ + "", "/root", "root/", "a//b", "a\\b", "~/a", "C:/a", ".", "..", "a/./b", "a/../b", + "a\nb", + ] { + assert!(value.parse::().is_err(), "accepted {value:?}"); + } + } + + #[test] + fn enforces_byte_and_component_limits() { + assert!( + "a".repeat(MAX_WORKFLOW_PATH_BYTES) + .parse::() + .is_ok() + ); + assert!( + "a".repeat(MAX_WORKFLOW_PATH_BYTES + 1) + .parse::() + .is_err() + ); + assert!( + vec!["a"; MAX_WORKFLOW_PATH_COMPONENTS] + .join("/") + .parse::() + .is_ok() + ); + assert!( + vec!["a"; MAX_WORKFLOW_PATH_COMPONENTS + 1] + .join("/") + .parse::() + .is_err() + ); + } + + #[test] + fn resolves_references_without_escaping_root() { + let graph: WorkflowPath = "graphs/nested/main.fabro".parse().unwrap(); + assert_eq!( + graph.resolve_reference("../prompts/plan.md").unwrap(), + "graphs/prompts/plan.md".parse().unwrap() + ); + assert!(graph.resolve_reference("../../../outside.md").is_err()); + assert!(graph.resolve_reference("prompts//plan.md").is_err()); + assert!(graph.resolve_reference("prompts/").is_err()); + } + + #[test] + fn ancestor_checks_component_boundaries() { + let parent: WorkflowPath = "dir/file".parse().unwrap(); + assert!(parent.is_ancestor_of(&"dir/file/child".parse().unwrap())); + assert!(!parent.is_ancestor_of(&"dir/filename".parse().unwrap())); + } + + #[test] + fn serde_and_ordered_map_keys_preserve_canonical_text() { + let paths = BTreeMap::from([ + ("z/last.md".parse::().unwrap(), 2), + ("a/first.md".parse::().unwrap(), 1), + ]); + + assert_eq!( + serde_json::to_value(&paths).unwrap(), + json!({"a/first.md": 1, "z/last.md": 2}) + ); + assert_eq!( + serde_json::from_value::>(json!({ + "a/first.md": 1, + "z/last.md": 2 + })) + .unwrap(), + paths + ); + } + + #[test] + fn byte_limit_counts_utf8_bytes() { + assert!( + "é".repeat(MAX_WORKFLOW_PATH_BYTES / 2) + .parse::() + .is_ok() + ); + assert!( + "é".repeat(MAX_WORKFLOW_PATH_BYTES / 2 + 1) + .parse::() + .is_err() + ); + assert!("notes/\u{85}.md".parse::().is_ok()); + } +} diff --git a/lib/foundation/fabro-types/src/workflow_version.rs b/lib/foundation/fabro-types/src/workflow_version.rs new file mode 100644 index 000000000..c0b35f8ad --- /dev/null +++ b/lib/foundation/fabro-types/src/workflow_version.rs @@ -0,0 +1,419 @@ +use std::collections::BTreeMap; +use std::fmt; +use std::marker::PhantomData; + +use serde::de::{Error as _, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use thiserror::Error; + +use crate::{WorkflowPath, WorkflowVersionId}; + +pub const MAX_WORKFLOW_VERSION_FILES: usize = 512; +pub const MAX_WORKFLOW_VERSION_DEPENDENCIES: usize = 512; +pub const MAX_WORKFLOW_VERSION_FILE_BYTES: usize = 512 * 1024; +pub const MAX_WORKFLOW_VERSION_BYTES: usize = 2 * 1024 * 1024; + +#[derive(Debug, Error)] +pub enum WorkflowVersionShapeError { + #[error("workflow version has {actual} files; maximum is {maximum}")] + TooManyFiles { actual: usize, maximum: usize }, + #[error("workflow version has {actual} workflow dependencies; maximum is {maximum}")] + TooManyWorkflowDependencies { actual: usize, maximum: usize }, + #[error("workflow file `{path}` is {actual} bytes; maximum is {maximum}")] + FileTooLarge { + path: WorkflowPath, + actual: usize, + maximum: usize, + }, + #[error("workflow version is {actual} canonical bytes; maximum is {maximum}")] + VersionTooLarge { actual: usize, maximum: usize }, + #[error("entrypoint `{path}` is not present in workflow files")] + MissingEntrypoint { path: WorkflowPath }, + #[error("workflow paths collide: `{first}` and `{second}`")] + PathCollision { + first: WorkflowPath, + second: WorkflowPath, + }, + #[error("failed to serialize canonical workflow version")] + Serialization { + #[source] + source: serde_json::Error, + }, +} + +/// Canonical wire form of an immutable workflow version. +/// +/// Construction (and therefore deserialization) enforces the structural +/// invariants: file-count and byte-size limits, entrypoint presence, unique +/// map keys, and collision-free paths. Semantic validation of graph, config, +/// and template content is a separate concern owned by +/// `fabro-workflow-version`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct WorkflowVersion { + entrypoint: WorkflowPath, + files: BTreeMap, + workflow_dependencies: BTreeMap, +} + +impl WorkflowVersion { + pub fn new( + entrypoint: WorkflowPath, + files: BTreeMap, + workflow_dependencies: BTreeMap, + ) -> Result { + let version = Self { + entrypoint, + files, + workflow_dependencies, + }; + version.validate_shape()?; + version.canonical_bytes()?; + Ok(version) + } + + #[must_use] + pub fn entrypoint(&self) -> &WorkflowPath { + &self.entrypoint + } + + #[must_use] + pub fn files(&self) -> &BTreeMap { + &self.files + } + + #[must_use] + pub fn workflow_dependencies(&self) -> &BTreeMap { + &self.workflow_dependencies + } + + /// Serialize to the canonical wire form. + /// + /// Structural validity is guaranteed by construction, so this only + /// serializes and enforces the canonical size limit. + pub fn canonical_bytes(&self) -> Result, WorkflowVersionShapeError> { + let bytes = serde_json::to_vec(self) + .map_err(|source| WorkflowVersionShapeError::Serialization { source })?; + if bytes.len() > MAX_WORKFLOW_VERSION_BYTES { + return Err(WorkflowVersionShapeError::VersionTooLarge { + actual: bytes.len(), + maximum: MAX_WORKFLOW_VERSION_BYTES, + }); + } + Ok(bytes) + } + + fn validate_shape(&self) -> Result<(), WorkflowVersionShapeError> { + if self.files.len() > MAX_WORKFLOW_VERSION_FILES { + return Err(WorkflowVersionShapeError::TooManyFiles { + actual: self.files.len(), + maximum: MAX_WORKFLOW_VERSION_FILES, + }); + } + if self.workflow_dependencies.len() > MAX_WORKFLOW_VERSION_DEPENDENCIES { + return Err(WorkflowVersionShapeError::TooManyWorkflowDependencies { + actual: self.workflow_dependencies.len(), + maximum: MAX_WORKFLOW_VERSION_DEPENDENCIES, + }); + } + for (path, content) in &self.files { + if content.len() > MAX_WORKFLOW_VERSION_FILE_BYTES { + return Err(WorkflowVersionShapeError::FileTooLarge { + path: path.clone(), + actual: content.len(), + maximum: MAX_WORKFLOW_VERSION_FILE_BYTES, + }); + } + } + if !self.files.contains_key(&self.entrypoint) { + return Err(WorkflowVersionShapeError::MissingEntrypoint { + path: self.entrypoint.clone(), + }); + } + self.validate_path_collisions() + } + + fn validate_path_collisions(&self) -> Result<(), WorkflowVersionShapeError> { + // Keys are unique within each map, so equality can only collide + // across files and workflow dependencies. + let mut paths = self + .files + .keys() + .chain(self.workflow_dependencies.keys()) + .collect::>(); + paths.sort_unstable(); + for pair in paths.windows(2) { + let [first, second] = pair else { + unreachable!("a two-item window must contain two paths") + }; + if first == second || first.is_ancestor_of(second) { + return Err(WorkflowVersionShapeError::PathCollision { + first: (*first).clone(), + second: (*second).clone(), + }); + } + } + Ok(()) + } +} + +impl<'de> Deserialize<'de> for WorkflowVersion { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Wire { + entrypoint: WorkflowPath, + files: UniqueBTreeMap, + workflow_dependencies: UniqueBTreeMap, + } + + let wire = Wire::deserialize(deserializer)?; + Self::new(wire.entrypoint, wire.files.0, wire.workflow_dependencies.0) + .map_err(D::Error::custom) + } +} + +struct UniqueBTreeMap(BTreeMap); + +impl<'de, K, V> Deserialize<'de> for UniqueBTreeMap +where + K: Deserialize<'de> + Ord + fmt::Display, + V: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MapVisitor(PhantomData<(K, V)>); + + impl<'de, K, V> Visitor<'de> for MapVisitor + where + K: Deserialize<'de> + Ord + fmt::Display, + V: Deserialize<'de>, + { + type Value = UniqueBTreeMap; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map with unique keys") + } + + fn visit_map(self, mut access: A) -> Result + where + A: MapAccess<'de>, + { + let mut values = BTreeMap::new(); + while let Some((key, value)) = access.next_entry::()? { + if values.insert(key, value).is_some() { + return Err(A::Error::custom("duplicate workflow map key")); + } + } + Ok(UniqueBTreeMap(values)) + } + } + + deserializer.deserialize_map(MapVisitor(PhantomData)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::{ + MAX_WORKFLOW_VERSION_BYTES, MAX_WORKFLOW_VERSION_DEPENDENCIES, + MAX_WORKFLOW_VERSION_FILE_BYTES, MAX_WORKFLOW_VERSION_FILES, WorkflowVersion, + WorkflowVersionShapeError, + }; + use crate::{BlobHash, WorkflowPath, WorkflowVersionId}; + + fn path(value: &str) -> WorkflowPath { + value.parse().unwrap() + } + + #[test] + fn canonical_bytes_have_fixed_field_and_map_order() { + let version = WorkflowVersion::new( + path("workflow.fabro"), + BTreeMap::from([ + (path("z.txt"), "Z".to_string()), + (path("workflow.fabro"), "digraph W {}".to_string()), + (path("a.txt"), "A".to_string()), + ]), + BTreeMap::new(), + ) + .unwrap(); + + assert_eq!( + String::from_utf8(version.canonical_bytes().unwrap()).unwrap(), + r#"{"entrypoint":"workflow.fabro","files":{"a.txt":"A","workflow.fabro":"digraph W {}","z.txt":"Z"},"workflow_dependencies":{}}"# + ); + } + + #[test] + fn rejects_missing_entrypoint() { + let error = WorkflowVersion::new( + path("missing.fabro"), + BTreeMap::from([(path("workflow.fabro"), "digraph W {}".to_string())]), + BTreeMap::new(), + ) + .unwrap_err(); + assert!(matches!( + error, + WorkflowVersionShapeError::MissingEntrypoint { .. } + )); + } + + #[test] + fn rejects_path_collisions_and_large_files() { + let collision = WorkflowVersion::new( + path("workflow.fabro"), + BTreeMap::from([ + (path("workflow.fabro"), "digraph W {}".to_string()), + (path("assets"), "file".to_string()), + (path("assets/item.txt"), "nested".to_string()), + ]), + BTreeMap::new(), + ) + .unwrap_err(); + assert!(matches!( + collision, + WorkflowVersionShapeError::PathCollision { .. } + )); + + let mut files = BTreeMap::from([(path("workflow.fabro"), "digraph W {}".to_string())]); + files.insert( + path("large.txt"), + "x".repeat(MAX_WORKFLOW_VERSION_FILE_BYTES + 1), + ); + let large = + WorkflowVersion::new(path("workflow.fabro"), files, BTreeMap::new()).unwrap_err(); + assert!(matches!( + large, + WorkflowVersionShapeError::FileTooLarge { .. } + )); + } + + #[test] + fn enforces_file_count_file_size_and_canonical_size_boundaries() { + let mut files = BTreeMap::from([(path("workflow.fabro"), "digraph W {}".to_string())]); + for index in 0..MAX_WORKFLOW_VERSION_FILES - 1 { + files.insert(path(&format!("file-{index:03}.txt")), String::new()); + } + assert!( + WorkflowVersion::new(path("workflow.fabro"), files.clone(), BTreeMap::new()).is_ok() + ); + files.insert(path("too-many.txt"), String::new()); + assert!(matches!( + WorkflowVersion::new(path("workflow.fabro"), files, BTreeMap::new()).unwrap_err(), + WorkflowVersionShapeError::TooManyFiles { .. } + )); + + let exact_file = BTreeMap::from([ + (path("workflow.fabro"), "digraph W {}".to_string()), + ( + path("payload.txt"), + "x".repeat(MAX_WORKFLOW_VERSION_FILE_BYTES), + ), + ]); + assert!( + WorkflowVersion::new(path("workflow.fabro"), exact_file.clone(), BTreeMap::new()) + .is_ok() + ); + let mut oversized_file = exact_file; + oversized_file + .get_mut(&path("payload.txt")) + .unwrap() + .push('x'); + assert!(matches!( + WorkflowVersion::new(path("workflow.fabro"), oversized_file, BTreeMap::new()) + .unwrap_err(), + WorkflowVersionShapeError::FileTooLarge { .. } + )); + + let mut exact_version_files = + BTreeMap::from([(path("workflow.fabro"), "digraph W {}".to_string())]); + for index in 0..4 { + exact_version_files.insert(path(&format!("payload-{index}.txt")), String::new()); + } + let empty = WorkflowVersion::new( + path("workflow.fabro"), + exact_version_files.clone(), + BTreeMap::new(), + ) + .unwrap(); + let remaining = MAX_WORKFLOW_VERSION_BYTES - empty.canonical_bytes().unwrap().len(); + let per_file = remaining / 4; + let remainder = remaining % 4; + for index in 0..4 { + let length = per_file + usize::from(index < remainder); + assert!(length <= MAX_WORKFLOW_VERSION_FILE_BYTES); + exact_version_files.insert(path(&format!("payload-{index}.txt")), "x".repeat(length)); + } + let exact_version = WorkflowVersion::new( + path("workflow.fabro"), + exact_version_files.clone(), + BTreeMap::new(), + ) + .unwrap(); + assert_eq!( + exact_version.canonical_bytes().unwrap().len(), + MAX_WORKFLOW_VERSION_BYTES + ); + exact_version_files + .get_mut(&path("payload-0.txt")) + .unwrap() + .push('x'); + assert!(matches!( + WorkflowVersion::new(path("workflow.fabro"), exact_version_files, BTreeMap::new()) + .unwrap_err(), + WorkflowVersionShapeError::VersionTooLarge { .. } + )); + } + + #[test] + fn enforces_workflow_dependency_count_boundary() { + let dependencies = (0..MAX_WORKFLOW_VERSION_DEPENDENCIES) + .map(|index| { + ( + path(&format!("dependency-{index:03}.fabro")), + WorkflowVersionId::from(BlobHash::new(index.to_string().as_bytes())), + ) + }) + .collect::>(); + let files = BTreeMap::from([(path("workflow.fabro"), "digraph W {}".to_owned())]); + assert!( + WorkflowVersion::new(path("workflow.fabro"), files.clone(), dependencies.clone()) + .is_ok() + ); + + let mut oversized = dependencies; + oversized.insert( + path("too-many.fabro"), + WorkflowVersionId::from(BlobHash::new(b"too many")), + ); + assert!(matches!( + WorkflowVersion::new(path("workflow.fabro"), files, oversized).unwrap_err(), + WorkflowVersionShapeError::TooManyWorkflowDependencies { .. } + )); + } + + #[test] + fn deserialize_rejects_unknown_fields_and_duplicate_keys() { + let unknown = r#"{ + "entrypoint":"workflow.fabro", + "files":{"workflow.fabro":"digraph W {}"}, + "workflow_dependencies":{}, + "metadata":{} + }"#; + assert!(serde_json::from_str::(unknown).is_err()); + + let duplicate = r#"{ + "entrypoint":"workflow.fabro", + "files":{"workflow.fabro":"digraph W {}","workflow.fabro":"digraph X {}"}, + "workflow_dependencies":{} + }"#; + assert!(serde_json::from_str::(duplicate).is_err()); + } +} diff --git a/lib/foundation/fabro-types/src/workflow_version_id.rs b/lib/foundation/fabro-types/src/workflow_version_id.rs new file mode 100644 index 000000000..f3a618482 --- /dev/null +++ b/lib/foundation/fabro-types/src/workflow_version_id.rs @@ -0,0 +1,96 @@ +use std::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::BlobHash; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(into = "String", try_from = "String")] +pub struct WorkflowVersionId(BlobHash); + +impl From for WorkflowVersionId { + fn from(value: BlobHash) -> Self { + Self(value) + } +} + +impl From for BlobHash { + fn from(value: WorkflowVersionId) -> Self { + value.0 + } +} + +impl fmt::Display for WorkflowVersionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl From for String { + fn from(value: WorkflowVersionId) -> Self { + value.to_string() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] +#[error("workflow version ID must be exactly 64 lowercase hexadecimal characters")] +pub struct WorkflowVersionIdParseError; + +impl FromStr for WorkflowVersionId { + type Err = WorkflowVersionIdParseError; + + fn from_str(value: &str) -> Result { + // `BlobHash` enforces length and hex charset but accepts uppercase digits; + // the canonical wire form is lowercase only. + if value.bytes().any(|byte| byte.is_ascii_uppercase()) { + return Err(WorkflowVersionIdParseError); + } + value + .parse::() + .map(Self) + .map_err(|_| WorkflowVersionIdParseError) + } +} + +impl TryFrom for WorkflowVersionId { + type Error = WorkflowVersionIdParseError; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +#[cfg(test)] +mod tests { + use crate::{BlobHash, WorkflowVersionId}; + + #[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); + } + + #[test] + fn parse_and_serde_require_lowercase_hex() { + let value = BlobHash::new(b"workflow").to_string(); + let id: WorkflowVersionId = value.parse().unwrap(); + assert_eq!(serde_json::to_value(id).unwrap(), value); + assert!(value.to_uppercase().parse::().is_err()); + for invalid in [ + String::new(), + "0".repeat(63), + "0".repeat(65), + "g".repeat(64), + ] { + assert!(invalid.parse::().is_err()); + } + assert!( + serde_json::from_value::(serde_json::json!(value.to_uppercase())) + .is_err() + ); + } +} diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 4f99a6d3d..4f8d5f871 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -22,6 +22,7 @@ api/sessions-api.ts api/settings-api.ts api/system-api.ts api/variables-api.ts +api/workflow-versions-api.ts api/workflows-api.ts base.ts common.ts @@ -105,6 +106,7 @@ models/create-run-pull-request-request.ts models/create-run-session-request.ts models/create-secret-request.ts models/create-variable-request.ts +models/create-workflow-version-response.ts models/delete-run-response.ts models/delete-run-sandbox.ts models/delete-secret-request.ts @@ -541,4 +543,5 @@ models/workflow-ref.ts models/workflow-reference.ts models/workflow-schedule-summary.ts models/workflow-settings.ts +models/workflow-version.ts models/write-blob-response.ts diff --git a/lib/packages/fabro-api-client/src/api.ts b/lib/packages/fabro-api-client/src/api.ts index 02fa11e7b..3d4ddb3c7 100644 --- a/lib/packages/fabro-api-client/src/api.ts +++ b/lib/packages/fabro-api-client/src/api.ts @@ -37,4 +37,5 @@ export * from './api/sessions-api'; export * from './api/settings-api'; export * from './api/system-api'; export * from './api/variables-api'; +export * from './api/workflow-versions-api'; export * from './api/workflows-api'; 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 new file mode 100644 index 000000000..edd6a2ccf --- /dev/null +++ b/lib/packages/fabro-api-client/src/api/workflow-versions-api.ts @@ -0,0 +1,134 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction, replaceWithSerializableTypeIfNeeded } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { CreateWorkflowVersionResponse } from '../models'; +// @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore +import type { WorkflowVersion } from '../models'; +/** + * WorkflowVersionsApi - axios parameter creator + */ +export const WorkflowVersionsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * Validates and stores an immutable workflow package in content-addressed storage. Repeating the same canonical content returns the same identifier. + * @summary Create Workflow Version + * @param {WorkflowVersion} workflowVersion + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorkflowVersion: async (workflowVersion: WorkflowVersion, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'workflowVersion' is not null or undefined + assertParamExists('createWorkflowVersion', 'workflowVersion', workflowVersion) + const localVarPath = `/api/v1/workflow-versions`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Content-Type'] = 'application/json'; + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(workflowVersion, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * WorkflowVersionsApi - functional programming interface + */ +export const WorkflowVersionsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = WorkflowVersionsApiAxiosParamCreator(configuration) + return { + /** + * Validates and stores an immutable workflow package in content-addressed storage. Repeating the same canonical content returns the same identifier. + * @summary Create Workflow Version + * @param {WorkflowVersion} workflowVersion + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createWorkflowVersion(workflowVersion: WorkflowVersion, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createWorkflowVersion(workflowVersion, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['WorkflowVersionsApi.createWorkflowVersion']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * WorkflowVersionsApi - factory interface + */ +export const WorkflowVersionsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = WorkflowVersionsApiFp(configuration) + return { + /** + * Validates and stores an immutable workflow package in content-addressed storage. Repeating the same canonical content returns the same identifier. + * @summary Create Workflow Version + * @param {WorkflowVersion} workflowVersion + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorkflowVersion(workflowVersion: WorkflowVersion, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.createWorkflowVersion(workflowVersion, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * WorkflowVersionsApi - object-oriented interface + */ +export class WorkflowVersionsApi extends BaseAPI { + /** + * Validates and stores an immutable workflow package in content-addressed storage. Repeating the same canonical content returns the same identifier. + * @summary Create Workflow Version + * @param {WorkflowVersion} workflowVersion + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public createWorkflowVersion(workflowVersion: WorkflowVersion, options?: RawAxiosRequestConfig) { + return WorkflowVersionsApiFp(this.configuration).createWorkflowVersion(workflowVersion, options).then((request) => request(this.axios, this.basePath)); + } +} 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 new file mode 100644 index 000000000..79ede2d1b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/create-workflow-version-response.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Identity of the stored immutable workflow version. + */ +export interface CreateWorkflowVersionResponse { + /** + * SHA-256 identity of validated canonical workflow-version bytes. + */ + 'workflow_version_id': string; +} diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 2fdd2e9fe..04df237be 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -76,6 +76,7 @@ export * from './create-run-pull-request-request'; export * from './create-run-session-request'; export * from './create-secret-request'; export * from './create-variable-request'; +export * from './create-workflow-version-response'; export * from './delete-run-response'; export * from './delete-run-sandbox'; export * from './delete-secret-request'; @@ -511,4 +512,5 @@ export * from './workflow-ref'; export * from './workflow-reference'; export * from './workflow-schedule-summary'; export * from './workflow-settings'; +export * from './workflow-version'; export * from './write-blob-response'; diff --git a/lib/packages/fabro-api-client/src/models/workflow-version.ts b/lib/packages/fabro-api-client/src/models/workflow-version.ts new file mode 100644 index 000000000..d54f4064e --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/workflow-version.ts @@ -0,0 +1,33 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Complete immutable package for one rooted workflow. It contains at most 512 files and 512 workflow dependencies, each file is at most 512 KiB of UTF-8 content, and its compact canonical JSON representation is at most 2 MiB. + */ +export interface WorkflowVersion { + /** + * Canonical portable path inside one workflow version. Paths are UTF-8, relative, at most 240 bytes and 16 components, and cannot contain empty, dot, parent, backslash, control, tilde-root, or drive-letter segments. Map keys receive stricter byte and structural validation in the domain model than OpenAPI can express. + */ + 'entrypoint': string; + /** + * Workflow-local text files keyed by canonical path. Keys receive stricter domain validation than OpenAPI can express; each value is limited to 512 KiB of UTF-8 bytes. + */ + 'files': { [key: string]: string; }; + /** + * Exact stored workflow-version IDs keyed by resolved child-workflow path. Keys receive stricter domain validation than OpenAPI can express. + */ + 'workflow_dependencies': { [key: string]: string; }; +} From 79e44262aa230b21fae212a5fe40eb1537c4caa2 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 10:37:19 -0400 Subject: [PATCH 19/62] Detect workflow path collisions hidden by sort order The adjacent-pair scan over the byte-sorted path list missed file/directory collisions whenever a sibling path sorted between the ancestor and its descendant (any byte below '/' after the shared prefix, e.g. "assets.txt" between "assets" and "assets/item.txt"). Replace it with an exhaustive ancestor-prefix lookup over a path set, which also catches equal paths across files and workflow dependencies. Co-Authored-By: Claude Fable 5 --- .../fabro-types/src/workflow_version.rs | 104 +++++++++++++++--- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/lib/foundation/fabro-types/src/workflow_version.rs b/lib/foundation/fabro-types/src/workflow_version.rs index c0b35f8ad..cba3a04c1 100644 --- a/lib/foundation/fabro-types/src/workflow_version.rs +++ b/lib/foundation/fabro-types/src/workflow_version.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt; use std::marker::PhantomData; @@ -135,23 +135,27 @@ impl WorkflowVersion { fn validate_path_collisions(&self) -> Result<(), WorkflowVersionShapeError> { // Keys are unique within each map, so equality can only collide // across files and workflow dependencies. - let mut paths = self - .files - .keys() - .chain(self.workflow_dependencies.keys()) - .collect::>(); - paths.sort_unstable(); - for pair in paths.windows(2) { - let [first, second] = pair else { - unreachable!("a two-item window must contain two paths") - }; - if first == second || first.is_ancestor_of(second) { + let mut by_text = + HashMap::with_capacity(self.files.len() + self.workflow_dependencies.len()); + for path in self.files.keys().chain(self.workflow_dependencies.keys()) { + if let Some(existing) = by_text.insert(path.as_str(), path) { return Err(WorkflowVersionShapeError::PathCollision { - first: (*first).clone(), - second: (*second).clone(), + first: existing.clone(), + second: path.clone(), }); } } + for path in self.files.keys().chain(self.workflow_dependencies.keys()) { + let text = path.as_str(); + for (index, _) in text.match_indices('/') { + if let Some(ancestor) = by_text.get(&text[..index]) { + return Err(WorkflowVersionShapeError::PathCollision { + first: (*ancestor).clone(), + second: path.clone(), + }); + } + } + } Ok(()) } } @@ -295,6 +299,78 @@ mod tests { )); } + #[test] + fn rejects_ancestor_collisions_hidden_by_sort_order() { + // `assets.txt` sorts between `assets` and `assets/item.txt` because + // '.' precedes '/', so an adjacent-pair scan over the sorted list + // would miss this collision. + let error = WorkflowVersion::new( + path("workflow.fabro"), + BTreeMap::from([ + (path("workflow.fabro"), "digraph W {}".to_string()), + (path("assets"), "file".to_string()), + (path("assets.txt"), "sibling".to_string()), + (path("assets/item.txt"), "nested".to_string()), + ]), + BTreeMap::new(), + ) + .unwrap_err(); + assert!(matches!( + error, + WorkflowVersionShapeError::PathCollision { first, second } + if first.as_str() == "assets" && second.as_str() == "assets/item.txt" + )); + + assert!( + WorkflowVersion::new( + path("workflow.fabro"), + BTreeMap::from([ + (path("workflow.fabro"), "digraph W {}".to_string()), + (path("assets.txt"), "sibling".to_string()), + (path("assets/item.txt"), "nested".to_string()), + ]), + BTreeMap::new(), + ) + .is_ok() + ); + } + + #[test] + fn rejects_collisions_across_files_and_workflow_dependencies() { + let dependency_id = WorkflowVersionId::from(BlobHash::new(b"child")); + + let equal = WorkflowVersion::new( + path("workflow.fabro"), + BTreeMap::from([ + (path("workflow.fabro"), "digraph W {}".to_string()), + (path("child.fabro"), "digraph C {}".to_string()), + ]), + BTreeMap::from([(path("child.fabro"), dependency_id)]), + ) + .unwrap_err(); + assert!(matches!( + equal, + WorkflowVersionShapeError::PathCollision { first, second } + if first == second && first.as_str() == "child.fabro" + )); + + let ancestor = WorkflowVersion::new( + path("workflow.fabro"), + BTreeMap::from([ + (path("workflow.fabro"), "digraph W {}".to_string()), + (path("libs"), "file".to_string()), + (path("libs.md"), "sibling".to_string()), + ]), + BTreeMap::from([(path("libs/child.fabro"), dependency_id)]), + ) + .unwrap_err(); + assert!(matches!( + ancestor, + WorkflowVersionShapeError::PathCollision { first, second } + if first.as_str() == "libs" && second.as_str() == "libs/child.fabro" + )); + } + #[test] fn enforces_file_count_file_size_and_canonical_size_boundaries() { let mut files = BTreeMap::from([(path("workflow.fabro"), "digraph W {}".to_string())]); From e688bd98768575c74e0d3d0b890496a6decc1ad2 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 10:37:19 -0400 Subject: [PATCH 20/62] Return 422 for invalid workflow-version dependencies DependencyInvalid fell through to the curated 500 even though the OpenAPI contract promises 422 workflow_version_dependency_not_found for an absent, invalid, or non-canonical dependency. Route it to that response alongside DependencyNotFound; the top-level message only names the caller-supplied path and id, so no internal chain leaks. Drop the InvalidVersion/InvalidShape arms, which were unreachable from the only call site. Co-Authored-By: Claude Fable 5 --- .../src/server/handler/workflow_versions.rs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 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 ebff58d82..beb5802f5 100644 --- a/lib/apps/fabro-server/src/server/handler/workflow_versions.rs +++ b/lib/apps/fabro-server/src/server/handler/workflow_versions.rs @@ -83,21 +83,14 @@ fn json_rejection(rejection: JsonRejection) -> ApiError { fn store_error(err: WorkflowVersionStoreError) -> ApiError { match err { - err @ WorkflowVersionStoreError::DependencyNotFound { .. } => ApiError::with_code( + // The top-level message names the offending dependency without its + // internal source chain, so it is safe to surface to the caller. + err @ (WorkflowVersionStoreError::DependencyNotFound { .. } + | WorkflowVersionStoreError::DependencyInvalid { .. }) => ApiError::with_code( StatusCode::UNPROCESSABLE_ENTITY, err.to_string(), DEPENDENCY_NOT_FOUND_CODE, ), - WorkflowVersionStoreError::InvalidVersion(source) => ApiError::with_code( - StatusCode::UNPROCESSABLE_ENTITY, - source.to_string(), - INVALID_VERSION_CODE, - ), - WorkflowVersionStoreError::InvalidShape(source) => ApiError::with_code( - StatusCode::UNPROCESSABLE_ENTITY, - source.to_string(), - INVALID_VERSION_CODE, - ), err => { tracing::error!( error = %err, @@ -264,7 +257,7 @@ mod tests { } #[tokio::test] - async fn corrupt_stored_dependency_returns_curated_internal_error() { + async fn invalid_stored_dependency_is_a_client_error_without_internals() { let state = TestAppStateBuilder::new().build(); let app = test_support::build_test_router(Arc::clone(&state)); let dependency_id = WorkflowVersionId::from( @@ -289,11 +282,14 @@ mod tests { .oneshot(request(serde_json::to_vec(&root).unwrap())) .await .unwrap(); - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let body = response_json(response).await; - assert_eq!( - body["errors"][0]["detail"], - "workflow version store operation failed" + assert_eq!(error_code(&body), DEPENDENCY_NOT_FOUND_CODE); + assert!( + body["errors"][0]["detail"] + .as_str() + .unwrap() + .contains("child.fabro") ); assert!(!body.to_string().contains("cannot be decoded")); } From 20c9fba0b13975c95d95d18e94684bb70dbda10e Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 10:42:16 -0400 Subject: [PATCH 21/62] Serialize workflow-version canonical bytes once at construction WorkflowVersion::new serialized the whole version just to enforce the size limit and threw the bytes away, the store re-serialized them to write the blob, and every read re-serialized a third time for the canonicality comparison. Cache the canonical bytes on the struct at construction (skipped during serde) and expose them as an infallible borrow; the now-unconstructable InvalidShape store error variant goes away with it. Co-Authored-By: Claude Fable 5 --- .../fabro-workflow-version/src/store.rs | 28 +++++-------- .../fabro-types/src/workflow_version.rs | 40 ++++++++++--------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs index 80dcebd53..fd52cebe5 100644 --- a/lib/components/fabro-workflow-version/src/store.rs +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, HashSet, VecDeque}; use std::sync::Arc; use fabro_store::BlobStore; -use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId, WorkflowVersionShapeError}; +use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId}; use thiserror::Error; use crate::{ValidatedWorkflowVersion, WorkflowVersionError}; @@ -11,8 +11,6 @@ use crate::{ValidatedWorkflowVersion, WorkflowVersionError}; pub enum WorkflowVersionStoreError { #[error(transparent)] InvalidVersion(#[from] WorkflowVersionError), - #[error(transparent)] - InvalidShape(#[from] WorkflowVersionShapeError), #[error("workflow-version dependency `{id}` at `{path}` is not stored")] DependencyNotFound { path: WorkflowPath, @@ -60,11 +58,10 @@ impl WorkflowVersionStore { &self, version: &ValidatedWorkflowVersion, ) -> Result { - let canonical = version.version().canonical_bytes()?; self.validate_dependency_closure(version.version().workflow_dependencies()) .await?; self.blobs - .write(&canonical) + .write(version.version().canonical_bytes()) .await .map(WorkflowVersionId::from) .map_err(|source| WorkflowVersionStoreError::Storage { source }) @@ -98,8 +95,7 @@ impl WorkflowVersionStore { let version = serde_json::from_slice::(&bytes) .map_err(|source| WorkflowVersionStoreError::Decode { id: *id, source })?; let validated = ValidatedWorkflowVersion::new(version)?; - let canonical = validated.version().canonical_bytes()?; - if canonical.as_slice() != bytes.as_ref() { + if validated.version().canonical_bytes() != bytes.as_ref() { return Err(WorkflowVersionStoreError::NonCanonical { id: *id }); } Ok(Some(validated)) @@ -196,7 +192,7 @@ mod tests { async fn put_get_reuses_exact_blob_digest() { let (blobs, store) = stores().await; let version = version("digraph W {}", BTreeMap::new()); - let expected_bytes = version.version().canonical_bytes().unwrap(); + let expected_bytes = version.version().canonical_bytes().to_vec(); let expected_id = WorkflowVersionId::from(fabro_types::BlobHash::new(&expected_bytes)); let id = store.put(&version).await.unwrap(); @@ -228,15 +224,14 @@ mod tests { 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(), + child.version().canonical_bytes(), )); 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 = + WorkflowVersionId::from(fabro_types::BlobHash::new(root.version().canonical_bytes())); let error = store.put(&root).await.unwrap_err(); assert!(matches!( @@ -256,15 +251,14 @@ mod tests { r#"digraph Child { grandchild [stack.child_workflow="grandchild.fabro"] }"#, BTreeMap::from([(path("grandchild.fabro"), missing_grandchild_id)]), ); - let child_bytes = child.version().canonical_bytes().unwrap(); - let child_id = WorkflowVersionId::from(blobs.write(&child_bytes).await.unwrap()); + let child_bytes = child.version().canonical_bytes(); + let child_id = WorkflowVersionId::from(blobs.write(child_bytes).await.unwrap()); 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 = + WorkflowVersionId::from(fabro_types::BlobHash::new(root.version().canonical_bytes())); assert!(matches!( store.put(&root).await.unwrap_err(), diff --git a/lib/foundation/fabro-types/src/workflow_version.rs b/lib/foundation/fabro-types/src/workflow_version.rs index cba3a04c1..9b4570943 100644 --- a/lib/foundation/fabro-types/src/workflow_version.rs +++ b/lib/foundation/fabro-types/src/workflow_version.rs @@ -53,6 +53,8 @@ pub struct WorkflowVersion { entrypoint: WorkflowPath, files: BTreeMap, workflow_dependencies: BTreeMap, + #[serde(skip)] + canonical: Vec, } impl WorkflowVersion { @@ -61,13 +63,22 @@ impl WorkflowVersion { files: BTreeMap, workflow_dependencies: BTreeMap, ) -> Result { - let version = Self { + let mut version = Self { entrypoint, files, workflow_dependencies, + canonical: Vec::new(), }; version.validate_shape()?; - version.canonical_bytes()?; + let canonical = serde_json::to_vec(&version) + .map_err(|source| WorkflowVersionShapeError::Serialization { source })?; + if canonical.len() > MAX_WORKFLOW_VERSION_BYTES { + return Err(WorkflowVersionShapeError::VersionTooLarge { + actual: canonical.len(), + maximum: MAX_WORKFLOW_VERSION_BYTES, + }); + } + version.canonical = canonical; Ok(version) } @@ -86,20 +97,11 @@ impl WorkflowVersion { &self.workflow_dependencies } - /// Serialize to the canonical wire form. - /// - /// Structural validity is guaranteed by construction, so this only - /// serializes and enforces the canonical size limit. - pub fn canonical_bytes(&self) -> Result, WorkflowVersionShapeError> { - let bytes = serde_json::to_vec(self) - .map_err(|source| WorkflowVersionShapeError::Serialization { source })?; - if bytes.len() > MAX_WORKFLOW_VERSION_BYTES { - return Err(WorkflowVersionShapeError::VersionTooLarge { - actual: bytes.len(), - maximum: MAX_WORKFLOW_VERSION_BYTES, - }); - } - Ok(bytes) + /// Canonical wire bytes, serialized and size-checked once at + /// construction. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical } fn validate_shape(&self) -> Result<(), WorkflowVersionShapeError> { @@ -250,7 +252,7 @@ mod tests { .unwrap(); assert_eq!( - String::from_utf8(version.canonical_bytes().unwrap()).unwrap(), + String::from_utf8(version.canonical_bytes().to_vec()).unwrap(), r#"{"entrypoint":"workflow.fabro","files":{"a.txt":"A","workflow.fabro":"digraph W {}","z.txt":"Z"},"workflow_dependencies":{}}"# ); } @@ -419,7 +421,7 @@ mod tests { BTreeMap::new(), ) .unwrap(); - let remaining = MAX_WORKFLOW_VERSION_BYTES - empty.canonical_bytes().unwrap().len(); + let remaining = MAX_WORKFLOW_VERSION_BYTES - empty.canonical_bytes().len(); let per_file = remaining / 4; let remainder = remaining % 4; for index in 0..4 { @@ -434,7 +436,7 @@ mod tests { ) .unwrap(); assert_eq!( - exact_version.canonical_bytes().unwrap().len(), + exact_version.canonical_bytes().len(), MAX_WORKFLOW_VERSION_BYTES ); exact_version_files From cc711027da7559c3e28687a3a2e5bf0988bb37f0 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 10:43:26 -0400 Subject: [PATCH 22/62] Parse workflow version IDs case-insensitively MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorkflowVersionId bolted a lowercase-only byte scan onto BlobHash parsing, giving the same 64-hex concept two parse behaviors across entry points. Identity is the decoded 32-byte digest and canonical serialization always emits lowercase, so accepting either case on input is lossless — the stored-blob canonicality check still rejects non-canonical bytes independently. Delegate straight to BlobHash. Co-Authored-By: Claude Fable 5 --- .../fabro-types/src/workflow_version_id.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/foundation/fabro-types/src/workflow_version_id.rs b/lib/foundation/fabro-types/src/workflow_version_id.rs index f3a618482..4bf597f47 100644 --- a/lib/foundation/fabro-types/src/workflow_version_id.rs +++ b/lib/foundation/fabro-types/src/workflow_version_id.rs @@ -35,18 +35,13 @@ impl From for String { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] -#[error("workflow version ID must be exactly 64 lowercase hexadecimal characters")] +#[error("workflow version ID must be exactly 64 hexadecimal characters")] pub struct WorkflowVersionIdParseError; impl FromStr for WorkflowVersionId { type Err = WorkflowVersionIdParseError; fn from_str(value: &str) -> Result { - // `BlobHash` enforces length and hex charset but accepts uppercase digits; - // the canonical wire form is lowercase only. - if value.bytes().any(|byte| byte.is_ascii_uppercase()) { - return Err(WorkflowVersionIdParseError); - } value .parse::() .map(Self) @@ -75,11 +70,14 @@ mod tests { } #[test] - fn parse_and_serde_require_lowercase_hex() { + fn parse_accepts_any_case_and_serializes_lowercase() { let value = BlobHash::new(b"workflow").to_string(); let id: WorkflowVersionId = value.parse().unwrap(); assert_eq!(serde_json::to_value(id).unwrap(), value); - assert!(value.to_uppercase().parse::().is_err()); + assert_eq!( + value.to_uppercase().parse::().unwrap(), + id + ); for invalid in [ String::new(), "0".repeat(63), @@ -88,9 +86,11 @@ mod tests { ] { assert!(invalid.parse::().is_err()); } - assert!( + assert_eq!( serde_json::from_value::(serde_json::json!(value.to_uppercase())) - .is_err() + .unwrap() + .to_string(), + value ); } } From 75fb1e3a1d41aa36a1571738ec5db57282665dc9 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 10:44:58 -0400 Subject: [PATCH 23/62] Prove workflow-version ID JSON parity with the OpenAPI schema The round-trip fixtures only used empty workflow_dependencies, so no WorkflowVersionId value ever appeared on the wire in a fabro-api assertion and CreateWorkflowVersionResponse had no coverage at all. Put a real 64-hex id in the fixture, round-trip the response type, and pin serialization to the schema's ^[0-9a-f]{64}$ pattern including lowercase normalization of case-insensitive input. Co-Authored-By: Claude Fable 5 --- .../tests/workflow_version_round_trip.rs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) 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 894c1c910..3bf0c8835 100644 --- a/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs +++ b/lib/foundation/fabro-api/tests/workflow_version_round_trip.rs @@ -1,12 +1,14 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ - WorkflowPath as ApiWorkflowPath, WorkflowVersion as ApiWorkflowVersion, - WorkflowVersionId as ApiWorkflowVersionId, + CreateWorkflowVersionResponse, WorkflowPath as ApiWorkflowPath, + WorkflowVersion as ApiWorkflowVersion, WorkflowVersionId as ApiWorkflowVersionId, }; use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId}; use serde_json::json; +const DEPENDENCY_ID: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + #[test] fn workflow_version_schemas_reuse_domain_types() { assert_same_type::(); @@ -22,13 +24,38 @@ fn workflow_version_round_trips_exact_wire_shape() { "prompts/goal.md": "Ship it", "workflow.fabro": "digraph W { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" }, - "workflow_dependencies": {} + "workflow_dependencies": { "children/check.fabro": DEPENDENCY_ID } }); let version: ApiWorkflowVersion = serde_json::from_value(value.clone()).unwrap(); assert_eq!(serde_json::to_value(version).unwrap(), value); } +#[test] +fn create_workflow_version_response_round_trips_exact_wire_shape() { + let value = json!({ "workflow_version_id": DEPENDENCY_ID }); + + let response: CreateWorkflowVersionResponse = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(&response).unwrap(), value); +} + +#[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}$`. + let id = serde_json::from_value::(json!(DEPENDENCY_ID.to_uppercase())) + .unwrap(); + let emitted = serde_json::to_value(id).unwrap(); + assert_eq!(emitted, json!(DEPENDENCY_ID)); + + 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 workflow_version_replacement_rejects_unknown_fields() { let value = json!({ From 180330c117423816f0a222cc8abbdf06bde9d334 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 10:48:45 -0400 Subject: [PATCH 24/62] Remove unused WorkflowPath::parent and is_ancestor_of Neither method has callers anywhere in the workspace: resolve_reference splits on '/' directly, and the path-collision validator now checks ancestor prefixes against a path set. parent() also constructed Self without going through validate(), so dropping it removes an unvalidated construction path from the wire type's public API. Co-Authored-By: Claude Fable 5 --- .../fabro-types/src/workflow_path.rs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/lib/foundation/fabro-types/src/workflow_path.rs b/lib/foundation/fabro-types/src/workflow_path.rs index 32297d93e..a489153a9 100644 --- a/lib/foundation/fabro-types/src/workflow_path.rs +++ b/lib/foundation/fabro-types/src/workflow_path.rs @@ -49,20 +49,6 @@ impl WorkflowPath { &self.0 } - #[must_use] - pub fn parent(&self) -> Option { - self.0 - .rsplit_once('/') - .map(|(parent, _)| Self(parent.to_owned())) - } - - #[must_use] - pub fn is_ancestor_of(&self, other: &Self) -> bool { - other.0.len() > self.0.len() - && other.0.starts_with(self.0.as_str()) - && other.0.as_bytes()[self.0.len()] == b'/' - } - pub fn resolve_reference(&self, reference: &str) -> Result { validate_reference_shape(reference)?; let mut components = self @@ -256,13 +242,6 @@ mod tests { assert!(graph.resolve_reference("prompts/").is_err()); } - #[test] - fn ancestor_checks_component_boundaries() { - let parent: WorkflowPath = "dir/file".parse().unwrap(); - assert!(parent.is_ancestor_of(&"dir/file/child".parse().unwrap())); - assert!(!parent.is_ancestor_of(&"dir/filename".parse().unwrap())); - } - #[test] fn serde_and_ordered_map_keys_preserve_canonical_text() { let paths = BTreeMap::from([ From 8c3ff6216cb4ef049fdb20b7dbaa0403b39d7b9b Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 16:02:12 -0400 Subject: [PATCH 25/62] Revert "Serialize workflow-version canonical bytes once at construction" This reverts commit 8090d7030984862564a929ee9264e93911014e00. The cached canonical field was optimizing an unmeasured path: without the (deferred) O(closure) dependency re-validation multiplier, the repeated serialization is microseconds for realistic versions. Compute canonical bytes on demand like the environment, automation, and MCP stores do, rather than carrying a serde-skipped cache field, a construction bootstrap, and doubled memory for it. Purely in-memory: stored blobs and version IDs are unchanged. Co-Authored-By: Claude Fable 5 --- .../fabro-workflow-version/src/store.rs | 28 ++++++++----- .../fabro-types/src/workflow_version.rs | 40 +++++++++---------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs index fd52cebe5..80dcebd53 100644 --- a/lib/components/fabro-workflow-version/src/store.rs +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, HashSet, VecDeque}; use std::sync::Arc; use fabro_store::BlobStore; -use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId}; +use fabro_types::{WorkflowPath, WorkflowVersion, WorkflowVersionId, WorkflowVersionShapeError}; use thiserror::Error; use crate::{ValidatedWorkflowVersion, WorkflowVersionError}; @@ -11,6 +11,8 @@ use crate::{ValidatedWorkflowVersion, WorkflowVersionError}; pub enum WorkflowVersionStoreError { #[error(transparent)] InvalidVersion(#[from] WorkflowVersionError), + #[error(transparent)] + InvalidShape(#[from] WorkflowVersionShapeError), #[error("workflow-version dependency `{id}` at `{path}` is not stored")] DependencyNotFound { path: WorkflowPath, @@ -58,10 +60,11 @@ impl WorkflowVersionStore { &self, version: &ValidatedWorkflowVersion, ) -> Result { + let canonical = version.version().canonical_bytes()?; self.validate_dependency_closure(version.version().workflow_dependencies()) .await?; self.blobs - .write(version.version().canonical_bytes()) + .write(&canonical) .await .map(WorkflowVersionId::from) .map_err(|source| WorkflowVersionStoreError::Storage { source }) @@ -95,7 +98,8 @@ impl WorkflowVersionStore { let version = serde_json::from_slice::(&bytes) .map_err(|source| WorkflowVersionStoreError::Decode { id: *id, source })?; let validated = ValidatedWorkflowVersion::new(version)?; - if validated.version().canonical_bytes() != bytes.as_ref() { + let canonical = validated.version().canonical_bytes()?; + if canonical.as_slice() != bytes.as_ref() { return Err(WorkflowVersionStoreError::NonCanonical { id: *id }); } Ok(Some(validated)) @@ -192,7 +196,7 @@ mod tests { async fn put_get_reuses_exact_blob_digest() { let (blobs, store) = stores().await; let version = version("digraph W {}", BTreeMap::new()); - let expected_bytes = version.version().canonical_bytes().to_vec(); + let expected_bytes = version.version().canonical_bytes().unwrap(); let expected_id = WorkflowVersionId::from(fabro_types::BlobHash::new(&expected_bytes)); let id = store.put(&version).await.unwrap(); @@ -224,14 +228,15 @@ mod tests { 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(), + &child.version().canonical_bytes().unwrap(), )); 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())); + let root_id = WorkflowVersionId::from(fabro_types::BlobHash::new( + &root.version().canonical_bytes().unwrap(), + )); let error = store.put(&root).await.unwrap_err(); assert!(matches!( @@ -251,14 +256,15 @@ mod tests { r#"digraph Child { grandchild [stack.child_workflow="grandchild.fabro"] }"#, BTreeMap::from([(path("grandchild.fabro"), missing_grandchild_id)]), ); - let child_bytes = child.version().canonical_bytes(); - let child_id = WorkflowVersionId::from(blobs.write(child_bytes).await.unwrap()); + let child_bytes = child.version().canonical_bytes().unwrap(); + let child_id = WorkflowVersionId::from(blobs.write(&child_bytes).await.unwrap()); 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())); + let root_id = WorkflowVersionId::from(fabro_types::BlobHash::new( + &root.version().canonical_bytes().unwrap(), + )); assert!(matches!( store.put(&root).await.unwrap_err(), diff --git a/lib/foundation/fabro-types/src/workflow_version.rs b/lib/foundation/fabro-types/src/workflow_version.rs index 9b4570943..cba3a04c1 100644 --- a/lib/foundation/fabro-types/src/workflow_version.rs +++ b/lib/foundation/fabro-types/src/workflow_version.rs @@ -53,8 +53,6 @@ pub struct WorkflowVersion { entrypoint: WorkflowPath, files: BTreeMap, workflow_dependencies: BTreeMap, - #[serde(skip)] - canonical: Vec, } impl WorkflowVersion { @@ -63,22 +61,13 @@ impl WorkflowVersion { files: BTreeMap, workflow_dependencies: BTreeMap, ) -> Result { - let mut version = Self { + let version = Self { entrypoint, files, workflow_dependencies, - canonical: Vec::new(), }; version.validate_shape()?; - let canonical = serde_json::to_vec(&version) - .map_err(|source| WorkflowVersionShapeError::Serialization { source })?; - if canonical.len() > MAX_WORKFLOW_VERSION_BYTES { - return Err(WorkflowVersionShapeError::VersionTooLarge { - actual: canonical.len(), - maximum: MAX_WORKFLOW_VERSION_BYTES, - }); - } - version.canonical = canonical; + version.canonical_bytes()?; Ok(version) } @@ -97,11 +86,20 @@ impl WorkflowVersion { &self.workflow_dependencies } - /// Canonical wire bytes, serialized and size-checked once at - /// construction. - #[must_use] - pub fn canonical_bytes(&self) -> &[u8] { - &self.canonical + /// Serialize to the canonical wire form. + /// + /// Structural validity is guaranteed by construction, so this only + /// serializes and enforces the canonical size limit. + pub fn canonical_bytes(&self) -> Result, WorkflowVersionShapeError> { + let bytes = serde_json::to_vec(self) + .map_err(|source| WorkflowVersionShapeError::Serialization { source })?; + if bytes.len() > MAX_WORKFLOW_VERSION_BYTES { + return Err(WorkflowVersionShapeError::VersionTooLarge { + actual: bytes.len(), + maximum: MAX_WORKFLOW_VERSION_BYTES, + }); + } + Ok(bytes) } fn validate_shape(&self) -> Result<(), WorkflowVersionShapeError> { @@ -252,7 +250,7 @@ mod tests { .unwrap(); assert_eq!( - String::from_utf8(version.canonical_bytes().to_vec()).unwrap(), + String::from_utf8(version.canonical_bytes().unwrap()).unwrap(), r#"{"entrypoint":"workflow.fabro","files":{"a.txt":"A","workflow.fabro":"digraph W {}","z.txt":"Z"},"workflow_dependencies":{}}"# ); } @@ -421,7 +419,7 @@ mod tests { BTreeMap::new(), ) .unwrap(); - let remaining = MAX_WORKFLOW_VERSION_BYTES - empty.canonical_bytes().len(); + let remaining = MAX_WORKFLOW_VERSION_BYTES - empty.canonical_bytes().unwrap().len(); let per_file = remaining / 4; let remainder = remaining % 4; for index in 0..4 { @@ -436,7 +434,7 @@ mod tests { ) .unwrap(); assert_eq!( - exact_version.canonical_bytes().len(), + exact_version.canonical_bytes().unwrap().len(), MAX_WORKFLOW_VERSION_BYTES ); exact_version_files From 535e333970809141b0f11b8953430876239559ce Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Fri, 14 Aug 2026 09:41:55 +0000 Subject: [PATCH 26/62] Bump version to 0.325.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 93c745736..d2f53dfa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2673,7 +2673,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2688,7 +2688,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "async-trait", "fabro-agent", @@ -2711,7 +2711,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2721,7 +2721,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2740,7 +2740,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2755,7 +2755,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2797,7 +2797,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2808,7 +2808,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "fabro-api", @@ -2829,7 +2829,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2895,7 +2895,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2933,7 +2933,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2941,7 +2941,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "cc", "libc", @@ -2950,7 +2950,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2966,7 +2966,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3010,7 +3010,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3127,18 +3127,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" [[package]] name = "fabro-store" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3168,7 +3168,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3208,7 +3208,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3233,7 +3233,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3254,7 +3254,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3268,7 +3268,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3291,7 +3291,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3314,7 +3314,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3327,7 +3327,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3344,7 +3344,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3433,7 +3433,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8544,7 +8544,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "axum", "base64", @@ -8563,7 +8563,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index 80bb89390..0923bda8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.324.0-nightly.0" +version = "0.325.0-nightly.0" license = "MIT" [workspace.dependencies] From a045ea4cb02635b87184cac20819f49c39b75fd5 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 17 Aug 2026 12:02:10 -0400 Subject: [PATCH 27/62] 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 28/62] 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 29/62] 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 30/62] 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 31/62] 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 32/62] 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 33/62] 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 34/62] 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 35/62] 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 36/62] 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 37/62] 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 38/62] 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 39/62] 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 40/62] 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 41/62] 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 42/62] 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 43/62] 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 44/62] 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 45/62] 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 46/62] 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 47/62] 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 48/62] 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 49/62] 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 50/62] 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 51/62] 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 52/62] 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 53/62] 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 54/62] 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 55/62] 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 56/62] 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 57/62] 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 58/62] 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 59/62] 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 60/62] 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 0845c331cb38c25db5265d2e1056dc3eb0fee6ec Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 17:26:13 -0400 Subject: [PATCH 61/62] 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 f88df59163ad287a093bf26dc600c13be5343daa Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 17:39:19 -0400 Subject: [PATCH 62/62] 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!(